wenay-common2 1.0.69 → 1.0.73

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 (89) hide show
  1. package/README.md +11 -1
  2. package/demo/client.ts +222 -0
  3. package/demo/index.html +58 -0
  4. package/demo/server.ts +93 -0
  5. package/doc/INTENT.md +31 -0
  6. package/doc/ROADMAP.md +19 -1
  7. package/doc/changes/1.0.70.md +4 -0
  8. package/doc/changes/1.0.71.md +9 -0
  9. package/doc/changes/1.0.72.md +6 -0
  10. package/doc/changes/1.0.73.md +6 -0
  11. package/doc/wenay-common2-rare.md +29 -8
  12. package/doc/wenay-common2.md +13 -2
  13. package/lib/Common/media/media-index.d.ts +1 -0
  14. package/lib/Common/media/media-index.js +1 -0
  15. package/lib/Common/media/media-source.d.ts +3 -0
  16. package/lib/Common/media/media-source.js +136 -27
  17. package/lib/Common/media/media-view.d.ts +42 -0
  18. package/lib/Common/media/media-view.js +191 -0
  19. package/lib/Common/peer/peer-client.d.ts +12 -5
  20. package/lib/Common/peer/peer-client.js +53 -3
  21. package/lib/Common/peer/peer-host.d.ts +8 -4
  22. package/lib/Common/peer/peer-host.js +2 -2
  23. package/lib/Common/peer/peer-relay.d.ts +10 -2
  24. package/lib/Common/peer/peer-relay.js +40 -18
  25. package/observe/PLAN.md +131 -0
  26. package/observe/README.md +226 -0
  27. package/observe/hot-write.test.ts +95 -0
  28. package/observe/listen-core.test.ts +66 -0
  29. package/observe/listen-store.test.ts +56 -0
  30. package/observe/listen.test.ts +92 -0
  31. package/observe/reactive.test.ts +270 -0
  32. package/observe/reactive.ts +1 -0
  33. package/observe/store-manager.test.ts +118 -0
  34. package/observe/store-mirror.example.ts +74 -0
  35. package/observe/store.test.ts +235 -0
  36. package/observe/store.ts +1 -0
  37. package/observe/usage-real-socket.ts +174 -0
  38. package/observe/usage.ts +200 -0
  39. package/oracle/PASSED.md +27 -0
  40. package/oracle/README.md +12 -0
  41. package/oracle/fixes-primitives.spec.ts +90 -0
  42. package/oracle/realsocket/_rs.ts +106 -0
  43. package/oracle/realsocket/auth.spec.ts +91 -0
  44. package/oracle/realsocket/callbacks.spec.ts +122 -0
  45. package/oracle/realsocket/caps.spec.ts +124 -0
  46. package/oracle/realsocket/core.spec.ts +49 -0
  47. package/oracle/realsocket/dedupe.spec.ts +142 -0
  48. package/oracle/realsocket/errors.spec.ts +121 -0
  49. package/oracle/realsocket/lifecycle.spec.ts +128 -0
  50. package/oracle/realsocket/limits.spec.ts +98 -0
  51. package/oracle/realsocket/pipe.spec.ts +101 -0
  52. package/oracle/realsocket/shape.spec.ts +124 -0
  53. package/oracle/realsocket/slimv2.spec.ts +116 -0
  54. package/oracle/realsocket/stress.spec.ts +132 -0
  55. package/oracle/regression/_clientapiall-replay.fixture.ts +57 -0
  56. package/oracle/regression/async-queues.spec.ts +254 -0
  57. package/oracle/regression/bytestream.spec.ts +152 -0
  58. package/oracle/regression/clientapiall-replay-types.spec.ts +47 -0
  59. package/oracle/regression/core-clone-equal.spec.ts +124 -0
  60. package/oracle/regression/data-structures.spec.ts +135 -0
  61. package/oracle/regression/listen-events.spec.ts +206 -0
  62. package/oracle/regression/observe-core.spec.ts +278 -0
  63. package/oracle/regression/package-export.spec.ts +120 -0
  64. package/oracle/regression/rpc-dedupe-callbacks.spec.ts +195 -0
  65. package/oracle/regression/rpc-lifecycle.spec.ts +150 -0
  66. package/oracle/regression/store-each.spec.ts +209 -0
  67. package/package.json +6 -1
  68. package/replay/PLAN.md +171 -0
  69. package/replay/canvas-socket.test.ts +187 -0
  70. package/replay/coalesce.test.ts +260 -0
  71. package/replay/conflate-socket.test.ts +288 -0
  72. package/replay/conflate.test.ts +225 -0
  73. package/replay/history.test.ts +222 -0
  74. package/replay/media-socket.test.ts +157 -0
  75. package/replay/media-view.test.ts +164 -0
  76. package/replay/offline-store-socket.test.ts +290 -0
  77. package/replay/offline-store.test.ts +156 -0
  78. package/replay/peer-repair.test.ts +184 -0
  79. package/replay/peer-sdk.test.ts +228 -0
  80. package/replay/replay-listen.test.ts +156 -0
  81. package/replay/route-coordinator.test.ts +314 -0
  82. package/replay/route-handoff.test.ts +150 -0
  83. package/replay/route-webrtc.test.ts +321 -0
  84. package/replay/rpc-auto.test.ts +256 -0
  85. package/replay/socket-replay.test.ts +199 -0
  86. package/replay/staleness.test.ts +176 -0
  87. package/replay/store-replay.test.ts +151 -0
  88. package/replay/video-socket.demo.ts +200 -0
  89. package/doc/changes/1.0.63.md +0 -8
@@ -0,0 +1,142 @@
1
+ // REAL-SOCKET dedupe: subscription dedupe + refcount + server stats + cap. Port 4106.
2
+ import {startRealServer, startRealClient, makeChecker, delay} from './_rs'
3
+ import {listen as createListenPair} from '../../src/Common/events/Listen'
4
+
5
+ const PORT = 4106
6
+
7
+ // One stream node per facade; emit pushes a value to whoever subscribed.
8
+ // `feed` is a server-side method the client can call to drive an emission.
9
+ function makeObject() {
10
+ const [emitA, listenA] = createListenPair<number>()
11
+ const [emitB, listenB] = createListenPair<number>()
12
+ return {
13
+ // The Listen handle itself IS the node; resolveTransform turns it into
14
+ // {callback, removeCallback}, so the wire path is `streamA.callback`.
15
+ streamA: listenA,
16
+ streamB: listenB,
17
+ // server-side drivers: tell the server to fire an event into a stream.
18
+ feedA: async (v: number) => { emitA(v); return v },
19
+ feedB: async (v: number) => { emitB(v); return v },
20
+ }
21
+ }
22
+
23
+ async function main() {
24
+ const {check, done} = makeChecker('dedupe')
25
+ const watchdog = setTimeout(() => { console.error('WATCHDOG timeout'); process.exit(3) }, 40000)
26
+
27
+ // capture per-connection server api for subscription stats
28
+ let serverApi: any = null
29
+ const srv = await startRealServer({port: PORT, makeObject, onServer: (api) => { serverApi = api }})
30
+ const cli = await startRealClient<ReturnType<typeof makeObject>>({port: PORT})
31
+ const api = cli.api
32
+
33
+ await delay(50) // let handshake (MAP / declared listens) settle
34
+
35
+ // =====================================================================
36
+ // Part 1: two local consumers, SAME address → ONE wire subscription.
37
+ // =====================================================================
38
+ const gotC1: number[] = []
39
+ const gotC2: number[] = []
40
+ const off1 = api.streamA.callback((v: number) => gotC1.push(v))
41
+ const off2 = api.streamA.callback((v: number) => gotC2.push(v))
42
+ await delay(80) // subscription round-trip
43
+
44
+ // Client side: ONE wire sub, with TWO local consumers.
45
+ await check('client wireSubs count == 1 (deduped)',
46
+ () => cli.client.api.subscriptions().length, 1)
47
+ await check('client consumers == 2 (two local on same address)',
48
+ () => cli.client.api.subscriptions()[0].consumers, 2)
49
+
50
+ // Server side: ONE Listen node, ONE wire subscriber (dedupe collapsed the two).
51
+ await check('server subscriptions: 1 node',
52
+ () => serverApi.subscriptions().length, 1)
53
+ await check('server consumers == 1 (single shared wire sub)',
54
+ () => serverApi.subscriptions()[0].consumers, 1)
55
+
56
+ // Fan-out: one server emit reaches BOTH local consumers.
57
+ await api.feedA(7)
58
+ await delay(80)
59
+ await check('fan-out: consumer1 received [7]', () => gotC1, [7])
60
+ await check('fan-out: consumer2 received [7]', () => gotC2, [7])
61
+
62
+ // =====================================================================
63
+ // Part 2: refcount — wire stop only when the LAST consumer leaves.
64
+ // =====================================================================
65
+ off1() // first consumer leaves
66
+ await delay(80)
67
+ await check('after off1: still 1 wire sub (refcount > 0)',
68
+ () => cli.client.api.subscriptions().length, 1)
69
+ await check('after off1: consumers dropped to 1',
70
+ () => cli.client.api.subscriptions()[0]?.consumers, 1)
71
+ await check('after off1: server STILL has the node (wire alive)',
72
+ () => serverApi.subscriptions().length, 1)
73
+
74
+ // Surviving consumer still receives events.
75
+ await api.feedA(11)
76
+ await delay(80)
77
+ await check('after off1: consumer2 still gets events [7,11]', () => gotC2, [7, 11])
78
+ await check('after off1: consumer1 stopped getting events [7]', () => gotC1, [7])
79
+
80
+ off2() // LAST consumer leaves → wire stop fires
81
+ await delay(120) // removeCallback is fire-and-forget; give it round-trip time
82
+ await check('after last off: client wireSubs empty',
83
+ () => cli.client.api.subscriptions().length, 0)
84
+ await check('after last off: server node removed (wire stop fired)',
85
+ () => serverApi.subscriptions().length, 0)
86
+
87
+ // =====================================================================
88
+ // Part 3: maxPerListen cap — over-cap subscriptions silently ignored.
89
+ // Distinct addresses (different non-cb args) on the SAME Listen node each
90
+ // create a wire sub; the cap is per Listen node on the server.
91
+ // =====================================================================
92
+ cli.close()
93
+ await srv.close()
94
+ await delay(50)
95
+
96
+ const CAP = 2
97
+ let serverApi2: any = null
98
+ const srv2 = await startRealServer({
99
+ port: PORT, makeObject,
100
+ serverOpts: {maxPerListen: CAP},
101
+ onServer: (api) => { serverApi2 = api },
102
+ })
103
+ const cli2 = await startRealClient<ReturnType<typeof makeObject>>({port: PORT})
104
+ const api2 = cli2.api
105
+ await delay(50)
106
+
107
+ // Three DISTINCT-address subscriptions on streamA: the callback comes FIRST
108
+ // (so server-side listenSocket.callback(z) still gets the fn), a trailing tag
109
+ // arg makes each dedupe address unique → three independent wire subs. Cap is 2.
110
+ const recv: Record<string, number[]> = {s0: [], s1: [], s2: []}
111
+ api2.streamA.callback((v: number) => recv.s0.push(v), 0)
112
+ api2.streamA.callback((v: number) => recv.s1.push(v), 1)
113
+ api2.streamA.callback((v: number) => recv.s2.push(v), 2)
114
+ await delay(120)
115
+
116
+ // The client issues three distinct-address wire subs, but the over-cap one is
117
+ // silently resolved by the server (subscribe returns Promise.resolve()), which
118
+ // fires the client's .then(finish) and tears that wire entry down locally — so
119
+ // the client settles back to CAP live wire subs. (Server-side cap below is the
120
+ // load-bearing assertion; this documents the client-side consequence.)
121
+ await check('client settles to CAP live wire subs (over-cap torn down)',
122
+ () => cli2.client.api.subscriptions().length, CAP)
123
+
124
+ // Server accepted only CAP subscribers on the node; over-cap silently ignored.
125
+ await check('server node consumers capped at maxPerListen',
126
+ () => serverApi2.subscriptions()[0]?.consumers, CAP)
127
+
128
+ // Emit reaches exactly the accepted subscribers (first CAP), not the dropped one.
129
+ await api2.feedA(99)
130
+ await delay(100)
131
+ await check('capped: accepted sub s0 got [99]', () => recv.s0, [99])
132
+ await check('capped: accepted sub s1 got [99]', () => recv.s1, [99])
133
+ await check('capped: over-cap sub s2 got nothing []', () => recv.s2, [])
134
+
135
+ clearTimeout(watchdog)
136
+ cli2.close()
137
+ await srv2.close()
138
+
139
+ process.exit(done() === 0 ? 0 : 1)
140
+ }
141
+
142
+ main().catch(e => { console.error(e); process.exit(2) })
@@ -0,0 +1,121 @@
1
+ // ============================================================
2
+ // oracle/realsocket/errors.spec.ts — DISPOSABLE real-socket oracle
3
+ // CATEGORY "errors": error propagation across a real WebSocket.
4
+ // - plain Error -> client rejects with the message
5
+ // - MyError with code+data -> code + data preserved over the wire
6
+ // - a rejected CALL does not kill the connection (next call still works)
7
+ // ============================================================
8
+ import {startRealServer, startRealClient, makeChecker, delay} from './_rs'
9
+ import {MyError} from '../../src/toError/myThrow'
10
+
11
+ const PORT = 4104
12
+
13
+ function makeObject() {
14
+ return {
15
+ // happy path — used to prove the socket survives rejections
16
+ add: (a: number, b: number) => a + b,
17
+ ping: () => 'pong',
18
+
19
+ // ===== error-throwing handlers =====
20
+ throwPlain: () => { throw new Error('boom-plain') },
21
+ throwMyErr: () => { throw new MyError('payload-msg', 'E_TEST', {x: 1, tag: 'rich'}) },
22
+ // MyError carrying structured (wire-safe) data — verify nested data survives
23
+ throwNested: () => {
24
+ throw new MyError('nested-data', 'E_NESTED', {list: [1, 2, 3], meta: {ok: true, deep: {y: 9}}})
25
+ },
26
+ // async rejection
27
+ rejectAsync: async () => { await delay(5); throw new MyError('async-fail', 'E_ASYNC', {k: 'v'}) },
28
+ // MyError whose data carries a BigInt — probes errToObj pack behavior.
29
+ // FINDING: errToObj copies e.data RAW (no pack walk), so a BigInt here is
30
+ // handed to socket.io's JSON.stringify and throws server-side, killing
31
+ // the connection. Rich types round-trip in normal RESULTS but NOT in errors.
32
+ throwBigErr: () => { throw new MyError('big-fail', 'E_BIG', {n: 42n}) },
33
+ }
34
+ }
35
+
36
+ // The server runs in THIS same process. The BigInt-in-error-data probe makes the
37
+ // server's socket emit throw synchronously OUTSIDE the rpc try/catch, which would
38
+ // otherwise crash the whole process before ##RESULT## prints. Swallow it so the
39
+ // finding is recorded as a FAIL by the client-side race below, not a hard crash.
40
+ let sawSerializeCrash = false
41
+ process.on('uncaughtException', (e: any) => {
42
+ if (String(e?.message ?? e).includes('BigInt')) { sawSerializeCrash = true; return }
43
+ console.error('UNEXPECTED uncaughtException', e); process.exit(2)
44
+ })
45
+
46
+ async function main() {
47
+ const {check, done} = makeChecker('errors')
48
+ const watchdog = setTimeout(() => { console.error('WATCHDOG timeout'); process.exit(3) }, 40000)
49
+
50
+ const srv = await startRealServer({port: PORT, makeObject})
51
+ const cli = await startRealClient({port: PORT})
52
+ const api = cli.api
53
+
54
+ await delay(20)
55
+
56
+ // sanity: happy call works before we start throwing
57
+ await check('sanity: add(2,3)=5', () => api.add(2, 3), 5)
58
+
59
+ // plain Error -> client rejects with the same message
60
+ await check('plain Error -> message',
61
+ () => api.throwPlain().then(() => '__no_throw__', (e: any) => e?.message), 'boom-plain')
62
+
63
+ // connection survives a rejected call: a subsequent call still works
64
+ await check('socket alive after plain reject', () => api.ping(), 'pong')
65
+
66
+ // MyError -> revived as a real Error instance on the client
67
+ await check('MyError -> instanceof Error',
68
+ () => api.throwMyErr().then(() => false, (e: any) => e instanceof Error), true)
69
+
70
+ // MyError -> name preserved
71
+ await check('MyError -> name',
72
+ () => api.throwMyErr().then(() => '', (e: any) => e?.name), 'MyError')
73
+
74
+ // MyError -> message preserved
75
+ await check('MyError -> message',
76
+ () => api.throwMyErr().then(() => '', (e: any) => e?.message), 'payload-msg')
77
+
78
+ // MyError -> machine code preserved across the wire
79
+ await check('MyError -> code',
80
+ () => api.throwMyErr().then(() => '', (e: any) => e?.code), 'E_TEST')
81
+
82
+ // MyError -> arbitrary data payload preserved across the wire
83
+ await check('MyError -> data',
84
+ () => api.throwMyErr().then(() => null, (e: any) => e?.data), {x: 1, tag: 'rich'})
85
+
86
+ // socket still alive after the MyError reject
87
+ await check('socket alive after MyError reject', () => api.add(10, 20), 30)
88
+
89
+ // nested/structured (wire-safe) data inside MyError.data survives intact
90
+ await check('MyError -> nested data',
91
+ () => api.throwNested().then(() => null, (e: any) => e?.data),
92
+ {list: [1, 2, 3], meta: {ok: true, deep: {y: 9}}})
93
+
94
+ // async rejection: code + data preserved
95
+ await check('async MyError -> [code, data]',
96
+ () => api.rejectAsync().then(() => null, (e: any) => [e?.code, e?.data]),
97
+ ['E_ASYNC', {k: 'v'}])
98
+
99
+ // final liveness BEFORE probing the BigInt-in-error crash
100
+ await check('socket alive before big-err probe', () => api.ping(), 'pong')
101
+
102
+ // FINDING probe: BigInt inside MyError.data. errToObj copies data raw, so the
103
+ // server's socket emit throws "Do not know how to serialize a BigInt" and the
104
+ // connection dies. We RACE the call against a short timeout so a server-side
105
+ // crash surfaces here as a recorded FAIL instead of taking down the process
106
+ // before ##RESULT## prints. Expected (correct) behavior would be a clean
107
+ // rejection with code E_BIG; anything else is the bug.
108
+ await check('MyError BigInt data -> clean reject (FINDING)',
109
+ () => Promise.race([
110
+ api.throwBigErr().then(() => 'resolved', (e: any) => 'rejected:' + e?.code),
111
+ delay(800).then(() => 'no-response(connection-killed)'),
112
+ ]),
113
+ 'rejected:E_BIG')
114
+
115
+ clearTimeout(watchdog)
116
+ cli.close()
117
+ await srv.close()
118
+ process.exit(done() === 0 ? 0 : 1)
119
+ }
120
+
121
+ main().catch(e => { console.error(e); process.exit(2) })
@@ -0,0 +1,128 @@
1
+ // REAL-SOCKET lifecycle: disconnect teardown drains live subscriptions, fires
2
+ // onDisconnect (client + hub), and hard setToken rotation tears down old subs then
3
+ // reconnects (connectCount++). Default = teardown, NO auto-resubscribe. Port 4108.
4
+ import {startRealServer, startRealClient, makeChecker, delay} from './_rs'
5
+ import {listen as createListenPair} from '../../src/Common/events/Listen'
6
+
7
+ const PORT = 4108
8
+
9
+ // ===== facade: a method + two listen stream nodes =====
10
+ // The server fabricates a fresh facade per connection (see _rs.startRealServer),
11
+ // so each connection gets its own emit handle. We stash the latest emit on a
12
+ // module-level ref so the test body can push ticks into the live stream.
13
+ let emitTicks: ((n: number) => void) | null = null
14
+ let emitClock: ((s: string) => void) | null = null
15
+
16
+ function makeObject() {
17
+ const [tick, tickListen] = createListenPair<number>()
18
+ const [clock, clockListen] = createListenPair<string>()
19
+ emitTicks = tick
20
+ emitClock = clock
21
+ return {
22
+ ping: async () => 'pong',
23
+ // Listen nodes: client subscribes via api.<node>.callback(fn)
24
+ ticks: tickListen,
25
+ clock: clockListen,
26
+ }
27
+ }
28
+
29
+ async function main() {
30
+ const {check, done} = makeChecker('lifecycle')
31
+ const watchdog = setTimeout(() => { console.error('WATCHDOG timeout'); process.exit(3) }, 40000)
32
+
33
+ const srv = await startRealServer({port: PORT, makeObject})
34
+
35
+ // ============================================================
36
+ // PART 1 — client.dispose() drains a live subscription
37
+ // ============================================================
38
+ const cli = await startRealClient({port: PORT})
39
+ const api = cli.api
40
+
41
+ // sanity: a plain call round-trips over the real socket
42
+ await check('CALL ping', () => api.ping(), 'pong')
43
+
44
+ // subscribe to the `ticks` stream; collect ticks; the off-handle is a
45
+ // callable thenable that RESOLVES when the stream ends (teardown/drain).
46
+ const got1: number[] = []
47
+ let drained1 = false
48
+ const off1 = api.ticks.callback((n: number) => got1.push(n))
49
+ off1.then(() => { drained1 = true }) // settle on stream end
50
+
51
+ await delay(60) // let the subscription round-trip to the server
52
+ emitTicks?.(1)
53
+ emitTicks?.(2)
54
+ await delay(60) // ticks travel back over the wire
55
+
56
+ await check('subscription receives live ticks', () => got1.slice(), [1, 2])
57
+ await check('1 live wire subscription before dispose', () => cli.client.api.subscriptions().length, 1)
58
+
59
+ // dispose drains: the consumer promise settles, NO auto-resubscribe
60
+ let cliDisconnectReason: string | null = null
61
+ cli.client.onDisconnect((r: string) => { cliDisconnectReason = r })
62
+ cli.client.dispose('test dispose')
63
+
64
+ await delay(60)
65
+ await check('client.onDisconnect fires with reason', () => cliDisconnectReason, 'test dispose')
66
+ await check('dispose drained the consumer promise (resolved)', () => drained1, true)
67
+ await check('no live subscriptions after dispose', () => cli.client.api.subscriptions().length, 0)
68
+
69
+ // post-dispose ticks must NOT reach the (drained) consumer — honest teardown
70
+ emitTicks?.(99)
71
+ await delay(40)
72
+ await check('no auto-resubscribe: stale ticks ignored after dispose', () => got1.slice(), [1, 2])
73
+
74
+ cli.close()
75
+
76
+ // ============================================================
77
+ // PART 2 — hub.setToken hard rotation: teardown old subs, then reconnect
78
+ // ============================================================
79
+ const cli2 = await startRealClient({port: PORT})
80
+
81
+ let hubDisconnectReason: string | null = null
82
+ let connectFiredCount = 0
83
+ cli2.hub.onDisconnect((r: string) => { hubDisconnectReason = r })
84
+ cli2.hub.onConnect((count: number) => { connectFiredCount = count })
85
+
86
+ await check('connectCount after initial connect', () => cli2.hub.connectCount(), 1)
87
+
88
+ // subscribe on the clock stream of the FIRST connection
89
+ const got2: string[] = []
90
+ let drained2 = false
91
+ const off2 = cli2.api.clock.callback((s: string) => got2.push(s))
92
+ off2.then(() => { drained2 = true })
93
+
94
+ await delay(60)
95
+ emitClock?.('a')
96
+ await delay(60)
97
+ await check('rotation: sub receives tick on old connection', () => got2.slice(), ['a'])
98
+ await check('1 live subscription before rotation', () => cli2.client.api.subscriptions().length, 1)
99
+
100
+ // capture the OLD per-connection client surface (the facade swaps on rotation)
101
+ const oldClient = cli2.client
102
+
103
+ // HARD rotation: setToken tears down old subs (socketAlive:false), fires hub
104
+ // onDisconnect, opens a fresh socket and fires onConnect again (connectCount++).
105
+ await cli2.hub.setToken('rotated-token')
106
+ await delay(120) // let the fresh socket connect + reconnect handshake
107
+
108
+ await check('hub.onDisconnect fired on rotation', () => hubDisconnectReason, 'token rotated')
109
+ await check('old subscription drained on rotation (no auto-resubscribe)', () => drained2, true)
110
+ await check('old client has 0 live subscriptions after rotation', () => oldClient.api.subscriptions().length, 0)
111
+ await check('connectCount incremented to 2 on rotation', () => cli2.hub.connectCount(), 2)
112
+ await check('hub.onConnect fired again with fresh count', () => connectFiredCount, 2)
113
+
114
+ // the NEW connection has NO carried-over subscription (default = teardown)
115
+ await check('fresh connection starts with 0 subscriptions', () => {
116
+ // facade[key] is the new per-connection client after rotation
117
+ const fresh = (cli2.hub.facade as any).api
118
+ return cli2.hub.connectCount() == 2 && fresh.api.subscriptions().length == 0
119
+ }, true)
120
+
121
+ cli2.close()
122
+
123
+ clearTimeout(watchdog)
124
+ await srv.close()
125
+ process.exit(done() === 0 ? 0 : 1)
126
+ }
127
+
128
+ main().catch(e => { console.error(e); process.exit(2) })
@@ -0,0 +1,98 @@
1
+ // ============================================================
2
+ // oracle/realsocket/limits.spec.ts — DISPOSABLE real-socket oracle
3
+ // CATEGORY: limits — server-side incoming payload caps (serverOpts.limits).
4
+ //
5
+ // The server resolves limits via resolveLimits(serverOpts.limits) and runs
6
+ // every incoming CALL's args through unpack(..., lim). A violation throws a
7
+ // PayloadLimitError (rpc-limits.ts), which the CALL try/catch sends back as a
8
+ // Pkt.RESP error → the client promise rejects with an Error whose name is
9
+ // "PayloadLimitError" and message "Payload limit exceeded: <reason>".
10
+ // Under-limit calls of the same method round-trip normally.
11
+ //
12
+ // Direction note: the configurable cap on a real socket goes through
13
+ // serverOpts.limits → INCOMING (client→server) only. The client-side response
14
+ // cap (unpackResult lim) is opt-in via the client `limits` option, which the
15
+ // shared substrate (startRealClient) does not expose — so we exercise the
16
+ // incoming direction, covering several distinct limit dimensions.
17
+ // ============================================================
18
+ import {startRealServer, startRealClient, makeChecker, delay} from './_rs'
19
+
20
+ const PORT = 4109
21
+
22
+ // echo bounces its single arg straight back; sink takes many args and reports count.
23
+ function makeObject() {
24
+ return {
25
+ echo: (x: any) => x,
26
+ sink: (...xs: any[]) => xs.length,
27
+ }
28
+ }
29
+
30
+ // classify a call as 'ok'(+value) / 'limit'(PayloadLimitError) / 'other'(unexpected reject)
31
+ async function verdict(p: Promise<any>) {
32
+ try {
33
+ const v = await p
34
+ return {tag: 'ok', value: v}
35
+ } catch (e: any) {
36
+ const name = e?.name
37
+ const msg = String(e?.message ?? e)
38
+ if (name == 'PayloadLimitError' || /Payload limit exceeded/.test(msg)) return {tag: 'limit'}
39
+ return {tag: 'other', error: msg}
40
+ }
41
+ }
42
+
43
+ async function main() {
44
+ const {check, done} = makeChecker('limits')
45
+ const watchdog = setTimeout(() => { console.error('WATCHDOG timeout'); process.exit(3) }, 40000)
46
+
47
+ // Tight incoming caps. Defaults (rpc-limits.ts) are huge; we shrink several
48
+ // dimensions so each is easy to cross with a deliberately oversized payload.
49
+ const limits = {
50
+ maxArgs: 3,
51
+ maxStringLen: 10,
52
+ maxDepth: 4,
53
+ maxKeys: 5,
54
+ maxArrayLen: 4,
55
+ }
56
+
57
+ const srv = await startRealServer({port: PORT, makeObject, serverOpts: {limits}})
58
+ const cli = await startRealClient({port: PORT})
59
+ const api = cli.api
60
+ await delay(30) // let the strict handshake settle over the real socket
61
+
62
+ // ---- sanity: a plainly under-limit call works end-to-end ----
63
+ await check('under-limit echo round-trips', () => api.echo('short'), 'short')
64
+
65
+ // ===== maxArgs (incoming) =====
66
+ await check('maxArgs: 3 args ok (count)', () => api.sink(1, 2, 3), 3)
67
+ await check('maxArgs: 4 args -> limit', async () => (await verdict(api.sink(1, 2, 3, 4))).tag, 'limit')
68
+
69
+ // ===== maxStringLen (incoming) =====
70
+ await check('maxStringLen: 10-char ok', () => api.echo('0123456789'), '0123456789')
71
+ await check('maxStringLen: 11-char -> limit', async () => (await verdict(api.echo('0123456789X'))).tag, 'limit')
72
+
73
+ // ===== maxDepth (incoming) =====
74
+ // depth counted by walk(): each nested object level is +1. {a:{b:{c:{d:1}}}} = depth 4 → ok; +1 level → limit.
75
+ await check('maxDepth: depth 4 ok', () => api.echo({a: {b: {c: {d: 1}}}}), {a: {b: {c: {d: 1}}}})
76
+ await check('maxDepth: depth 6 -> limit', async () =>
77
+ (await verdict(api.echo({a: {b: {c: {d: {e: {f: 1}}}}}}))).tag, 'limit')
78
+
79
+ // ===== maxKeys (incoming) =====
80
+ await check('maxKeys: 5 keys ok', () => api.echo({a: 1, b: 2, c: 3, d: 4, e: 5}), {a: 1, b: 2, c: 3, d: 4, e: 5})
81
+ await check('maxKeys: 6 keys -> limit', async () =>
82
+ (await verdict(api.echo({a: 1, b: 2, c: 3, d: 4, e: 5, f: 6}))).tag, 'limit')
83
+
84
+ // ===== maxArrayLen (incoming) =====
85
+ await check('maxArrayLen: len 4 ok', () => api.echo([1, 2, 3, 4]), [1, 2, 3, 4])
86
+ await check('maxArrayLen: len 5 -> limit', async () => (await verdict(api.echo([1, 2, 3, 4, 5]))).tag, 'limit')
87
+
88
+ // ===== rejection carries the typed error shape (name + message) =====
89
+ await check('over-limit rejects, under-limit still works after', async () => {
90
+ const over = await verdict(api.echo('this-is-way-too-long'))
91
+ const under = await api.echo('ok')
92
+ return [over.tag, under]
93
+ }, ['limit', 'ok'])
94
+
95
+ clearTimeout(watchdog); cli.close(); await srv.close(); process.exit(done() === 0 ? 0 : 1)
96
+ }
97
+
98
+ main().catch(e => { console.error(e); process.exit(2) })
@@ -0,0 +1,101 @@
1
+ // ============================================================
2
+ // oracle/realsocket/pipe.spec.ts — DISPOSABLE real-socket oracle for PIPE chains
3
+ //
4
+ // Category "pipe": a method returning a transformation chain that the server
5
+ // walks step-by-step (get/call) over a real WebSocket. Covers:
6
+ // • single-stage chain (.value)
7
+ // • multi-stage chain (>1 call stage: .add().mul().value)
8
+ // • rich-type values flowing THROUGH the pipe (Date/Map/BigInt as call args
9
+ // and as final result)
10
+ // • transparent relay (a pipe method returning another pipe handle —
11
+ // server detects IS_RPC_PIPE and __executeRemainingPipe-continues)
12
+ // ============================================================
13
+ import {startRealServer, startRealClient, makeChecker, delay} from './_rs'
14
+
15
+ const PORT = 4103
16
+
17
+ // ===== facade with pipe-able chain methods =====
18
+ function makeObject() {
19
+ // makeBox returns a fresh chainable box; every transform returns a new box,
20
+ // so the server walks `.add(..).mul(..).value` as a real pipe.
21
+ function makeBox(n: number): any {
22
+ return {
23
+ value: n,
24
+ add: (m: number) => makeBox(n + m),
25
+ mul: (m: number) => makeBox(n * m),
26
+ }
27
+ }
28
+ return {
29
+ makeBox,
30
+ // rich-type pipe: takes a Date arg, returns a chainable carrying rich data
31
+ makeWhen(d: Date): any {
32
+ return {
33
+ date: d,
34
+ // shift the date forward by ms (rich arg flows through a pipe call stage)
35
+ shift: (ms: number) => makeWhenInner(new Date(d.valueOf() + ms)),
36
+ tags: new Map<string, number>([['a', 1], ['b', 2]]),
37
+ big: 9007199254740993n,
38
+ }
39
+ },
40
+ }
41
+
42
+ function makeWhenInner(d: Date): any {
43
+ return {
44
+ date: d,
45
+ shift: (ms: number) => makeWhenInner(new Date(d.valueOf() + ms)),
46
+ tags: new Map<string, number>([['a', 1], ['b', 2]]),
47
+ big: 9007199254740993n,
48
+ }
49
+ }
50
+ }
51
+
52
+ async function main() {
53
+ const {check, done} = makeChecker('pipe')
54
+ const watchdog = setTimeout(() => { console.error('WATCHDOG timeout'); process.exit(3) }, 40000)
55
+
56
+ const srv = await startRealServer({port: PORT, makeObject})
57
+ const cli = await startRealClient({port: PORT})
58
+ const P = cli.client.pipe as any // pipe proxy: supports chaining .a().b().value
59
+
60
+ await delay(20)
61
+
62
+ // --- single-stage chain: method result -> property get ---
63
+ await check('makeBox(10).value', () => P.makeBox(10).value, 10)
64
+
65
+ // --- two-stage chain: one call stage then value ---
66
+ await check('makeBox(10).add(5).value', () => P.makeBox(10).add(5).value, 15)
67
+
68
+ // --- multi-stage chain (>2 call stages): transparent relay across boxes ---
69
+ await check('makeBox(2).add(3).mul(4).value', () => P.makeBox(2).add(3).mul(4).value, 20)
70
+
71
+ // --- order matters: mul before add ---
72
+ await check('makeBox(2).mul(4).add(3).value', () => P.makeBox(2).mul(4).add(3).value, 11)
73
+
74
+ // --- awaiting the chain object itself (no trailing .value): rich object out ---
75
+ await check('makeBox(7).add(1) -> {value:8}', async () => {
76
+ const box = await P.makeBox(7).add(1)
77
+ return box.value
78
+ }, 8)
79
+
80
+ // --- rich type as a Date arg flowing INTO a pipe + Date out ---
81
+ const base = new Date(1700000000000)
82
+ await check('makeWhen(date).date round-trips Date', () => P.makeWhen(base).date, base)
83
+
84
+ // --- rich Date arg + call-stage transform: shift forward by 1000ms ---
85
+ await check('makeWhen(date).shift(1000).date', () => P.makeWhen(base).shift(1000).date, new Date(1700000001000))
86
+
87
+ // --- chained shifts: two call stages over a Date ---
88
+ await check('makeWhen(date).shift(1000).shift(500).date',
89
+ () => P.makeWhen(base).shift(1000).shift(500).date,
90
+ new Date(1700000001500))
91
+
92
+ // --- Map flowing out through a pipe get ---
93
+ await check('makeWhen(date).tags is Map', () => P.makeWhen(base).tags, new Map([['a', 1], ['b', 2]]))
94
+
95
+ // --- BigInt flowing out through a pipe (after a transform stage) ---
96
+ await check('makeWhen(date).shift(1).big is BigInt', () => P.makeWhen(base).shift(1).big, 9007199254740993n)
97
+
98
+ clearTimeout(watchdog); cli.close(); await srv.close(); process.exit(done() === 0 ? 0 : 1)
99
+ }
100
+
101
+ main().catch(e => { console.error(e); process.exit(2) })