comq 0.23.0 → 0.24.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/discussions/amqp-1.0.md +127 -0
- package/features/heartbeat.feature +16 -0
- package/features/rpc.call.feature +23 -1
- package/features/rpc.call.recovery.feature +2 -23
- package/features/rpc.parked.feature +6 -6
- package/features/rpc.shards.retransmission.feature +46 -0
- package/features/shards.feature +1 -1
- package/features/silence.feature +3 -2
- package/features/steps/.types/context.d.ts +6 -1
- package/features/steps/brokers.js +1 -1
- package/features/steps/call.js +90 -30
- package/features/steps/connection.js +23 -0
- package/features/steps/context.js +1 -2
- package/features/steps/events.js +1 -1
- package/features/steps/hooks.js +0 -1
- package/features/steps/network.js +11 -2
- package/features/steps/networks.js +31 -2
- package/features/steps/parking.js +5 -5
- package/features/steps/retransmission.js +165 -0
- package/features/steps/rpc.js +0 -6
- package/package.json +20 -4
- package/readme.md +46 -46
- package/source/channel.js +3 -1
- package/source/connection.js +10 -90
- package/source/io.js +106 -25
- package/source/shards/channel.js +73 -12
- package/test/amqplib.mock.js +1 -1
- package/test/connection.mock.js +3 -2
- package/test/connection.test.js +16 -78
- package/test/{io.request.terms.test.js → io.call.terms.test.js} +31 -30
- package/test/io.call.test.js +5 -13
- package/test/io.reply.test.js +5 -3
- package/test/io.request.test.js +277 -2
- package/test/memory.test.js +1 -3
- package/test/shards/channel.test.js +168 -2
- package/types/amqp.d.ts +1 -1
- package/types/channel.d.ts +14 -5
- package/types/io.d.ts +18 -8
- package/features/rpc.timeout.feature +0 -19
- package/source/batch.js +0 -62
- package/test/batch.test.js +0 -94
|
@@ -230,7 +230,17 @@ describe.each(['consume', 'subscribe'])('%s', (method) => {
|
|
|
230
230
|
|
|
231
231
|
await channel[method](...args)
|
|
232
232
|
|
|
233
|
-
for (const chan of channels)
|
|
233
|
+
for (const chan of channels) {
|
|
234
|
+
const call = chan[method].mock.calls[0]
|
|
235
|
+
const message = /** @type {comq.amqp.Message} */ {}
|
|
236
|
+
|
|
237
|
+
expect(call.slice(0, -1)).toStrictEqual(args.slice(0, -1))
|
|
238
|
+
|
|
239
|
+
// a consumed message may be told which shard it arrived on on its way
|
|
240
|
+
await call.at(-1)(message)
|
|
241
|
+
|
|
242
|
+
expect(processor).toHaveBeenCalledWith(message)
|
|
243
|
+
}
|
|
234
244
|
})
|
|
235
245
|
|
|
236
246
|
it(`should not resolve ${method} until pending channels are created`, async () => {
|
|
@@ -593,6 +603,23 @@ describe('diagnose', () => {
|
|
|
593
603
|
expect(listener).not.toHaveBeenCalled()
|
|
594
604
|
})
|
|
595
605
|
|
|
606
|
+
it('should tell where a returned message is published next', async () => {
|
|
607
|
+
const listener = /** @type {jest.MockedFunction} */ jest.fn()
|
|
608
|
+
const index = random(channels.length)
|
|
609
|
+
const chan = channels[index]
|
|
610
|
+
const message = returned()
|
|
611
|
+
|
|
612
|
+
message.fields.exchange = generate()
|
|
613
|
+
channel.diagnose('reroute', listener)
|
|
614
|
+
|
|
615
|
+
emitReturn(chan, message)
|
|
616
|
+
|
|
617
|
+
const next = channels.find((one) => one.route.mock.calls.length > 0)
|
|
618
|
+
|
|
619
|
+
expect(next).not.toBe(chan)
|
|
620
|
+
expect(listener).toHaveBeenCalledWith(message, next.index)
|
|
621
|
+
})
|
|
622
|
+
|
|
596
623
|
it('should report a returned retry at once', async () => {
|
|
597
624
|
const listener = /** @type {jest.MockedFunction} */ jest.fn()
|
|
598
625
|
const index = random(channels.length)
|
|
@@ -620,7 +647,15 @@ describe('diagnose', () => {
|
|
|
620
647
|
|
|
621
648
|
await channel.held(exchange, queue, key, consumer)
|
|
622
649
|
|
|
623
|
-
for (const one of channels)
|
|
650
|
+
for (const one of channels) {
|
|
651
|
+
expect(one.held).toHaveBeenCalledWith(exchange, queue, key, expect.any(Function))
|
|
652
|
+
|
|
653
|
+
const message = /** @type {comq.amqp.Message} */ {}
|
|
654
|
+
|
|
655
|
+
await one.held.mock.calls[0][3](message)
|
|
656
|
+
|
|
657
|
+
expect(consumer).toHaveBeenCalledWith(message)
|
|
658
|
+
}
|
|
624
659
|
})
|
|
625
660
|
|
|
626
661
|
it('should hold a queue once any shard holds it', async () => {
|
|
@@ -819,3 +854,134 @@ describe('routed', () => {
|
|
|
819
854
|
expect(used[0].route).toHaveBeenCalledWith(exchange, key, buffer, undefined)
|
|
820
855
|
})
|
|
821
856
|
})
|
|
857
|
+
|
|
858
|
+
describe('shards of messages', () => {
|
|
859
|
+
const queue = generate()
|
|
860
|
+
const buffer = randomBytes(8)
|
|
861
|
+
|
|
862
|
+
/** @type {jest.MockedObject<comq.Channel>[]} */
|
|
863
|
+
let channels
|
|
864
|
+
|
|
865
|
+
beforeEach(async () => {
|
|
866
|
+
channel = await create(connections, type)
|
|
867
|
+
channels = await getCreatedChannels()
|
|
868
|
+
})
|
|
869
|
+
|
|
870
|
+
it.each(['send', 'route'])('should tell `%s` which shard a message goes through', async (method) => {
|
|
871
|
+
const via = /** @type {jest.MockedFunction} */ jest.fn()
|
|
872
|
+
const args = method === 'send' ? [queue, buffer, {}] : [generate(), queue, buffer, {}]
|
|
873
|
+
|
|
874
|
+
await channel[method](...args, via)
|
|
875
|
+
|
|
876
|
+
const used = channels.find((chan) => chan[method].mock.calls.length > 0)
|
|
877
|
+
|
|
878
|
+
expect(via).toHaveBeenCalledTimes(1)
|
|
879
|
+
expect(via).toHaveBeenCalledWith(used.index)
|
|
880
|
+
})
|
|
881
|
+
|
|
882
|
+
it('should tell which shard a message goes through before it is published', async () => {
|
|
883
|
+
const via = /** @type {jest.MockedFunction} */ jest.fn()
|
|
884
|
+
const publishing = new Promex()
|
|
885
|
+
|
|
886
|
+
for (const chan of channels) chan.send.mockImplementationOnce(() => publishing)
|
|
887
|
+
|
|
888
|
+
const sending = channel.send(queue, buffer, {}, via)
|
|
889
|
+
|
|
890
|
+
await immediate()
|
|
891
|
+
|
|
892
|
+
// a shard lost from now on has it on board
|
|
893
|
+
expect(via).toHaveBeenCalledTimes(1)
|
|
894
|
+
|
|
895
|
+
publishing.resolve()
|
|
896
|
+
|
|
897
|
+
await sending
|
|
898
|
+
})
|
|
899
|
+
|
|
900
|
+
it('should tell the shard a message fails over to', async () => {
|
|
901
|
+
const via = /** @type {jest.MockedFunction} */ jest.fn()
|
|
902
|
+
|
|
903
|
+
for (const chan of channels) chan.send.mockImplementationOnce(async () => { throw new Error() })
|
|
904
|
+
|
|
905
|
+
// both shards fail once, and the one recovered first takes the message
|
|
906
|
+
setImmediate(() => emitRecover(channels[1]))
|
|
907
|
+
|
|
908
|
+
await channel.send(queue, buffer, {}, via)
|
|
909
|
+
|
|
910
|
+
const [first, second] = via.mock.calls.map(([index]) => index)
|
|
911
|
+
|
|
912
|
+
expect(via).toHaveBeenCalledTimes(3)
|
|
913
|
+
expect(second).not.toStrictEqual(first)
|
|
914
|
+
expect(via).toHaveBeenLastCalledWith(1)
|
|
915
|
+
})
|
|
916
|
+
|
|
917
|
+
it.each([0, 1])('should fire a reply through the shard its request arrived on (%i)', async (index) => {
|
|
918
|
+
const consumer = /** @type {jest.MockedFunction} */ jest.fn()
|
|
919
|
+
|
|
920
|
+
await channel.consume(queue, consumer)
|
|
921
|
+
|
|
922
|
+
const request = /** @type {comq.amqp.Message} */ {}
|
|
923
|
+
|
|
924
|
+
await channels[index].consume.mock.calls[0][1](request)
|
|
925
|
+
|
|
926
|
+
for (let i = 0; i < 20; i++) await channel.fire(queue, buffer, {}, request)
|
|
927
|
+
|
|
928
|
+
expect(channels[index].fire).toHaveBeenCalledTimes(20)
|
|
929
|
+
expect(channels[1 - index].fire).not.toHaveBeenCalled()
|
|
930
|
+
})
|
|
931
|
+
|
|
932
|
+
it('should fire a reply through the shard a held request arrived on', async () => {
|
|
933
|
+
await channel.held(generate(), generate(), generate(), jest.fn())
|
|
934
|
+
|
|
935
|
+
const request = /** @type {comq.amqp.Message} */ {}
|
|
936
|
+
|
|
937
|
+
await channels[1].held.mock.calls[0][3](request)
|
|
938
|
+
|
|
939
|
+
for (let i = 0; i < 20; i++) await channel.fire(queue, buffer, {}, request)
|
|
940
|
+
|
|
941
|
+
expect(channels[1].fire).toHaveBeenCalledTimes(20)
|
|
942
|
+
expect(channels[0].fire).not.toHaveBeenCalled()
|
|
943
|
+
})
|
|
944
|
+
|
|
945
|
+
it('should fire a reply through another shard when its own is lost', async () => {
|
|
946
|
+
await channel.consume(queue, jest.fn())
|
|
947
|
+
|
|
948
|
+
const request = /** @type {comq.amqp.Message} */ {}
|
|
949
|
+
|
|
950
|
+
await channels[1].consume.mock.calls[0][1](request)
|
|
951
|
+
|
|
952
|
+
lose(connections[1])
|
|
953
|
+
|
|
954
|
+
await channel.fire(queue, buffer, {}, request)
|
|
955
|
+
|
|
956
|
+
expect(channels[0].fire).toHaveBeenCalledTimes(1)
|
|
957
|
+
expect(channels[1].fire).not.toHaveBeenCalled()
|
|
958
|
+
|
|
959
|
+
restore(connections[1])
|
|
960
|
+
})
|
|
961
|
+
|
|
962
|
+
it('should fire a reply through another shard when its own fails', async () => {
|
|
963
|
+
await channel.consume(queue, jest.fn())
|
|
964
|
+
|
|
965
|
+
const request = /** @type {comq.amqp.Message} */ {}
|
|
966
|
+
|
|
967
|
+
await channels[1].consume.mock.calls[0][1](request)
|
|
968
|
+
|
|
969
|
+
channels[1].fire.mockImplementationOnce(async () => { throw new Error() })
|
|
970
|
+
|
|
971
|
+
await channel.fire(queue, buffer, {}, request)
|
|
972
|
+
|
|
973
|
+
expect(channels[1].fire).toHaveBeenCalledTimes(1)
|
|
974
|
+
expect(channels[0].fire).toHaveBeenCalledTimes(1)
|
|
975
|
+
|
|
976
|
+
emitRecover(channels[1])
|
|
977
|
+
})
|
|
978
|
+
})
|
|
979
|
+
|
|
980
|
+
/**
|
|
981
|
+
* @param {jest.MockedObject<comq.Channel>} chan
|
|
982
|
+
*/
|
|
983
|
+
function emitRecover (chan) {
|
|
984
|
+
const calls = chan.diagnose.mock.calls.filter((call) => call[0] === 'recover')
|
|
985
|
+
|
|
986
|
+
for (const call of calls) call[1]()
|
|
987
|
+
}
|
package/types/amqp.d.ts
CHANGED
package/types/channel.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Message, Options, Connection } from 'amqplib'
|
|
1
|
+
import { Message, Options, Connection } from '@toa.io/amqplib'
|
|
2
2
|
import * as _diagnostics from './diagnostic'
|
|
3
3
|
|
|
4
4
|
declare namespace comq {
|
|
@@ -25,14 +25,22 @@ declare namespace comq {
|
|
|
25
25
|
/** consumes `queue`, exclusive to this connection and bound to a routed `exchange` under `key` */
|
|
26
26
|
held (exchange: string, queue: string, key: string, consumer: channels.Consumer): Promise<void>
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Over a sharded connection, `via` is told the index of the shard the message is published
|
|
30
|
+
* through, as soon as it is chosen and again whenever the publish fails over to another.
|
|
31
|
+
*/
|
|
32
|
+
send (queue: string, buffer: Buffer, options?: Options.Publish, via?: (index: number) => void): Promise<void>
|
|
29
33
|
|
|
30
34
|
publish (exchange: string, buffer: Buffer, options?: Options.Publish): Promise<void>
|
|
31
35
|
|
|
32
36
|
/** publishes to a routed exchange under `key`, where `publish` fans out */
|
|
33
|
-
route (exchange: string, key: string, buffer: Buffer, options?: Options.Publish): Promise<void>
|
|
37
|
+
route (exchange: string, key: string, buffer: Buffer, options?: Options.Publish, via?: (index: number) => void): Promise<void>
|
|
34
38
|
|
|
35
|
-
|
|
39
|
+
/**
|
|
40
|
+
* Over a sharded connection, the message goes back through the shard `origin`, the message it
|
|
41
|
+
* answers, arrived on, while that shard is reachable.
|
|
42
|
+
*/
|
|
43
|
+
fire (queue: string, buffer: Buffer, options?: Options.Publish, origin?: Message): Promise<boolean>
|
|
36
44
|
|
|
37
45
|
seal (): Promise<void>
|
|
38
46
|
|
|
@@ -40,7 +48,8 @@ declare namespace comq {
|
|
|
40
48
|
|
|
41
49
|
readonly closed: boolean
|
|
42
50
|
|
|
43
|
-
|
|
51
|
+
/** `reroute` is a sharded channel's own: a returned message is published on another shard */
|
|
52
|
+
diagnose (event: _diagnostics.Event | 'reroute', listener: Function): void
|
|
44
53
|
|
|
45
54
|
forget (event: _diagnostics.Event, listener: Function): void
|
|
46
55
|
|
package/types/io.d.ts
CHANGED
|
@@ -35,21 +35,30 @@ declare namespace comq {
|
|
|
35
35
|
emitter: ReplyEmitter
|
|
36
36
|
properties: _amqp.Properties
|
|
37
37
|
signal?: AbortSignal
|
|
38
|
+
|
|
39
|
+
/** The shard the Request is published through, over a sharded connection. */
|
|
40
|
+
shard?: number
|
|
41
|
+
|
|
42
|
+
/** Whether the Request is being published. */
|
|
43
|
+
publishing?: boolean
|
|
44
|
+
|
|
45
|
+
/** Whether its shard was lost while it was being published, which re-sends it once it is. */
|
|
46
|
+
lost?: boolean
|
|
38
47
|
}
|
|
39
48
|
|
|
40
|
-
/** How long a caller waits for a
|
|
41
|
-
interface
|
|
49
|
+
/** How long a caller waits for the Reply to a call. */
|
|
50
|
+
interface CallOptions {
|
|
42
51
|
encoding?: _encoding.Encoding
|
|
43
52
|
|
|
44
53
|
/**
|
|
45
|
-
* Milliseconds to wait for the Reply. A
|
|
54
|
+
* Milliseconds to wait for the Reply. A call nobody has taken by then is dropped by the
|
|
46
55
|
* broker; one already taken may still be processed, and its Reply is discarded.
|
|
47
56
|
*/
|
|
48
57
|
timeout?: number
|
|
49
58
|
|
|
50
59
|
/**
|
|
51
|
-
* Ends the wait when aborted, within the timeout. The
|
|
52
|
-
* timeout passes, or, without one, until
|
|
60
|
+
* Ends the wait when aborted, within the timeout. The call stays in its queue until the
|
|
61
|
+
* timeout passes, or, without one, until its holder takes it.
|
|
53
62
|
*/
|
|
54
63
|
signal?: AbortSignal
|
|
55
64
|
}
|
|
@@ -64,15 +73,16 @@ declare namespace comq {
|
|
|
64
73
|
interface IO extends _diagnostics.Diagnosable {
|
|
65
74
|
reply(queue: string, produce: Producer): Promise<void>
|
|
66
75
|
|
|
67
|
-
|
|
76
|
+
/** Waits for the Reply for as long as it takes. */
|
|
77
|
+
request<Reply = any, Request = any>(queue: string, payload: Request, encoding?: _encoding.Encoding): Promise<Reply> | Promise<Readable>
|
|
68
78
|
|
|
69
|
-
request(queue: string, stream: Readable,
|
|
79
|
+
request(queue: string, stream: Readable, encoding?: _encoding.Encoding): Promise<Readable>
|
|
70
80
|
|
|
71
81
|
/**
|
|
72
82
|
* Sends a Request to whoever holds `key` on the routed `exchange`, through `back`. One that
|
|
73
83
|
* nobody holds is refused with `Unroutable`.
|
|
74
84
|
*/
|
|
75
|
-
call<Reply = any, Request = any>(exchange: string, key: string, payload: Request, options?: _encoding.Encoding |
|
|
85
|
+
call<Reply = any, Request = any>(exchange: string, key: string, payload: Request, options?: _encoding.Encoding | CallOptions): Promise<Reply> | Promise<Readable>
|
|
76
86
|
|
|
77
87
|
/**
|
|
78
88
|
* Answers the Requests `call` sends under `key`, from a queue this connection alone holds.
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
Feature: Request timeout
|
|
2
|
-
|
|
3
|
-
A caller may say how long it waits for a reply. A request nobody took by then is dropped by the
|
|
4
|
-
broker; one already taken is still processed, and its reply is discarded.
|
|
5
|
-
|
|
6
|
-
Background:
|
|
7
|
-
Given an active connection to the broker
|
|
8
|
-
|
|
9
|
-
Scenario: A caller stops waiting at its timeout
|
|
10
|
-
Given a producer replying `slow` queue in 1000ms
|
|
11
|
-
When the consumer sends a request to the `slow` queue with a 200ms timeout
|
|
12
|
-
Then the consumer stops waiting
|
|
13
|
-
|
|
14
|
-
Scenario: A request nobody took in time is never processed
|
|
15
|
-
When the consumer sends a request to the `later` queue with a 200ms timeout
|
|
16
|
-
Then the consumer stops waiting
|
|
17
|
-
When after 300ms
|
|
18
|
-
And a producer counting requests to the `later` queue
|
|
19
|
-
Then the producer receives nothing
|
package/source/batch.js
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Writes as one what a connection sends in one turn of the event loop.
|
|
5
|
-
*
|
|
6
|
-
* amqplib hands its frames to the socket one write at a time, and a request served is two of
|
|
7
|
-
* them: the reply, and the acknowledgement of the request it answers. With Nagle's algorithm off,
|
|
8
|
-
* which is what a socket sending without delay runs under, each is a syscall and a packet of its
|
|
9
|
-
* own. Corked, they are held in the socket's buffer and leave together, in one `writev`, at the
|
|
10
|
-
* end of the same turn: nothing waits for a timer, and nothing waits for a write that may never
|
|
11
|
-
* come.
|
|
12
|
-
*
|
|
13
|
-
* This belongs in amqplib, whose `Mux` writes the frames one at a time, and where corking that
|
|
14
|
-
* loop is six lines and needs no wrapping of a socket the library does not own. It is done here
|
|
15
|
-
* until amqplib carries it, and goes when it does.
|
|
16
|
-
*
|
|
17
|
-
* @param {import('node:net').Socket} socket
|
|
18
|
-
*/
|
|
19
|
-
function batch (socket) {
|
|
20
|
-
// whatever a connection is carried over is left as it is unless it is a stream that corks
|
|
21
|
-
if (!corkable(socket) || socket[BATCHED] === true) return
|
|
22
|
-
|
|
23
|
-
socket[BATCHED] = true
|
|
24
|
-
|
|
25
|
-
const write = socket.write.bind(socket)
|
|
26
|
-
|
|
27
|
-
const flush = () => {
|
|
28
|
-
socket[CORKED] = false
|
|
29
|
-
socket.uncork()
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const cork = () => {
|
|
33
|
-
if (socket[CORKED] === true) return
|
|
34
|
-
|
|
35
|
-
socket[CORKED] = true
|
|
36
|
-
socket.cork()
|
|
37
|
-
|
|
38
|
-
process.nextTick(flush)
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
socket.write = (...args) => {
|
|
42
|
-
cork()
|
|
43
|
-
|
|
44
|
-
return write(...args)
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* @param {any} socket
|
|
50
|
-
* @returns {boolean}
|
|
51
|
-
*/
|
|
52
|
-
function corkable (socket) {
|
|
53
|
-
return socket !== null && socket !== undefined &&
|
|
54
|
-
typeof socket.write === 'function' &&
|
|
55
|
-
typeof socket.cork === 'function' &&
|
|
56
|
-
typeof socket.uncork === 'function'
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const BATCHED = Symbol('comq.batched')
|
|
60
|
-
const CORKED = Symbol('comq.corked')
|
|
61
|
-
|
|
62
|
-
exports.batch = batch
|
package/test/batch.test.js
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
const { Writable } = require('node:stream')
|
|
4
|
-
|
|
5
|
-
const { batch } = require('../source/batch')
|
|
6
|
-
|
|
7
|
-
/** A socket, as far as writing to it goes: what it was given, and how many times. */
|
|
8
|
-
function socket () {
|
|
9
|
-
const writes = []
|
|
10
|
-
|
|
11
|
-
return Object.assign(new Writable({
|
|
12
|
-
write (chunk, _encoding, callback) {
|
|
13
|
-
writes.push([chunk.toString()])
|
|
14
|
-
callback()
|
|
15
|
-
},
|
|
16
|
-
writev (chunks, callback) {
|
|
17
|
-
writes.push(chunks.map(({ chunk }) => chunk.toString()))
|
|
18
|
-
callback()
|
|
19
|
-
}
|
|
20
|
-
}), { writes })
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const tick = () => new Promise((resolve) => process.nextTick(resolve))
|
|
24
|
-
|
|
25
|
-
it('should be', async () => {
|
|
26
|
-
expect(batch).toBeDefined()
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
it('should write what a turn wrote as one', async () => {
|
|
30
|
-
const stream = socket()
|
|
31
|
-
|
|
32
|
-
batch(stream)
|
|
33
|
-
|
|
34
|
-
stream.write('a')
|
|
35
|
-
stream.write('b')
|
|
36
|
-
|
|
37
|
-
await tick()
|
|
38
|
-
|
|
39
|
-
expect(stream.writes).toStrictEqual([['a', 'b']])
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
it('should write what a later turn wrote on its own', async () => {
|
|
43
|
-
const stream = socket()
|
|
44
|
-
|
|
45
|
-
batch(stream)
|
|
46
|
-
|
|
47
|
-
stream.write('a')
|
|
48
|
-
|
|
49
|
-
await tick()
|
|
50
|
-
|
|
51
|
-
stream.write('b')
|
|
52
|
-
|
|
53
|
-
await tick()
|
|
54
|
-
|
|
55
|
-
expect(stream.writes).toStrictEqual([['a'], ['b']])
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
it('should not hold a write past its turn', async () => {
|
|
59
|
-
const stream = socket()
|
|
60
|
-
|
|
61
|
-
batch(stream)
|
|
62
|
-
|
|
63
|
-
stream.write('a')
|
|
64
|
-
|
|
65
|
-
await tick()
|
|
66
|
-
|
|
67
|
-
expect(stream.writes).toStrictEqual([['a']])
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
it('should take a socket once', async () => {
|
|
71
|
-
const stream = socket()
|
|
72
|
-
|
|
73
|
-
batch(stream)
|
|
74
|
-
|
|
75
|
-
const wrapped = stream.write
|
|
76
|
-
|
|
77
|
-
batch(stream)
|
|
78
|
-
|
|
79
|
-
expect(stream.write).toStrictEqual(wrapped)
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
it('should leave alone what does not cork', async () => {
|
|
83
|
-
for (const value of [undefined, null, {}, { write: () => undefined }]) {
|
|
84
|
-
expect(() => batch(value)).not.toThrow()
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const written = []
|
|
88
|
-
const plain = { write: (chunk) => written.push(chunk) }
|
|
89
|
-
|
|
90
|
-
batch(plain)
|
|
91
|
-
plain.write('a')
|
|
92
|
-
|
|
93
|
-
expect(written).toStrictEqual(['a'])
|
|
94
|
-
})
|