wenay-common2 1.0.74 → 1.0.76
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/doc/ROADMAP.md +7 -6
- package/doc/changes/1.0.75.md +10 -0
- package/doc/changes/1.0.76.md +4 -0
- package/doc/wenay-common2-rare.md +38 -16
- package/doc/wenay-common2.md +438 -427
- package/lib/Common/events/replay-channel.js +55 -6
- package/lib/Common/events/replay-route.js +5 -2
- package/lib/Common/events/replay-wire.js +137 -47
- package/lib/Common/events/transport-lifecycle.d.ts +23 -0
- package/lib/Common/events/transport-lifecycle.js +94 -0
- package/lib/Common/rcp/rpc-client.js +346 -111
- package/lib/Common/rcp/rpc-clientHub.d.ts +2 -0
- package/lib/Common/rcp/rpc-clientHub.js +120 -25
- package/lib/Common/rcp/rpc-server.js +33 -9
- package/observe/store-manager.test.ts +43 -1
- package/oracle/realsocket/lifecycle.spec.ts +3 -3
- package/oracle/realsocket/replay-reconnect-auth.spec.ts +111 -0
- package/oracle/realsocket/replay-reconnect.spec.ts +592 -0
- package/package.json +1 -1
- package/replay/route-webrtc.test.ts +15 -2
- package/rpc.md +19 -4
- package/doc/changes/1.0.65.md +0 -8
- package/doc/changes/1.0.66.md +0 -8
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.rpc = rpc;
|
|
4
4
|
exports.createRpcClientHub = createRpcClientHub;
|
|
5
5
|
const rpc_client_1 = require("./rpc-client");
|
|
6
|
+
const transport_lifecycle_1 = require("../events/transport-lifecycle");
|
|
6
7
|
function rpc(socketKey) {
|
|
7
8
|
return { socketKey };
|
|
8
9
|
}
|
|
@@ -14,43 +15,127 @@ function createRpcClientHub(createSocket, schemaBuilder, hubOpts) {
|
|
|
14
15
|
let connectCount = 0;
|
|
15
16
|
let onConnectCb = null;
|
|
16
17
|
let onDisconnectCb = null;
|
|
18
|
+
const connectCbs = new Set();
|
|
19
|
+
const disconnectCbs = new Set();
|
|
20
|
+
let activeContext = null;
|
|
17
21
|
let resolveFunc = null;
|
|
18
22
|
let promise = new Promise((resolve) => {
|
|
19
23
|
resolveFunc = resolve;
|
|
20
24
|
});
|
|
25
|
+
function callObserver(cb, value, errors) {
|
|
26
|
+
try {
|
|
27
|
+
cb(value);
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
errors.push(error);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function rethrowObserverErrors(errors) {
|
|
34
|
+
for (const error of errors) {
|
|
35
|
+
setTimeout(function rethrowLifecycleObserverError() { throw error; }, 0);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function notifyConnect(count) {
|
|
39
|
+
const errors = [];
|
|
40
|
+
const legacy = onConnectCb;
|
|
41
|
+
if (legacy)
|
|
42
|
+
callObserver(legacy, count, errors);
|
|
43
|
+
for (const cb of [...connectCbs])
|
|
44
|
+
callObserver(cb, count, errors);
|
|
45
|
+
rethrowObserverErrors(errors);
|
|
46
|
+
}
|
|
47
|
+
function notifyDisconnect(context, reason) {
|
|
48
|
+
if (context.disconnectNotified)
|
|
49
|
+
return;
|
|
50
|
+
context.disconnectNotified = true;
|
|
51
|
+
const errors = [];
|
|
52
|
+
const legacy = onDisconnectCb;
|
|
53
|
+
if (legacy)
|
|
54
|
+
callObserver(legacy, reason, errors);
|
|
55
|
+
for (const cb of [...disconnectCbs])
|
|
56
|
+
callObserver(cb, reason, errors);
|
|
57
|
+
rethrowObserverErrors(errors);
|
|
58
|
+
}
|
|
59
|
+
function closeContext(context, reason) {
|
|
60
|
+
if (context.terminal)
|
|
61
|
+
return;
|
|
62
|
+
context.terminal = true;
|
|
63
|
+
context.connected = false;
|
|
64
|
+
context.attempt++;
|
|
65
|
+
for (const client of context.clients)
|
|
66
|
+
client[transport_lifecycle_1.RPC_TRANSPORT_CONTROL]?.close(reason);
|
|
67
|
+
for (const client of context.clients)
|
|
68
|
+
client.dispose?.(reason, { socketAlive: false });
|
|
69
|
+
context.socket.disconnect?.();
|
|
70
|
+
notifyDisconnect(context, reason);
|
|
71
|
+
}
|
|
72
|
+
async function handshakeAndConnect(context, attempt, count) {
|
|
73
|
+
await Promise.all(context.clients.map(function initClient(client) {
|
|
74
|
+
return client.initStrict?.();
|
|
75
|
+
}));
|
|
76
|
+
if (activeContext != context || context.terminal || !context.connected || context.attempt != attempt)
|
|
77
|
+
return;
|
|
78
|
+
for (const client of context.clients)
|
|
79
|
+
client[transport_lifecycle_1.RPC_TRANSPORT_CONTROL]?.connect();
|
|
80
|
+
if (activeContext != context || context.terminal || !context.connected || context.attempt != attempt)
|
|
81
|
+
return;
|
|
82
|
+
notifyConnect(count);
|
|
83
|
+
if (activeContext != context || context.terminal || !context.connected || context.attempt != attempt)
|
|
84
|
+
return;
|
|
85
|
+
if (resolveFunc) {
|
|
86
|
+
const resolve = resolveFunc;
|
|
87
|
+
resolveFunc = null;
|
|
88
|
+
resolve(facade);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
21
91
|
function setToken(token) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
for (const key in schema)
|
|
25
|
-
facade[key]?.dispose?.("token rotated", { socketAlive: false });
|
|
26
|
-
if (hadSocket)
|
|
27
|
-
onDisconnectCb?.("token rotated");
|
|
92
|
+
if (activeContext)
|
|
93
|
+
closeContext(activeContext, 'token rotated');
|
|
28
94
|
if (!resolveFunc)
|
|
29
95
|
promise = new Promise((resolve) => { resolveFunc = resolve; });
|
|
30
96
|
currentToken = token;
|
|
31
|
-
|
|
97
|
+
const nextSocket = createSocket(token);
|
|
98
|
+
socket = nextSocket;
|
|
99
|
+
const clients = [];
|
|
32
100
|
for (const key in schema) {
|
|
33
101
|
const targetSocketKey = schema[key].socketKey || key;
|
|
34
|
-
const client = (0, rpc_client_1.createRpcClient)({ socketKey: targetSocketKey, socket, token, opt: hubOpts?.opt });
|
|
102
|
+
const client = (0, rpc_client_1.createRpcClient)({ socketKey: targetSocketKey, socket: nextSocket, token, opt: hubOpts?.opt });
|
|
103
|
+
const transportClient = client;
|
|
104
|
+
transportClient[transport_lifecycle_1.RPC_TRANSPORT_CONTROL]?.disconnect('RPC hub awaiting handshake');
|
|
35
105
|
facade[key] = client;
|
|
106
|
+
clients.push(transportClient);
|
|
36
107
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
108
|
+
const context = {
|
|
109
|
+
socket: nextSocket,
|
|
110
|
+
clients,
|
|
111
|
+
connected: false,
|
|
112
|
+
terminal: false,
|
|
113
|
+
attempt: 0,
|
|
114
|
+
disconnectNotified: false,
|
|
115
|
+
};
|
|
116
|
+
activeContext = context;
|
|
117
|
+
nextSocket.on('connect', function onSocketConnect() {
|
|
118
|
+
if (activeContext != context || context.terminal)
|
|
119
|
+
return;
|
|
120
|
+
context.connected = true;
|
|
121
|
+
context.disconnectNotified = false;
|
|
122
|
+
const attempt = ++context.attempt;
|
|
123
|
+
const count = ++connectCount;
|
|
124
|
+
handshakeAndConnect(context, attempt, count).catch(function reportHandshakeError(error) {
|
|
125
|
+
if (activeContext != context || context.terminal || context.attempt != attempt)
|
|
126
|
+
return;
|
|
127
|
+
setTimeout(function rethrowHandshakeError() { throw error; }, 0);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
nextSocket.on('disconnect', function onSocketDisconnect(reason) {
|
|
131
|
+
if (activeContext != context || context.terminal || !context.connected)
|
|
132
|
+
return;
|
|
133
|
+
context.connected = false;
|
|
134
|
+
context.attempt++;
|
|
135
|
+
const disconnectReason = typeof reason == 'string' ? reason : String(reason ?? 'socket disconnected');
|
|
136
|
+
for (const client of context.clients)
|
|
137
|
+
client[transport_lifecycle_1.RPC_TRANSPORT_CONTROL]?.disconnect(disconnectReason);
|
|
138
|
+
notifyDisconnect(context, disconnectReason);
|
|
54
139
|
});
|
|
55
140
|
return promise;
|
|
56
141
|
}
|
|
@@ -64,6 +149,14 @@ function createRpcClientHub(createSocket, schemaBuilder, hubOpts) {
|
|
|
64
149
|
}
|
|
65
150
|
return Promise.all(ps);
|
|
66
151
|
}
|
|
152
|
+
function connectListen(cb) {
|
|
153
|
+
connectCbs.add(cb);
|
|
154
|
+
return function offConnect() { connectCbs.delete(cb); };
|
|
155
|
+
}
|
|
156
|
+
function disconnectListen(cb) {
|
|
157
|
+
disconnectCbs.add(cb);
|
|
158
|
+
return function offDisconnect() { disconnectCbs.delete(cb); };
|
|
159
|
+
}
|
|
67
160
|
const result = {
|
|
68
161
|
get promise() { return promise; },
|
|
69
162
|
facade,
|
|
@@ -73,6 +166,8 @@ function createRpcClientHub(createSocket, schemaBuilder, hubOpts) {
|
|
|
73
166
|
get socket() { return socket; },
|
|
74
167
|
onConnect: (func) => { onConnectCb = func ?? null; },
|
|
75
168
|
onDisconnect: (func) => { onDisconnectCb = func ?? null; },
|
|
169
|
+
connectListen,
|
|
170
|
+
disconnectListen,
|
|
76
171
|
connectCount: () => connectCount,
|
|
77
172
|
};
|
|
78
173
|
return result;
|
|
@@ -125,10 +125,12 @@ function createServer(socket, key, target, hooks, limits, auth, opt) {
|
|
|
125
125
|
const sendCbEnd = (cbId) => { cbShapes.drop(cbId); send([rpc_protocol_1.Pkt.CB_END, cbId]); };
|
|
126
126
|
let authed = !auth?.gate;
|
|
127
127
|
let authAck = undefined;
|
|
128
|
+
let helloInFlight = null;
|
|
128
129
|
const sendMap = () => send(authAck !== undefined
|
|
129
130
|
? [rpc_protocol_1.Pkt.MAP, routeMap, strictSchema, listenPaths, authAck]
|
|
130
131
|
: [rpc_protocol_1.Pkt.MAP, routeMap, strictSchema, listenPaths]);
|
|
131
|
-
|
|
132
|
+
if (!auth?.resolveAuth)
|
|
133
|
+
sendMap();
|
|
132
134
|
if (serverCaps)
|
|
133
135
|
send([rpc_protocol_1.Pkt.CAPS, serverCaps]);
|
|
134
136
|
let detached = false;
|
|
@@ -147,6 +149,11 @@ function createServer(socket, key, target, hooks, limits, auth, opt) {
|
|
|
147
149
|
if (detached)
|
|
148
150
|
return;
|
|
149
151
|
if (msg == rpc_protocol_1.Pkt.STRICT) {
|
|
152
|
+
const hello = helloInFlight;
|
|
153
|
+
if (hello) {
|
|
154
|
+
await hello;
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
150
157
|
sendMap();
|
|
151
158
|
return;
|
|
152
159
|
}
|
|
@@ -159,21 +166,38 @@ function createServer(socket, key, target, hooks, limits, auth, opt) {
|
|
|
159
166
|
send([rpc_protocol_1.Pkt.MAP, routeMap, strictSchema, listenPaths, null]);
|
|
160
167
|
return;
|
|
161
168
|
}
|
|
169
|
+
async function resolveHello() {
|
|
170
|
+
try {
|
|
171
|
+
const r = await auth.resolveAuth(msg[1]);
|
|
172
|
+
if (r && r.object !== undefined)
|
|
173
|
+
buildDispatch(r.object);
|
|
174
|
+
authAck = r && r.ack !== undefined ? r.ack : { ok: true };
|
|
175
|
+
authed = authAck?.ok !== false;
|
|
176
|
+
sendMap();
|
|
177
|
+
}
|
|
178
|
+
catch (e) {
|
|
179
|
+
send([rpc_protocol_1.Pkt.MAP, routeMap, strictSchema, listenPaths, { ok: false, reason: e?.message ?? String(e) }]);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const hello = resolveHello();
|
|
183
|
+
helloInFlight = hello;
|
|
162
184
|
try {
|
|
163
|
-
|
|
164
|
-
if (r && r.object !== undefined)
|
|
165
|
-
buildDispatch(r.object);
|
|
166
|
-
authAck = r && r.ack !== undefined ? r.ack : { ok: true };
|
|
167
|
-
authed = authAck?.ok !== false;
|
|
168
|
-
sendMap();
|
|
185
|
+
await hello;
|
|
169
186
|
}
|
|
170
|
-
|
|
171
|
-
|
|
187
|
+
finally {
|
|
188
|
+
if (helloInFlight == hello)
|
|
189
|
+
helloInFlight = null;
|
|
172
190
|
}
|
|
173
191
|
return;
|
|
174
192
|
}
|
|
175
193
|
if (!Array.isArray(msg) || (msg[0] !== rpc_protocol_1.Pkt.CALL && msg[0] !== rpc_protocol_1.Pkt.PIPE))
|
|
176
194
|
return;
|
|
195
|
+
const hello = helloInFlight;
|
|
196
|
+
if (hello) {
|
|
197
|
+
await hello;
|
|
198
|
+
if (detached)
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
177
201
|
const isPipe = msg[0] === rpc_protocol_1.Pkt.PIPE;
|
|
178
202
|
const [, reqId, ref, rawArgsOrSteps, w] = msg;
|
|
179
203
|
const wait = w !== false;
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
flushReactive,
|
|
8
8
|
managedStore,
|
|
9
9
|
} from '../src/Common/Observe'
|
|
10
|
+
import {isNoStrict, noStrict} from '../src/Common/rcp/rpc-dynamic'
|
|
10
11
|
|
|
11
12
|
let fails = 0
|
|
12
13
|
const ok = (condition: any, message: string) => {
|
|
@@ -111,8 +112,49 @@ async function main() {
|
|
|
111
112
|
ok(manager.handles.rows.status().state == 'stopped', 'stopAll closes offline resource')
|
|
112
113
|
}
|
|
113
114
|
|
|
115
|
+
console.log('\n[store-manager] dynamic account-map lifecycle')
|
|
116
|
+
{
|
|
117
|
+
const sources = new Map<string, ReturnType<typeof createStore<Market>>>()
|
|
118
|
+
const remotes = noStrict(new Proxy({} as Record<string, any>, {
|
|
119
|
+
get(_target, key) {
|
|
120
|
+
if (typeof key != 'string') return undefined
|
|
121
|
+
let source = sources.get(key)
|
|
122
|
+
if (!source) {
|
|
123
|
+
source = createStore<Market>({data: {BTC: key.length}, meta: {status: 'relay'}}, {drain: 'micro'})
|
|
124
|
+
sources.set(key, source)
|
|
125
|
+
}
|
|
126
|
+
return exposeStoreReplay(source, {history: 20}).api.replay
|
|
127
|
+
},
|
|
128
|
+
}))
|
|
129
|
+
function accountResource(account: string) {
|
|
130
|
+
return managedStore.replay<Market>({
|
|
131
|
+
remote: remotes[account],
|
|
132
|
+
initial: {data: {}, meta: {}},
|
|
133
|
+
storeOpts: {drain: 'micro'},
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
const manager = createStoreManager({
|
|
137
|
+
alice: accountResource('alice'),
|
|
138
|
+
bob: accountResource('bob'),
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
ok(isNoStrict(remotes), 'runtime account map is explicitly noStrict, not a fixed schema')
|
|
142
|
+
const alice = await manager.start('alice')
|
|
143
|
+
ok(alice.state.data.BTC == 5 && manager.get('bob') == null, 'start selects one account mirror without materializing its peers')
|
|
144
|
+
|
|
145
|
+
const source = sources.get('alice')!
|
|
146
|
+
source.state.meta.status = 'direct'
|
|
147
|
+
await settle(source.state)
|
|
148
|
+
ok(alice.state.meta.status == 'direct', 'selected account mirror follows its replay route')
|
|
149
|
+
|
|
150
|
+
manager.stop('alice')
|
|
151
|
+
ok(manager.handles.alice.status().state == 'stopped' && manager.get('alice') != null,
|
|
152
|
+
'stopping a selected account detaches its route while retaining its local mirror for reuse')
|
|
153
|
+
manager.stopAll()
|
|
154
|
+
}
|
|
155
|
+
|
|
114
156
|
console.log(`\n${fails == 0 ? 'ALL GREEN' : fails + ' FAILURE(S)'}`)
|
|
115
157
|
process.exit(fails == 0 ? 0 : 1)
|
|
116
158
|
}
|
|
117
159
|
|
|
118
|
-
main().catch(e => { console.error(e); process.exit(1) })
|
|
160
|
+
main().catch(e => { console.error(e); process.exit(1) })
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
// REAL-SOCKET lifecycle:
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// REAL-SOCKET intentional teardown lifecycle: client.dispose() drains live subscriptions,
|
|
2
|
+
// and hard setToken rotation fires onDisconnect, tears down old subs, then connects a fresh
|
|
3
|
+
// socket generation (connectCount++). Neither hard boundary auto-resubscribes. Port 4108.
|
|
4
4
|
import {startRealServer, startRealClient, makeChecker, delay} from './_rs'
|
|
5
5
|
import {listen as createListenPair} from '../../src/Common/events/Listen'
|
|
6
6
|
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// REAL-SOCKET regression: reconnect recovery must wait for delayed in-band auth
|
|
2
|
+
// before recreating the physical replay subscription. Port 4125.
|
|
3
|
+
import {startRealServer, startRealClient, makeChecker, delay} from './_rs'
|
|
4
|
+
import {replayListen} from '../../src/Common/events/replay-listen'
|
|
5
|
+
import {exposeReplay, replaySubscribe, ReplayRemote} from '../../src/Common/events/replay-wire'
|
|
6
|
+
|
|
7
|
+
const PORT = 4125
|
|
8
|
+
const WAIT_MS = 10000
|
|
9
|
+
const AUTH_DELAY_MS = 180
|
|
10
|
+
|
|
11
|
+
const [emit, replay] = replayListen<[number]>({history: 16})
|
|
12
|
+
const principal = {replay: exposeReplay(replay)}
|
|
13
|
+
const serverApis: any[] = []
|
|
14
|
+
let authCount = 0
|
|
15
|
+
|
|
16
|
+
async function resolveAuth(token: any) {
|
|
17
|
+
authCount++
|
|
18
|
+
await delay(AUTH_DELAY_MS)
|
|
19
|
+
if (token != 'replay-token') throw new Error('bad token')
|
|
20
|
+
return {object: principal, ack: {ok: true}}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function rememberServer(api: any) {
|
|
24
|
+
serverApis.push(api)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function serverSubscriberCount() {
|
|
28
|
+
let total = 0
|
|
29
|
+
for (const api of serverApis) {
|
|
30
|
+
for (const sub of api.subscriptions()) total += sub.consumers
|
|
31
|
+
}
|
|
32
|
+
return total
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function waitFor(name: string, predicate: () => boolean, timeoutMs = WAIT_MS) {
|
|
36
|
+
const deadline = Date.now() + timeoutMs
|
|
37
|
+
while (Date.now() < deadline) {
|
|
38
|
+
if (predicate()) return
|
|
39
|
+
await delay(10)
|
|
40
|
+
}
|
|
41
|
+
throw new Error('timeout waiting for ' + name)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function main() {
|
|
45
|
+
const {check, done} = makeChecker('replay-reconnect-auth')
|
|
46
|
+
const watchdog = setTimeout(function watchdogExpired() {
|
|
47
|
+
console.error('WATCHDOG timeout')
|
|
48
|
+
process.exit(3)
|
|
49
|
+
}, 40000)
|
|
50
|
+
|
|
51
|
+
const srv = await startRealServer({
|
|
52
|
+
port: PORT,
|
|
53
|
+
makeObject: function makeGatedObject() { return {} },
|
|
54
|
+
serverOpts: {auth: {resolveAuth, gate: true}},
|
|
55
|
+
onServer: rememberServer,
|
|
56
|
+
})
|
|
57
|
+
const cli = await startRealClient({port: PORT, token: 'replay-token'})
|
|
58
|
+
const remote = (cli.client.func as any).replay as ReplayRemote<[number]>
|
|
59
|
+
const values: number[] = []
|
|
60
|
+
const seqs: number[] = []
|
|
61
|
+
const sub = replaySubscribe(remote, function receiveValue(value) {
|
|
62
|
+
values.push(value)
|
|
63
|
+
}, {
|
|
64
|
+
since: 0,
|
|
65
|
+
onSeq: function rememberSeq(seq) { seqs.push(seq) },
|
|
66
|
+
})
|
|
67
|
+
await sub.ready
|
|
68
|
+
|
|
69
|
+
emit(1)
|
|
70
|
+
await waitFor('initial live seq 1', () => values.length == 1)
|
|
71
|
+
|
|
72
|
+
const socket = cli.hub.socket as any
|
|
73
|
+
const connectCount = cli.hub.connectCount()
|
|
74
|
+
const authBeforeReconnect = authCount
|
|
75
|
+
const engine = socket.io?.engine
|
|
76
|
+
if (!engine?.close) throw new Error('Engine.IO close() is unavailable')
|
|
77
|
+
engine.close()
|
|
78
|
+
await waitFor('transport disconnect', () => !socket.connected)
|
|
79
|
+
|
|
80
|
+
emit(2)
|
|
81
|
+
await waitFor('same Socket.IO object reconnect', () =>
|
|
82
|
+
socket.connected && cli.hub.connectCount() == connectCount + 1)
|
|
83
|
+
await waitFor('authenticated catch-up seq 2', () =>
|
|
84
|
+
values.length == 2 && serverSubscriberCount() == 1)
|
|
85
|
+
|
|
86
|
+
// The physical line is installed and catch-up has drained, so seq 3 is live.
|
|
87
|
+
emit(3)
|
|
88
|
+
await waitFor('live seq 3 after reconnect', () => values.length == 3)
|
|
89
|
+
|
|
90
|
+
await check('reconnect performs one fresh delayed auth', () => authCount, authBeforeReconnect + 1)
|
|
91
|
+
await check('reconnect preserves Socket.IO object identity', () => cli.hub.socket === socket, true)
|
|
92
|
+
await check('reconnect increments connectCount exactly once', () => cli.hub.connectCount(), connectCount + 1)
|
|
93
|
+
await check('catch-up then live delivery has no gap or duplicate', () => values, [1, 2, 3])
|
|
94
|
+
await check('reported seq has no gap or duplicate', () => seqs, [1, 2, 3])
|
|
95
|
+
await check('one logical client subscription survives reconnect', () =>
|
|
96
|
+
cli.client.api.subscriptions().length, 1)
|
|
97
|
+
await check('one physical server subscription after reconnect', serverSubscriberCount, 1)
|
|
98
|
+
|
|
99
|
+
sub()
|
|
100
|
+
await waitFor('subscription cleanup', () =>
|
|
101
|
+
cli.client.api.subscriptions().length == 0 && serverSubscriberCount() == 0)
|
|
102
|
+
clearTimeout(watchdog)
|
|
103
|
+
cli.close()
|
|
104
|
+
await srv.close()
|
|
105
|
+
process.exit(done() == 0 ? 0 : 1)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
main().catch(function reportFailure(error) {
|
|
109
|
+
console.error(error)
|
|
110
|
+
process.exit(2)
|
|
111
|
+
})
|