Skip to content

Commit

Permalink
feat: introduce listeners map, rename _ctx to ctx
Browse files Browse the repository at this point in the history
  • Loading branch information
antongolub committed Mar 15, 2024
1 parent 86d19dd commit 616b091
Show file tree
Hide file tree
Showing 8 changed files with 72 additions and 47 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"scripts": {
"build": "concurrently 'npm:build:*'",
"build:js": "node ./src/scripts/build.mjs",
"build:dts": "tsc --emitDeclarationOnly --skipLibCheck --outDir target/dts",
"build:dts": "tsc --emitDeclarationOnly --outDir target/dts",
"build:docs": "typedoc --options src/main/typedoc",
"build:stamp": "npx buildstamp",
"test": "concurrently 'npm:test:*'",
Expand Down
4 changes: 2 additions & 2 deletions src/main/ts/mixin/pipe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ export const pipeMixin: TMixin = <T extends TZurk | TZurkPromise >($: TShell, re
const { fulfilled, stdout} = ctx
if (isZurkAny(stream)) {
if (fulfilled) {
stream._ctx.input = fulfilled.stdout
stream.ctx.input = fulfilled.stdout
} else {
stream._ctx.stdin = stdout as VoidWritable
stream.ctx.stdin = stdout as VoidWritable
}

return stream
Expand Down
4 changes: 2 additions & 2 deletions src/main/ts/mixin/timeout.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import process from 'node:process'
import { assign, noop } from '../util.js'
import { assign } from '../util.js'
import type { TMixin, TShell, TShellCtx } from '../x.js'
import { type TZurk, type TZurkPromise, isZurkPromise } from '../zurk.js'

Expand Down Expand Up @@ -31,7 +31,7 @@ export const timeoutMixin: TMixin = <T extends TZurk | TZurkPromise >($: TShell,
})

attachTimeout(ctx, result)
result.finally(() => clearTimeout((ctx as any).timer)).catch(noop)
result.finally(() => clearTimeout((ctx as any).timer))
}

return result
Expand Down
17 changes: 5 additions & 12 deletions src/main/ts/spawn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export type TSpawnResult = {
status: number | null
signal: NodeJS.Signals | null
duration: number
_ctx: TSpawnCtxNormalized
ctx: TSpawnCtxNormalized
error?: TSpawnError,
child?: TChild
}
Expand Down Expand Up @@ -42,8 +42,6 @@ export interface TSpawnCtxNormalized {
spawnSync: typeof cp.spawnSync
spawnOpts: Record<string, any>
callback: (err: TSpawnError, result: TSpawnResult) => void
onStdout: (data: string | Buffer) => void
onStderr: (data: string | Buffer) => void
stdin: Readable
stdout: Writable
stderr: Writable
Expand All @@ -69,8 +67,6 @@ export const normalizeCtx = (...ctxs: TSpawnCtx[]): TSpawnCtxNormalized => assig
spawnSync: cp.spawnSync,
spawnOpts: {},
callback: noop,
onStdout: noop,
onStderr: noop,
stdin: new VoidWritable(),
stdout: new VoidWritable(),
stderr: new VoidWritable(),
Expand Down Expand Up @@ -132,14 +128,13 @@ export const invoke = (c: TSpawnCtxNormalized): TSpawnCtxNormalized => {
stdio,
get stdall() { return this.stdout + this.stderr },
duration: Date.now() - now,
_ctx: c
ctx: c
})
c.ee.emit('end', c.fulfilled, c)

} else {
c.run(() => {
let error: any = null
// let status: number | null = null
const opts = buildSpawnOpts(c)
const stderr: string[] = []
const stdout: string[] = []
Expand All @@ -148,8 +143,6 @@ export const invoke = (c: TSpawnCtxNormalized): TSpawnCtxNormalized => {
c.child = child

opts.signal.addEventListener('abort', event => {
c.ee.emit('abort', event, c)

if (opts.detached && child.pid) {
try {
// https://github.com/nodejs/node/issues/51766
Expand All @@ -158,6 +151,7 @@ export const invoke = (c: TSpawnCtxNormalized): TSpawnCtxNormalized => {
child.kill()
}
}
c.ee.emit('abort', event, c)
})
processInput(child, c.input || c.stdin)

Expand All @@ -176,7 +170,6 @@ export const invoke = (c: TSpawnCtxNormalized): TSpawnCtxNormalized => {
error = e
c.ee.emit('err', error, c)
})
// .on('exit', (_status) => status = _status)
.on('close', (status, signal) => {
c.callback(error, c.fulfilled = {
error,
Expand All @@ -187,7 +180,7 @@ export const invoke = (c: TSpawnCtxNormalized): TSpawnCtxNormalized => {
stdall: stdall.join(''),
stdio: [c.stdin, c.stdout, c.stderr],
duration: Date.now() - now,
_ctx: c
ctx: c
})
c.ee.emit('end', c.fulfilled, c)
})
Expand All @@ -205,7 +198,7 @@ export const invoke = (c: TSpawnCtxNormalized): TSpawnCtxNormalized => {
stdall: '',
stdio,
duration: Date.now() - now,
_ctx: c
ctx: c
}
)
c.ee.emit('err', error, c)
Expand Down
13 changes: 7 additions & 6 deletions src/main/ts/x.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,10 @@ export type TShellOptions = Omit<TZurkOptions, 'input'> & {
input?: TShellCtx['input'] | TShellResponse | TShellResponseSync | null
} & TShellOptionsExtra

export interface TShellResponse extends Omit<Promisified<TZurk>, 'stdio' | '_ctx'>, Promise<TZurk & TShellResponseExtra<TShellResponse>>, TShellResponseExtra<TShellResponse> {
export interface TShellResponse extends Omit<Promisified<TZurk>, 'stdio' | 'ctx' | 'child'>, Promise<TZurk & TShellResponseExtra<TShellResponse>>, TShellResponseExtra<TShellResponse> {
child: TZurk['child']
stdio: [Readable | Writable, Writable, Writable]
_ctx: TShellCtx
ctx: TShellCtx
on: (event: string | symbol, listener: TZurkListener) => TShellResponse
}

Expand Down Expand Up @@ -104,24 +105,24 @@ const zurkMixin: TMixin = ($: TShell, target: TShellOptions | TZurk | TZurkPromi
return isPromiseLike(result)
? zurkifyPromise(
(result as TZurkPromise).then((r: TZurk) => applyMixins($, r, result)) as Promise<TZurk>,
result._ctx)
result.ctx)
: result as TZurk
}

$.mixins = [zurkMixin, killMixin, pipeMixin, timeoutMixin]

export const applyMixins = ($: TShell, result: TZurk | TZurkPromise | TShellOptions, parent?: TZurk | TZurkPromise) => {
let ctx: TShellCtx = (parent as TZurkPromise | TZurk)?._ctx
let ctx: TShellCtx = (parent as TZurkPromise | TZurk)?.ctx

return $.mixins.reduce((r, m) => {
ctx = ctx || (r as TZurkPromise | TZurk)._ctx
ctx = ctx || (r as TZurkPromise | TZurk).ctx
return m($, r as any, ctx)
}, result)
}

export const parseInput = (input: TShellOptions['input']): TShellCtx['input'] => {
if (typeof (input as TShellResponseSync)?.stdout === 'string') return (input as TShellResponseSync).stdout
if ((input as TShellResponse)?._ctx) return (input as TShellResponse)._ctx.stdout
if ((input as TShellResponse)?.ctx) return (input as TShellResponse).ctx.stdout

return input as TShellCtx['input']
}
Expand Down
68 changes: 45 additions & 23 deletions src/main/ts/zurk.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type EventEmitter from 'node:events'
import util from 'node:util'
import {
invoke,
Expand All @@ -11,26 +12,37 @@ export const ZURK = Symbol('Zurk')

export type TZurkListener = (value: any, ctx: TZurkCtx) => void

export type TZurkListeners = {
stdout: (data: Buffer, ctx: TZurkCtx) => void
stderr: (data: Buffer, ctx: TZurkCtx) => void
end: (result: TZurk, ctx: TZurkCtx) => void
err: (error: any, ctx: TZurkCtx) => void
abort: (error: any, ctx: TZurkCtx) => void
}

// TODO infer
export interface TZurkOn<R> {
on(name: 'stdout', listener: (data: Buffer, ctx: TZurkCtx) => void): R
on(name: 'stderr', listener: (data: Buffer, ctx: TZurkCtx) => void): R
on(name: 'end', listener: (result: TZurk, ctx: TZurkCtx) => void): R
on(name: 'err', listener: (error: any, ctx: TZurkCtx) => void): R
on(name: 'abort', listener: (error: any, ctx: TZurkCtx) => void): R
on<T extends 'stdout', L extends TZurkListeners[T]>(name: T, listener: L): R
on<T extends 'stderr', L extends TZurkListeners[T]>(name: T, listener: L): R
on<T extends 'end', L extends TZurkListeners[T]>(name: T, listener: L): R
on<T extends 'err', L extends TZurkListeners[T]>(name: T, listener: L): R
on<T extends 'abort', L extends TZurkListeners[T]>(name: T, listener: L): R
}

export interface TZurk extends TSpawnResult, TZurkOn<TZurk> {
_ctx: TZurkCtx
on(event: string | symbol, listener: TZurkListener): TZurk
ctx: TZurkCtx
}

export type TZurkCtx = TSpawnCtxNormalized & { nothrow?: boolean, nohandle?: boolean }

export type TZurkOptions = Partial<Omit<TZurkCtx, 'callback'>>
export type TZurkOptions = Partial<Omit<TZurkCtx, 'callback'>> & {
on?: Partial<TZurkListeners>
}

export type TZurkPromise = Promise<TZurk> & Promisified<TZurk> & TZurkOn<TZurkPromise> & {
_ctx: TZurkCtx
ctx: TZurkCtx
stdio: TZurkCtx['stdio']
child: TZurkCtx['child']
}

export const zurk = <T extends TZurkOptions = TZurkOptions, R = T extends {sync: true} ? TZurk : TZurkPromise>(opts: T): R =>
Expand All @@ -45,6 +57,7 @@ export const zurkAsync = (opts: TZurkOptions): TZurkPromise => {
ctx.error && !ctx.nothrow ? reject(ctx.error) : resolve(zurkFactory(ctx))
}
})
attachListeners(ctx.ee, opts.on)

invoke(ctx)

Expand All @@ -61,6 +74,7 @@ export const zurkSync = (opts: TZurkOptions): TZurk => {
response = zurkFactory(ctx)
}
})
attachListeners(ctx.ee, opts.on)

invoke(ctx)

Expand All @@ -81,7 +95,8 @@ export const zurkifyPromise = (target: Promise<TZurk> | TZurkPromise, ctx: TSpaw
if (p === 'catch') return target.catch.bind(target)
if (p === 'finally') return target.finally.bind(target)
if (p === 'stdio') return ctx.stdio
if (p === '_ctx') return ctx
if (p === 'ctx') return ctx
if (p === 'child') return ctx.child
if (p === 'on') return function (name: string, cb: VoidFunction){ ctx.ee.on(name, cb); return proxy }

if (p in target) return Reflect.get(target, p, receiver)
Expand All @@ -93,6 +108,12 @@ export const zurkifyPromise = (target: Promise<TZurk> | TZurkPromise, ctx: TSpaw
return proxy
}

export const attachListeners = (ee: EventEmitter, on: Partial<TZurkListeners> = {}) => {
for (const [name, listener] of Object.entries(on)) {
ee.on(name, listener as any)
}
}

export const getError = (data: TSpawnResult) => {
if (data.error) return data.error
if (data.status) return new Error(`Command failed with exit code ${data.status}`)
Expand All @@ -109,23 +130,24 @@ export const zurkFactory = <C extends TSpawnCtxNormalized>(ctx: C): TZurk => ne

class Zurk implements TZurk {
[ZURK] = ZURK
_ctx: TZurkCtx
ctx: TZurkCtx
constructor(ctx: TZurkCtx) {
this._ctx = ctx
this.ctx = ctx
}
on(name: string, cb: TVoidCallback): this { this._ctx.ee.on(name, cb); return this }
get status() { return this._ctx.fulfilled?.status ?? null }
get signal() { return this._ctx.fulfilled?.signal ?? null }
get error() { return this._ctx.error }
get stderr() { return this._ctx.fulfilled?.stderr || '' }
get stdout() { return this._ctx.fulfilled?.stdout || '' }
get stdall() { return this._ctx.fulfilled?.stdall || '' }
on(name: string, cb: TVoidCallback): this { this.ctx.ee.on(name, cb); return this }
get child() { return this.ctx.child }
get status() { return this.ctx.fulfilled?.status ?? null }
get signal() { return this.ctx.fulfilled?.signal ?? null }
get error() { return this.ctx.error }
get stderr() { return this.ctx.fulfilled?.stderr || '' }
get stdout() { return this.ctx.fulfilled?.stdout || '' }
get stdall() { return this.ctx.fulfilled?.stdall || '' }
get stdio(): TSpawnResult['stdio'] { return [
this._ctx.stdin,
this._ctx.stdout,
this._ctx.stderr
this.ctx.stdin,
this.ctx.stdout,
this.ctx.stderr
]}
get duration() { return this._ctx.fulfilled?.duration ?? 0 }
get duration() { return this.ctx.fulfilled?.duration ?? 0 }
toString(){ return this.stdall.trim() }
valueOf(){ return this.stdall.trim() }
}
2 changes: 1 addition & 1 deletion src/test/ts/x.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ describe('mixins', () => {
const p = $`sleep 10`
p.timeoutSignal = 'SIGALRM'
p.timeout = 25
p._ctx.nothrow = true
p.ctx.nothrow = true

const { error } = await p
assert.equal(error.message, 'Command failed with signal SIGALRM')
Expand Down
9 changes: 9 additions & 0 deletions src/test/ts/zurk.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as assert from 'node:assert'
import { describe, it } from 'node:test'
import { zurk, isZurk, isZurkPromise } from '../../main/ts/zurk.js'
import type { TSpawnResult } from '../../main/ts/spawn.js'

describe('zurk()', () => {
it('sync returns Zurk instance', async () => {
Expand All @@ -14,12 +15,20 @@ describe('zurk()', () => {

it('async returns ZurkPromise', async () => {
const result = zurk({ sync: false, cmd: 'echo', args: ['foo']})
let _result: TSpawnResult

result.on('end', data => _result = data)

assert.equal(result.child, result.ctx.child)
assert.equal((await result).toString(), 'foo')
assert.equal((await result).stdout, 'foo\n')
assert.equal(await result.stdout, 'foo\n')
assert.equal(await result.status, 0)
assert.equal(await result.signal, null)
assert.ok(isZurkPromise(result))
assert.ok(isZurk(await result))

// @ts-expect-error should be resolved by now
assert.equal(_result.stdout, 'foo\n')
})
})

0 comments on commit 616b091

Please sign in to comment.