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.
- package/README.md +12 -1
- package/demo/client.ts +100 -0
- package/demo/index.html +28 -0
- package/demo/server.ts +43 -0
- package/doc/ROADMAP.md +9 -3
- package/doc/changes/1.0.69.md +11 -0
- package/doc/changes/1.0.70.md +4 -0
- package/doc/wenay-common2-rare.md +8 -0
- package/doc/wenay-common2.md +42 -6
- package/lib/Common/events/route-signal-webrtc.js +4 -2
- package/lib/Common/peer/peer-client.d.ts +46 -0
- package/lib/Common/peer/peer-client.js +108 -0
- package/lib/Common/peer/peer-host.d.ts +32 -0
- package/lib/Common/peer/peer-host.js +45 -0
- package/lib/Common/peer/peer-index.d.ts +3 -0
- package/lib/Common/peer/peer-index.js +19 -0
- package/lib/Common/peer/peer-relay.d.ts +14 -0
- package/lib/Common/peer/peer-relay.js +77 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.js +2 -1
- package/observe/PLAN.md +131 -0
- package/observe/README.md +226 -0
- package/observe/hot-write.test.ts +95 -0
- package/observe/listen-core.test.ts +66 -0
- package/observe/listen-store.test.ts +56 -0
- package/observe/listen.test.ts +92 -0
- package/observe/reactive.test.ts +270 -0
- package/observe/reactive.ts +1 -0
- package/observe/store-manager.test.ts +118 -0
- package/observe/store-mirror.example.ts +74 -0
- package/observe/store.test.ts +235 -0
- package/observe/store.ts +1 -0
- package/observe/usage-real-socket.ts +174 -0
- package/observe/usage.ts +200 -0
- package/oracle/PASSED.md +27 -0
- package/oracle/README.md +12 -0
- package/oracle/fixes-primitives.spec.ts +90 -0
- package/oracle/realsocket/_rs.ts +106 -0
- package/oracle/realsocket/auth.spec.ts +91 -0
- package/oracle/realsocket/callbacks.spec.ts +122 -0
- package/oracle/realsocket/caps.spec.ts +124 -0
- package/oracle/realsocket/core.spec.ts +49 -0
- package/oracle/realsocket/dedupe.spec.ts +142 -0
- package/oracle/realsocket/errors.spec.ts +121 -0
- package/oracle/realsocket/lifecycle.spec.ts +128 -0
- package/oracle/realsocket/limits.spec.ts +98 -0
- package/oracle/realsocket/pipe.spec.ts +101 -0
- package/oracle/realsocket/shape.spec.ts +124 -0
- package/oracle/realsocket/slimv2.spec.ts +116 -0
- package/oracle/realsocket/stress.spec.ts +132 -0
- package/oracle/regression/_clientapiall-replay.fixture.ts +57 -0
- package/oracle/regression/async-queues.spec.ts +254 -0
- package/oracle/regression/bytestream.spec.ts +152 -0
- package/oracle/regression/clientapiall-replay-types.spec.ts +47 -0
- package/oracle/regression/core-clone-equal.spec.ts +124 -0
- package/oracle/regression/data-structures.spec.ts +135 -0
- package/oracle/regression/listen-events.spec.ts +206 -0
- package/oracle/regression/observe-core.spec.ts +278 -0
- package/oracle/regression/package-export.spec.ts +120 -0
- package/oracle/regression/rpc-dedupe-callbacks.spec.ts +195 -0
- package/oracle/regression/rpc-lifecycle.spec.ts +150 -0
- package/oracle/regression/store-each.spec.ts +209 -0
- package/package.json +8 -2
- package/replay/PLAN.md +171 -0
- package/replay/canvas-socket.test.ts +187 -0
- package/replay/coalesce.test.ts +260 -0
- package/replay/conflate-socket.test.ts +288 -0
- package/replay/conflate.test.ts +225 -0
- package/replay/history.test.ts +222 -0
- package/replay/media-socket.test.ts +157 -0
- package/replay/offline-store-socket.test.ts +290 -0
- package/replay/offline-store.test.ts +156 -0
- package/replay/peer-sdk.test.ts +228 -0
- package/replay/replay-listen.test.ts +156 -0
- package/replay/route-coordinator.test.ts +314 -0
- package/replay/route-handoff.test.ts +150 -0
- package/replay/route-webrtc.test.ts +321 -0
- package/replay/rpc-auto.test.ts +256 -0
- package/replay/socket-replay.test.ts +199 -0
- package/replay/staleness.test.ts +176 -0
- package/replay/store-replay.test.ts +151 -0
- package/replay/video-socket.demo.ts +200 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import {createStore, createStoreMirror, exposeStore} from './store'
|
|
2
|
+
import {flushReactive} from './reactive'
|
|
3
|
+
|
|
4
|
+
type Market = {
|
|
5
|
+
data: { BTC?: number; ETH?: number; SOL?: number }
|
|
6
|
+
meta: { status?: string }
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
let fails = 0
|
|
10
|
+
const ok = (c: any, m: string) => { if (!c) { fails++; console.log(' FAIL', m) } else console.log(' OK ', m) }
|
|
11
|
+
const tick = () => new Promise<void>(resolve => setTimeout(resolve, 0))
|
|
12
|
+
|
|
13
|
+
async function main() {
|
|
14
|
+
console.log('\n[store] primitive node current/on/once')
|
|
15
|
+
{
|
|
16
|
+
const store = createStore<Market>({data: {BTC: 1, ETH: 2}, meta: {status: 'ok'}}, {drain: 'micro'})
|
|
17
|
+
let current = 0
|
|
18
|
+
store.node.data.BTC.on(v => { current = v ?? 0 }, {current: true})
|
|
19
|
+
ok(current == 1, 'primitive leaf on(current) reads value')
|
|
20
|
+
const seen: any[] = []
|
|
21
|
+
store.node.data.BTC.once(v => seen.push(v), {current: true})
|
|
22
|
+
store.state.data.BTC = 3
|
|
23
|
+
await flushReactive(store.state)
|
|
24
|
+
ok(JSON.stringify(seen) == '[1]', 'primitive once(current) uses current store value')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
console.log('\n[store] path survives branch replacement')
|
|
28
|
+
{
|
|
29
|
+
const store = createStore<Market>({data: {BTC: 1, ETH: 2}, meta: {}}, {drain: 'micro'})
|
|
30
|
+
const got: any[] = []
|
|
31
|
+
store.node.data.BTC.on(v => got.push(v))
|
|
32
|
+
store.state.data = {BTC: 10, SOL: 5}
|
|
33
|
+
await flushReactive(store.state)
|
|
34
|
+
store.state.data.BTC = 11
|
|
35
|
+
await flushReactive(store.state)
|
|
36
|
+
ok(JSON.stringify(got) == '[10,11]', 'leaf subscription follows same path after whole branch replace: ' + JSON.stringify(got))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
console.log('\n[store] branch drain and current')
|
|
40
|
+
{
|
|
41
|
+
const store = createStore<Market>({data: {BTC: 1, ETH: 2}, meta: {}}, {drain: 'micro'})
|
|
42
|
+
const got: any[] = []
|
|
43
|
+
store.node.data.on(v => got.push({...v}), {current: true, drain: 'micro'})
|
|
44
|
+
store.state.data.BTC = 3
|
|
45
|
+
store.state.data.ETH = 4
|
|
46
|
+
await flushReactive(store.state)
|
|
47
|
+
await tick()
|
|
48
|
+
ok(got.length == 2 && got[0].BTC == 1 && got[1].BTC == 3 && got[1].ETH == 4, 'branch current + drained snapshot')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
console.log('\n[store] typed mask selection and routes')
|
|
52
|
+
{
|
|
53
|
+
const store = createStore<Market>({data: {BTC: 1, ETH: 2, SOL: 3}, meta: {status: 'ok'}}, {drain: 'micro'})
|
|
54
|
+
const sel = store.update({data: {BTC: true, ETH: true}, meta: {status: true}}, {current: true})
|
|
55
|
+
const snaps: any[] = []
|
|
56
|
+
const each: string[] = []
|
|
57
|
+
sel.on(v => snaps.push(v))
|
|
58
|
+
sel.onEach((v, ctx) => each.push(ctx.pathString + '=' + v))
|
|
59
|
+
store.state.data.BTC = 10
|
|
60
|
+
store.state.meta.status = 'warn'
|
|
61
|
+
await flushReactive(store.state)
|
|
62
|
+
await tick()
|
|
63
|
+
ok(snaps.length == 2 && snaps[0].data.BTC == 1 && snaps[1].data.BTC == 10 && snaps[1].meta.status == 'warn', 'selection sends current + one coalesced update')
|
|
64
|
+
ok(each.includes('data.BTC=10') && each.includes('meta.status=warn'), 'onEach exposes route strings: ' + JSON.stringify(each))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
console.log('\n[store] exposeStore + createStoreMirror sync')
|
|
68
|
+
{
|
|
69
|
+
const server = createStore<Market>({data: {BTC: 1, ETH: 2, SOL: 3}, meta: {status: 'ok'}}, {drain: 'micro'})
|
|
70
|
+
const api = exposeStore(server)
|
|
71
|
+
const mirror = createStoreMirror<Market>(api as any, {data: {}, meta: {}}, {drain: 'micro'})
|
|
72
|
+
await mirror.sync({data: {BTC: true}, meta: {status: true}}, {current: true, drain: 'micro'})
|
|
73
|
+
ok(mirror.state.data.BTC == 1 && mirror.state.data.ETH === undefined && mirror.state.meta.status == 'ok', 'initial masked mirror snapshot')
|
|
74
|
+
let btc = 0
|
|
75
|
+
mirror.node.data.BTC.on(v => { btc = v ?? 0 }, {current: true})
|
|
76
|
+
server.state.data.BTC = 9
|
|
77
|
+
server.state.data.ETH = 20
|
|
78
|
+
server.state.meta.status = 'warn'
|
|
79
|
+
await flushReactive(server.state)
|
|
80
|
+
await tick(); await tick()
|
|
81
|
+
ok(mirror.state.data.BTC == 9 && mirror.state.data.ETH === undefined && mirror.state.meta.status == 'warn', 'mirror updates only selected paths')
|
|
82
|
+
ok(btc == 9, 'mirror local node subscription fires from sync')
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
console.log('\n[store] changedPaths lets mirror pull only dirty selected paths')
|
|
86
|
+
{
|
|
87
|
+
const server = createStore<any>({strategies: {a: {status: false}}, meta: {status: 'ok'}}, {drain: 'micro'})
|
|
88
|
+
const exposed = exposeStore(server) as any
|
|
89
|
+
const masks: any[] = []
|
|
90
|
+
const api = {...exposed, get: (mask?: any) => { masks.push(mask); return exposed.get(mask) }}
|
|
91
|
+
const mirror = createStoreMirror<any>(api, {strategies: {}, meta: {}}, {drain: 'micro'})
|
|
92
|
+
await mirror.sync({strategies: true, meta: {status: true}}, {current: true, drain: 'micro'})
|
|
93
|
+
masks.length = 0
|
|
94
|
+
|
|
95
|
+
server.state.strategies.a.status = true
|
|
96
|
+
await flushReactive(server.state)
|
|
97
|
+
await tick(); await tick()
|
|
98
|
+
ok(mirror.state.strategies.a.status === true, 'deep strategy update reaches mirror')
|
|
99
|
+
ok(JSON.stringify(masks[masks.length - 1]) == '{"strategies":{"a":{"status":true}}}', 'deep update pulled dirty path only: ' + JSON.stringify(masks[masks.length - 1]))
|
|
100
|
+
|
|
101
|
+
server.state.strategies.b = {status: true}
|
|
102
|
+
await flushReactive(server.state)
|
|
103
|
+
await tick(); await tick()
|
|
104
|
+
ok(mirror.state.strategies.b.status === true, 'added strategy reaches mirror')
|
|
105
|
+
ok(JSON.stringify(masks[masks.length - 1]) == '{"strategies":{"b":true}}', 'add pulled added branch only: ' + JSON.stringify(masks[masks.length - 1]))
|
|
106
|
+
|
|
107
|
+
delete server.state.strategies.b
|
|
108
|
+
await flushReactive(server.state)
|
|
109
|
+
await tick(); await tick()
|
|
110
|
+
ok(!('b' in mirror.state.strategies), 'removed strategy is deleted from mirror')
|
|
111
|
+
ok(JSON.stringify(masks[masks.length - 1]) == '{"strategies":{"b":true}}', 'delete pulled removed branch only: ' + JSON.stringify(masks[masks.length - 1]))
|
|
112
|
+
|
|
113
|
+
server.state.meta.status = 'warn'
|
|
114
|
+
await flushReactive(server.state)
|
|
115
|
+
await tick(); await tick()
|
|
116
|
+
ok(mirror.state.meta.status == 'warn', 'non-strategy selected path still syncs')
|
|
117
|
+
ok(JSON.stringify(masks[masks.length - 1]) == '{"meta":{"status":true}}', 'meta update pulled selected leaf only: ' + JSON.stringify(masks[masks.length - 1]))
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
console.log('\n[store] optional push patches sync')
|
|
121
|
+
{
|
|
122
|
+
const server = createStore<Market>({data: {BTC: 1, ETH: 2, SOL: 3}, meta: {status: 'ok'}}, {drain: 'micro'})
|
|
123
|
+
const exposed = exposeStore(server, {push: true}) as any
|
|
124
|
+
const pulls: any[] = []
|
|
125
|
+
const api = {...exposed, get: (mask?: any) => { pulls.push(mask); return exposed.get(mask) }}
|
|
126
|
+
const mirror = createStoreMirror<Market>(api, {data: {}, meta: {}}, {drain: 'micro'})
|
|
127
|
+
await mirror.syncPatches({data: {BTC: true}, meta: {status: true}}, {current: true, drain: 'micro'})
|
|
128
|
+
ok(mirror.state.data.BTC == 1 && mirror.state.data.ETH === undefined && mirror.state.meta.status == 'ok', 'patch sync initial masked snapshot')
|
|
129
|
+
pulls.length = 0
|
|
130
|
+
|
|
131
|
+
server.state.data = {BTC: 7, ETH: 8, SOL: 9}
|
|
132
|
+
await flushReactive(server.state)
|
|
133
|
+
await tick(); await tick()
|
|
134
|
+
ok(mirror.state.data.BTC == 7 && mirror.state.data.ETH === undefined && mirror.state.data.SOL === undefined, 'patch branch replace applies only selected leaves')
|
|
135
|
+
ok(pulls.length == 0, 'patch sync does not pull after push event')
|
|
136
|
+
|
|
137
|
+
delete server.state.meta.status
|
|
138
|
+
await flushReactive(server.state)
|
|
139
|
+
await tick(); await tick()
|
|
140
|
+
ok(!('status' in mirror.state.meta), 'patch delete removes selected leaf')
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
console.log('\n[store] optional changedData sync')
|
|
144
|
+
{
|
|
145
|
+
const server = createStore<Market>({data: {BTC: 1, ETH: 2}, meta: {status: 'ok'}}, {drain: 'micro'})
|
|
146
|
+
const exposed = exposeStore(server, {push: true}) as any
|
|
147
|
+
const pulls: any[] = []
|
|
148
|
+
const api = {...exposed, get: (mask?: any) => { pulls.push(mask); return exposed.get(mask) }}
|
|
149
|
+
const mirror = createStoreMirror<Market>(api, {data: {}, meta: {}}, {drain: 'micro'})
|
|
150
|
+
await mirror.syncChangedData({data: {BTC: true}}, {current: true, drain: 'micro'})
|
|
151
|
+
pulls.length = 0
|
|
152
|
+
|
|
153
|
+
server.state.data = {BTC: 5, ETH: 10}
|
|
154
|
+
await flushReactive(server.state)
|
|
155
|
+
await tick(); await tick()
|
|
156
|
+
ok(mirror.state.data.BTC == 5 && mirror.state.data.ETH === undefined, 'changedData branch replace respects selected mask')
|
|
157
|
+
ok(pulls.length == 0, 'changedData sync does not pull after push event')
|
|
158
|
+
|
|
159
|
+
server.state.data.BTC = 6
|
|
160
|
+
server.state.meta.status = 'warn'
|
|
161
|
+
await flushReactive(server.state)
|
|
162
|
+
await tick(); await tick()
|
|
163
|
+
ok(mirror.state.data.BTC == 6 && mirror.state.meta.status === undefined, 'changedData ignores unselected dirty paths')
|
|
164
|
+
}
|
|
165
|
+
console.log('\n[store] node identity handles dotted keys, symbols, and once(current)')
|
|
166
|
+
{
|
|
167
|
+
const store = createStore<any>({data: {BTC: 1}}, {drain: 'micro'})
|
|
168
|
+
const symA = Symbol('x')
|
|
169
|
+
const symB = Symbol('x')
|
|
170
|
+
ok(store.node.at('a.b') !== store.node.at('a').at('b'), 'internal node cache separates dotted key from nested path')
|
|
171
|
+
ok(store.node.at(symA) !== store.node.at(symB), 'internal node cache separates distinct symbols with same description')
|
|
172
|
+
|
|
173
|
+
let hits = 0
|
|
174
|
+
const off = store.update({data: {BTC: true}}, {current: true}).once(() => hits++, {current: true})
|
|
175
|
+
store.state.data.BTC = 2
|
|
176
|
+
await flushReactive(store.state)
|
|
177
|
+
await tick()
|
|
178
|
+
off()
|
|
179
|
+
ok(hits == 1, 'selection.once(current) fires once and unsubscribes')
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
console.log('\n[store] snapshot: circular Map/Set and shared references')
|
|
183
|
+
{
|
|
184
|
+
const m = new Map<any, any>()
|
|
185
|
+
m.set('self', m)
|
|
186
|
+
const s = new Set<any>()
|
|
187
|
+
s.add(s)
|
|
188
|
+
const shared = new Map<any, any>([['x', 1]])
|
|
189
|
+
const store = createStore<any>({m, s, a: {shared}, b: {shared}}, {drain: 'micro'})
|
|
190
|
+
const snap = store.snapshot()
|
|
191
|
+
ok(snap.m instanceof Map && snap.m.get('self') === snap.m, 'self-containing Map snapshots as a cycle, no infinite recursion')
|
|
192
|
+
ok(snap.s instanceof Set && snap.s.has(snap.s), 'self-containing Set snapshots as a cycle')
|
|
193
|
+
ok(snap.a.shared === snap.b.shared && snap.a.shared.get('x') == 1, 'shared Map keeps ONE copy identity in the snapshot')
|
|
194
|
+
ok(snap.m !== m && snap.a.shared !== shared, 'snapshot copies, does not alias the originals')
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
console.log('\n[store] mirror sync error path: failed pull reports, chain survives')
|
|
198
|
+
{
|
|
199
|
+
const settle = async () => { for (let i = 0; i < 5; i++) await tick() }
|
|
200
|
+
const server = createStore<Market>({data: {BTC: 1, ETH: 2}, meta: {status: 'ok'}}, {drain: 'micro'})
|
|
201
|
+
const exposed = exposeStore(server) as any
|
|
202
|
+
let failNext = false
|
|
203
|
+
const api = {...exposed, get: (mask?: any) => {
|
|
204
|
+
if (failNext) throw new Error('pull boom')
|
|
205
|
+
return exposed.get(mask)
|
|
206
|
+
}}
|
|
207
|
+
const mirror = createStoreMirror<Market>(api, {data: {}, meta: {}}, {drain: 'micro'})
|
|
208
|
+
const errors: any[] = []
|
|
209
|
+
const stop = await mirror.sync({data: {BTC: true}}, {current: true, drain: 'micro', onError: e => errors.push(e)})
|
|
210
|
+
ok(mirror.state.data.BTC == 1, 'initial pull ok')
|
|
211
|
+
|
|
212
|
+
failNext = true
|
|
213
|
+
server.state.data.BTC = 2
|
|
214
|
+
await flushReactive(server.state)
|
|
215
|
+
await settle()
|
|
216
|
+
ok(errors.length == 1 && String(errors[0]).includes('pull boom'), 'failed re-pull lands in onError, not as an unhandled rejection')
|
|
217
|
+
|
|
218
|
+
failNext = false
|
|
219
|
+
server.state.data.BTC = 3
|
|
220
|
+
await flushReactive(server.state)
|
|
221
|
+
await settle()
|
|
222
|
+
ok(mirror.state.data.BTC == 3, 'sync chain survives the failure and catches up on the next change')
|
|
223
|
+
stop()
|
|
224
|
+
|
|
225
|
+
failNext = true
|
|
226
|
+
let threw = false
|
|
227
|
+
try { await mirror.sync({data: {BTC: true}}, {current: true}) } catch { threw = true }
|
|
228
|
+
ok(threw, 'initial pull failure rejects the awaited sync() itself')
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
console.log(`\n${fails == 0 ? 'ALL GREEN' : fails + ' FAILURE(S)'}`)
|
|
232
|
+
process.exit(fails == 0 ? 0 : 1)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
main().catch(e => { console.error(e); process.exit(1) })
|
package/observe/store.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '../src/Common/Observe/store';
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// observe/usage-real-socket.ts
|
|
3
|
+
//
|
|
4
|
+
// Тот же store mirror add/delete, но через настоящий Socket.IO
|
|
5
|
+
// localhost WebSocket, а не in-memory loopback.
|
|
6
|
+
// Запуск:
|
|
7
|
+
// npx tsx observe/usage-real-socket.ts
|
|
8
|
+
// ============================================================
|
|
9
|
+
|
|
10
|
+
import express from 'express'
|
|
11
|
+
import {createServer} from 'http'
|
|
12
|
+
import type {AddressInfo} from 'net'
|
|
13
|
+
import {Server as SocketIOServer} from 'socket.io'
|
|
14
|
+
import {io} from 'socket.io-client'
|
|
15
|
+
import {listen as createListenPair} from '../src/Common/events/Listen'
|
|
16
|
+
import {createRpcClientHub} from '../src/Common/rcp/rpc-clientHub'
|
|
17
|
+
import {createRpcServerAuto} from '../src/Common/rcp/rpc-server-auto'
|
|
18
|
+
import type {DeepSocketListen} from '../src/Common/rcp/listen-deep'
|
|
19
|
+
import {createStore, createStoreMirror, exposeStore} from './store'
|
|
20
|
+
import {flushReactive} from './reactive'
|
|
21
|
+
|
|
22
|
+
type Strategy = {
|
|
23
|
+
status: boolean
|
|
24
|
+
limit: number
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type StrategyStore = {
|
|
28
|
+
strategies: Record<string, Strategy>
|
|
29
|
+
meta: {status: string}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const delay = (ms: number) => new Promise<void>(resolve => setTimeout(resolve, ms))
|
|
33
|
+
const keys = (state: StrategyStore) => Object.keys(state.strategies).sort()
|
|
34
|
+
const json = (v: any) => JSON.stringify(v)
|
|
35
|
+
|
|
36
|
+
function scenario(title: string, expected: string) {
|
|
37
|
+
console.log(`\n ${title}`)
|
|
38
|
+
console.log(` ожидаем: ${expected}`)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function expectEq(label: string, got: any, expected: any) {
|
|
42
|
+
const g = json(got)
|
|
43
|
+
const e = json(expected)
|
|
44
|
+
console.log(` ${label}:`, g)
|
|
45
|
+
if (g !== e) throw new Error(`${label}: expected ${e}, got ${g}`)
|
|
46
|
+
console.log(' OK')
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function waitFor(label: string, ok: () => boolean) {
|
|
50
|
+
for (let i = 0; i < 80; i++) {
|
|
51
|
+
if (ok()) return
|
|
52
|
+
await delay(25)
|
|
53
|
+
}
|
|
54
|
+
throw new Error(`timeout: ${label}`)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function startRealServer(object: object) {
|
|
58
|
+
const app = express()
|
|
59
|
+
const httpServer = createServer(app)
|
|
60
|
+
const ioServer = new SocketIOServer(httpServer, {maxHttpBufferSize: 1e8})
|
|
61
|
+
|
|
62
|
+
ioServer.on('connection', socket => {
|
|
63
|
+
console.log(' [server] client connected')
|
|
64
|
+
const [disconnect, disconnectListen] = createListenPair()
|
|
65
|
+
socket.on('disconnect', () => disconnect())
|
|
66
|
+
createRpcServerAuto({
|
|
67
|
+
socket: {
|
|
68
|
+
emit: (key, data) => socket.emit(key, data),
|
|
69
|
+
on: (key, cb) => socket.on(key, cb),
|
|
70
|
+
},
|
|
71
|
+
socketKey: 'store2',
|
|
72
|
+
object,
|
|
73
|
+
disconnectListen,
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
await new Promise<void>((resolve, reject) => {
|
|
78
|
+
httpServer.once('error', reject)
|
|
79
|
+
httpServer.listen(0, '127.0.0.1', resolve)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
const port = (httpServer.address() as AddressInfo).port
|
|
83
|
+
console.log(` [server] listening on 127.0.0.1:${port}`)
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
port,
|
|
87
|
+
close: () => new Promise<void>(resolve => {
|
|
88
|
+
ioServer.close()
|
|
89
|
+
httpServer.close(() => resolve())
|
|
90
|
+
}),
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function startRealClient<T extends object>(port: number) {
|
|
95
|
+
const hub = createRpcClientHub(
|
|
96
|
+
() => io(`http://127.0.0.1:${port}`, {transports: ['websocket'], forceNew: true}),
|
|
97
|
+
r => ({api: r<T>('store2')}) as const,
|
|
98
|
+
)
|
|
99
|
+
const clients = await hub.setToken(null)
|
|
100
|
+
await clients.api.readyStrict()
|
|
101
|
+
console.log(' [client] connected and schema is ready')
|
|
102
|
+
return {
|
|
103
|
+
client: clients.api,
|
|
104
|
+
close: () => hub.socket?.disconnect?.(),
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function main() {
|
|
109
|
+
console.log('\n[real socket] Observe store mirror add/delete')
|
|
110
|
+
|
|
111
|
+
const backend = createStore<StrategyStore>({
|
|
112
|
+
strategies: {alpha: {status: false, limit: 100}},
|
|
113
|
+
meta: {status: 'ok'},
|
|
114
|
+
}, {drain: 'micro'})
|
|
115
|
+
|
|
116
|
+
const exposed = exposeStore(backend)
|
|
117
|
+
const pulls: any[] = []
|
|
118
|
+
const facade = {
|
|
119
|
+
...exposed,
|
|
120
|
+
get(mask?: any) {
|
|
121
|
+
pulls.push(mask)
|
|
122
|
+
console.log(' [server] get mask =', json(mask))
|
|
123
|
+
return exposed.get(mask)
|
|
124
|
+
},
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
let server: Awaited<ReturnType<typeof startRealServer>> | null = null
|
|
128
|
+
let client: Awaited<ReturnType<typeof startRealClient<typeof facade>>> | null = null
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
server = await startRealServer(facade)
|
|
132
|
+
client = await startRealClient<typeof facade>(server.port)
|
|
133
|
+
|
|
134
|
+
const listen = client.client.func as unknown as DeepSocketListen<typeof facade>
|
|
135
|
+
const remote = {
|
|
136
|
+
get: (mask?: any) => client!.client.func.get(mask),
|
|
137
|
+
changed: listen.changed,
|
|
138
|
+
changedPaths: listen.changedPaths,
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const mirror = createStoreMirror<StrategyStore>(remote, {strategies: {}, meta: {status: ''}}, {drain: 'micro'})
|
|
142
|
+
const stopSync = await mirror.sync({strategies: true}, {current: true, drain: 'micro'})
|
|
143
|
+
await waitFor('initial mirror sync over socket', () => 'alpha' in mirror.state.strategies)
|
|
144
|
+
await delay(80) // даем сетевой подписке changedPaths точно встать
|
|
145
|
+
pulls.length = 0
|
|
146
|
+
|
|
147
|
+
scenario('case A: server добавляет beta', 'client mirror увидит alpha,beta; по сети уйдет pull mask {strategies:{beta:true}}')
|
|
148
|
+
backend.state.strategies.beta = {status: true, limit: 200}
|
|
149
|
+
await flushReactive(backend.state)
|
|
150
|
+
await waitFor('client mirror got beta', () => 'beta' in mirror.state.strategies)
|
|
151
|
+
expectEq('client mirror keys после add', keys(mirror.state), ['alpha', 'beta'])
|
|
152
|
+
expectEq('server last get mask после add', pulls[pulls.length - 1], {strategies: {beta: true}})
|
|
153
|
+
|
|
154
|
+
scenario('case B: server удаляет beta', 'client mirror удалит beta; по сети уйдет pull mask {strategies:{beta:true}}')
|
|
155
|
+
pulls.length = 0
|
|
156
|
+
delete backend.state.strategies.beta
|
|
157
|
+
await flushReactive(backend.state)
|
|
158
|
+
await waitFor('client mirror removed beta', () => !('beta' in mirror.state.strategies))
|
|
159
|
+
expectEq('client mirror keys после delete', keys(mirror.state), ['alpha'])
|
|
160
|
+
expectEq('server last get mask после delete', pulls[pulls.length - 1], {strategies: {beta: true}})
|
|
161
|
+
|
|
162
|
+
stopSync()
|
|
163
|
+
console.log('\nALL OK: real socket usage example passed')
|
|
164
|
+
} finally {
|
|
165
|
+
client?.close()
|
|
166
|
+
await delay(20)
|
|
167
|
+
await server?.close()
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
main().catch(e => {
|
|
172
|
+
console.error(e)
|
|
173
|
+
process.exit(1)
|
|
174
|
+
})
|
package/observe/usage.ts
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// observe/usage.ts
|
|
3
|
+
//
|
|
4
|
+
// Локальные self-check примеры для Observe / reactive.
|
|
5
|
+
// Запуск:
|
|
6
|
+
// npx tsx observe/usage.ts
|
|
7
|
+
// ============================================================
|
|
8
|
+
|
|
9
|
+
import {flushReactive, onUpdate, onUpdatePaths, reactive} from './reactive'
|
|
10
|
+
import {createStore, createStoreMirror, exposeStore} from './store'
|
|
11
|
+
|
|
12
|
+
type Coin = 'BTC' | 'ETH' | 'SOL' | 'DOT' | 'ADA'
|
|
13
|
+
|
|
14
|
+
type Position = {
|
|
15
|
+
qty: number
|
|
16
|
+
entry: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type AccountState = {
|
|
20
|
+
account: {
|
|
21
|
+
balances: Partial<Record<Coin, number>>
|
|
22
|
+
positions: Partial<Record<Coin, Position>>
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type Strategy = {
|
|
27
|
+
status: boolean
|
|
28
|
+
limit: number
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type StrategyStore = {
|
|
32
|
+
strategies: Record<string, Strategy>
|
|
33
|
+
meta: {status: string}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const tick = () => new Promise<void>(resolve => setTimeout(resolve, 0))
|
|
37
|
+
const coinKeys = (positions: Partial<Record<Coin, Position>>) => Object.keys(positions).sort()
|
|
38
|
+
const strategyKeys = (state: StrategyStore) => Object.keys(state.strategies).sort()
|
|
39
|
+
const pathText = (path: PropertyKey[]) => path.map(String).join('.') || '<self>'
|
|
40
|
+
const json = (v: any) => JSON.stringify(v)
|
|
41
|
+
|
|
42
|
+
function section(title: string) {
|
|
43
|
+
console.log(`\n${title}`)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function scenario(title: string, expected: string) {
|
|
47
|
+
console.log(`\n ${title}`)
|
|
48
|
+
console.log(` ожидаем: ${expected}`)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function expectEq(label: string, got: any, expected: any) {
|
|
52
|
+
const g = json(got)
|
|
53
|
+
const e = json(expected)
|
|
54
|
+
console.log(` ${label}:`, g)
|
|
55
|
+
if (g !== e) throw new Error(`${label}: expected ${e}, got ${g}`)
|
|
56
|
+
console.log(' OK')
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function expectThrow(label: string, fn: () => void, expectedPart: string) {
|
|
60
|
+
console.log(`\n ${label}`)
|
|
61
|
+
console.log(` ожидаем: ошибка содержит "${expectedPart}"`)
|
|
62
|
+
let message = ''
|
|
63
|
+
try { fn() }
|
|
64
|
+
catch (e: any) { message = String(e?.message ?? e) }
|
|
65
|
+
console.log(' получили:', message || '<ошибки не было>')
|
|
66
|
+
if (!message.includes(expectedPart)) throw new Error(`${label}: expected error containing ${expectedPart}, got ${message}`)
|
|
67
|
+
console.log(' OK')
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function waitFor(label: string, ok: () => boolean) {
|
|
71
|
+
for (let i = 0; i < 50; i++) {
|
|
72
|
+
if (ok()) return
|
|
73
|
+
await tick()
|
|
74
|
+
}
|
|
75
|
+
throw new Error(`timeout: ${label}`)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function localReactiveAddDelete() {
|
|
79
|
+
section('[1] Локально: add/delete в reactive object')
|
|
80
|
+
|
|
81
|
+
const state = reactive<AccountState>({
|
|
82
|
+
account: {
|
|
83
|
+
balances: {BTC: 100},
|
|
84
|
+
positions: {BTC: {qty: 0.5, entry: 60000}},
|
|
85
|
+
},
|
|
86
|
+
}, {drain: 'micro'})
|
|
87
|
+
|
|
88
|
+
let positionsEvents = 0
|
|
89
|
+
let lastPaths: string[] = []
|
|
90
|
+
|
|
91
|
+
onUpdate(state.account.positions, () => {
|
|
92
|
+
positionsEvents++
|
|
93
|
+
console.log(' событие positions, keys =', coinKeys(state.account.positions).join(',') || '-')
|
|
94
|
+
})
|
|
95
|
+
onUpdatePaths(state.account.positions, change => {
|
|
96
|
+
lastPaths = change.paths.map(pathText).sort()
|
|
97
|
+
console.log(' dirty paths =', lastPaths.join(',') || '-')
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
scenario('case A: добавляем SOL', 'keys = BTC,SOL; dirty path = SOL; positionsEvents = 1')
|
|
101
|
+
positionsEvents = 0
|
|
102
|
+
lastPaths = []
|
|
103
|
+
state.account.positions.SOL = {qty: 10, entry: 130}
|
|
104
|
+
await flushReactive(state)
|
|
105
|
+
expectEq('keys после add', coinKeys(state.account.positions), ['BTC', 'SOL'])
|
|
106
|
+
expectEq('dirty paths после add', lastPaths, ['SOL'])
|
|
107
|
+
expectEq('events после add', positionsEvents, 1)
|
|
108
|
+
|
|
109
|
+
scenario('case B: удаляем BTC', 'keys = SOL; dirty path = BTC; positionsEvents = 1')
|
|
110
|
+
positionsEvents = 0
|
|
111
|
+
lastPaths = []
|
|
112
|
+
delete state.account.positions.BTC
|
|
113
|
+
await flushReactive(state)
|
|
114
|
+
expectEq('keys после delete', coinKeys(state.account.positions), ['SOL'])
|
|
115
|
+
expectEq('dirty paths после delete', lastPaths, ['BTC'])
|
|
116
|
+
expectEq('events после delete', positionsEvents, 1)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function localMirrorAddDelete() {
|
|
120
|
+
section('[2] Локально: backend store -> mirror store без сети')
|
|
121
|
+
|
|
122
|
+
const backend = createStore<StrategyStore>({
|
|
123
|
+
strategies: {alpha: {status: false, limit: 100}},
|
|
124
|
+
meta: {status: 'ok'},
|
|
125
|
+
}, {drain: 'micro'})
|
|
126
|
+
|
|
127
|
+
const exposed = exposeStore(backend)
|
|
128
|
+
const pulls: any[] = []
|
|
129
|
+
const api = {
|
|
130
|
+
...exposed,
|
|
131
|
+
get(mask?: any) {
|
|
132
|
+
pulls.push(mask)
|
|
133
|
+
console.log(' mirror pull mask =', json(mask))
|
|
134
|
+
return exposed.get(mask)
|
|
135
|
+
},
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const mirror = createStoreMirror<StrategyStore>(api, {strategies: {}, meta: {status: ''}}, {drain: 'micro'})
|
|
139
|
+
const stopSync = await mirror.sync({strategies: true}, {current: true, drain: 'micro'})
|
|
140
|
+
await waitFor('initial mirror sync', () => 'alpha' in mirror.state.strategies)
|
|
141
|
+
pulls.length = 0
|
|
142
|
+
|
|
143
|
+
const stopUi = mirror.node.strategies.on(() => {
|
|
144
|
+
console.log(' UI видит strategies keys =', strategyKeys(mirror.state).join(',') || '-')
|
|
145
|
+
}, {current: true, drain: 'micro'})
|
|
146
|
+
|
|
147
|
+
scenario('case A: backend добавляет beta', 'mirror увидит alpha,beta; подтянется только mask {strategies:{beta:true}}')
|
|
148
|
+
backend.state.strategies.beta = {status: true, limit: 200}
|
|
149
|
+
await flushReactive(backend.state)
|
|
150
|
+
await waitFor('mirror got beta', () => 'beta' in mirror.state.strategies)
|
|
151
|
+
await flushReactive(mirror.state)
|
|
152
|
+
expectEq('mirror keys после add', strategyKeys(mirror.state), ['alpha', 'beta'])
|
|
153
|
+
expectEq('последний pull mask после add', pulls[pulls.length - 1], {strategies: {beta: true}})
|
|
154
|
+
|
|
155
|
+
scenario('case B: backend удаляет beta', 'mirror удалит beta; подтянется тот же точечный mask {strategies:{beta:true}}')
|
|
156
|
+
pulls.length = 0
|
|
157
|
+
delete backend.state.strategies.beta
|
|
158
|
+
await flushReactive(backend.state)
|
|
159
|
+
await waitFor('mirror removed beta', () => !('beta' in mirror.state.strategies))
|
|
160
|
+
await flushReactive(mirror.state)
|
|
161
|
+
expectEq('mirror keys после delete', strategyKeys(mirror.state), ['alpha'])
|
|
162
|
+
expectEq('последний pull mask после delete', pulls[pulls.length - 1], {strategies: {beta: true}})
|
|
163
|
+
|
|
164
|
+
stopUi()
|
|
165
|
+
stopSync()
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function missingKeyCases() {
|
|
169
|
+
section('[3] Если обращаемся туда, чего нет')
|
|
170
|
+
|
|
171
|
+
const state = reactive<AccountState>({
|
|
172
|
+
account: {
|
|
173
|
+
balances: {},
|
|
174
|
+
positions: {},
|
|
175
|
+
},
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
expectThrow('case A: onUpdate(state.account.positions.BTC!, ...)', () => {
|
|
179
|
+
onUpdate(state.account.positions.BTC!, () => {})
|
|
180
|
+
}, 'onUpdate: not a reactive object')
|
|
181
|
+
|
|
182
|
+
expectThrow('case B: state.account.positions.BTC!.qty = 0.7', () => {
|
|
183
|
+
state.account.positions.BTC!.qty = 0.7
|
|
184
|
+
}, "Cannot set properties of undefined")
|
|
185
|
+
|
|
186
|
+
console.log('\n вывод: `!` не создает объект. Если ключ может отсутствовать, сначала делаем:')
|
|
187
|
+
console.log(' state.account.positions.BTC = {qty: 0.7, entry: 60000}')
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function main() {
|
|
191
|
+
await localReactiveAddDelete()
|
|
192
|
+
await localMirrorAddDelete()
|
|
193
|
+
missingKeyCases()
|
|
194
|
+
console.log('\nALL OK: local usage examples passed')
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
main().catch(e => {
|
|
198
|
+
console.error(e)
|
|
199
|
+
process.exit(1)
|
|
200
|
+
})
|
package/oracle/PASSED.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Passed oracles — marks for deleted disposable tests & completed plan items
|
|
2
|
+
|
|
3
|
+
Format: `date · task · verified by · result / consumer caveats`
|
|
4
|
+
|
|
5
|
+
- 2026-06-12 · ByteStream int24/48/64 + dates (P0) · pre-folder oracle · PASS. ⚠️ data written by the OLD corrupt code now reads back differently (it was already corrupt).
|
|
6
|
+
- 2026-06-12 · Rate-limiter byWeight ban-bug (`?? 0` → `?? arr[i][0]`) · pre-folder oracle · PASS.
|
|
7
|
+
- 2026-06-12 · RPC: `pipeStrict` honesty JSDoc note (≡ `pipe` for now) · harness (permanent) · green.
|
|
8
|
+
- 2026-06-12 · RPC: `errToObj` code/data/cause + client `reviveErr` → `MyError` · harness · green. ⚠️ rejected RPC promises now fail with an `Error` instance (was: plain object); `message`/`name`/`stack` reads unchanged.
|
|
9
|
+
- 2026-06-12 · RPC: rich values nested in `Map`/`Set` packed/revived recursively (NEW finding, was not in plan) · harness · green. ⚠️ new server + old client → nested values arrive as marker objects (was: silently mangled strings).
|
|
10
|
+
- 2026-06-12 · RPC: PIPE callback args `packResult`-ed like CALL (rpc-server:169) · harness · green.
|
|
11
|
+
- 2026-06-12 · RPC: opt-in `limits` option on client (`unpackResult` with `lim`); fixed `.map(unpackResult)` passing index as `lim`; over-limit RESP rejects that request, malformed CB dropped · harness · green.
|
|
12
|
+
- 2026-06-12 · RPC: `maxArgs`/`maxPathLen` enforced (unpack / string-ref validation) · harness · green. ⚠️ >64 args or >16 non-routeMap path segments now rejected with `PayloadLimitError` (previously accepted).
|
|
13
|
+
- 2026-06-12 · `WebSocketServerHook`: memoized per-tag wrapper (was: fresh wrapper per proxy access → duplicate listeners on re-subscribe) · `socket-server-hook-memo.oracle.ts` (deleted) · PASS 2/2. ⚠️ `get.tag` is now reference-stable across accesses.
|
|
14
|
+
- 2026-06-12 · `node_console`: removed PRIVATE dead `setupLogs2` + never-assigned `wrapCallSite` and its dead branches; exported `__LineFile2`/`__LineFiles` kept & documented (positions mapped by the runtime) · `node-console-cleanup.oracle.ts` (deleted) · PASS 4/4, behavior unchanged.
|
|
15
|
+
- 2026-06-12 · `CParams`: top-level executable scratch (incl. `GetSimpleParams(new Test())` running on every import) wrapped into never-called `_typeCheck_*` functions — type assertions still compile, zero import side effects; private `function test()` left as-is (declaration only, no side effect) · `cparams-import-sideeffect.oracle.ts` (deleted) · PASS 3/3.
|
|
16
|
+
- 2026-06-12 · `Time.durationToStr`: characterized — compound output WORKS ("1д 23ч"); the <1.1→smaller-unit ("65м") and >10→single-unit ("25ч") heuristics are deliberate and PRESERVED byte-for-byte. Fixed only the real defect: second-unit rounding overflow ("1д 24ч" → now "2д") · `duration-to-str.oracle.ts` (deleted) · PASS 10/10. ⚠️ output changes ONLY for durations where the second unit rounded up to a full first unit.
|
|
17
|
+
- 2026-06-12 · RPC P0 id-reuse: aborted/cleared request ids now go to a zombie set and return to the pool only on their late RESP/CB_END — a reused id can no longer deliver a stale response to a new caller; stray CB_END no longer releases foreign ids · harness (permanent) · green.
|
|
18
|
+
- 2026-06-12 · RPC double-init: second `createRpcServer` on same socket+key detaches the previous handler (console.warn; SocketTmpl has no `off`); second `createRpcClient` on same socket+key now SHARES the id pool → concurrent clients coexist without reqId collisions · harness · green. ⚠️ duplicate-server re-init now last-wins (was: both handlers fired, side effects doubled).
|
|
19
|
+
- 2026-06-12 · RPC `dispose(reason?)` added to client (additive API): rejects pending, ignores further packets (still recycles zombie ids to the shared pool), rejects new calls · harness · green.
|
|
20
|
+
- 2026-06-12 · `rpc-server-auto`: per-subscriber multiplexer — a second RPC subscription to the same server `Listen` no longer clobbers the first (each subscriber gets its own `listenSocket`; entries pruned when a subscription ends) · harness · green. ⚠️ re-subscribe semantics changed from "replace previous" to "coexist".
|
|
21
|
+
- 2026-06-12 · `rpc-clientHub.setToken`: now `dispose`s previous facade clients (pending rejected with "token rotated") and issues a FRESH `promise` when the old one already resolved — awaiting after rotation waits for the new connect · harness · green.
|
|
22
|
+
- 2026-06-12 · `"___STOP"` unified into `RPC_STOP` const in `rpc-protocol.ts` (string, NOT Symbol — it crosses the JSON wire; value unchanged); `isRpcPipe` was already `Symbol.for` · harness · green.
|
|
23
|
+
- 2026-06-12 · `listen-deep`: `extends typeof Promise` (constructor-only) → added `extends Promise<any>` so Promise-valued fields pass through unmangled (type-level only); `listen-socket` lazy unsubscribe documented as by-design (no status-change event exists; cleans on next emission) · tsc + harness · green.
|
|
24
|
+
- 2026-06-12 · `WebHook3`: Express-5-safe client unsubscribe — one persistent route per path + tag→handler map instead of `app._router.stack` mutation; re-subscribe no longer stacks duplicate handlers; `subscribers.json` saved atomically (temp+rename) · `webhook-unsubscribe.oracle.ts` (deleted) · PASS 4/4. ⚠️ after unsubscribe the route stays mounted and answers 404.
|
|
25
|
+
- 2026-06-12 · `createSignatureFunction`: investigated — no internal caller; the HTTP query is built by CONSUMERS, so reordering keys library-side would break live signatures. Insertion-order contract documented in JSDoc; no code change (correct resolution).
|
|
26
|
+
- 2026-06-12 · RPC subscription dedupe v1 (`dedupeListen`, default ON): one wire subscription per client per Listen address (`*.callback` path + non-callback args); local fan-out for extra consumers; `.unsubscribe()` on the subscribe promise (refcount; wire `removeCallback` after the last leaves); `api.subscriptions()` stats · harness (permanent, 8 cases) · green. ⚠️ default-ON changes behavior: repeated identical subscribes now share one wire stream (each callback still fires); set `dedupeListen: false` to restore raw per-call subscriptions.
|
|
27
|
+
- 2026-06-12 · Dedupe exact addressing: server declares Listen addresses as the additive 4th element of `Pkt.MAP` (`IS_RPC_LISTEN` symbol mark, survives `transformTree` copy); client dedupes ONLY declared addresses, heuristic kept as fallback for old servers · harness (incl. concurrent non-Listen `callback`-named method case) · green 44/44.
|
package/oracle/README.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# oracle/ — disposable test oracles
|
|
2
|
+
|
|
3
|
+
Convention (see PLAN.md "Working agreement"):
|
|
4
|
+
|
|
5
|
+
- One file per task being verified: `oracle/<task-slug>.oracle.ts`.
|
|
6
|
+
- Log-based: prints `PASS`/`FAIL` lines, exits non-zero on failure.
|
|
7
|
+
Run: `node node_modules/ts-node/dist/bin.js --transpile-only oracle/<name>.oracle.ts`
|
|
8
|
+
- **Disposable**: after the fix goes green, the oracle file is DELETED and a one-line
|
|
9
|
+
mark is appended to `PASSED.md`. The completed task is then removed from PLAN.md/PLAN_ru.md
|
|
10
|
+
(history lives in git).
|
|
11
|
+
- The only PERMANENT test in the repo is the RPC harness: `src/Common/rcp/rpc.harness.spec.ts`.
|
|
12
|
+
- This folder is excluded from the published build (not under `src/`).
|