wenay-common2 1.0.69 → 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 (67) hide show
  1. package/README.md +10 -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/changes/1.0.70.md +4 -0
  6. package/observe/PLAN.md +131 -0
  7. package/observe/README.md +226 -0
  8. package/observe/hot-write.test.ts +95 -0
  9. package/observe/listen-core.test.ts +66 -0
  10. package/observe/listen-store.test.ts +56 -0
  11. package/observe/listen.test.ts +92 -0
  12. package/observe/reactive.test.ts +270 -0
  13. package/observe/reactive.ts +1 -0
  14. package/observe/store-manager.test.ts +118 -0
  15. package/observe/store-mirror.example.ts +74 -0
  16. package/observe/store.test.ts +235 -0
  17. package/observe/store.ts +1 -0
  18. package/observe/usage-real-socket.ts +174 -0
  19. package/observe/usage.ts +200 -0
  20. package/oracle/PASSED.md +27 -0
  21. package/oracle/README.md +12 -0
  22. package/oracle/fixes-primitives.spec.ts +90 -0
  23. package/oracle/realsocket/_rs.ts +106 -0
  24. package/oracle/realsocket/auth.spec.ts +91 -0
  25. package/oracle/realsocket/callbacks.spec.ts +122 -0
  26. package/oracle/realsocket/caps.spec.ts +124 -0
  27. package/oracle/realsocket/core.spec.ts +49 -0
  28. package/oracle/realsocket/dedupe.spec.ts +142 -0
  29. package/oracle/realsocket/errors.spec.ts +121 -0
  30. package/oracle/realsocket/lifecycle.spec.ts +128 -0
  31. package/oracle/realsocket/limits.spec.ts +98 -0
  32. package/oracle/realsocket/pipe.spec.ts +101 -0
  33. package/oracle/realsocket/shape.spec.ts +124 -0
  34. package/oracle/realsocket/slimv2.spec.ts +116 -0
  35. package/oracle/realsocket/stress.spec.ts +132 -0
  36. package/oracle/regression/_clientapiall-replay.fixture.ts +57 -0
  37. package/oracle/regression/async-queues.spec.ts +254 -0
  38. package/oracle/regression/bytestream.spec.ts +152 -0
  39. package/oracle/regression/clientapiall-replay-types.spec.ts +47 -0
  40. package/oracle/regression/core-clone-equal.spec.ts +124 -0
  41. package/oracle/regression/data-structures.spec.ts +135 -0
  42. package/oracle/regression/listen-events.spec.ts +206 -0
  43. package/oracle/regression/observe-core.spec.ts +278 -0
  44. package/oracle/regression/package-export.spec.ts +120 -0
  45. package/oracle/regression/rpc-dedupe-callbacks.spec.ts +195 -0
  46. package/oracle/regression/rpc-lifecycle.spec.ts +150 -0
  47. package/oracle/regression/store-each.spec.ts +209 -0
  48. package/package.json +6 -1
  49. package/replay/PLAN.md +171 -0
  50. package/replay/canvas-socket.test.ts +187 -0
  51. package/replay/coalesce.test.ts +260 -0
  52. package/replay/conflate-socket.test.ts +288 -0
  53. package/replay/conflate.test.ts +225 -0
  54. package/replay/history.test.ts +222 -0
  55. package/replay/media-socket.test.ts +157 -0
  56. package/replay/offline-store-socket.test.ts +290 -0
  57. package/replay/offline-store.test.ts +156 -0
  58. package/replay/peer-sdk.test.ts +228 -0
  59. package/replay/replay-listen.test.ts +156 -0
  60. package/replay/route-coordinator.test.ts +314 -0
  61. package/replay/route-handoff.test.ts +150 -0
  62. package/replay/route-webrtc.test.ts +321 -0
  63. package/replay/rpc-auto.test.ts +256 -0
  64. package/replay/socket-replay.test.ts +199 -0
  65. package/replay/staleness.test.ts +176 -0
  66. package/replay/store-replay.test.ts +151 -0
  67. package/replay/video-socket.demo.ts +200 -0
@@ -0,0 +1,90 @@
1
+ // DISPOSABLE ORACLE — verifies the primitive bug-fixes found while reviewing:
2
+ // #1 promiseProgress — .all()/.allSettled() must actually RUN factory tasks (not leave thunks)
3
+ // #2 joinListens.destroy — present + actually unsubscribes (type fix; runtime sanity here)
4
+ // #3 joinListens — no empty-bucket leak in `pending` after a group fires
5
+ // #5 createIterableObject — delete on a read-only proxy must NOT throw (consistent with set())
6
+ // Run: node node_modules/ts-node/dist/bin.js --transpile-only oracle/fixes-primitives.spec.ts
7
+ import {promiseProgress} from '../src/Common/async/promiseProgress'
8
+ import {joinListens} from '../src/Common/events/joinListens'
9
+ import {listen as createListenPair} from '../src/Common/events/Listen'
10
+ import {createIterableObject} from '../src/Common/async/createIterableObject'
11
+ import {StructMap} from '../src/Common/core/common'
12
+
13
+ let fails = 0
14
+ function assert(cond: any, msg: string) {
15
+ if (cond) console.log(' ok :', msg)
16
+ else { fails++; console.log(' FAIL:', msg) }
17
+ }
18
+
19
+ async function main() {
20
+ // ===== #1 — factories must actually run via .all() =====
21
+ {
22
+ let ran = 0
23
+ const okSeen: number[] = []
24
+ const p = promiseProgress<number>([
25
+ () => { ran++; return 10 }, // sync factory
26
+ async () => { ran++; return 20 }, // async factory
27
+ Promise.resolve(30), // already-a-promise
28
+ ])
29
+ p.onOk((data) => okSeen.push(data))
30
+ const res = await p.all()
31
+ assert(ran === 2, '#1 both factories were invoked by .all() (ran=' + ran + ')')
32
+ assert(p.stats().ok === 3, '#1 status.ok counts all 3 tasks (ok=' + p.stats().ok + ')')
33
+ assert(okSeen.length === 3, '#1 listenOk fired for all 3 (got ' + okSeen.length + ')')
34
+ assert(res.every(v => typeof v !== 'function'), '#1 .all() resolves values, not un-run thunks')
35
+ }
36
+
37
+ // ===== #1b — factory runs once across .all()+.allSettled() (memoized, no double-run) =====
38
+ {
39
+ let ran = 0
40
+ const p = promiseProgress<number>([() => { ran++; return 1 }])
41
+ await p.all()
42
+ await p.allSettled()
43
+ assert(ran === 1, '#1b factory runs once across .all()+.allSettled() (ran=' + ran + ')')
44
+ }
45
+
46
+ // ===== #3 — pending must not retain an empty bucket after a group fires =====
47
+ {
48
+ const [emitA, listenA] = createListenPair<[string]>()
49
+ const [emitB, listenB] = createListenPair<[string]>()
50
+ const joined = joinListens([listenA, listenB], (d: any) => d) // key = the value itself
51
+ emitA('x'); emitB('x') // completes group "x" → fires
52
+ assert(!joined.pending.has('x'), '#3 pending has no leftover empty bucket for fired tid')
53
+ assert(joined.pending.size === 0, '#3 pending empty after the only group fired (size=' + joined.pending.size + ')')
54
+ }
55
+
56
+ // ===== #2 — destroy() present + actually unsubscribes (runtime sanity; type checked by tsc) =====
57
+ {
58
+ const [, listenA] = createListenPair<[string]>()
59
+ const joined = joinListens([listenA])
60
+ assert(typeof joined.destroy === 'function', '#2 destroy() is present on the result')
61
+ assert(listenA.count() === 1, '#2 join subscribed to the source')
62
+ joined.destroy()
63
+ assert(listenA.count() === 0, '#2 destroy() unsubscribed from the source')
64
+ }
65
+
66
+ // ===== #5 — delete on a read-only proxy must not throw =====
67
+ {
68
+ const store = new Map<string, number>([['a', 1]])
69
+ const ro = createIterableObject<number>({ resolve: () => store })
70
+ let threw = false
71
+ try { delete (ro as any)['a'] } catch { threw = true }
72
+ assert(!threw, '#5 delete on read-only proxy does not throw')
73
+ assert(store.has('a'), '#5 read-only delete is a no-op (key still present)')
74
+ }
75
+
76
+ // ===== #6 — StructMap.has prefix miss against primitive leaf returns false =====
77
+ {
78
+ const m = new StructMap<readonly number[], number>()
79
+ m.set([1], 42)
80
+ let threw = false
81
+ let has = true
82
+ try { has = m.has([1, 2]) } catch { threw = true }
83
+ assert(!threw, '#6 StructMap.has([1, 2]) does not throw when [1] stores a primitive')
84
+ assert(has === false, '#6 StructMap.has([1, 2]) returns false for a missing composite key')
85
+ }
86
+
87
+ console.log(`\n${fails === 0 ? 'ALL GREEN' : fails + ' FAILURE(S)'}`)
88
+ process.exit(fails === 0 ? 0 : 1)
89
+ }
90
+ main()
@@ -0,0 +1,106 @@
1
+ // ============================================================
2
+ // oracle/realsocket/_rs.ts — DISPOSABLE shared harness for REAL-SOCKET mass tests
3
+ //
4
+ // Boots a genuine socket.io server (express + http) on a TCP port and a real
5
+ // socket.io-client hub — the SAME transport pattern as src/Common/rcp/test.ts.
6
+ // Not the in-memory loopback: messages cross a real WebSocket. Each spec uses
7
+ // its OWN port so specs run concurrently without clashing.
8
+ //
9
+ // Disposable oracle (project rule: no standing test suite; real harness is the
10
+ // one in src/Common/rcp/rpc.harness.spec.ts). Delete this dir after the pass.
11
+ // ============================================================
12
+ import express from 'express'
13
+ import {createServer} from 'http'
14
+ import {Server as SocketIOServer} from 'socket.io'
15
+ import {io} from 'socket.io-client'
16
+ import {createRpcServerAuto} from '../../src/Common/rcp/rpc-server-auto'
17
+ import {createRpcClientHub} from '../../src/Common/rcp/rpc-clientHub'
18
+ import {listen as createListenPair} from '../../src/Common/events/Listen'
19
+
20
+ export const delay = (ms: number) => new Promise(r => setTimeout(r, ms))
21
+
22
+ // ===== real socket.io server =====
23
+ type ServerOpts = {
24
+ port: number
25
+ // фабрика объекта-фасада на КАЖДОЕ подключение (как в реальном сервере)
26
+ makeObject: () => any
27
+ // прокидывается в createRpcServerAuto (auth / limits / maxPerListen / throttle / debug)
28
+ serverOpts?: Record<string, any>
29
+ socketKey?: string
30
+ // доступ к per-connection серверным api (subscriptions stats и т.п.)
31
+ onServer?: (api: any, socket: any) => void
32
+ }
33
+
34
+ export async function startRealServer(opts: ServerOpts) {
35
+ const {port, makeObject, serverOpts = {}, socketKey = 'rpc', onServer} = opts
36
+ const app = express()
37
+ const httpServer = createServer(app)
38
+ const ioServer = new SocketIOServer(httpServer, {maxHttpBufferSize: 1e8})
39
+
40
+ ioServer.on('connection', socket => {
41
+ const [disconnect, disconnectListen] = createListenPair()
42
+ socket.on('disconnect', () => disconnect())
43
+ const adapter = {
44
+ emit: (key: string, data: any) => socket.emit(key, data),
45
+ on: (key: string, cb: any) => socket.on(key, cb),
46
+ }
47
+ const server = createRpcServerAuto({socket: adapter, socketKey, object: makeObject(), disconnectListen, ...serverOpts})
48
+ onServer?.(server.api, socket)
49
+ })
50
+
51
+ await new Promise<void>(resolve => httpServer.listen(port, resolve))
52
+ return {
53
+ httpServer, ioServer,
54
+ close: () => new Promise<void>(resolve => { ioServer.close(); httpServer.close(() => resolve()) }),
55
+ }
56
+ }
57
+
58
+ // ===== real socket.io client (via hub) =====
59
+ type ClientOpts = {port: number, token?: string | null, socketKey?: string, opt?: {compact?: boolean}}
60
+
61
+ export async function startRealClient<T extends object = any>(opts: ClientOpts) {
62
+ const {port, token = null, socketKey = 'rpc', opt} = opts
63
+ const hub = createRpcClientHub(
64
+ () => io(`http://localhost:${port}`, {transports: ['websocket'], forceNew: true}),
65
+ (r) => ({api: r<T>(socketKey)}) as const,
66
+ { opt },
67
+ )
68
+ const clients = await hub.setToken(token)
69
+ await clients.api.readyStrict()
70
+ return {
71
+ hub,
72
+ client: clients.api, // createRpcClient surface (auth/reauth/dispose/onDisconnect/subscriptions/schema)
73
+ api: clients.api.func as any, // transparent proxy — call site
74
+ close: () => hub.socket?.disconnect?.(),
75
+ }
76
+ }
77
+
78
+ // ===== assertions =====
79
+ export type Failure = {name: string, got?: any, expected?: any, error?: string}
80
+
81
+ function eq(a: any, b: any) {
82
+ // нормализуем Date → ISO для сравнения round-trip rich-типов
83
+ const norm = (v: any) => JSON.stringify(v, (_k, val) => val instanceof Date ? ['__date', val.toISOString()]
84
+ : val instanceof Map ? ['__map', [...val]] : val instanceof Set ? ['__set', [...val]]
85
+ : typeof val === 'bigint' ? ['__big', val.toString()] : val)
86
+ return norm(a) === norm(b)
87
+ }
88
+
89
+ export function makeChecker(label: string) {
90
+ let pass = 0, fail = 0
91
+ const failures: Failure[] = []
92
+ async function check(name: string, fn: () => any, expected: any) {
93
+ try {
94
+ const got = await fn()
95
+ if (eq(got, expected)) { pass++; console.log(`PASS [${label}] ${name}`) }
96
+ else { fail++; failures.push({name, got, expected}); console.log(`FAIL [${label}] ${name} got=${JSON.stringify(got)} exp=${JSON.stringify(expected)}`) }
97
+ } catch (e: any) {
98
+ fail++; failures.push({name, error: String(e?.stack ?? e)}); console.log(`FAIL [${label}] ${name} ERROR=${String(e?.message ?? e)}`)
99
+ }
100
+ }
101
+ return {
102
+ check,
103
+ summary: () => ({label, total: pass + fail, passed: pass, failed: fail, failures}),
104
+ done: () => { console.log(`\n##RESULT## ${JSON.stringify({label, total: pass + fail, passed: pass, failed: fail, failures})}`); return fail },
105
+ }
106
+ }
@@ -0,0 +1,91 @@
1
+ // ============================================================
2
+ // oracle/realsocket/auth.spec.ts — DISPOSABLE real-socket test
3
+ // Category "auth": in-band auth over a genuine WebSocket.
4
+ // (1) valid token → auth() ok, principal methods callable
5
+ // (2) bad / gated → calls rejected with E_UNAUTHORIZED
6
+ // (3) soft reauth on the LIVE socket → principal swaps (user→admin)
7
+ // WITHOUT dropping a live subscription.
8
+ // ============================================================
9
+ import {startRealServer, startRealClient, makeChecker, delay} from './_rs'
10
+ import {listen as createListenPair} from '../../src/Common/events/Listen'
11
+
12
+ const PORT = 4105
13
+
14
+ // One stream node, shared by BOTH principals → the subscription survives a principal
15
+ // swap (same socket, same Listen handle). Distinct principals: user (read-only) vs
16
+ // admin (read + write) so the swap is observable in method visibility.
17
+ const [emit, listen] = createListenPair<number>()
18
+ const facades: Record<string, any> = {
19
+ 'tok-user': {
20
+ stream: listen,
21
+ whoami: () => 'user',
22
+ read: () => 'public-data',
23
+ pulse: (n: number) => { emit(n); return n }, // drive the stream over the wire
24
+ },
25
+ 'tok-admin': {
26
+ stream: listen,
27
+ whoami: () => 'admin',
28
+ read: () => 'public-data',
29
+ write: (x: number) => x * 10, // admin-only method
30
+ pulse: (n: number) => { emit(n); return n },
31
+ },
32
+ }
33
+ function resolveAuth(token: any) {
34
+ const object = facades[token]
35
+ if (!object) throw new Error('bad token')
36
+ return {object, ack: {ok: true, who: object.whoami()}}
37
+ }
38
+
39
+ async function main() {
40
+ const {check, done} = makeChecker('auth')
41
+ const watchdog = setTimeout(() => { console.error('WATCHDOG timeout'); process.exit(3) }, 40000)
42
+
43
+ // gate=true: the EMPTY initial object is protected; the real principal surface only
44
+ // appears after a successful HELLO (resolveAuth → principal facade).
45
+ const srv = await startRealServer({
46
+ port: PORT,
47
+ makeObject: () => ({}), // empty until HELLO
48
+ serverOpts: {auth: {resolveAuth, gate: true}},
49
+ })
50
+
51
+ // ---- (1) valid token: auth resolves ok, principal methods callable ----
52
+ const cliUser = await startRealClient({port: PORT, token: 'tok-user'})
53
+ const userApi = cliUser.api
54
+
55
+ await check('valid token: authAck ok', async () => (await cliUser.client.auth())?.ok, true)
56
+ await check('valid token: authAck who=user', async () => (await cliUser.client.auth())?.who, 'user')
57
+ await check('user principal: read() callable', () => userApi.read(), 'public-data')
58
+ await check('user principal: whoami=user', () => userApi.whoami(), 'user')
59
+ // user facade has NO write → must reject (method absent in principal routeMap)
60
+ await check('user principal: write() rejected (not in facade)',
61
+ () => userApi.write(5).then(() => 'ok', () => 'rejected'), 'rejected')
62
+
63
+ // ---- (2) bad token / gated: calls rejected with E_UNAUTHORIZED ----
64
+ const cliBad = await startRealClient({port: PORT, token: 'tok-nope'})
65
+ await check('bad token: authAck ok=false', async () => (await cliBad.client.auth())?.ok, false)
66
+ await check('gated: call rejected code E_UNAUTHORIZED',
67
+ () => (cliBad.api.read() as Promise<any>).catch((e: any) => e?.code), 'E_UNAUTHORIZED')
68
+
69
+ // ---- (3) soft reauth on the LIVE socket: user → admin, subscription survives ----
70
+ const got: number[] = []
71
+ const off = cliUser.api.stream.callback((v: number) => got.push(v))
72
+ await delay(50) // let the subscription round-trip
73
+ await userApi.pulse(1) // emit BEFORE reauth (as user)
74
+ await delay(50)
75
+ await check('pre-reauth: stream tick received', async () => got.slice(), [1])
76
+
77
+ // swap principal on the SAME live socket (no disconnect) via hub.reauth
78
+ const acks = await cliUser.hub.reauth('tok-admin')
79
+ await check('reauth: hub.reauth resolved ok', async () => acks?.[0]?.ok, true)
80
+ await check('reauth: principal swapped who=admin', async () => (await cliUser.client.auth())?.who, 'admin')
81
+ await check('reauth: admin sees write() now', () => cliUser.api.write(4), 40)
82
+ await check('reauth: whoami flipped to admin', () => cliUser.api.whoami(), 'admin')
83
+
84
+ await userApi.pulse(2) // emit AFTER reauth — same subscription
85
+ await delay(50)
86
+ await check('reauth: subscription survived (live tick after swap)', async () => got.slice(), [1, 2])
87
+
88
+ off()
89
+ clearTimeout(watchdog); cliUser.close(); cliBad.close(); await srv.close(); process.exit(done() === 0 ? 0 : 1)
90
+ }
91
+ main().catch(e => { console.error(e); process.exit(2) })
@@ -0,0 +1,122 @@
1
+ // ============================================================
2
+ // oracle/realsocket/callbacks.spec.ts — DISPOSABLE real-socket oracle
3
+ // CATEGORY "callbacks": callbacks-as-args (Pkt.CB) + listen streaming
4
+ // subscriptions over a genuine WebSocket. Port 4102 (own port, no clash).
5
+ // ============================================================
6
+ import {startRealServer, startRealClient, makeChecker, delay} from './_rs'
7
+ import {listen as createListenPair} from '../../src/Common/events/Listen'
8
+
9
+ const PORT = 4102
10
+
11
+ // Stream nodes live in module scope so the facade factory closes over the SAME
12
+ // listen handles every connection — we drive them from the test below.
13
+ const [tick, tickListen] = createListenPair<[{n: number; at: Date; tags: Set<string>}]>()
14
+ const [multiTick, multiListen] = createListenPair<[number, string]>()
15
+
16
+ function makeObject() {
17
+ return {
18
+ // ── callback-as-arg (Pkt.CB): server invokes the passed callback N times ──
19
+ counter: {
20
+ run: async (a: {n: number; callback: (i: number, when: Date) => void}) => {
21
+ // emit `n` ticks in order, each with a rich Date arg; resolve when done
22
+ for (let i = 0; i < a.n; i++) {
23
+ a.callback(i, new Date(Date.UTC(2026, 0, 1) + i * 1000))
24
+ await delay(5)
25
+ }
26
+ return a.n
27
+ },
28
+ },
29
+ // ── listen stream node: client subscribes via api.stream.callback(fn) ──
30
+ stream: tickListen, // single rich object arg
31
+ multi: multiListen, // two args (number, string)
32
+ // ── control surface to end the single-arg stream (→ CB_END / await off) ──
33
+ endStream: async () => { tickListen.close(); return true },
34
+ }
35
+ }
36
+
37
+ async function main() {
38
+ const {check, done} = makeChecker('callbacks')
39
+ const watchdog = setTimeout(() => { console.error('WATCHDOG timeout'); process.exit(3) }, 40000)
40
+
41
+ const srv = await startRealServer({port: PORT, makeObject})
42
+ const cli = await startRealClient({port: PORT})
43
+ const api = cli.api
44
+
45
+ // ===== 1. Callback-as-arg (Pkt.CB): all ticks arrive in order, rich Date =====
46
+ {
47
+ const got: Array<{i: number; when: Date}> = []
48
+ const ret = await api.counter.run({n: 4, callback: (i: number, when: Date) => { got.push({i, when}) }})
49
+ await delay(30) // let any trailing CB packets land after RESP
50
+ await check('cb-arg: return value = n', () => ret, 4)
51
+ await check('cb-arg: tick count', () => got.length, 4)
52
+ await check('cb-arg: indices in order', () => got.map(g => g.i), [0, 1, 2, 3])
53
+ await check('cb-arg: rich Date round-trips',
54
+ () => got.map(g => g.when),
55
+ [0, 1, 2, 3].map(i => new Date(Date.UTC(2026, 0, 1) + i * 1000)))
56
+ }
57
+
58
+ // ===== 2. listen stream: ticks arrive with rich values =====
59
+ {
60
+ const a: Array<{n: number; at: Date; tags: Set<string>}> = []
61
+ const off = (api as any).stream.callback((v: any) => a.push(v))
62
+ await delay(40) // subscription round-trips over the real socket
63
+ tick({n: 1, at: new Date('2026-03-03T00:00:00.000Z'), tags: new Set(['x', 'y'])})
64
+ tick({n: 2, at: new Date('2026-03-04T00:00:00.000Z'), tags: new Set(['z'])})
65
+ await delay(40)
66
+ await check('stream: tick count', () => a.length, 2)
67
+ await check('stream: rich values (Date+Set) round-trip',
68
+ () => a,
69
+ [
70
+ {n: 1, at: new Date('2026-03-03T00:00:00.000Z'), tags: new Set(['x', 'y'])},
71
+ {n: 2, at: new Date('2026-03-04T00:00:00.000Z'), tags: new Set(['z'])},
72
+ ])
73
+ // off() stops further ticks
74
+ off()
75
+ await delay(20)
76
+ tick({n: 3, at: new Date('2026-03-05T00:00:00.000Z'), tags: new Set()})
77
+ await delay(40)
78
+ await check('stream: off() stops further ticks', () => a.length, 2)
79
+ }
80
+
81
+ // ===== 3. MULTIPLE independent subscribers each get every tick =====
82
+ {
83
+ const b: number[] = [], c: number[] = []
84
+ const offB = (api as any).multi.callback((n: number, _s: string) => b.push(n))
85
+ await delay(20)
86
+ const offC = (api as any).multi.callback((n: number, _s: string) => c.push(n))
87
+ await delay(40)
88
+ multiTick(10, 'a')
89
+ multiTick(20, 'b')
90
+ await delay(40)
91
+ await check('multi-sub: first subscriber got every tick', () => b, [10, 20])
92
+ await check('multi-sub: second subscriber got every tick', () => c, [10, 20])
93
+ // drop one; the other keeps receiving
94
+ offB()
95
+ await delay(20)
96
+ multiTick(30, 'c')
97
+ await delay(40)
98
+ await check('multi-sub: dropped subscriber stops', () => b, [10, 20])
99
+ await check('multi-sub: surviving subscriber continues', () => c, [10, 20, 30])
100
+ offC()
101
+ await delay(20)
102
+ }
103
+
104
+ // ===== 4. CB_END / stream end resolves the off-handle (await off) =====
105
+ {
106
+ let resolved = false
107
+ const off = (api as any).stream.callback(() => {})
108
+ await delay(40)
109
+ const awaited = (async () => { await off; resolved = true })()
110
+ // close the server stream → CB_END → off-handle promise resolves
111
+ await api.endStream()
112
+ await Promise.race([awaited, delay(500)])
113
+ await check('stream-end: await off resolved on CB_END', () => resolved, true)
114
+ }
115
+
116
+ clearTimeout(watchdog)
117
+ cli.close()
118
+ await srv.close()
119
+ process.exit(done() === 0 ? 0 : 1)
120
+ }
121
+
122
+ main().catch(e => { console.error(e); process.exit(2) })
@@ -0,0 +1,124 @@
1
+ // ============================================================
2
+ // oracle/realsocket/caps.spec.ts — DISPOSABLE real-socket oracle
3
+ // CATEGORY "caps": capability NEGOTIATION over a REAL WebSocket.
4
+ //
5
+ // Verifies the handshake-negotiated optimization switch (opt.compact):
6
+ // 1) default (both new) → COMPACT negotiated → server uses Pkt.CBV (CBV>0).
7
+ // 2) client opt:{compact:false}→ client advertises NO COMPACT (silent) → server stays
8
+ // on plain Pkt.CB (CBV==0, CB>0). Ticks still intact.
9
+ // 3) server opt:{compact:false}→ server advertises caps without COMPACT → no compaction
10
+ // even though the client supports it (CBV==0, CB>0).
11
+ // In all cases data round-trips byte-correct (incl. Date), proving the optimization is
12
+ // purely a wire choice negotiated at handshake — outcome-equivalent either way.
13
+ // ============================================================
14
+ import {startRealServer, startRealClient, makeChecker, delay} from './_rs'
15
+ import {listen as createListenPair} from '../../src/Common/events/Listen'
16
+
17
+ const PORT = 4120
18
+
19
+ // one scenario: boot a server (opt = serverOpt) + connect a client (opt = clientOpt),
20
+ // fire N monomorphic ticks, count CBV(9)/CB(2) packets on the real server socket.
21
+ async function scenario(port: number, serverOpt?: {compact?: boolean}, clientOpt?: {compact?: boolean}) {
22
+ let emit: ((v: any) => void) | null = null
23
+ function makeObject() {
24
+ const [e, listen] = createListenPair<any>()
25
+ emit = e
26
+ return {stream: listen}
27
+ }
28
+ let cbv = 0, cb = 0
29
+ const srv = await startRealServer({
30
+ port, makeObject,
31
+ serverOpts: serverOpt ? {opt: serverOpt} : {},
32
+ onServer: (_api, socket) => {
33
+ const orig = socket.emit.bind(socket)
34
+ socket.emit = (key: string, d: any) => {
35
+ if (Array.isArray(d)) { if (d[0] === 9) cbv++; else if (d[0] === 2) cb++ }
36
+ return orig(key, d)
37
+ }
38
+ },
39
+ })
40
+ const cli = await startRealClient({port, opt: clientOpt})
41
+ const got: any[] = []
42
+ const off = cli.api.stream.callback((v: any) => got.push(v))
43
+ await delay(60) // subscription round-trip + CAPS handshake over the real socket
44
+ const N = 10
45
+ for (let i = 0; i < N; i++) { emit!({a: i, when: new Date(i * 1000), tag: 'x'}); await delay(8) }
46
+ await delay(120)
47
+ off()
48
+ await delay(20)
49
+ cli.close()
50
+ await srv.close()
51
+ return {got, cbv, cb, N}
52
+ }
53
+
54
+ async function main() {
55
+ const {check, done} = makeChecker('caps')
56
+ const watchdog = setTimeout(() => { console.error('WATCHDOG timeout'); process.exit(3) }, 60000)
57
+
58
+ // ---- 1) default: COMPACT negotiated (both peers new) ----
59
+ {
60
+ const r = await scenario(PORT)
61
+ await check('default: all ticks intact', () => r.got.length, r.N)
62
+ await check('default: last tick exact (Date)', () => [r.got[r.N - 1].a, r.got[r.N - 1].tag, r.got[r.N - 1].when], [r.N - 1, 'x', new Date((r.N - 1) * 1000)])
63
+ await check('default: compaction engaged (CBV>0)', () => r.cbv > 0, true)
64
+ }
65
+
66
+ // ---- 2) client opt:{compact:false}: client does NOT advertise → server plain CB ----
67
+ {
68
+ const r = await scenario(PORT + 1, undefined, {compact: false})
69
+ await check('client-off: all ticks intact', () => r.got.length, r.N)
70
+ await check('client-off: last tick exact (Date)', () => [r.got[r.N - 1].a, r.got[r.N - 1].when], [r.N - 1, new Date((r.N - 1) * 1000)])
71
+ await check('client-off: NO compaction (CBV==0)', () => r.cbv, 0)
72
+ await check('client-off: server sent plain CB (CB>0)', () => r.cb > 0, true)
73
+ }
74
+
75
+ // ---- 3) server opt:{compact:false}: server advertises no COMPACT → no compaction ----
76
+ {
77
+ const r = await scenario(PORT + 2, {compact: false})
78
+ await check('server-off: all ticks intact', () => r.got.length, r.N)
79
+ await check('server-off: last tick exact (Date)', () => [r.got[r.N - 1].a, r.got[r.N - 1].when], [r.N - 1, new Date((r.N - 1) * 1000)])
80
+ await check('server-off: NO compaction (CBV==0)', () => r.cbv, 0)
81
+ await check('server-off: server sent plain CB (CB>0)', () => r.cb > 0, true)
82
+ }
83
+
84
+ // ---- 4) subscribe via .on(cb) over the REAL web (idiomatic alias of .callback) ----
85
+ {
86
+ let emit: ((v: any) => void) | null = null
87
+ const srv = await startRealServer({port: PORT + 3, makeObject: () => { const [e, l] = createListenPair<any>(); emit = e; return {stream: l} }})
88
+ const cli = await startRealClient({port: PORT + 3})
89
+ const got: any[] = []
90
+ const off = (cli.api.stream as any).on((v: any) => got.push(v)) // .on вместо .callback — «факт установки колбэка»
91
+ await delay(60)
92
+ for (let i = 0; i < 5; i++) { emit!({n: i}); await delay(8) }
93
+ await delay(100)
94
+ await check('on-web: .on(cb) subscribed + streamed', () => got.map((v: any) => v.n), [0, 1, 2, 3, 4])
95
+ if (typeof off === 'function') off()
96
+ await delay(20)
97
+ cli.close()
98
+ await srv.close()
99
+ }
100
+
101
+ // ---- 5) bare-on exposure ({ stream: listen.on }) + once over the REAL web ----
102
+ {
103
+ let emit: ((v: any) => void) | null = null
104
+ const srv = await startRealServer({port: PORT + 4, makeObject: () => { const [e, l] = createListenPair<any>(); emit = e; return {stream: (l as any).on} }})
105
+ const cli = await startRealClient({port: PORT + 4})
106
+ const got: any[] = []
107
+ ;(cli.api.stream as any).on((v: any) => got.push(v)) // exposed ТОЛЬКО listen.on → клиент получил подписку
108
+ await delay(60)
109
+ for (let i = 0; i < 4; i++) { emit!({n: i}); await delay(8) }
110
+ await delay(100)
111
+ await check('bare-on: { stream: listen.on } streams over real web', () => got.map((v: any) => v.n), [0, 1, 2, 3])
112
+ const one: any[] = []
113
+ ;(cli.api.stream as any).once((v: any) => one.push(v))
114
+ await delay(40); emit!({n: 99}); await delay(8); emit!({n: 100}); await delay(50)
115
+ await check('once: exactly one event over real web', () => one.map((v: any) => v.n), [99])
116
+ cli.close()
117
+ await srv.close()
118
+ }
119
+
120
+ clearTimeout(watchdog)
121
+ process.exit(done() === 0 ? 0 : 1)
122
+ }
123
+
124
+ main().catch(e => { console.error(e); process.exit(2) })
@@ -0,0 +1,49 @@
1
+ // REAL-SOCKET core: CALL + rich-type round-trip both ways. Port 4101.
2
+ import {startRealServer, startRealClient, makeChecker, delay} from './_rs'
3
+
4
+ const PORT = 4101
5
+
6
+ function makeObject() {
7
+ return {
8
+ math: {
9
+ add: async (a: number, b: number) => a + b,
10
+ mul: async (a: number, b: number) => a * b,
11
+ },
12
+ echo: async (v: any) => v,
13
+ rich: async () => ({
14
+ when: new Date(123456),
15
+ map: new Map<string, number>([['a', 1], ['b', 2]]),
16
+ set: new Set([1, 2, 3]),
17
+ big: 9007199254740993n,
18
+ }),
19
+ roundtrip: async (d: Date, m: Map<string, number>) => ({d, m, n: (m.get('x') ?? 0) + 1}),
20
+ }
21
+ }
22
+
23
+ async function main() {
24
+ const {check, done} = makeChecker('core')
25
+ const srv = await startRealServer({port: PORT, makeObject})
26
+ const cli = await startRealClient<ReturnType<typeof makeObject>>({port: PORT})
27
+ const api = cli.api
28
+
29
+ await check('CALL math.add', () => api.math.add(5, 7), 12)
30
+ await check('CALL math.mul', () => api.math.mul(3, 4), 12)
31
+ await check('CALL echo primitive', () => api.echo('hello'), 'hello')
32
+ await check('CALL echo nested obj', () => api.echo({a: [1, 2], b: {c: true}}), {a: [1, 2], b: {c: true}})
33
+
34
+ await check('rich: Date round-trips', async () => (await api.rich()).when, new Date(123456))
35
+ await check('rich: Map round-trips', async () => [...(await api.rich()).map], [['a', 1], ['b', 2]])
36
+ await check('rich: Set round-trips', async () => [...(await api.rich()).set], [1, 2, 3])
37
+ await check('rich: BigInt round-trips', async () => (await api.rich()).big === 9007199254740993n, true)
38
+
39
+ await check('arg→server: Date+Map sent up', async () => {
40
+ const r = await api.roundtrip(new Date(999), new Map([['x', 41]]))
41
+ return [r.d, [...r.m], r.n]
42
+ }, [new Date(999), [['x', 41]], 42])
43
+
44
+ cli.close()
45
+ await srv.close()
46
+ process.exit(done() === 0 ? 0 : 1)
47
+ }
48
+
49
+ main().catch(e => { console.error(e); process.exit(2) })