channel.spec.ts 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. import { Duplex, PassThrough } from 'node:stream'
  2. import { describe, expect, it, onTestFinished } from 'vitest'
  3. import { JsonChannel } from '../src/channel.ts'
  4. function pair() {
  5. const left = new PassThrough()
  6. const right = new PassThrough()
  7. const a = Duplex.from({ readable: left, writable: right })
  8. const b = Duplex.from({ readable: right, writable: left })
  9. // Duplex.from forwards peer destruction as ABORT_ERR; channel owners observe their own failures.
  10. a.on('error', () => {})
  11. b.on('error', () => {})
  12. onTestFinished(() => { a.destroy(); b.destroy() })
  13. return { a, b }
  14. }
  15. function frame(value: unknown): Buffer {
  16. const body = Buffer.from(JSON.stringify(value))
  17. const header = Buffer.alloc(4)
  18. header.writeUInt32BE(body.length)
  19. return Buffer.concat([header, body])
  20. }
  21. describe('bounded process frames', () => {
  22. it('reassembles split binary headers and UTF-8 bodies', async () => {
  23. const { a, b } = pair()
  24. const received = Promise.withResolvers<unknown>()
  25. const channel = new JsonChannel(a, 1000, (value) => { received.resolve(value) }, (error) => { received.reject(error) })
  26. onTestFinished(() => { channel.close() })
  27. const bytes = frame({ text: '你好🙂' })
  28. for (const byte of bytes) b.write(Buffer.from([byte]))
  29. expect(await received.promise).toEqual({ text: '你好🙂' })
  30. })
  31. it('accepts consecutive frames and writes complete responses', async () => {
  32. const { a, b } = pair()
  33. const received: unknown[] = []
  34. const done = Promise.withResolvers<undefined>()
  35. const channel = new JsonChannel(a, 1000, (value) => {
  36. received.push(value)
  37. if (received.length === 2) done.resolve(undefined)
  38. }, (error) => { done.reject(error) })
  39. const peer = new JsonChannel(b, 1000, () => {}, (error) => { done.reject(error) })
  40. onTestFinished(() => { channel.close(); peer.close() })
  41. await Promise.all([peer.send({ n: 1 }), peer.send({ n: 2 })])
  42. await done.promise
  43. expect(received).toEqual([{ n: 1 }, { n: 2 }])
  44. await peer.drain()
  45. })
  46. it.each([0, 65])('rejects a declared %i-byte frame before accepting its body', async (length) => {
  47. const { a, b } = pair()
  48. const failure = Promise.withResolvers<Error>()
  49. const channel = new JsonChannel(a, 64, () => { throw new Error('must not dispatch') }, (error) => { failure.resolve(error) })
  50. onTestFinished(() => { channel.close() })
  51. const header = Buffer.alloc(4)
  52. header.writeUInt32BE(length)
  53. b.write(header)
  54. expect((await failure.promise).message).toContain('control frame')
  55. })
  56. it('contains malformed JSON, invalid UTF-8 and receiver failures', async () => {
  57. for (const payload of [Buffer.from('{'), Buffer.from([0xff]), Buffer.from('{}')]) {
  58. const { a, b } = pair()
  59. const failure = Promise.withResolvers<Error>()
  60. const channel = new JsonChannel(a, 64, () => { throw new Error('receiver failed') }, (error) => { failure.resolve(error) })
  61. const header = Buffer.alloc(4)
  62. header.writeUInt32BE(payload.length)
  63. b.write(Buffer.concat([header, payload]))
  64. expect(await failure.promise).toBeInstanceOf(Error)
  65. channel.close()
  66. }
  67. })
  68. it('rejects oversized writes and closed channels', async () => {
  69. const { a } = pair()
  70. const channel = new JsonChannel(a, 20, () => {}, () => {})
  71. await expect(channel.send({ text: 'x'.repeat(30) })).rejects.toThrow('queued bytes')
  72. channel.close()
  73. channel.close()
  74. await expect(channel.send({})).rejects.toThrow('closed')
  75. })
  76. it('reports peer EOF while a program is active', async () => {
  77. const { a, b } = pair()
  78. const failure = Promise.withResolvers<Error>()
  79. const channel = new JsonChannel(a, 64, () => {}, (error) => { failure.resolve(error) })
  80. onTestFinished(() => { channel.close() })
  81. b.end()
  82. expect((await failure.promise).message).toContain('ended')
  83. })
  84. })
  85. it('retains a partial header and partial payload across distinct stream deliveries', () => {
  86. const { a } = pair()
  87. const values: unknown[] = []
  88. const channel = new JsonChannel(a, 64, (value) => { values.push(value) }, (error) => { throw error })
  89. const bytes = frame({ value: 1 })
  90. a.emit('data', bytes.subarray(0, 2))
  91. a.emit('data', bytes.subarray(2, 6))
  92. a.emit('data', bytes.subarray(6))
  93. expect(values).toEqual([{ value: 1 }])
  94. channel.close()
  95. })
  96. it('contains a receiver throwing a non-Error value', () => {
  97. const { a } = pair()
  98. const failures: string[] = []
  99. const channel = new JsonChannel(a, 64, () => { throw 'receiver failure' }, (error) => { failures.push(error.message) })
  100. a.emit('data', frame({}))
  101. expect(failures).toEqual(['receiver failure'])
  102. channel.close()
  103. })
  104. it('ignores callbacks already captured by an emission when an earlier listener closes the channel', () => {
  105. for (const event of ['data', 'end']) {
  106. const { a } = pair()
  107. let failures = 0
  108. const channel = new JsonChannel(a, 64, () => { throw new Error('closed channel dispatched') }, () => { failures += 1 })
  109. a.prependOnceListener(event, () => { channel.close() })
  110. a.emit(event, frame({}))
  111. a.emit('error', new Error('late closed stream error'))
  112. expect(failures).toBe(0)
  113. }
  114. })
  115. it('rejects queued writes if the owner closes before they start', async () => {
  116. const { a } = pair()
  117. const channel = new JsonChannel(a, 64, () => {}, () => {})
  118. const pending = channel.send({ n: 1 })
  119. channel.close()
  120. await expect(pending).rejects.toThrow('closed')
  121. await channel.drain()
  122. })
  123. it('bounds queued frames while a receiver is not reading', async () => {
  124. const stream = new Duplex({ read() {}, write(_chunk, _encoding, _callback) {} })
  125. const channel = new JsonChannel(stream, 32, () => {}, () => {})
  126. const pending = channel.send({ value: '1234567890' })
  127. await expect(channel.send({ value: '1234567890' })).rejects.toThrow('queued bytes')
  128. channel.close()
  129. await expect(pending).rejects.toThrow('closed')
  130. })
  131. it.each(['error', 'close'])('settles a blocked write when the stream emits %s', async (event) => {
  132. const entered = Promise.withResolvers<undefined>()
  133. const stream = new Duplex({ read() {}, write(_chunk, _encoding, _callback) { entered.resolve(undefined) } })
  134. let failure: Error | undefined
  135. const channel = new JsonChannel(stream, 64, () => {}, (error) => { failure = error })
  136. const pending = channel.send({ value: 1 })
  137. await entered.promise
  138. stream.emit(event, new Error('transport failed'))
  139. await expect(pending).rejects.toThrow(event === 'error' ? 'transport failed' : 'closed')
  140. if (event === 'error') expect(failure?.message).toBe('transport failed')
  141. channel.close()
  142. })