statewire 0.18.1 → 0.19.0
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/dist/client/connection.d.ts +8 -5
- package/dist/client/connection.d.ts.map +1 -1
- package/dist/client/connection.js +24 -13
- package/dist/client/connection.js.map +1 -1
- package/dist/client/session.d.ts +13 -15
- package/dist/client/session.d.ts.map +1 -1
- package/dist/client/session.js +296 -401
- package/dist/client/session.js.map +1 -1
- package/dist/client/timing.d.ts +2 -1
- package/dist/client/timing.d.ts.map +1 -1
- package/dist/client/timing.js +2 -1
- package/dist/client/timing.js.map +1 -1
- package/dist/host/engine.d.ts +2 -0
- package/dist/host/engine.d.ts.map +1 -1
- package/dist/host/engine.js +2 -0
- package/dist/host/engine.js.map +1 -1
- package/dist/host/host.d.ts +3 -1
- package/dist/host/host.d.ts.map +1 -1
- package/dist/host/host.js +38 -7
- package/dist/host/host.js.map +1 -1
- package/dist/host/http.d.ts.map +1 -1
- package/dist/host/http.js +9 -19
- package/dist/host/http.js.map +1 -1
- package/dist/host/socket.d.ts +48 -25
- package/dist/host/socket.d.ts.map +1 -1
- package/dist/host/socket.js +399 -266
- package/dist/host/socket.js.map +1 -1
- package/dist/host/synced.d.ts +9 -0
- package/dist/host/synced.d.ts.map +1 -0
- package/dist/host/synced.js +45 -0
- package/dist/host/synced.js.map +1 -0
- package/dist/host-internal.d.ts +20 -12
- package/dist/host-internal.d.ts.map +1 -1
- package/dist/host.d.ts +33 -9
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +2 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -1
- package/dist/statewire.d.ts +8 -20
- package/dist/statewire.d.ts.map +1 -1
- package/dist/statewire.js +0 -14
- package/dist/statewire.js.map +1 -1
- package/dist/storage.d.ts +2 -5
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +54 -89
- package/dist/storage.js.map +1 -1
- package/dist/transports/remote.d.ts.map +1 -1
- package/dist/transports/remote.js +1 -6
- package/dist/transports/remote.js.map +1 -1
- package/dist/transports/sse.d.ts +2 -5
- package/dist/transports/sse.d.ts.map +1 -1
- package/dist/transports/sse.js.map +1 -1
- package/dist/transports/testing.d.ts +7 -5
- package/dist/transports/testing.d.ts.map +1 -1
- package/dist/transports/testing.js +19 -18
- package/dist/transports/testing.js.map +1 -1
- package/dist/types.d.ts +3 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/wire/limits.d.ts +2 -2
- package/dist/wire/limits.js +2 -2
- package/dist/wire/limits.js.map +1 -1
- package/dist/wire/protocol.d.ts +6 -1
- package/dist/wire/protocol.d.ts.map +1 -1
- package/dist/wire/protocol.js +33 -12
- package/dist/wire/protocol.js.map +1 -1
- package/package.json +1 -1
- package/src/client/connection.ts +46 -33
- package/src/client/session.ts +442 -517
- package/src/client/timing.ts +2 -0
- package/src/host/engine.ts +3 -1
- package/src/host/host.ts +50 -3
- package/src/host/http.ts +23 -31
- package/src/host/socket.ts +560 -339
- package/src/host/synced.ts +60 -0
- package/src/host-internal.ts +21 -7
- package/src/host.ts +40 -8
- package/src/index.ts +1 -0
- package/src/statewire.ts +8 -37
- package/src/storage.ts +90 -157
- package/src/transports/remote.ts +0 -7
- package/src/transports/sse.ts +8 -3
- package/src/transports/testing.ts +34 -18
- package/src/types.ts +3 -5
- package/src/wire/limits.ts +1 -1
- package/src/wire/protocol.ts +40 -11
package/dist/host/socket.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { encodeDocument, reviveDocumentValue } from "../document.js";
|
|
2
2
|
import { DEFAULT_PROTOCOL, WIRE_VERSION, negotiateProtocols } from "../wire/protocol.js";
|
|
3
|
-
import {
|
|
3
|
+
import { sanitizeValue } from "../wire/ops.js";
|
|
4
4
|
import { StatewireSendError } from "../types.js";
|
|
5
|
-
import { getCommandsHost, getStateHost } from "./host.js";
|
|
5
|
+
import { documentIdentity, getCommandsHost, getStateHost } from "./host.js";
|
|
6
6
|
import { COMMAND_CAPACITY_RETRY_MS, MAX_INFLIGHT_COMMANDS_PER_HOST } from "../wire/limits.js";
|
|
7
7
|
import { createMacrotaskPoster } from "../util/macrotask.js";
|
|
8
8
|
import { frames, stream } from "./http.js";
|
|
@@ -35,62 +35,37 @@ const violation = (reason) => {
|
|
|
35
35
|
};
|
|
36
36
|
const violationReason = (error) => error instanceof Error && error.name === "StatewireViolation" ? error.message : null;
|
|
37
37
|
const isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
38
|
-
const parseLane = (value) => {
|
|
39
|
-
if (!Array.isArray(value)) return violation("malformed frame");
|
|
40
|
-
if (value.length === 0) return violation("empty lane");
|
|
41
|
-
const statements = [];
|
|
42
|
-
let prev = null;
|
|
43
|
-
for (const statement of value) {
|
|
44
|
-
if (!isPlainObject(statement)) return violation("malformed statement");
|
|
45
|
-
const seq = statement["seq"];
|
|
46
|
-
if (typeof seq !== "number" || !Number.isInteger(seq) || seq < 1) return violation("malformed statement");
|
|
47
|
-
if (prev !== null && seq !== prev + 1) return violation("malformed frame");
|
|
48
|
-
prev = seq;
|
|
49
|
-
statements.push(statement);
|
|
50
|
-
}
|
|
51
|
-
return statements;
|
|
52
|
-
};
|
|
53
38
|
const frameEncoder = new TextEncoder();
|
|
54
39
|
const parseFrameValue = (frame) => {
|
|
55
40
|
if (!isPlainObject(frame)) return violation("malformed frame");
|
|
56
41
|
const keys = Object.keys(frame);
|
|
57
42
|
if (keys.length === 0) return violation("malformed frame");
|
|
58
|
-
const unknown = keys.filter((key) => key !== "
|
|
43
|
+
const unknown = keys.filter((key) => key !== "cmd").sort();
|
|
59
44
|
if (unknown.length > 0) return violation(`unknown frame key: '${unknown[0]}'`);
|
|
60
|
-
const
|
|
61
|
-
if (
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const ops = statement["ops"];
|
|
66
|
-
if (!Array.isArray(ops) || !ops.length) return violation("invalid-context");
|
|
67
|
-
if (frameEncoder.encode(JSON.stringify(ops)).byteLength > 65536) return violation("oversized-context");
|
|
68
|
-
ctx.push({
|
|
69
|
-
seq: statement["seq"],
|
|
70
|
-
ops
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
}
|
|
45
|
+
const lane = frame["cmd"];
|
|
46
|
+
if (!Array.isArray(lane)) return violation("malformed frame");
|
|
47
|
+
if (lane.length === 0) return violation("empty lane");
|
|
48
|
+
if (lane.length > 256) return violation("too many commands");
|
|
49
|
+
const prev = /* @__PURE__ */ new Map();
|
|
74
50
|
const cmd = [];
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
51
|
+
for (const statement of lane) {
|
|
52
|
+
if (!isPlainObject(statement)) return violation("malformed statement");
|
|
53
|
+
const seq = statement["seq"];
|
|
54
|
+
const method = statement["method"];
|
|
55
|
+
const protocol = statement["protocol"];
|
|
56
|
+
if (typeof seq !== "number" || !Number.isInteger(seq) || seq < 1 || typeof method !== "string" || typeof protocol !== "string" || !("params" in statement)) return violation("malformed statement");
|
|
57
|
+
const last = prev.get(protocol);
|
|
58
|
+
if (last !== void 0 && seq !== last + 1) return violation("malformed frame");
|
|
59
|
+
prev.set(protocol, seq);
|
|
60
|
+
cmd.push({
|
|
61
|
+
protocol,
|
|
62
|
+
seq,
|
|
63
|
+
method,
|
|
64
|
+
params: statement["params"],
|
|
65
|
+
...statement["meta"] !== void 0 && { meta: statement["meta"] }
|
|
66
|
+
});
|
|
89
67
|
}
|
|
90
|
-
return {
|
|
91
|
-
ctx,
|
|
92
|
-
cmd
|
|
93
|
-
};
|
|
68
|
+
return { cmd };
|
|
94
69
|
};
|
|
95
70
|
const parseFrame = (raw) => {
|
|
96
71
|
let frame;
|
|
@@ -111,19 +86,23 @@ const encodePayload = (payload) => {
|
|
|
111
86
|
};
|
|
112
87
|
};
|
|
113
88
|
const cmdOf = (adm, outcome) => {
|
|
89
|
+
const at = {
|
|
90
|
+
protocol: adm.protocol,
|
|
91
|
+
seq: adm.seq
|
|
92
|
+
};
|
|
114
93
|
if (outcome.type === "accepted") return {
|
|
115
|
-
|
|
94
|
+
...at,
|
|
116
95
|
type: "result",
|
|
117
96
|
...encodePayload(outcome.payload)
|
|
118
97
|
};
|
|
119
98
|
const failure = outcome.error instanceof StatewireSendError ? outcome.error.failure : null;
|
|
120
99
|
switch (failure?.fate) {
|
|
121
100
|
case "crashed": return {
|
|
122
|
-
|
|
101
|
+
...at,
|
|
123
102
|
type: "crashed"
|
|
124
103
|
};
|
|
125
104
|
case "failed": return {
|
|
126
|
-
|
|
105
|
+
...at,
|
|
127
106
|
type: "failed",
|
|
128
107
|
...failure.message !== void 0 && { message: failure.message }
|
|
129
108
|
};
|
|
@@ -131,20 +110,20 @@ const cmdOf = (adm, outcome) => {
|
|
|
131
110
|
if (failure.code !== void 0) {
|
|
132
111
|
const message = failure.code === "unknown-command" ? `unknown method: '${adm.method}'` : failure.message;
|
|
133
112
|
return {
|
|
134
|
-
|
|
113
|
+
...at,
|
|
135
114
|
type: "rejected",
|
|
136
115
|
code: failure.code,
|
|
137
116
|
...message !== void 0 && { message }
|
|
138
117
|
};
|
|
139
118
|
}
|
|
140
119
|
return {
|
|
141
|
-
|
|
120
|
+
...at,
|
|
142
121
|
type: "rejected",
|
|
143
122
|
...failure.message !== void 0 && { message: failure.message },
|
|
144
123
|
...encodePayload(failure.payload)
|
|
145
124
|
};
|
|
146
125
|
default: return {
|
|
147
|
-
|
|
126
|
+
...at,
|
|
148
127
|
type: "failed",
|
|
149
128
|
message: outcome.error instanceof Error ? outcome.error.message : String(outcome.error)
|
|
150
129
|
};
|
|
@@ -163,10 +142,7 @@ const parseInitialFrame = (raw) => {
|
|
|
163
142
|
if (!isPlainObject(headers) || Object.values(headers).some((v) => typeof v !== "string")) return violation("malformed headers");
|
|
164
143
|
return {
|
|
165
144
|
headers,
|
|
166
|
-
frame: Object.keys(lanes).length ? parseFrameValue(lanes) : {
|
|
167
|
-
ctx: [],
|
|
168
|
-
cmd: []
|
|
169
|
-
}
|
|
145
|
+
frame: Object.keys(lanes).length ? parseFrameValue(lanes) : { cmd: [] }
|
|
170
146
|
};
|
|
171
147
|
};
|
|
172
148
|
const scheduleFlush = (h) => {
|
|
@@ -174,6 +150,10 @@ const scheduleFlush = (h) => {
|
|
|
174
150
|
h.flushScheduled = true;
|
|
175
151
|
h.post();
|
|
176
152
|
};
|
|
153
|
+
const markViews = (h) => {
|
|
154
|
+
h.viewsDirty = true;
|
|
155
|
+
scheduleFlush(h);
|
|
156
|
+
};
|
|
177
157
|
const recordOp = (h, op) => {
|
|
178
158
|
if (op.op === "replace" || op.op === "add") {
|
|
179
159
|
const { value, meta } = encodeDocument(op.value);
|
|
@@ -193,8 +173,10 @@ const attach = (h, next) => {
|
|
|
193
173
|
if (!nextState || !nextCommands?.submit) throw new Error("StatewireSocketHost requires a statewire/host transport (useStatewireState with object state, useStatewireCommands)");
|
|
194
174
|
if (nextState !== h.stateHost) {
|
|
195
175
|
h.stateHost?.setOnOp(null);
|
|
176
|
+
h.stateHost?.setOnDocuments(null);
|
|
196
177
|
h.stateHost = nextState;
|
|
197
178
|
h.stateHost.setOnOp((op) => recordOp(h, op));
|
|
179
|
+
h.stateHost.setOnDocuments(() => markViews(h));
|
|
198
180
|
}
|
|
199
181
|
h.commandsHost = nextCommands;
|
|
200
182
|
};
|
|
@@ -205,6 +187,7 @@ const closeConn = (h, conn, code, reason) => {
|
|
|
205
187
|
try {
|
|
206
188
|
conn.socket.close(code, reason);
|
|
207
189
|
} catch {}
|
|
190
|
+
h.current?.detached?.(conn.attach);
|
|
208
191
|
if (h.conns.size === 0 && h.heartbeatTimer !== null) {
|
|
209
192
|
clearInterval(h.heartbeatTimer);
|
|
210
193
|
h.heartbeatTimer = null;
|
|
@@ -219,6 +202,13 @@ const sendTo = (h, conn, data) => {
|
|
|
219
202
|
closeConn(h, conn);
|
|
220
203
|
}
|
|
221
204
|
};
|
|
205
|
+
const violate = (h, conn, reason) => {
|
|
206
|
+
sendTo(h, conn, JSON.stringify({ fin: {
|
|
207
|
+
reason: "protocol-error",
|
|
208
|
+
message: reason
|
|
209
|
+
} }));
|
|
210
|
+
closeConn(h, conn, 1008, reason);
|
|
211
|
+
};
|
|
222
212
|
const atSubscriberLimit = (h) => h.conns.size + [...h.pendingConns].filter((conn) => conn.authorizing).length >= h.maxSubscribers;
|
|
223
213
|
const ensureHeartbeat = (h) => {
|
|
224
214
|
if (h.heartbeatTimer !== null || h.heartbeatMs <= 0) return;
|
|
@@ -237,46 +227,146 @@ const setHeartbeatMs = (h, ms) => {
|
|
|
237
227
|
ensureHeartbeat(h);
|
|
238
228
|
}
|
|
239
229
|
};
|
|
230
|
+
const durableLanes = function* (h) {
|
|
231
|
+
for (const rec of h.clients.values()) yield* rec.lanes.values();
|
|
232
|
+
};
|
|
233
|
+
const allLanes = function* (h) {
|
|
234
|
+
yield* durableLanes(h);
|
|
235
|
+
for (const conn of h.conns) yield* conn.lanes.values();
|
|
236
|
+
};
|
|
240
237
|
const idleLevel = (h) => {
|
|
241
238
|
if (h.current?.work?.isIdle === false) return false;
|
|
242
|
-
for (const
|
|
239
|
+
for (const lane of allLanes(h)) if (lane.inflight.size > 0) return false;
|
|
243
240
|
return true;
|
|
244
241
|
};
|
|
242
|
+
const selects = (conn, protocol) => conn.selection.protocols.some((p) => p.name === protocol);
|
|
243
|
+
const viewOf = (h, conn) => {
|
|
244
|
+
const view = /* @__PURE__ */ new Map();
|
|
245
|
+
const shared = h.stateHost.documents();
|
|
246
|
+
const hostMain = documentIdentity(h.protocol.name);
|
|
247
|
+
for (const p of conn.selection.protocols) if (p.record === "ephemeral") {
|
|
248
|
+
for (const [id, doc] of conn.documents) if (doc.protocol === p.name) view.set(id, doc);
|
|
249
|
+
} else for (const [id, doc] of shared) if (doc.protocol === p.name && id !== hostMain && (doc.main || conn.included.has(id))) view.set(id, doc);
|
|
250
|
+
return view;
|
|
251
|
+
};
|
|
252
|
+
const diffView = (h, conn) => {
|
|
253
|
+
const current = viewOf(h, conn);
|
|
254
|
+
const ops = [];
|
|
255
|
+
for (const [id, entry] of conn.view) {
|
|
256
|
+
if (current.has(id)) continue;
|
|
257
|
+
conn.view.delete(id);
|
|
258
|
+
ops.push({
|
|
259
|
+
op: "unmount",
|
|
260
|
+
doc: entry.doc
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
for (const [id, doc] of current) {
|
|
264
|
+
const entry = conn.view.get(id);
|
|
265
|
+
if (entry !== void 0 && entry.value === doc.value) continue;
|
|
266
|
+
const { value, meta } = encodeDocument(doc.value);
|
|
267
|
+
if (entry === void 0) {
|
|
268
|
+
const number = conn.nextDoc++;
|
|
269
|
+
conn.view.set(id, {
|
|
270
|
+
doc: number,
|
|
271
|
+
value: doc.value
|
|
272
|
+
});
|
|
273
|
+
ops.push({
|
|
274
|
+
op: "mount",
|
|
275
|
+
doc: number,
|
|
276
|
+
protocol: doc.protocol,
|
|
277
|
+
...doc.key !== null && { key: doc.key },
|
|
278
|
+
...doc.main && { main: true },
|
|
279
|
+
value,
|
|
280
|
+
...meta !== void 0 && { meta }
|
|
281
|
+
});
|
|
282
|
+
} else {
|
|
283
|
+
entry.value = doc.value;
|
|
284
|
+
ops.push({
|
|
285
|
+
op: "replace",
|
|
286
|
+
doc: entry.doc,
|
|
287
|
+
path: [],
|
|
288
|
+
value,
|
|
289
|
+
...meta !== void 0 && { meta }
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return ops;
|
|
294
|
+
};
|
|
295
|
+
const attachOf = (h, conn) => {
|
|
296
|
+
const requireSelected = (protocol) => {
|
|
297
|
+
const entry = conn.selection.protocols.find((p) => p.name === protocol);
|
|
298
|
+
if (entry === void 0) throw new Error(`statewire: protocol ${protocol} was not negotiated`);
|
|
299
|
+
return entry;
|
|
300
|
+
};
|
|
301
|
+
return {
|
|
302
|
+
document: (protocol, key) => {
|
|
303
|
+
if (key === void 0 && protocol === h.protocol.name) return h.stateHost.snapshot();
|
|
304
|
+
const view = viewOf(h, conn);
|
|
305
|
+
if (key !== void 0) return view.get(documentIdentity(protocol, key))?.value;
|
|
306
|
+
for (const doc of view.values()) if (doc.protocol === protocol && doc.main) return doc.value;
|
|
307
|
+
},
|
|
308
|
+
mount: (protocol, value, key, main = key === void 0) => {
|
|
309
|
+
if (requireSelected(protocol).record !== "ephemeral") throw new Error(`statewire: protocol ${protocol} is not ephemeral`);
|
|
310
|
+
const encoded = encodeDocument(value);
|
|
311
|
+
if (frameEncoder.encode(JSON.stringify(encoded)).byteLength > 65536) throw new Error("statewire: oversized ephemeral document");
|
|
312
|
+
const id = documentIdentity(protocol, key);
|
|
313
|
+
if (main) {
|
|
314
|
+
for (const [other, doc] of conn.documents) if (doc.protocol === protocol && doc.main && other !== id) throw new Error(`statewire: ${protocol} already has a main document`);
|
|
315
|
+
}
|
|
316
|
+
conn.documents.set(id, {
|
|
317
|
+
protocol,
|
|
318
|
+
key: key === void 0 ? null : [...key],
|
|
319
|
+
main,
|
|
320
|
+
value: sanitizeValue(value)
|
|
321
|
+
});
|
|
322
|
+
markViews(h);
|
|
323
|
+
},
|
|
324
|
+
unmount: (protocol, key) => {
|
|
325
|
+
if (!conn.documents.delete(documentIdentity(protocol, key))) throw new Error("statewire: document is not mounted");
|
|
326
|
+
markViews(h);
|
|
327
|
+
},
|
|
328
|
+
include: (protocol, key) => {
|
|
329
|
+
if (requireSelected(protocol).record !== void 0) throw new Error(`statewire: protocol ${protocol} is not durable`);
|
|
330
|
+
conn.included.add(documentIdentity(protocol, key));
|
|
331
|
+
markViews(h);
|
|
332
|
+
},
|
|
333
|
+
exclude: (protocol, key) => {
|
|
334
|
+
conn.included.delete(documentIdentity(protocol, key));
|
|
335
|
+
markViews(h);
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
};
|
|
245
339
|
const flush = (h, fin) => {
|
|
246
340
|
flushTapSync(() => {});
|
|
247
341
|
h.stateHost?.takeChanged();
|
|
248
|
-
for (const
|
|
342
|
+
for (const lane of allLanes(h)) for (const adm of lane.inflight.values()) if (adm.applied && !adm.settled && !adm.appliedSent) {
|
|
249
343
|
adm.appliedSent = true;
|
|
250
|
-
queueApplied(h,
|
|
344
|
+
queueApplied(h, lane, adm);
|
|
251
345
|
}
|
|
252
346
|
const level = idleLevel(h);
|
|
253
347
|
const edge = level !== h.idleWire;
|
|
254
|
-
if (h.pendingOps.length === 0 && h.pendingApplied.size === 0 && h.pendingCmds.size === 0 && fin === void 0 && !edge) return;
|
|
348
|
+
if (h.pendingOps.length === 0 && h.pendingApplied.size === 0 && h.pendingCmds.size === 0 && !h.viewsDirty && fin === void 0 && !edge && ![...h.conns].some((conn) => conn.pendingApplied.length > 0 || conn.pendingCmds.length > 0)) return;
|
|
255
349
|
h.idleWire = level;
|
|
350
|
+
h.viewsDirty = false;
|
|
256
351
|
if (fin === void 0 && edge && level && h.finOnIdle) fin = { reason: "idle" };
|
|
257
|
-
const
|
|
352
|
+
const mainOps = h.pendingOps;
|
|
258
353
|
const applied = h.pendingApplied;
|
|
259
354
|
const cmds = h.pendingCmds;
|
|
260
355
|
h.pendingOps = [];
|
|
261
356
|
h.pendingApplied = /* @__PURE__ */ new Map();
|
|
262
357
|
h.pendingCmds = /* @__PURE__ */ new Map();
|
|
263
358
|
for (const conn of [...h.conns]) {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
359
|
+
if (!conn.ready) continue;
|
|
360
|
+
const forConn = (list) => (list ?? []).filter((cmd) => selects(conn, cmd.protocol));
|
|
361
|
+
const ops = [...selects(conn, h.protocol.name) ? mainOps : [], ...diffView(h, conn)];
|
|
362
|
+
const appliedEntries = [...forConn(applied.get(conn.clientId)), ...conn.pendingApplied.splice(0)];
|
|
363
|
+
if (ops.length > 0) sendTo(h, conn, JSON.stringify({
|
|
364
|
+
ops,
|
|
365
|
+
...appliedEntries.length > 0 && { cmd: appliedEntries },
|
|
366
|
+
...edge && { idle: level }
|
|
267
367
|
}));
|
|
268
|
-
if (
|
|
269
|
-
|
|
270
|
-
ops: conn.selection.protocols.some((p) => p.name === h.protocol.name) ? ops : [],
|
|
271
|
-
...appliedEntries.length > 0 && { cmd: appliedEntries },
|
|
272
|
-
...edge && { idle: level }
|
|
273
|
-
};
|
|
274
|
-
sendTo(h, conn, JSON.stringify(packet));
|
|
275
|
-
} else if (appliedEntries.length > 0) sendTo(h, conn, JSON.stringify({ cmd: appliedEntries }));
|
|
276
|
-
const verdicts = (cmds.get(conn.clientId) ?? []).map((cmd) => conn.selection.protocols.some((p) => p.name === h.protocol.name) ? cmd : {
|
|
277
|
-
seq: cmd.seq,
|
|
278
|
-
...cmd.dur === void 0 ? {} : { dur: cmd.dur }
|
|
279
|
-
});
|
|
368
|
+
else if (appliedEntries.length > 0) sendTo(h, conn, JSON.stringify({ cmd: appliedEntries }));
|
|
369
|
+
const verdicts = [...forConn(cmds.get(conn.clientId)), ...conn.pendingCmds.splice(0)];
|
|
280
370
|
if (verdicts.length > 0) sendTo(h, conn, JSON.stringify({ cmd: verdicts }));
|
|
281
371
|
if (edge && ops.length === 0) sendTo(h, conn, JSON.stringify({ idle: level }));
|
|
282
372
|
if (fin !== void 0) {
|
|
@@ -285,221 +375,237 @@ const flush = (h, fin) => {
|
|
|
285
375
|
}
|
|
286
376
|
}
|
|
287
377
|
};
|
|
288
|
-
const queueApplied = (h,
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
|
|
378
|
+
const queueApplied = (h, lane, adm) => {
|
|
379
|
+
const cmd = {
|
|
380
|
+
protocol: adm.protocol,
|
|
381
|
+
seq: adm.seq,
|
|
382
|
+
type: "applied"
|
|
383
|
+
};
|
|
384
|
+
if (lane.conn !== null) lane.conn.pendingApplied.push(cmd);
|
|
385
|
+
else {
|
|
386
|
+
const list = h.pendingApplied.get(lane.clientId);
|
|
387
|
+
if (list) list.push(cmd);
|
|
388
|
+
else h.pendingApplied.set(lane.clientId, [cmd]);
|
|
389
|
+
}
|
|
292
390
|
scheduleFlush(h);
|
|
293
391
|
};
|
|
294
|
-
const queueCmd = (h,
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
392
|
+
const queueCmd = (h, lane, cmd) => {
|
|
393
|
+
if (lane.conn !== null) lane.conn.pendingCmds.push(cmd);
|
|
394
|
+
else {
|
|
395
|
+
const list = h.pendingCmds.get(lane.clientId);
|
|
396
|
+
if (list) list.push(cmd);
|
|
397
|
+
else h.pendingCmds.set(lane.clientId, [cmd]);
|
|
398
|
+
}
|
|
298
399
|
scheduleFlush(h);
|
|
299
400
|
};
|
|
300
401
|
const notifyClientRecords = (h) => {
|
|
301
402
|
h.persistence?.onClientRecordsChange?.([...h.clients.values()].map((rec) => ({
|
|
302
403
|
id: rec.id,
|
|
303
|
-
lastSeq: admittedThrough(rec),
|
|
304
|
-
ctxSeq: rec.ctxSeq,
|
|
305
404
|
touched: rec.touched,
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
405
|
+
lanes: Object.fromEntries([...rec.lanes.values()].map((lane) => [lane.protocol, {
|
|
406
|
+
lastSeq: admittedThrough(lane),
|
|
407
|
+
results: [...lane.results].map(([seq, entry]) => ({
|
|
408
|
+
seq,
|
|
409
|
+
ts: entry.ts,
|
|
410
|
+
cmd: entry.cmd
|
|
411
|
+
}))
|
|
412
|
+
}])),
|
|
313
413
|
principal: rec.principal
|
|
314
414
|
})));
|
|
315
415
|
};
|
|
316
|
-
const retain = (
|
|
416
|
+
const retain = (lane, cmd) => {
|
|
317
417
|
const now = Date.now();
|
|
318
|
-
|
|
418
|
+
lane.results.set(cmd.seq, {
|
|
319
419
|
ts: now,
|
|
320
420
|
cmd
|
|
321
421
|
});
|
|
322
|
-
const newest = Math.max(...
|
|
323
|
-
for (const [seq, entry] of
|
|
324
|
-
while (
|
|
325
|
-
};
|
|
326
|
-
/**
|
|
327
|
-
const admittedThrough = (
|
|
328
|
-
const
|
|
329
|
-
const
|
|
422
|
+
const newest = Math.max(...lane.results.keys());
|
|
423
|
+
for (const [seq, entry] of lane.results) if (seq !== newest && now - entry.ts > RESULT_TTL_MS) lane.results.delete(seq);
|
|
424
|
+
while (lane.results.size > RESULT_LIMIT) lane.results.delete(Math.min(...lane.results.keys()));
|
|
425
|
+
};
|
|
426
|
+
/** Lane watermark: the last statement admitted, settled or not. */
|
|
427
|
+
const admittedThrough = (lane) => Math.max(lane.lastSeq, ...lane.inflight.keys());
|
|
428
|
+
const watermark = (h, conn, protocol) => {
|
|
429
|
+
const lane = conn.selection.protocols.find((p) => p.name === protocol).record === "ephemeral" ? conn.lanes.get(protocol) : h.clients.get(conn.clientId)?.lanes.get(protocol);
|
|
430
|
+
return lane === void 0 ? 0 : admittedThrough(lane);
|
|
431
|
+
};
|
|
432
|
+
const laneOf = (h, conn, protocol) => {
|
|
433
|
+
const entry = conn.selection.protocols.find((p) => p.name === protocol);
|
|
434
|
+
if (entry === void 0) return violation("unsupported protocol");
|
|
435
|
+
const ephemeral = entry.record === "ephemeral";
|
|
436
|
+
const lanes = ephemeral ? conn.lanes : h.clients.get(conn.clientId).lanes;
|
|
437
|
+
let lane = lanes.get(protocol);
|
|
438
|
+
if (lane === void 0) {
|
|
439
|
+
lane = {
|
|
440
|
+
protocol,
|
|
441
|
+
clientId: conn.clientId,
|
|
442
|
+
conn: ephemeral ? conn : null,
|
|
443
|
+
lastSeq: 0,
|
|
444
|
+
results: /* @__PURE__ */ new Map(),
|
|
445
|
+
inflight: /* @__PURE__ */ new Map()
|
|
446
|
+
};
|
|
447
|
+
lanes.set(protocol, lane);
|
|
448
|
+
}
|
|
449
|
+
return lane;
|
|
450
|
+
};
|
|
451
|
+
const reemit = (h, lane, seq) => {
|
|
452
|
+
const cached = lane.results.get(seq);
|
|
330
453
|
if (cached) {
|
|
331
|
-
if (seq === Math.max(...
|
|
332
|
-
queueCmd(h,
|
|
454
|
+
if (seq === Math.max(...lane.results.keys()) || Date.now() - cached.ts <= RESULT_TTL_MS) {
|
|
455
|
+
queueCmd(h, lane, cached.cmd);
|
|
333
456
|
return;
|
|
334
457
|
}
|
|
335
|
-
|
|
458
|
+
lane.results.delete(seq);
|
|
336
459
|
notifyClientRecords(h);
|
|
337
460
|
}
|
|
338
|
-
const adm =
|
|
461
|
+
const adm = lane.inflight.get(seq);
|
|
339
462
|
if (adm) {
|
|
340
|
-
if (adm.applied) queueApplied(h,
|
|
463
|
+
if (adm.applied) queueApplied(h, lane, adm);
|
|
341
464
|
return;
|
|
342
465
|
}
|
|
343
|
-
queueCmd(h,
|
|
466
|
+
queueCmd(h, lane, {
|
|
467
|
+
protocol: lane.protocol,
|
|
344
468
|
seq,
|
|
345
469
|
type: "expired"
|
|
346
470
|
});
|
|
347
471
|
};
|
|
348
|
-
const settleCmd = (h,
|
|
472
|
+
const settleCmd = (h, lane, adm, cmd) => {
|
|
349
473
|
if (adm.settled) return;
|
|
350
474
|
adm.settled = true;
|
|
351
|
-
|
|
352
|
-
retain(
|
|
353
|
-
|
|
475
|
+
lane.inflight.delete(adm.seq);
|
|
476
|
+
retain(lane, cmd);
|
|
477
|
+
lane.lastSeq = Math.max(lane.lastSeq, adm.seq);
|
|
354
478
|
adm.applied = true;
|
|
355
|
-
queueCmd(h,
|
|
356
|
-
notifyClientRecords(h);
|
|
479
|
+
queueCmd(h, lane, cmd);
|
|
480
|
+
if (lane.conn === null) notifyClientRecords(h);
|
|
357
481
|
};
|
|
358
|
-
const settleWire = (h,
|
|
359
|
-
const callerOf = (h, rec,
|
|
482
|
+
const settleWire = (h, lane, adm, outcome) => settleCmd(h, lane, adm, cmdOf(adm, outcome));
|
|
483
|
+
const callerOf = (h, rec, conn) => ({
|
|
360
484
|
clientId: rec.id,
|
|
361
|
-
auth,
|
|
362
|
-
get context() {
|
|
363
|
-
return rec.context;
|
|
364
|
-
},
|
|
485
|
+
auth: conn.auth,
|
|
365
486
|
get isConnected() {
|
|
366
487
|
if (h.clients.get(rec.id) !== rec) return false;
|
|
367
488
|
for (const conn of h.conns) if (conn.clientId === rec.id && conn.open) return true;
|
|
368
489
|
return false;
|
|
369
490
|
}
|
|
370
491
|
});
|
|
371
|
-
const rejectStatement = (h,
|
|
492
|
+
const rejectStatement = (h, lane, adm, message) => settleCmd(h, lane, adm, {
|
|
493
|
+
protocol: adm.protocol,
|
|
372
494
|
seq: adm.seq,
|
|
373
495
|
type: "rejected",
|
|
374
496
|
code: "invalid-params",
|
|
375
497
|
message
|
|
376
498
|
});
|
|
377
|
-
const admitCmd = (h,
|
|
499
|
+
const admitCmd = (h, lane, conn, statement) => {
|
|
500
|
+
const rec = h.clients.get(conn.clientId);
|
|
378
501
|
const adm = {
|
|
502
|
+
protocol: statement.protocol,
|
|
379
503
|
seq: statement.seq,
|
|
380
504
|
method: statement.method,
|
|
381
505
|
applied: false,
|
|
382
506
|
appliedSent: false,
|
|
383
507
|
settled: false
|
|
384
508
|
};
|
|
385
|
-
|
|
386
|
-
notifyClientRecords(h);
|
|
387
|
-
if (statement.protocol !== h.protocol.name || !selection.protocols.some((p) => p.name === statement.protocol)) {
|
|
388
|
-
settleCmd(h, rec, adm, {
|
|
389
|
-
seq: adm.seq,
|
|
390
|
-
type: "rejected",
|
|
391
|
-
code: "unsupported-protocol",
|
|
392
|
-
message: "command protocol was not negotiated"
|
|
393
|
-
});
|
|
394
|
-
return;
|
|
395
|
-
}
|
|
509
|
+
lane.inflight.set(statement.seq, adm);
|
|
510
|
+
if (lane.conn === null) notifyClientRecords(h);
|
|
396
511
|
if (!Array.isArray(statement.params)) {
|
|
397
|
-
rejectStatement(h,
|
|
512
|
+
rejectStatement(h, lane, adm, "params must be an array");
|
|
398
513
|
return;
|
|
399
514
|
}
|
|
400
515
|
let params = statement.params;
|
|
401
516
|
if (statement.meta !== void 0) try {
|
|
402
517
|
params = reviveDocumentValue(params, statement.meta, { rejectUnknown: true });
|
|
403
518
|
} catch (error) {
|
|
404
|
-
rejectStatement(h,
|
|
519
|
+
rejectStatement(h, lane, adm, `invalid params: ${error.message}`);
|
|
405
520
|
return;
|
|
406
521
|
}
|
|
407
522
|
if (statement.method === "statewire/commands") {
|
|
408
523
|
if (params.length > 0) {
|
|
409
|
-
rejectStatement(h,
|
|
524
|
+
rejectStatement(h, lane, adm, "statewire/commands takes no params");
|
|
410
525
|
return;
|
|
411
526
|
}
|
|
412
527
|
const names = [...h.commandsHost?.methods?.() ?? [], "statewire/commands"].sort();
|
|
413
|
-
settleCmd(h,
|
|
528
|
+
settleCmd(h, lane, adm, {
|
|
529
|
+
protocol: adm.protocol,
|
|
414
530
|
seq: adm.seq,
|
|
415
531
|
type: "result",
|
|
416
532
|
payload: names
|
|
417
533
|
});
|
|
418
534
|
return;
|
|
419
535
|
}
|
|
536
|
+
const method = statement.protocol === h.protocol.name ? statement.method : `${statement.protocol}/${statement.method}`;
|
|
420
537
|
h.commandsHost.submit({
|
|
421
|
-
method
|
|
538
|
+
method,
|
|
422
539
|
params,
|
|
423
540
|
clientId: rec.id,
|
|
424
|
-
caller: callerOf(h, rec,
|
|
541
|
+
caller: callerOf(h, rec, conn),
|
|
542
|
+
attach: conn.attach,
|
|
425
543
|
onApplied: () => {
|
|
426
544
|
if (adm.settled || adm.applied) return;
|
|
427
545
|
adm.applied = true;
|
|
428
|
-
|
|
429
|
-
notifyClientRecords(h);
|
|
546
|
+
lane.lastSeq = Math.max(lane.lastSeq, adm.seq);
|
|
547
|
+
if (lane.conn === null) notifyClientRecords(h);
|
|
430
548
|
scheduleFlush(h);
|
|
431
549
|
},
|
|
432
|
-
onSettle: (outcome) => settleWire(h,
|
|
550
|
+
onSettle: (outcome) => settleWire(h, lane, adm, outcome)
|
|
433
551
|
}).catch((error) => {
|
|
434
|
-
if (!adm.settled) settleWire(h,
|
|
552
|
+
if (!adm.settled) settleWire(h, lane, adm, {
|
|
435
553
|
type: "rejected",
|
|
436
554
|
error
|
|
437
555
|
});
|
|
438
556
|
});
|
|
439
557
|
};
|
|
440
|
-
const
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
})
|
|
558
|
+
const refusal = (h, conn, retry) => ({
|
|
559
|
+
type: "refused",
|
|
560
|
+
syn: { syn: {
|
|
561
|
+
protocols: conn.selection.protocols.map((p) => ({
|
|
562
|
+
name: p.name,
|
|
563
|
+
seq: watermark(h, conn, p.name)
|
|
564
|
+
})),
|
|
565
|
+
...retry !== void 0 && { retry }
|
|
566
|
+
} }
|
|
567
|
+
});
|
|
568
|
+
const admitFrame = (h, conn, frame) => {
|
|
569
|
+
const rec = h.clients.get(conn.clientId);
|
|
447
570
|
rec.touched = Date.now();
|
|
448
|
-
const
|
|
449
|
-
|
|
450
|
-
const
|
|
451
|
-
const
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
}
|
|
571
|
+
const lanes = new Map(frame.cmd.map((s) => [s.protocol, laneOf(h, conn, s.protocol)]));
|
|
572
|
+
const through = new Map([...lanes].map(([protocol, lane]) => [protocol, admittedThrough(lane)]));
|
|
573
|
+
const fresh = frame.cmd.filter((s) => s.seq > through.get(s.protocol));
|
|
574
|
+
for (const s of frame.cmd) if (s.seq <= through.get(s.protocol)) reemit(h, lanes.get(s.protocol), s.seq);
|
|
575
|
+
let durableNew = 0;
|
|
576
|
+
for (const [protocol, lane] of lanes) {
|
|
577
|
+
const first = fresh.find((s) => s.protocol === protocol);
|
|
578
|
+
if (first === void 0) continue;
|
|
579
|
+
if (first.seq !== through.get(protocol) + 1) {
|
|
580
|
+
if (lane.conn !== null) return violation("ephemeral lane gap");
|
|
581
|
+
notifyClientRecords(h);
|
|
582
|
+
return refusal(h, conn);
|
|
583
|
+
}
|
|
584
|
+
if (lane.conn === null) durableNew += fresh.filter((s) => s.protocol === protocol).length;
|
|
461
585
|
}
|
|
462
|
-
if (
|
|
463
|
-
let
|
|
464
|
-
for (const
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
seq: through,
|
|
469
|
-
ctxSeq: rec.ctxSeq,
|
|
470
|
-
retry: COMMAND_CAPACITY_RETRY_MS
|
|
471
|
-
} }
|
|
472
|
-
};
|
|
586
|
+
if (durableNew > 0) {
|
|
587
|
+
let clientLoad = 0;
|
|
588
|
+
for (const lane of rec.lanes.values()) clientLoad += lane.inflight.size;
|
|
589
|
+
let hostLoad = 0;
|
|
590
|
+
for (const lane of durableLanes(h)) hostLoad += lane.inflight.size;
|
|
591
|
+
if (clientLoad + durableNew > 256 || hostLoad + durableNew > h.maxInflightCommands) return refusal(h, conn, COMMAND_CAPACITY_RETRY_MS);
|
|
473
592
|
}
|
|
474
|
-
if (
|
|
593
|
+
if (fresh.length > 0 && h.pushback) {
|
|
475
594
|
const retry = h.pushback();
|
|
476
595
|
if (retry !== null) {
|
|
477
596
|
if (!Number.isSafeInteger(retry) || retry < 0) throw new TypeError("pushback must return a nonnegative safe integer or null");
|
|
478
|
-
return
|
|
479
|
-
type: "refused",
|
|
480
|
-
syn: { syn: {
|
|
481
|
-
seq: through,
|
|
482
|
-
ctxSeq: rec.ctxSeq,
|
|
483
|
-
retry
|
|
484
|
-
} }
|
|
485
|
-
};
|
|
597
|
+
return refusal(h, conn, retry);
|
|
486
598
|
}
|
|
487
599
|
}
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
if (
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
if (ctxNew.length) rec.ctxSeq = ctxNew.at(-1).seq;
|
|
498
|
-
for (const statement of cmdNew) admitCmd(h, rec, statement, auth, selection);
|
|
499
|
-
if (cmdNew.length > 0) queueCmd(h, rec.id, {
|
|
500
|
-
seq: cmdNew.at(-1).seq,
|
|
501
|
-
dur: true
|
|
502
|
-
});
|
|
600
|
+
for (const s of fresh) admitCmd(h, lanes.get(s.protocol), conn, s);
|
|
601
|
+
for (const [protocol, lane] of lanes) {
|
|
602
|
+
const last = fresh.findLast((s) => s.protocol === protocol);
|
|
603
|
+
if (last !== void 0 && lane.conn === null) queueCmd(h, lane, {
|
|
604
|
+
protocol,
|
|
605
|
+
seq: last.seq,
|
|
606
|
+
dur: true
|
|
607
|
+
});
|
|
608
|
+
}
|
|
503
609
|
notifyClientRecords(h);
|
|
504
610
|
scheduleFlush(h);
|
|
505
611
|
return { type: "ok" };
|
|
@@ -516,32 +622,15 @@ const message = (h, conn, data) => {
|
|
|
516
622
|
}
|
|
517
623
|
const first = !conn.sawFrame;
|
|
518
624
|
conn.sawFrame = true;
|
|
519
|
-
let frame;
|
|
520
|
-
try {
|
|
521
|
-
frame = first ? parseInitialFrame(data).frame : parseFrame(data);
|
|
522
|
-
} catch (error) {
|
|
523
|
-
const reason = violationReason(error);
|
|
524
|
-
if (reason === null) throw error;
|
|
525
|
-
sendTo(h, conn, JSON.stringify({ fin: {
|
|
526
|
-
reason: "protocol-error",
|
|
527
|
-
message: reason
|
|
528
|
-
} }));
|
|
529
|
-
closeConn(h, conn, 1008, reason);
|
|
530
|
-
return;
|
|
531
|
-
}
|
|
532
|
-
if (frame.ctx.length === 0 && frame.cmd.length === 0) return;
|
|
533
|
-
const rec = h.clients.get(conn.clientId);
|
|
534
625
|
let admission;
|
|
535
626
|
try {
|
|
536
|
-
|
|
627
|
+
const frame = first ? parseInitialFrame(data).frame : parseFrame(data);
|
|
628
|
+
if (frame.cmd.length === 0) return;
|
|
629
|
+
admission = admitFrame(h, conn, frame);
|
|
537
630
|
} catch (error) {
|
|
538
631
|
const reason = violationReason(error);
|
|
539
632
|
if (reason === null) throw error;
|
|
540
|
-
|
|
541
|
-
reason: "protocol-error",
|
|
542
|
-
message: reason
|
|
543
|
-
} }));
|
|
544
|
-
closeConn(h, conn, 1008, reason);
|
|
633
|
+
violate(h, conn, reason);
|
|
545
634
|
return;
|
|
546
635
|
}
|
|
547
636
|
if (admission.type === "refused") sendTo(h, conn, JSON.stringify(admission.syn));
|
|
@@ -701,13 +790,15 @@ const connectAuthorized = (h, clientId, socket, options) => {
|
|
|
701
790
|
pending.open = false;
|
|
702
791
|
h.pendingConns.delete(pending);
|
|
703
792
|
if (pending.timer !== null) clearTimeout(pending.timer);
|
|
704
|
-
|
|
793
|
+
let registration;
|
|
794
|
+
try {
|
|
795
|
+
registration = register(h, clientId, socket, true, auth, selection, frame);
|
|
796
|
+
} catch (error) {
|
|
797
|
+
if (violationReason(error) === null) throw error;
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
705
800
|
if (registration === null) return;
|
|
706
801
|
pending.connection = registration.connection;
|
|
707
|
-
if (frame.ctx.length || frame.cmd.length) message(h, registration.conn, JSON.stringify({
|
|
708
|
-
...frame.ctx.length ? { ctx: frame.ctx } : {},
|
|
709
|
-
...frame.cmd.length ? { cmd: frame.cmd } : {}
|
|
710
|
-
}));
|
|
711
802
|
for (const data of pending.queued.splice(0)) registration.connection.message(data);
|
|
712
803
|
}).catch((error) => {
|
|
713
804
|
if (pending.open) rejectPendingAuthorization(h, pending, error);
|
|
@@ -727,20 +818,25 @@ const gcClients = (h) => {
|
|
|
727
818
|
const oldest = [...h.clients.values()].sort((a, b) => a.touched - b.touched);
|
|
728
819
|
for (const rec of oldest) {
|
|
729
820
|
if (excess <= 0) break;
|
|
730
|
-
if (live.has(rec.id) || rec.inflight.size > 0) continue;
|
|
821
|
+
if (live.has(rec.id) || [...rec.lanes.values()].some((lane) => lane.inflight.size > 0)) continue;
|
|
731
822
|
h.clients.delete(rec.id);
|
|
732
823
|
changed = true;
|
|
733
824
|
excess -= 1;
|
|
734
825
|
}
|
|
735
826
|
if (changed) notifyClientRecords(h);
|
|
736
827
|
};
|
|
828
|
+
/**
|
|
829
|
+
* Attach a socket: ephemeral-lane statements of the attach frame are admitted
|
|
830
|
+
* before the snapshot (their documents and answers ride the hello), durable
|
|
831
|
+
* ones after it. A violation in the attach frame closes the attach and throws.
|
|
832
|
+
*/
|
|
737
833
|
const register = (h, clientId, socket, sawFrame = false, auth = void 0, selection = {
|
|
738
834
|
version: h.wireVersion.version,
|
|
739
835
|
protocols: [{
|
|
740
836
|
name: h.protocol.name,
|
|
741
837
|
version: h.protocol.version
|
|
742
838
|
}]
|
|
743
|
-
}) => {
|
|
839
|
+
}, frame = { cmd: [] }) => {
|
|
744
840
|
const refuse = (code, reason) => {
|
|
745
841
|
try {
|
|
746
842
|
socket.close(code, reason);
|
|
@@ -772,16 +868,8 @@ const register = (h, clientId, socket, sawFrame = false, auth = void 0, selectio
|
|
|
772
868
|
const rec = known ?? (() => {
|
|
773
869
|
const created = {
|
|
774
870
|
id: clientId,
|
|
775
|
-
lastSeq: 0,
|
|
776
|
-
ctxSeq: 0,
|
|
777
871
|
touched: Date.now(),
|
|
778
|
-
|
|
779
|
-
contextDocuments: {
|
|
780
|
-
mode: "mounted",
|
|
781
|
-
documents: /* @__PURE__ */ new Map()
|
|
782
|
-
},
|
|
783
|
-
results: /* @__PURE__ */ new Map(),
|
|
784
|
-
inflight: /* @__PURE__ */ new Map(),
|
|
872
|
+
lanes: /* @__PURE__ */ new Map(),
|
|
785
873
|
principal: auth
|
|
786
874
|
};
|
|
787
875
|
h.clients.set(clientId, created);
|
|
@@ -793,35 +881,72 @@ const register = (h, clientId, socket, sawFrame = false, auth = void 0, selectio
|
|
|
793
881
|
clientId,
|
|
794
882
|
socket,
|
|
795
883
|
open: true,
|
|
884
|
+
ready: false,
|
|
796
885
|
sawFrame,
|
|
797
886
|
lastSentAt: 0,
|
|
798
887
|
lease: null,
|
|
799
|
-
auth
|
|
888
|
+
auth,
|
|
889
|
+
lanes: /* @__PURE__ */ new Map(),
|
|
890
|
+
documents: /* @__PURE__ */ new Map(),
|
|
891
|
+
included: /* @__PURE__ */ new Set(),
|
|
892
|
+
view: /* @__PURE__ */ new Map(),
|
|
893
|
+
nextDoc: 1,
|
|
894
|
+
pendingApplied: [],
|
|
895
|
+
pendingCmds: [],
|
|
896
|
+
attach: void 0
|
|
800
897
|
};
|
|
898
|
+
conn.attach = attachOf(h, conn);
|
|
801
899
|
h.conns.add(conn);
|
|
802
900
|
gcClients(h);
|
|
803
|
-
notifyClientRecords(h);
|
|
804
901
|
ensureHeartbeat(h);
|
|
805
|
-
const
|
|
806
|
-
const
|
|
807
|
-
|
|
902
|
+
const isEphemeral = (s) => selection.protocols.find((p) => p.name === s.protocol)?.record === "ephemeral";
|
|
903
|
+
const early = frame.cmd.filter(isEphemeral);
|
|
904
|
+
const late = frame.cmd.filter((s) => !isEphemeral(s));
|
|
905
|
+
const admit = (statements) => {
|
|
906
|
+
if (statements.length === 0) return null;
|
|
907
|
+
try {
|
|
908
|
+
return admitFrame(h, conn, { cmd: statements });
|
|
909
|
+
} catch (error) {
|
|
910
|
+
const reason = violationReason(error);
|
|
911
|
+
if (reason === null) throw error;
|
|
912
|
+
violate(h, conn, reason);
|
|
913
|
+
throw error;
|
|
914
|
+
}
|
|
915
|
+
};
|
|
916
|
+
const earlyAdmission = admit(early);
|
|
917
|
+
if (early.length > 0) flush(h);
|
|
918
|
+
notifyClientRecords(h);
|
|
919
|
+
const ops = [];
|
|
920
|
+
if (selects(conn, h.protocol.name)) {
|
|
921
|
+
const { value, meta } = encodeDocument(h.stateHost.snapshot());
|
|
922
|
+
ops.push({
|
|
808
923
|
op: "mount",
|
|
809
924
|
protocol: h.protocol.name,
|
|
810
925
|
main: true,
|
|
811
926
|
value,
|
|
812
927
|
...meta !== void 0 && { meta }
|
|
813
|
-
}
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
ops.push(...diffView(h, conn));
|
|
931
|
+
const answers = [...conn.pendingApplied.splice(0), ...conn.pendingCmds.splice(0)];
|
|
932
|
+
const hello = {
|
|
933
|
+
ops,
|
|
814
934
|
syn: {
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
935
|
+
version: selection.version,
|
|
936
|
+
protocols: selection.protocols.map((p) => ({
|
|
937
|
+
...p,
|
|
938
|
+
seq: p.record === "ephemeral" ? 0 : watermark(h, conn, p.name)
|
|
939
|
+
})),
|
|
818
940
|
...h.idleWire && { idle: true }
|
|
819
|
-
}
|
|
941
|
+
},
|
|
942
|
+
...answers.length > 0 && { cmd: answers }
|
|
820
943
|
};
|
|
821
|
-
|
|
944
|
+
conn.ready = true;
|
|
822
945
|
sendTo(h, conn, JSON.stringify(hello));
|
|
946
|
+
if (earlyAdmission?.type === "refused") sendTo(h, conn, JSON.stringify(earlyAdmission.syn));
|
|
947
|
+
const lateAdmission = admit(late);
|
|
948
|
+
if (lateAdmission?.type === "refused") sendTo(h, conn, JSON.stringify(lateAdmission.syn));
|
|
823
949
|
return {
|
|
824
|
-
rec,
|
|
825
950
|
conn,
|
|
826
951
|
connection: {
|
|
827
952
|
clientId,
|
|
@@ -847,6 +972,7 @@ const finish = (h, fin) => {
|
|
|
847
972
|
const resume = (h) => {
|
|
848
973
|
h.disposed = false;
|
|
849
974
|
h.stateHost?.setOnOp((op) => recordOp(h, op));
|
|
975
|
+
h.stateHost?.setOnDocuments(() => markViews(h));
|
|
850
976
|
};
|
|
851
977
|
const suspend = (h) => {
|
|
852
978
|
h.disposed = true;
|
|
@@ -855,18 +981,23 @@ const suspend = (h) => {
|
|
|
855
981
|
h.heartbeatTimer = null;
|
|
856
982
|
}
|
|
857
983
|
h.stateHost?.setOnOp(null);
|
|
984
|
+
h.stateHost?.setOnDocuments(null);
|
|
858
985
|
};
|
|
859
986
|
const restoreClientRecords = (records = []) => new Map(records.map((record) => [record.id, {
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
cmd
|
|
987
|
+
id: record.id,
|
|
988
|
+
touched: record.touched,
|
|
989
|
+
lanes: new Map(Object.entries(record.lanes).map(([protocol, lane]) => [protocol, {
|
|
990
|
+
protocol,
|
|
991
|
+
clientId: record.id,
|
|
992
|
+
conn: null,
|
|
993
|
+
lastSeq: lane.lastSeq,
|
|
994
|
+
results: new Map(lane.results.map(({ seq, ts, cmd }) => [seq, {
|
|
995
|
+
ts,
|
|
996
|
+
cmd
|
|
997
|
+
}])),
|
|
998
|
+
inflight: /* @__PURE__ */ new Map()
|
|
868
999
|
}])),
|
|
869
|
-
|
|
1000
|
+
principal: record.principal
|
|
870
1001
|
}]));
|
|
871
1002
|
const useSocketHost = (transport, options, persistence) => {
|
|
872
1003
|
const [h] = useState(() => {
|
|
@@ -891,6 +1022,7 @@ const useSocketHost = (transport, options, persistence) => {
|
|
|
891
1022
|
pendingOps: [],
|
|
892
1023
|
pendingApplied: /* @__PURE__ */ new Map(),
|
|
893
1024
|
pendingCmds: /* @__PURE__ */ new Map(),
|
|
1025
|
+
viewsDirty: false,
|
|
894
1026
|
idleWire: true,
|
|
895
1027
|
finished: null,
|
|
896
1028
|
disposed: false,
|
|
@@ -913,9 +1045,9 @@ const useSocketHost = (transport, options, persistence) => {
|
|
|
913
1045
|
});
|
|
914
1046
|
h.protocol = options?.protocol ?? {
|
|
915
1047
|
name: "default",
|
|
916
|
-
version: "2026-09-
|
|
1048
|
+
version: "2026-09-13"
|
|
917
1049
|
};
|
|
918
|
-
h.wireVersion = options?.wireVersion ?? { version: "2026-09-
|
|
1050
|
+
h.wireVersion = options?.wireVersion ?? { version: "2026-09-13" };
|
|
919
1051
|
setHeartbeatMs(h, options?.heartbeatIntervalMs ?? 15e3);
|
|
920
1052
|
h.pushback = options?.pushback;
|
|
921
1053
|
h.wsInitTimeoutMs = options?.wsInitTimeoutMs ?? 5e3;
|
|
@@ -947,9 +1079,10 @@ const useSocketHost = (transport, options, persistence) => {
|
|
|
947
1079
|
* Transport-agnostic WS binding over a statewire/host transport:
|
|
948
1080
|
* feed it sockets (anything with `send`/`close`) and raw text frames, and it
|
|
949
1081
|
* speaks the Statewire packet protocol — snapshot-first hello with `syn`,
|
|
950
|
-
*
|
|
951
|
-
* arrays fanned out to every attach of a client id
|
|
952
|
-
*
|
|
1082
|
+
* per-protocol statement lanes under one admission law, `{cmd}` answer
|
|
1083
|
+
* arrays fanned out to every attach of a client id (durable lanes) or the
|
|
1084
|
+
* admitting attach (ephemeral lanes), per-attach document views, idle edges,
|
|
1085
|
+
* and `fin` on finish.
|
|
953
1086
|
*/
|
|
954
1087
|
const StatewireSocketHost = (transport, options, persistence) => {
|
|
955
1088
|
const SocketHostRoot = () => {
|