wenay-common2 1.0.68 → 1.0.70

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.
Files changed (82) hide show
  1. package/README.md +12 -1
  2. package/demo/client.ts +100 -0
  3. package/demo/index.html +28 -0
  4. package/demo/server.ts +43 -0
  5. package/doc/ROADMAP.md +9 -3
  6. package/doc/changes/1.0.69.md +11 -0
  7. package/doc/changes/1.0.70.md +4 -0
  8. package/doc/wenay-common2-rare.md +8 -0
  9. package/doc/wenay-common2.md +42 -6
  10. package/lib/Common/events/route-signal-webrtc.js +4 -2
  11. package/lib/Common/peer/peer-client.d.ts +46 -0
  12. package/lib/Common/peer/peer-client.js +108 -0
  13. package/lib/Common/peer/peer-host.d.ts +32 -0
  14. package/lib/Common/peer/peer-host.js +45 -0
  15. package/lib/Common/peer/peer-index.d.ts +3 -0
  16. package/lib/Common/peer/peer-index.js +19 -0
  17. package/lib/Common/peer/peer-relay.d.ts +14 -0
  18. package/lib/Common/peer/peer-relay.js +77 -0
  19. package/lib/index.d.ts +1 -0
  20. package/lib/index.js +2 -1
  21. package/observe/PLAN.md +131 -0
  22. package/observe/README.md +226 -0
  23. package/observe/hot-write.test.ts +95 -0
  24. package/observe/listen-core.test.ts +66 -0
  25. package/observe/listen-store.test.ts +56 -0
  26. package/observe/listen.test.ts +92 -0
  27. package/observe/reactive.test.ts +270 -0
  28. package/observe/reactive.ts +1 -0
  29. package/observe/store-manager.test.ts +118 -0
  30. package/observe/store-mirror.example.ts +74 -0
  31. package/observe/store.test.ts +235 -0
  32. package/observe/store.ts +1 -0
  33. package/observe/usage-real-socket.ts +174 -0
  34. package/observe/usage.ts +200 -0
  35. package/oracle/PASSED.md +27 -0
  36. package/oracle/README.md +12 -0
  37. package/oracle/fixes-primitives.spec.ts +90 -0
  38. package/oracle/realsocket/_rs.ts +106 -0
  39. package/oracle/realsocket/auth.spec.ts +91 -0
  40. package/oracle/realsocket/callbacks.spec.ts +122 -0
  41. package/oracle/realsocket/caps.spec.ts +124 -0
  42. package/oracle/realsocket/core.spec.ts +49 -0
  43. package/oracle/realsocket/dedupe.spec.ts +142 -0
  44. package/oracle/realsocket/errors.spec.ts +121 -0
  45. package/oracle/realsocket/lifecycle.spec.ts +128 -0
  46. package/oracle/realsocket/limits.spec.ts +98 -0
  47. package/oracle/realsocket/pipe.spec.ts +101 -0
  48. package/oracle/realsocket/shape.spec.ts +124 -0
  49. package/oracle/realsocket/slimv2.spec.ts +116 -0
  50. package/oracle/realsocket/stress.spec.ts +132 -0
  51. package/oracle/regression/_clientapiall-replay.fixture.ts +57 -0
  52. package/oracle/regression/async-queues.spec.ts +254 -0
  53. package/oracle/regression/bytestream.spec.ts +152 -0
  54. package/oracle/regression/clientapiall-replay-types.spec.ts +47 -0
  55. package/oracle/regression/core-clone-equal.spec.ts +124 -0
  56. package/oracle/regression/data-structures.spec.ts +135 -0
  57. package/oracle/regression/listen-events.spec.ts +206 -0
  58. package/oracle/regression/observe-core.spec.ts +278 -0
  59. package/oracle/regression/package-export.spec.ts +120 -0
  60. package/oracle/regression/rpc-dedupe-callbacks.spec.ts +195 -0
  61. package/oracle/regression/rpc-lifecycle.spec.ts +150 -0
  62. package/oracle/regression/store-each.spec.ts +209 -0
  63. package/package.json +8 -2
  64. package/replay/PLAN.md +171 -0
  65. package/replay/canvas-socket.test.ts +187 -0
  66. package/replay/coalesce.test.ts +260 -0
  67. package/replay/conflate-socket.test.ts +288 -0
  68. package/replay/conflate.test.ts +225 -0
  69. package/replay/history.test.ts +222 -0
  70. package/replay/media-socket.test.ts +157 -0
  71. package/replay/offline-store-socket.test.ts +290 -0
  72. package/replay/offline-store.test.ts +156 -0
  73. package/replay/peer-sdk.test.ts +228 -0
  74. package/replay/replay-listen.test.ts +156 -0
  75. package/replay/route-coordinator.test.ts +314 -0
  76. package/replay/route-handoff.test.ts +150 -0
  77. package/replay/route-webrtc.test.ts +321 -0
  78. package/replay/rpc-auto.test.ts +256 -0
  79. package/replay/socket-replay.test.ts +199 -0
  80. package/replay/staleness.test.ts +176 -0
  81. package/replay/store-replay.test.ts +151 -0
  82. package/replay/video-socket.demo.ts +200 -0
@@ -0,0 +1,124 @@
1
+ // ============================================================
2
+ // oracle/realsocket/shape.spec.ts — DISPOSABLE real-socket oracle
3
+ // CATEGORY "shape": adaptive shape compaction (Pkt.SHAPE/CBV) over a REAL WebSocket.
4
+ //
5
+ // Two server streams:
6
+ // monoStream — monomorphic high-freq object stream. After THRESHOLD(5) ticks of the
7
+ // same shape the server standardizes it and switches to compact Pkt.CBV.
8
+ // We assert ALL ticks (>=8) arrive intact incl. Date values, AND that
9
+ // compaction actually engaged (CBV>0) by wrapping socket.emit in onServer.
10
+ // polyStream — polymorphic stream (changing shapes). Adaptive fallback: no shape ever
11
+ // reaches threshold → no data loss; we also assert CBV stayed 0.
12
+ // ============================================================
13
+ import {startRealServer, startRealClient, makeChecker, delay} from './_rs'
14
+ import {listen as createListenPair} from '../../src/Common/events/Listen'
15
+
16
+ const PORT = 4107
17
+
18
+ // captured server-side emit handles (filled when makeObject runs on the connection)
19
+ let emitMono: ((v: any) => void) | null = null
20
+ let emitPoly: ((v: any) => void) | null = null
21
+
22
+ function makeObject() {
23
+ const [mono, monoListen] = createListenPair<any>()
24
+ const [poly, polyListen] = createListenPair<any>()
25
+ emitMono = mono
26
+ emitPoly = poly
27
+ return {monoStream: monoListen, polyStream: polyListen}
28
+ }
29
+
30
+ async function main() {
31
+ const {check, done} = makeChecker('shape')
32
+ const watchdog = setTimeout(() => { console.error('WATCHDOG timeout'); process.exit(3) }, 40000)
33
+
34
+ // counters of raw packet types seen on the REAL server socket
35
+ let cbvCount = 0 // Pkt.CBV = 9 (compact values tick)
36
+ let shapeCount = 0 // Pkt.SHAPE = 8 (shape declaration)
37
+ let cbCount = 0 // Pkt.CB = 2 (full callback tick)
38
+
39
+ const srv = await startRealServer({
40
+ port: PORT,
41
+ makeObject,
42
+ onServer: (_api, socket) => {
43
+ // wrap the genuine socket.emit BEFORE any tick is sent — the rpc-server
44
+ // adapter calls socket.emit(key, packetArray) at send time, so this sees
45
+ // every outgoing packet and lets us observe compaction on the real wire.
46
+ const orig = socket.emit.bind(socket)
47
+ socket.emit = (key: string, d: any) => {
48
+ if (Array.isArray(d)) {
49
+ if (d[0] === 9) cbvCount++
50
+ else if (d[0] === 8) shapeCount++
51
+ else if (d[0] === 2) cbCount++
52
+ }
53
+ return orig(key, d)
54
+ }
55
+ },
56
+ })
57
+
58
+ const cli = await startRealClient({port: PORT})
59
+ const api = cli.api
60
+
61
+ // give the connection a moment so makeObject ran and emit handles are captured
62
+ await delay(50)
63
+ await check('emit handles captured', () => emitMono != null && emitPoly != null, true)
64
+
65
+ // ---------- monomorphic stream: subscribe, then fire many same-shape ticks ----------
66
+ const gotMono: any[] = []
67
+ const offMono = api.monoStream.callback((v: any) => gotMono.push(v))
68
+ await delay(60) // subscription round-trip + CAPS handshake over real socket
69
+
70
+ const N = 12
71
+ for (let i = 0; i < N; i++) {
72
+ emitMono!({a: i, when: new Date(i * 1000), tag: 'x'})
73
+ await delay(8)
74
+ }
75
+ await delay(120) // let all compact ticks land
76
+
77
+ await check('mono: all ticks arrived', () => gotMono.length, N)
78
+ await check('mono: first tick values intact', () => [gotMono[0].a, gotMono[0].tag, gotMono[0].when], [0, 'x', new Date(0)])
79
+ await check('mono: last tick values intact (Date)', () => [gotMono[N - 1].a, gotMono[N - 1].tag, gotMono[N - 1].when], [N - 1, 'x', new Date((N - 1) * 1000)])
80
+ // verify EVERY tick is correct and in order (no drop / corruption / reordering)
81
+ await check('mono: every tick exact + ordered', () => gotMono.map(v => [v.a, v.tag, v.when]),
82
+ Array.from({length: N}, (_v, i) => [i, 'x', new Date(i * 1000)]))
83
+ // compaction engaged: after THRESHOLD(5) of the same shape, server declared SHAPE once and sent CBV
84
+ await check('mono: SHAPE declared exactly once', () => shapeCount, 1)
85
+ await check('mono: compaction engaged (CBV>0)', () => cbvCount > 0, true)
86
+ // with threshold 5: ticks 1..5 full (the 5th triggers register→CBV), 6..12 compact.
87
+ // register sends SHAPE+CBV, then each later same-shape tick a CBV → CBV count = N - 4.
88
+ await check('mono: CBV count == N-4 (ticks after threshold)', () => cbvCount, N - 4)
89
+
90
+ offMono()
91
+ await delay(30)
92
+
93
+ // ---------- polymorphic stream: each tick a different shape → adaptive fallback ----------
94
+ const cbvBefore = cbvCount
95
+ const gotPoly: any[] = []
96
+ const offPoly = api.polyStream.callback((v: any) => gotPoly.push(v))
97
+ await delay(60)
98
+
99
+ // 8 ticks, each a distinct shape — no single shape ever reaches THRESHOLD(5)
100
+ emitPoly!({a: 1})
101
+ await delay(8); emitPoly!({b: 2})
102
+ await delay(8); emitPoly!({c: 3, d: 4})
103
+ await delay(8); emitPoly!({e: 5})
104
+ await delay(8); emitPoly!({f: 6, g: 7})
105
+ await delay(8); emitPoly!({h: 8})
106
+ await delay(8); emitPoly!({i: 9, j: 10, k: 11})
107
+ await delay(8); emitPoly!({l: 12})
108
+ await delay(120)
109
+
110
+ await check('poly: all 8 ticks arrived (no loss)', () => gotPoly.length, 8)
111
+ await check('poly: values intact across shapes', () => [gotPoly[0].a, gotPoly[1].b, gotPoly[2].c, gotPoly[2].d, gotPoly[6].i, gotPoly[6].k, gotPoly[7].l],
112
+ [1, 2, 3, 4, 9, 11, 12])
113
+ await check('poly: no compaction (no new CBV)', () => cbvCount - cbvBefore, 0)
114
+
115
+ offPoly()
116
+ await delay(30)
117
+
118
+ clearTimeout(watchdog)
119
+ cli.close()
120
+ await srv.close()
121
+ process.exit(done() === 0 ? 0 : 1)
122
+ }
123
+
124
+ main().catch(e => { console.error(e); process.exit(2) })
@@ -0,0 +1,116 @@
1
+ // REAL-SOCKET slimv2: NEW slim Listen v2 (slimListen) — unit-surface + end-to-end wire. Port 4110.
2
+ //
3
+ // FINDING (verified against source, see notes in ##RESULT##):
4
+ // isListenCallback() in src/Common/events/Listen.ts identifies a "listen" by
5
+ // STRUCTURAL key-set equality against createListen's full api
6
+ // (func/isRun/run/close/onClose/on/off/count/keys()).
7
+ // A slim SlimListen exposes only {on, close, count}, so isListenCallback(slimListen) === false
8
+ // and rpc-server-auto's resolveTransform will NOT treat a bare SlimListen as a stream node.
9
+ // Therefore the WIRE part uses the FULL listen handle (the one slimListen wraps), while the
10
+ // SLIM surface (on/off/count/close) is unit-asserted directly. We also prove the slim view
11
+ // and the full handle share ONE underlying impl (count() agrees across both faces), so a
12
+ // slimListen stream interoperates with the RPC listen-socket layer exactly like listen —
13
+ // via its full handle.
14
+ import {startRealServer, startRealClient, makeChecker, delay} from './_rs'
15
+ import {slimListen, toSlimListen, createListen, isListenCallback, listen as createListenPair} from '../../src/Common/events/Listen'
16
+
17
+ const PORT = 4110
18
+
19
+ // emit fns + full handles created at module scope so makeObject (per-connection) can place
20
+ // the FULL listen as the wire stream node while the spec drives emit / inspects slim surface.
21
+ const [emitWire, fullWire] = createListenPair<number>() // full handle → goes on the wire
22
+ const slimWire = toSlimListen<number>(fullWire) // slim VIEW over the SAME impl as fullWire
23
+
24
+ function makeObject() {
25
+ // Wire a real stream node. isListenCallback only accepts the FULL listen, so place that.
26
+ // The slim SlimListen (slimWire) is a view over this very handle — same subscriber set.
27
+ return {
28
+ ticks: fullWire,
29
+ }
30
+ }
31
+
32
+ async function main() {
33
+ const {check, done} = makeChecker('slimv2')
34
+ const watchdog = setTimeout(() => { console.error('WATCHDOG timeout'); process.exit(3) }, 40000)
35
+
36
+ // ===== 0. Static finding: SlimListen is NOT detected as a listen by isListenCallback =====
37
+ const [, slimStandalone] = slimListen<number>()
38
+ await check('isListenCallback(full listen) === true', () => isListenCallback(fullWire), true)
39
+ await check('isListenCallback(slim SlimListen) === false', () => isListenCallback(slimStandalone as any), false)
40
+
41
+ // ===== 1. Slim surface unit tests (on/off/count/close) on a standalone slimListen =====
42
+ {
43
+ const [emit, listen] = slimListen<number>()
44
+ const got: number[] = []
45
+ await check('count() starts at 0', () => listen.count(), 0)
46
+
47
+ const off = listen.on(v => got.push(v))
48
+ await check('count() === 1 after on()', () => listen.count(), 1)
49
+ await check('on() returns a function (off)', () => typeof off, 'function')
50
+
51
+ emit(10); emit(20)
52
+ await check('on(cb) receives ticks', () => got.slice(), [10, 20])
53
+
54
+ off()
55
+ await check('count() === 0 after off()', () => listen.count(), 0)
56
+ emit(30)
57
+ await check('off() stops delivery', () => got.slice(), [10, 20])
58
+
59
+ // re-subscribe two, then close() tears everything down
60
+ const g2: number[] = []
61
+ listen.on(v => g2.push(v))
62
+ listen.on(v => g2.push(v))
63
+ await check('count() === 2 after two on()', () => listen.count(), 2)
64
+ emit(7)
65
+ await check('two subscribers each get tick', () => g2.slice(), [7, 7])
66
+ listen.close()
67
+ await check('count() === 0 after close()', () => listen.count(), 0)
68
+ emit(99)
69
+ await check('close() stops delivery', () => g2.slice(), [7, 7])
70
+ }
71
+
72
+ // ===== 2. Slim view interoperates with full handle (shared impl) =====
73
+ // slimWire is toSlimListen(fullWire); a subscription via the full handle must be visible
74
+ // through slim.count(), proving they are ONE Listen (the same impl slimListen wraps).
75
+ {
76
+ const offFull = fullWire.on(() => {})
77
+ await check('slim.count() reflects full.on', () => slimWire.count(), 1)
78
+ offFull()
79
+ await check('slim.count() reflects full off()', () => slimWire.count(), 0)
80
+ }
81
+
82
+ // ===== 3. END-TO-END wire: full handle (the one slimListen wraps) over a REAL socket =====
83
+ const srv = await startRealServer({port: PORT, makeObject})
84
+ const cli = await startRealClient({port: PORT})
85
+ const api = cli.api
86
+
87
+ const received: number[] = []
88
+ const sub: any = api.ticks.callback((v: number) => received.push(v))
89
+ await delay(150) // let the subscription round-trip over the real WebSocket
90
+
91
+ // server-side subscriber present (the wire created a listen-socket subscriber on fullWire)
92
+ await check('wire: server has >=1 subscriber on the listen', () => fullWire.count() >= 1, true)
93
+
94
+ emitWire(101)
95
+ emitWire(202)
96
+ emitWire(303)
97
+ await delay(200) // real WS latency for emitted ticks to arrive
98
+
99
+ await check('wire: ticks arrive end-to-end', () => received.slice(), [101, 202, 303])
100
+ // slim view sees the SAME live subscriber count as the full wire handle
101
+ await check('wire: slim view agrees with full handle count', () => slimWire.count(), fullWire.count())
102
+
103
+ // unsubscribe over the wire and confirm no further ticks
104
+ sub()
105
+ await delay(150)
106
+ emitWire(404)
107
+ await delay(150)
108
+ await check('wire: off() stops end-to-end delivery', () => received.slice(), [101, 202, 303])
109
+
110
+ clearTimeout(watchdog)
111
+ cli.close()
112
+ await srv.close()
113
+ process.exit(done() === 0 ? 0 : 1)
114
+ }
115
+
116
+ main().catch(e => { console.error(e); process.exit(2) })
@@ -0,0 +1,132 @@
1
+ // REAL-SOCKET stress: MASS / volume over a genuine WebSocket. Port 4111.
2
+ // (1) 500+ concurrent CALLs — id-pool integrity, no dropped/mismatched RESP.
3
+ // (2) 50+ concurrent subscribers + 100+ high-freq ticks — no loss, clean teardown to 0.
4
+ // (3) mixed concurrent calls + streams.
5
+ import {startRealServer, startRealClient, makeChecker, delay} from './_rs'
6
+ import {listen as createListenPair} from '../../src/Common/events/Listen'
7
+
8
+ const PORT = 4111
9
+
10
+ // ===================================================================
11
+ // facade — captured emit handles (single client connection ⇒ makeObject
12
+ // runs once; we stash the per-connection emitters at module scope).
13
+ // ===================================================================
14
+ let emitTick: ((n: number) => void) | null = null // stream A: high-frequency ticks
15
+ let emitMix: ((n: number) => void) | null = null // stream B: used in the mixed test
16
+
17
+ function makeObject() {
18
+ const [tick, tickListen] = createListenPair<number>()
19
+ const [mix, mixListen] = createListenPair<number>()
20
+ emitTick = tick
21
+ emitMix = mix
22
+ return {
23
+ // identity call — server echoes the request index back; lets us assert
24
+ // every one of N concurrent responses landed on the right promise.
25
+ idn: async (i: number) => i,
26
+ // arithmetic so a mismatched correlation would produce a wrong value, not just a wrong tag.
27
+ sq: async (i: number) => i * i,
28
+ ticks: tickListen,
29
+ mixStream: mixListen,
30
+ }
31
+ }
32
+
33
+ async function main() {
34
+ const {check, done} = makeChecker('stress')
35
+ const watchdog = setTimeout(() => { console.error('WATCHDOG timeout'); process.exit(3) }, 40000)
36
+
37
+ // capture the per-connection server api (for subscriptions() leak checks)
38
+ let srvApi: any = null
39
+ const srv = await startRealServer({port: PORT, makeObject, onServer: (api) => { srvApi = api }})
40
+ const cli = await startRealClient<ReturnType<typeof makeObject>>({port: PORT})
41
+ const api = cli.api
42
+
43
+ // =============================================================
44
+ // (1) MASS CALLs — 600 concurrent, assert every result correct
45
+ // =============================================================
46
+ const N = 600
47
+ const idxs = Array.from({length: N}, (_, i) => i)
48
+ const results = await Promise.all(idxs.map(i => api.idn(i)))
49
+ // every response matches its request index (no mismatched correlation)
50
+ await check('mass: 600 idn results in order', () => results, idxs)
51
+ await check('mass: zero mismatches', () => results.filter((v, i) => v !== i).length, 0)
52
+
53
+ // a second wave through the SAME id-pool — proves released ids are reusable & clean
54
+ const sqResults = await Promise.all(idxs.map(i => api.sq(i)))
55
+ await check('mass: 600 sq second wave', () => sqResults.filter((v, i) => v !== i * i).length, 0)
56
+ // id-pool fully drained back: no pending requests left hanging
57
+ await delay(50)
58
+ await check('mass: pending drained to 0', () => (cli.client.api as any).pending(), 0)
59
+
60
+ // =============================================================
61
+ // (2) MASS SUBSCRIBERS — 60 subscribers, 120 high-freq ticks
62
+ // =============================================================
63
+ const SUBS = 60
64
+ const TICKS = 120
65
+ const buckets: number[][] = Array.from({length: SUBS}, () => [])
66
+ // NB: callback() returns a callable off-handle (a thenable that resolves only
67
+ // when the STREAM ENDS) — do NOT await it, or Promise.all would block forever.
68
+ const offs = buckets.map((b) => (api.ticks as any).callback((v: number) => b.push(v)))
69
+ // give every subscription a round-trip to register on the server
70
+ await delay(120)
71
+
72
+ // Client-side dedup (on by default): 60 identical `ticks.callback(fn)` subscribers
73
+ // collapse to ONE wire subscription; the 60-way fan-out happens locally. So:
74
+ // • client subscriptions(): one entry, consumers == 60 (all local consumers)
75
+ // • server subscriptions(): one node, consumers == 1 (the single wire callback)
76
+ await check('subs: client one wire sub, 60 local consumers', () => {
77
+ const list = (cli.client.api as any).subscriptions() as {consumers: number}[]
78
+ return [list.length, list[0]?.consumers]
79
+ }, [1, SUBS])
80
+ await check('subs: server sees one wire consumer (dedup)', () => {
81
+ const subsList = srvApi.subscriptions() as {consumers: number}[]
82
+ return [subsList.length, subsList.reduce((a, e) => a + e.consumers, 0)]
83
+ }, [1, 1])
84
+
85
+ // high-frequency burst: no awaits between emits
86
+ for (let i = 0; i < TICKS; i++) emitTick!(i)
87
+ // generous drain for the real socket to deliver every CB
88
+ await delay(400)
89
+
90
+ // every subscriber received every tick — no loss, no dup
91
+ const expectedSeq = Array.from({length: TICKS}, (_, i) => i)
92
+ await check('subs: every bucket has all 120 ticks', () => buckets.filter(b => b.length !== TICKS).length, 0)
93
+ await check('subs: total CBs == 60*120', () => buckets.reduce((a, b) => a + b.length, 0), SUBS * TICKS)
94
+ await check('subs: bucket #0 exact sequence', () => buckets[0], expectedSeq)
95
+ await check('subs: bucket #59 exact sequence', () => buckets[SUBS - 1], expectedSeq)
96
+
97
+ // clean teardown — off() each, then both client and server registries empty
98
+ for (const off of offs) off()
99
+ await delay(200)
100
+ await check('subs: client wireSubs back to 0', () => (cli.client.api as any).subscriptions().length, 0)
101
+ await check('subs: server subscriptions back to 0', () => srvApi.subscriptions().length, 0)
102
+
103
+ // post-teardown ticks must reach nobody (no leak / dangling consumer)
104
+ const before = buckets.reduce((a, b) => a + b.length, 0)
105
+ for (let i = 0; i < 30; i++) emitTick!(999)
106
+ await delay(150)
107
+ await check('subs: no delivery after teardown', () => buckets.reduce((a, b) => a + b.length, 0), before)
108
+
109
+ // =============================================================
110
+ // (3) MIXED — concurrent calls + a fresh stream burst together
111
+ // =============================================================
112
+ const mixBuckets: number[][] = Array.from({length: 10}, () => [])
113
+ const mixOffs = mixBuckets.map(b => (api.mixStream as any).callback((v: number) => b.push(v)))
114
+ await delay(80)
115
+ // fire 200 calls AND 50 ticks interleaved
116
+ const callP = Promise.all(Array.from({length: 200}, (_, i) => api.sq(i)))
117
+ for (let i = 0; i < 50; i++) emitMix!(i)
118
+ const mixCalls = await callP
119
+ await delay(250)
120
+ await check('mixed: 200 calls all correct under stream load', () => mixCalls.filter((v, i) => v !== i * i).length, 0)
121
+ await check('mixed: each of 10 subs got all 50 ticks', () => mixBuckets.filter(b => b.length !== 50).length, 0)
122
+ for (const off of mixOffs) off()
123
+ await delay(150)
124
+ await check('mixed: server subscriptions back to 0', () => srvApi.subscriptions().length, 0)
125
+
126
+ clearTimeout(watchdog)
127
+ cli.close()
128
+ await srv.close()
129
+ process.exit(done() === 0 ? 0 : 1)
130
+ }
131
+
132
+ main().catch(e => { console.error(e); process.exit(2) })
@@ -0,0 +1,57 @@
1
+ // Compile-only fixture (checked by clientapiall-replay-types.spec.ts via `tsc --noEmit`).
2
+ // Contract: replay-listen members of an rpc<T>() client project into the client replay
3
+ // surface (ReplaySocketListen) in BOTH typed paths (func/strict) — so
4
+ // `replaySubscribe(client.func.key, cb)` needs NO casts. Never executed at runtime.
5
+
6
+ import {createRpcClient} from '../../src/Common/rcp/rpc-client'
7
+ import type {ListenReplayApi, ReplayEvent} from '../../src/Common/events/replay-listen'
8
+ import {replaySubscribe} from '../../src/Common/events/replay-wire'
9
+
10
+ type tMsg = {text: string, n: number}
11
+ type tApi = {
12
+ hello(name: string): string
13
+ listenMsg: ListenReplayApi<[tMsg]>
14
+ nested: {inner: ListenReplayApi<[number, string]>}
15
+ }
16
+
17
+ // exact-type equality (invariant check, not just assignability)
18
+ type Eq<A, B> = (<T>() => T extends A ? 1 : 2) extends (<T>() => T extends B ? 1 : 2) ? true : false
19
+
20
+ declare const socket: any
21
+
22
+ export function compileCase() {
23
+ const client = createRpcClient<tApi>({socket, socketKey: 'x'})
24
+
25
+ // the projected member carries the exact envelope type on its line
26
+ type tProjectedEv = Parameters<Parameters<typeof client.func.listenMsg.line.on>[0]>[0]
27
+ const evExact: Eq<tProjectedEv, ReplayEvent<[tMsg]>> = true
28
+ void evExact
29
+
30
+ // inferred subscribe — no casts, cb args typed from the API shape
31
+ const sub = replaySubscribe(client.func.listenMsg, function onMsg(msg) {
32
+ const text: string = msg.text
33
+ void text
34
+ })
35
+ const seq: number = sub.seq()
36
+ void seq
37
+ sub()
38
+
39
+ // explicit generic — remote param accepted with no casts
40
+ const sub2 = replaySubscribe<[number, string]>(client.func.nested.inner, function onPair(n, s) {
41
+ const a: number = n
42
+ const b: string = s
43
+ void a; void b
44
+ })
45
+ sub2()
46
+
47
+ // strict path projects the same way
48
+ const sub3 = replaySubscribe(client.strict.listenMsg, function onMsg2(msg) {
49
+ const n: number = msg.n
50
+ void n
51
+ })
52
+ sub3()
53
+
54
+ // plain functions are untouched by the replay arm
55
+ const p: Promise<string> = client.func.hello('x')
56
+ void p
57
+ }
@@ -0,0 +1,254 @@
1
+ import {
2
+ createAsyncQueue,
3
+ createReadyGate,
4
+ createThrottle,
5
+ enhancedQueueRun,
6
+ } from "../../src/Common/async/waitRun";
7
+ import {promiseProgress} from "../../src/Common/async/promiseProgress";
8
+
9
+ type Test = {
10
+ name: string;
11
+ fn: () => void | Promise<void>;
12
+ };
13
+
14
+ const tests: Test[] = [];
15
+
16
+ function test(name: string, fn: Test["fn"]) {
17
+ tests.push({name, fn});
18
+ }
19
+
20
+ function assert(condition: unknown, message: string): asserts condition {
21
+ if (!condition) throw new Error(message);
22
+ }
23
+
24
+ function assertEq<T>(actual: T, expected: T, message: string) {
25
+ if (actual !== expected) {
26
+ throw new Error(`${message}: expected ${String(expected)}, got ${String(actual)}`);
27
+ }
28
+ }
29
+
30
+ async function assertRejects(promise: Promise<unknown>, expected: unknown, message: string) {
31
+ try {
32
+ await promise;
33
+ } catch (error) {
34
+ assert(error === expected, message);
35
+ return;
36
+ }
37
+ throw new Error(`${message}: promise resolved`);
38
+ }
39
+
40
+ function assertThrows(fn: () => unknown, message: string) {
41
+ try {
42
+ fn();
43
+ } catch {
44
+ return;
45
+ }
46
+ throw new Error(`${message}: function did not throw`);
47
+ }
48
+
49
+ function sleep(ms = 0) {
50
+ return new Promise<void>(resolve => setTimeout(resolve, ms));
51
+ }
52
+
53
+ test("createAsyncQueue rejects concurrency 0", () => {
54
+ assertThrows(() => createAsyncQueue(0), "concurrency 0 throws");
55
+ });
56
+
57
+ test("createAsyncQueue resolves, rejects, limits concurrency, and onIdle waits", async () => {
58
+ const queue = createAsyncQueue(2);
59
+ const failure = new Error("queue failure");
60
+ const events: string[] = [];
61
+ let active = 0;
62
+ let maxActive = 0;
63
+
64
+ const makeTask = (name: string, ms: number, value: string, reject = false) => async () => {
65
+ events.push(`start:${name}`);
66
+ active++;
67
+ maxActive = Math.max(maxActive, active);
68
+ await sleep(ms);
69
+ active--;
70
+ events.push(`end:${name}`);
71
+ if (reject) throw failure;
72
+ return value;
73
+ };
74
+
75
+ const first = queue.add(makeTask("first", 8, "one"));
76
+ const second = queue.add(makeTask("second", 3, "two", true));
77
+ const third = queue.add(makeTask("third", 1, "three"));
78
+ const secondRejected = assertRejects(second, failure, "rejected task rejects add promise");
79
+
80
+ assertEq(queue.size, 1, "third task is queued while first two are active");
81
+ assertEq(await first, "one", "resolved task returns its value");
82
+ await secondRejected;
83
+ assertEq(await third, "three", "queue continues after rejected task");
84
+ await queue.onIdle();
85
+
86
+ assertEq(queue.size, 0, "queue is empty after onIdle");
87
+ assertEq(active, 0, "no tasks remain active after onIdle");
88
+ assert(maxActive <= 2, "queue never exceeds configured concurrency");
89
+ assert(events.includes("start:third"), "queued task eventually starts");
90
+ });
91
+
92
+ test("createThrottle survives rejected throttled task", async () => {
93
+ const throttle = createThrottle();
94
+ const events: string[] = [];
95
+
96
+ throttle.throttle(0, async () => {
97
+ events.push("first");
98
+ throw new Error("ignored");
99
+ });
100
+
101
+ await sleep(5);
102
+
103
+ throttle.throttle(0, async () => {
104
+ events.push("second");
105
+ });
106
+
107
+ await sleep(5);
108
+ assertEq(events.join(","), "first,second", "throttle runs after a rejected task");
109
+ });
110
+
111
+ test("enhancedQueueRun keeps draining after rejected tasks", async () => {
112
+ const queue = enhancedQueueRun(1);
113
+ const failure = new Error("enhanced failure");
114
+ const events: string[] = [];
115
+
116
+ queue.enqueue(async () => {
117
+ events.push("fire-and-forget reject");
118
+ throw failure;
119
+ });
120
+ queue.enqueue(async () => {
121
+ events.push("after fire-and-forget");
122
+ });
123
+
124
+ await queue.runAll();
125
+
126
+ await assertRejects(
127
+ queue.enqueueAndRun(async () => {
128
+ events.push("reported reject");
129
+ throw failure;
130
+ }),
131
+ failure,
132
+ "enqueueAndRun reports task rejection",
133
+ );
134
+
135
+ await queue.enqueueAndRun(async () => {
136
+ events.push("after reported reject");
137
+ });
138
+ await queue.runAll();
139
+
140
+ assertEq(
141
+ events.join(","),
142
+ "fire-and-forget reject,after fire-and-forget,reported reject,after reported reject",
143
+ "enhancedQueueRun continues after both swallowed and reported rejections",
144
+ );
145
+ assertEq(queue.queueSize, 0, "enhancedQueueRun queue is empty after runAll");
146
+ });
147
+
148
+ test("promiseProgress all returns values and emits ok events", async () => {
149
+ const listened = promiseProgress<string>([
150
+ () => Promise.resolve("alpha"),
151
+ async () => {
152
+ await sleep(1);
153
+ return "beta";
154
+ },
155
+ ]);
156
+ const okEvents: string[] = [];
157
+ const errorEvents: string[] = [];
158
+
159
+ listened.onOk((data, i, ok, errors, count) => {
160
+ okEvents.push(`${i}:${data}:${ok}:${errors}:${count}`);
161
+ });
162
+ listened.onError(error => {
163
+ errorEvents.push(String(error));
164
+ });
165
+
166
+ const values = await listened.all();
167
+
168
+ assertEq(values.join(","), "alpha,beta", "promiseProgress all resolves values in input order");
169
+ assertEq(okEvents.join("|"), "0:alpha:1:0:2|1:beta:2:0:2", "ok events fire with counts");
170
+ assertEq(errorEvents.length, 0, "no error events fire for successful all");
171
+ assertEq(listened.stats().ok, 2, "status tracks ok count");
172
+ });
173
+
174
+ test("promiseProgress all rejects on errors while events fire", async () => {
175
+ const failure = new Error("listen failure");
176
+ const listened = promiseProgress<string>([
177
+ () => Promise.resolve("alpha"),
178
+ () => Promise.reject(failure),
179
+ async () => {
180
+ await sleep(1);
181
+ return "gamma";
182
+ },
183
+ ]);
184
+ const okEvents: string[] = [];
185
+ const errorEvents: string[] = [];
186
+
187
+ listened.onOk((data, i, ok, errors, count) => {
188
+ okEvents.push(`${i}:${data}:${ok}:${errors}:${count}`);
189
+ });
190
+ listened.onError((error, i, ok, errors, count) => {
191
+ errorEvents.push(`${i}:${error === failure}:${ok}:${errors}:${count}`);
192
+ });
193
+
194
+ await assertRejects(listened.all(), failure, "promiseProgress all rejects with task error");
195
+ await Promise.allSettled(listened.items());
196
+
197
+ assertEq(errorEvents.join("|"), "1:true:1:1:3", "error event fires with counts");
198
+ assertEq(okEvents.join("|"), "0:alpha:1:0:3|2:gamma:2:1:3", "ok events still fire around rejection");
199
+ assertEq(listened.stats().error, 1, "status tracks error count");
200
+ assertEq(listened.stats().ok, 2, "status tracks successful count after rejection");
201
+ });
202
+
203
+ test("ReadyGate continues after thrown task and clears queued tasks", async () => {
204
+ const gate = createReadyGate();
205
+ const failure = new Error("ready failure");
206
+ const events: string[] = [];
207
+
208
+ gate.add(() => {
209
+ events.push("first");
210
+ throw failure;
211
+ });
212
+ gate.add(async () => {
213
+ await sleep(1);
214
+ events.push("second");
215
+ });
216
+
217
+ assertEq(gate.tasks().length, 2, "gate stores tasks before ready");
218
+ await assertRejects(gate.ready(), failure, "ready reports first thrown task");
219
+
220
+ assertEq(events.join(","), "first,second", "ready continues after thrown task");
221
+ assertEq(gate.tasks().length, 0, "ready clears queued tasks");
222
+ assert(gate.isReady(), "gate remains ready after thrown task");
223
+
224
+ gate.add(() => {
225
+ events.push("after-ready");
226
+ });
227
+ assertEq(events.join(","), "first,second,after-ready", "tasks added after ready run immediately");
228
+ });
229
+
230
+ async function main() {
231
+ let failed = 0;
232
+
233
+ for (const {name, fn} of tests) {
234
+ try {
235
+ await fn();
236
+ console.log(`ok - ${name}`);
237
+ } catch (error) {
238
+ failed++;
239
+ console.error(`not ok - ${name}`);
240
+ console.error(error);
241
+ }
242
+ }
243
+
244
+ if (failed > 0) {
245
+ throw new Error(`${failed} async queue regression test(s) failed`);
246
+ }
247
+
248
+ console.log(`${tests.length} async queue regression tests passed`);
249
+ }
250
+
251
+ main().catch(error => {
252
+ console.error(error);
253
+ process.exit(1);
254
+ });