Skip to content

Commit

Permalink
feat: provide chunks store customization
Browse files Browse the repository at this point in the history
  • Loading branch information
antongolub committed Jun 12, 2024
1 parent 9873489 commit e6fded9
Show file tree
Hide file tree
Showing 4 changed files with 114 additions and 15 deletions.
28 changes: 27 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,33 @@ const { error } = await p
error.message // 'The operation was aborted'
```

- [ ] Stdout limit
- [x] Stdout limit

```ts
import {type TSpawnStore, $} from 'zurk'

const getFixedSizeArray = (size: number) => {
const arr: any[] = []
return new Proxy(arr, {
get: (target: any, prop) =>
prop === 'push' && arr.length >= size
? () => {}
: target[prop]
})
}
const store: TSpawnStore = {
stdout: getFixedSizeArray(1),
stderr: getFixedSizeArray(2),
stdall: getFixedSizeArray(0),
getStdout() { return this.stdout.join('') },
getStderr() { return this.stdout.join('') },
getStdall() { return '' },
}

const result = await $({store})`echo hello`
result.stdout // 'hello\n'
result.stdall // ''
```

## License
[MIT](./LICENSE)
48 changes: 35 additions & 13 deletions src/main/ts/spawn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,21 @@ import * as cp from 'node:child_process'
import process from 'node:process'
import EventEmitter from 'node:events'
import { Readable, Writable, Stream, Transform } from 'node:stream'
import { assign, noop } from './util.js'
import { assign, noop, once } from './util.js'

export * from './util.js'

export type TSpawnError = any

export type TSpawnStore = {
stdout: Array<string | Buffer>
stderr: Array<string | Buffer>
stdall: Array<string | Buffer>
getStdout: () => string
getStderr: () => string
getStdall: () => string
}

export type TSpawnResult = {
stderr: string
stdout: string
Expand Down Expand Up @@ -54,6 +63,7 @@ export interface TSpawnCtxNormalized {
spawn: typeof cp.spawn
spawnSync: typeof cp.spawnSync
spawnOpts: Record<string, any>
store: TSpawnStore
callback: (err: TSpawnError, result: TSpawnResult) => void
stdin: Readable
stdout: Writable
Expand Down Expand Up @@ -81,6 +91,7 @@ export const normalizeCtx = (...ctxs: TSpawnCtx[]): TSpawnCtxNormalized => assig
spawn: cp.spawn,
spawnSync: cp.spawnSync,
spawnOpts: {},
store: createStore(),
callback: noop,
stdin: new VoidWritable(),
stdout: new VoidWritable(),
Expand Down Expand Up @@ -125,6 +136,18 @@ export const attachListeners = (ee: EventEmitter, on: Partial<TSpawnListeners> =
}
}

export const createStore = (): TSpawnStore => {
const store: TSpawnStore = {
stdout: [],
stderr: [],
stdall: [],
getStdout: once(() => store.stdout.join('')),
getStderr: once(() => store.stderr.join('')),
getStdall: once(() => store.stdall.join(''))
}
return store
}

// eslint-disable-next-line sonarjs/cognitive-complexity
export const invoke = (c: TSpawnCtxNormalized): TSpawnCtxNormalized => {
const now = Date.now()
Expand All @@ -137,17 +160,19 @@ export const invoke = (c: TSpawnCtxNormalized): TSpawnCtxNormalized => {
const result = c.spawnSync(c.cmd, c.args, opts)
c.ee.emit('start', result, c)
if (result.stdout.length > 0) {
c.store.stdout.push(result.stdout)
c.stdout.write(result.stdout)
c.ee.emit('stdout', result.stdout, c)
}
if (result.stderr.length > 0) {
c.store.stderr.push(result.stderr)
c.stderr.write(result.stderr)
c.ee.emit('stderr', result.stderr, c)
}
c.callback(null, c.fulfilled = {
...result,
stdout: result.stdout.toString(),
stderr: result.stderr.toString(),
get stdout() { return c.store.getStdout() },
get stderr() { return c.store.getStderr() },
stdio,
get stdall() { return this.stdout + this.stderr },
duration: Date.now() - now,
Expand All @@ -161,9 +186,6 @@ export const invoke = (c: TSpawnCtxNormalized): TSpawnCtxNormalized => {

let error: any = null
const opts = buildSpawnOpts(c)
const stderr: string[] = []
const stdout: string[] = []
const stdall: string[] = []
const child = c.spawn(c.cmd, c.args, opts)
c.child = child

Expand All @@ -183,13 +205,13 @@ export const invoke = (c: TSpawnCtxNormalized): TSpawnCtxNormalized => {
processInput(child, c.input || c.stdin)

child.stdout?.on('data', d => {
stdout.push(d)
stdall.push(d)
c.store.stdout.push(d)
c.store.stdall.push(d)
c.ee.emit('stdout', d, c)
}).pipe(c.stdout)
child.stderr?.on('data', d => {
stderr.push(d)
stdall.push(d)
c.store.stderr.push(d)
c.store.stdall.push(d)
c.ee.emit('stderr', d, c)
}).pipe(c.stderr)
child
Expand All @@ -202,9 +224,9 @@ export const invoke = (c: TSpawnCtxNormalized): TSpawnCtxNormalized => {
error,
status,
signal,
stdout: stdout.join(''),
stderr: stderr.join(''),
stdall: stdall.join(''),
get stdout() { return c.store.getStdout() },
get stderr() { return c.store.getStderr() },
get stdall() { return c.store.getStdall() },
stdio: [c.stdin, c.stdout, c.stderr],
duration: Date.now() - now,
ctx: c
Expand Down
9 changes: 9 additions & 0 deletions src/main/ts/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,12 @@ export const parseInput = (input: any): string | Buffer | Stream | null => {
}

export const pFinally = (p: Promise<any>, cb: any) => p.finally?.(cb) || p.then(cb, cb)

export const once = (fn: any) => {
let memo: any

return function (...args: any) {
if (memo === undefined) memo = fn(...args)
return memo
}
}
44 changes: 43 additions & 1 deletion src/test/ts/spawn.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import * as assert from 'node:assert'
import { describe, it } from 'node:test'
import EventEmitter from 'node:events'
import { invoke, normalizeCtx, TSpawnCtx, TSpawnResult } from '../../main/ts/spawn.js'
import {
exec,
invoke,
normalizeCtx,
TSpawnCtx,
TSpawnResult,
TSpawnStore
} from '../../main/ts/spawn.js'
import { makeDeferred } from '../../main/ts/util.js'

describe('invoke()', () => {
Expand Down Expand Up @@ -67,5 +74,40 @@ describe('normalizeCtx()', () => {
assert.equal(normalized.signal, signal)
assert.ok(normalized.ee instanceof EventEmitter)
assert.ok(normalized.ac instanceof AbortController)
assert.deepEqual(normalized.store.stdout, [])
assert.deepEqual(normalized.store.stderr, [])
})
})

describe('exec()', () => {
it('supports custom stores', async () => {
// eslint-disable-next-line unicorn/consistent-function-scoping
const getFixedSizeArray = (size: number) => {
const arr: any[] = []
return new Proxy(arr, {
get: (target: any, prop) =>
prop === 'push' && arr.length >= size
? () => { /* noop */ }
: target[prop]
})
}
const { promise, resolve, reject } = makeDeferred<TSpawnResult>()
const callback: TSpawnCtx['callback'] = (err, result) => err ? reject(err) : resolve(result)
const store: TSpawnStore = {
stdout: getFixedSizeArray(1),
stderr: getFixedSizeArray(2),
stdall: getFixedSizeArray(0),
getStdout() { return store.stdout.join('')},
getStderr() { return store.stderr.join('')},
getStdall() { return store.stdall.join('')},
}

const ctx = exec({sync: false, callback, store, cmd: 'echo', args: ['hello']})
const result = await promise

assert.equal(ctx.store.getStdall(), '')
assert.equal(ctx.store.getStdout(), 'hello\n')
assert.equal(result.stdout, 'hello\n')
assert.equal(result.stdall, '')
})
})

0 comments on commit e6fded9

Please sign in to comment.