statewire 0.18.2 → 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 +291 -256
- 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 +8 -17
- package/dist/host/http.js.map +1 -1
- package/dist/host/socket.d.ts +47 -23
- package/dist/host/socket.d.ts.map +1 -1
- package/dist/host/socket.js +389 -245
- 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 -8
- 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 -10
- 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 +55 -41
- 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 -19
- 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 +434 -365
- 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 +22 -23
- package/src/host/socket.ts +550 -321
- package/src/host/synced.ts +60 -0
- package/src/host-internal.ts +21 -3
- package/src/host.ts +40 -8
- package/src/index.ts +1 -0
- package/src/statewire.ts +8 -30
- package/src/storage.ts +96 -79
- package/src/transports/remote.ts +0 -7
- package/src/transports/sse.ts +8 -3
- package/src/transports/testing.ts +34 -19
- 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,213 +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
404
|
touched: rec.touched,
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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
|
+
}])),
|
|
310
413
|
principal: rec.principal
|
|
311
414
|
})));
|
|
312
415
|
};
|
|
313
|
-
const retain = (
|
|
416
|
+
const retain = (lane, cmd) => {
|
|
314
417
|
const now = Date.now();
|
|
315
|
-
|
|
418
|
+
lane.results.set(cmd.seq, {
|
|
316
419
|
ts: now,
|
|
317
420
|
cmd
|
|
318
421
|
});
|
|
319
|
-
const newest = Math.max(...
|
|
320
|
-
for (const [seq, entry] of
|
|
321
|
-
while (
|
|
322
|
-
};
|
|
323
|
-
/**
|
|
324
|
-
const admittedThrough = (
|
|
325
|
-
const
|
|
326
|
-
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);
|
|
327
453
|
if (cached) {
|
|
328
|
-
if (seq === Math.max(...
|
|
329
|
-
queueCmd(h,
|
|
454
|
+
if (seq === Math.max(...lane.results.keys()) || Date.now() - cached.ts <= RESULT_TTL_MS) {
|
|
455
|
+
queueCmd(h, lane, cached.cmd);
|
|
330
456
|
return;
|
|
331
457
|
}
|
|
332
|
-
|
|
458
|
+
lane.results.delete(seq);
|
|
333
459
|
notifyClientRecords(h);
|
|
334
460
|
}
|
|
335
|
-
const adm =
|
|
461
|
+
const adm = lane.inflight.get(seq);
|
|
336
462
|
if (adm) {
|
|
337
|
-
if (adm.applied) queueApplied(h,
|
|
463
|
+
if (adm.applied) queueApplied(h, lane, adm);
|
|
338
464
|
return;
|
|
339
465
|
}
|
|
340
|
-
queueCmd(h,
|
|
466
|
+
queueCmd(h, lane, {
|
|
467
|
+
protocol: lane.protocol,
|
|
341
468
|
seq,
|
|
342
469
|
type: "expired"
|
|
343
470
|
});
|
|
344
471
|
};
|
|
345
|
-
const settleCmd = (h,
|
|
472
|
+
const settleCmd = (h, lane, adm, cmd) => {
|
|
346
473
|
if (adm.settled) return;
|
|
347
474
|
adm.settled = true;
|
|
348
|
-
|
|
349
|
-
retain(
|
|
350
|
-
|
|
475
|
+
lane.inflight.delete(adm.seq);
|
|
476
|
+
retain(lane, cmd);
|
|
477
|
+
lane.lastSeq = Math.max(lane.lastSeq, adm.seq);
|
|
351
478
|
adm.applied = true;
|
|
352
|
-
queueCmd(h,
|
|
353
|
-
notifyClientRecords(h);
|
|
479
|
+
queueCmd(h, lane, cmd);
|
|
480
|
+
if (lane.conn === null) notifyClientRecords(h);
|
|
354
481
|
};
|
|
355
|
-
const settleWire = (h,
|
|
482
|
+
const settleWire = (h, lane, adm, outcome) => settleCmd(h, lane, adm, cmdOf(adm, outcome));
|
|
356
483
|
const callerOf = (h, rec, conn) => ({
|
|
357
484
|
clientId: rec.id,
|
|
358
485
|
auth: conn.auth,
|
|
359
|
-
get context() {
|
|
360
|
-
return conn.context;
|
|
361
|
-
},
|
|
362
486
|
get isConnected() {
|
|
363
487
|
if (h.clients.get(rec.id) !== rec) return false;
|
|
364
488
|
for (const conn of h.conns) if (conn.clientId === rec.id && conn.open) return true;
|
|
365
489
|
return false;
|
|
366
490
|
}
|
|
367
491
|
});
|
|
368
|
-
const rejectStatement = (h,
|
|
492
|
+
const rejectStatement = (h, lane, adm, message) => settleCmd(h, lane, adm, {
|
|
493
|
+
protocol: adm.protocol,
|
|
369
494
|
seq: adm.seq,
|
|
370
495
|
type: "rejected",
|
|
371
496
|
code: "invalid-params",
|
|
372
497
|
message
|
|
373
498
|
});
|
|
374
|
-
const admitCmd = (h,
|
|
499
|
+
const admitCmd = (h, lane, conn, statement) => {
|
|
500
|
+
const rec = h.clients.get(conn.clientId);
|
|
375
501
|
const adm = {
|
|
502
|
+
protocol: statement.protocol,
|
|
376
503
|
seq: statement.seq,
|
|
377
504
|
method: statement.method,
|
|
378
505
|
applied: false,
|
|
379
506
|
appliedSent: false,
|
|
380
507
|
settled: false
|
|
381
508
|
};
|
|
382
|
-
|
|
383
|
-
notifyClientRecords(h);
|
|
384
|
-
if (statement.protocol !== h.protocol.name || !conn.selection.protocols.some((p) => p.name === statement.protocol)) {
|
|
385
|
-
settleCmd(h, rec, adm, {
|
|
386
|
-
seq: adm.seq,
|
|
387
|
-
type: "rejected",
|
|
388
|
-
code: "unsupported-protocol",
|
|
389
|
-
message: "command protocol was not negotiated"
|
|
390
|
-
});
|
|
391
|
-
return;
|
|
392
|
-
}
|
|
509
|
+
lane.inflight.set(statement.seq, adm);
|
|
510
|
+
if (lane.conn === null) notifyClientRecords(h);
|
|
393
511
|
if (!Array.isArray(statement.params)) {
|
|
394
|
-
rejectStatement(h,
|
|
512
|
+
rejectStatement(h, lane, adm, "params must be an array");
|
|
395
513
|
return;
|
|
396
514
|
}
|
|
397
515
|
let params = statement.params;
|
|
398
516
|
if (statement.meta !== void 0) try {
|
|
399
517
|
params = reviveDocumentValue(params, statement.meta, { rejectUnknown: true });
|
|
400
518
|
} catch (error) {
|
|
401
|
-
rejectStatement(h,
|
|
519
|
+
rejectStatement(h, lane, adm, `invalid params: ${error.message}`);
|
|
402
520
|
return;
|
|
403
521
|
}
|
|
404
522
|
if (statement.method === "statewire/commands") {
|
|
405
523
|
if (params.length > 0) {
|
|
406
|
-
rejectStatement(h,
|
|
524
|
+
rejectStatement(h, lane, adm, "statewire/commands takes no params");
|
|
407
525
|
return;
|
|
408
526
|
}
|
|
409
527
|
const names = [...h.commandsHost?.methods?.() ?? [], "statewire/commands"].sort();
|
|
410
|
-
settleCmd(h,
|
|
528
|
+
settleCmd(h, lane, adm, {
|
|
529
|
+
protocol: adm.protocol,
|
|
411
530
|
seq: adm.seq,
|
|
412
531
|
type: "result",
|
|
413
532
|
payload: names
|
|
414
533
|
});
|
|
415
534
|
return;
|
|
416
535
|
}
|
|
536
|
+
const method = statement.protocol === h.protocol.name ? statement.method : `${statement.protocol}/${statement.method}`;
|
|
417
537
|
h.commandsHost.submit({
|
|
418
|
-
method
|
|
538
|
+
method,
|
|
419
539
|
params,
|
|
420
540
|
clientId: rec.id,
|
|
421
541
|
caller: callerOf(h, rec, conn),
|
|
542
|
+
attach: conn.attach,
|
|
422
543
|
onApplied: () => {
|
|
423
544
|
if (adm.settled || adm.applied) return;
|
|
424
545
|
adm.applied = true;
|
|
425
|
-
|
|
426
|
-
notifyClientRecords(h);
|
|
546
|
+
lane.lastSeq = Math.max(lane.lastSeq, adm.seq);
|
|
547
|
+
if (lane.conn === null) notifyClientRecords(h);
|
|
427
548
|
scheduleFlush(h);
|
|
428
549
|
},
|
|
429
|
-
onSettle: (outcome) => settleWire(h,
|
|
550
|
+
onSettle: (outcome) => settleWire(h, lane, adm, outcome)
|
|
430
551
|
}).catch((error) => {
|
|
431
|
-
if (!adm.settled) settleWire(h,
|
|
552
|
+
if (!adm.settled) settleWire(h, lane, adm, {
|
|
432
553
|
type: "rejected",
|
|
433
554
|
error
|
|
434
555
|
});
|
|
435
556
|
});
|
|
436
557
|
};
|
|
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
|
+
});
|
|
437
568
|
const admitFrame = (h, conn, frame) => {
|
|
438
569
|
const rec = h.clients.get(conn.clientId);
|
|
439
570
|
rec.touched = Date.now();
|
|
440
|
-
const
|
|
441
|
-
|
|
442
|
-
const
|
|
443
|
-
const
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
}
|
|
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;
|
|
453
585
|
}
|
|
454
|
-
if (
|
|
455
|
-
let
|
|
456
|
-
for (const
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
seq: through,
|
|
461
|
-
ctxSeq: conn.ctxSeq,
|
|
462
|
-
retry: COMMAND_CAPACITY_RETRY_MS
|
|
463
|
-
} }
|
|
464
|
-
};
|
|
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);
|
|
465
592
|
}
|
|
466
|
-
if (
|
|
593
|
+
if (fresh.length > 0 && h.pushback) {
|
|
467
594
|
const retry = h.pushback();
|
|
468
595
|
if (retry !== null) {
|
|
469
596
|
if (!Number.isSafeInteger(retry) || retry < 0) throw new TypeError("pushback must return a nonnegative safe integer or null");
|
|
470
|
-
return
|
|
471
|
-
type: "refused",
|
|
472
|
-
syn: { syn: {
|
|
473
|
-
seq: through,
|
|
474
|
-
ctxSeq: conn.ctxSeq,
|
|
475
|
-
retry
|
|
476
|
-
} }
|
|
477
|
-
};
|
|
597
|
+
return refusal(h, conn, retry);
|
|
478
598
|
}
|
|
479
599
|
}
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
if (
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
if (ctxNew.length) conn.ctxSeq = ctxNew.at(-1).seq;
|
|
490
|
-
for (const statement of cmdNew) admitCmd(h, rec, conn, statement);
|
|
491
|
-
if (cmdNew.length > 0) queueCmd(h, rec.id, {
|
|
492
|
-
seq: cmdNew.at(-1).seq,
|
|
493
|
-
dur: true
|
|
494
|
-
});
|
|
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
|
+
}
|
|
495
609
|
notifyClientRecords(h);
|
|
496
610
|
scheduleFlush(h);
|
|
497
611
|
return { type: "ok" };
|
|
@@ -508,31 +622,15 @@ const message = (h, conn, data) => {
|
|
|
508
622
|
}
|
|
509
623
|
const first = !conn.sawFrame;
|
|
510
624
|
conn.sawFrame = true;
|
|
511
|
-
let frame;
|
|
512
|
-
try {
|
|
513
|
-
frame = first ? parseInitialFrame(data).frame : parseFrame(data);
|
|
514
|
-
} catch (error) {
|
|
515
|
-
const reason = violationReason(error);
|
|
516
|
-
if (reason === null) throw error;
|
|
517
|
-
sendTo(h, conn, JSON.stringify({ fin: {
|
|
518
|
-
reason: "protocol-error",
|
|
519
|
-
message: reason
|
|
520
|
-
} }));
|
|
521
|
-
closeConn(h, conn, 1008, reason);
|
|
522
|
-
return;
|
|
523
|
-
}
|
|
524
|
-
if (frame.ctx.length === 0 && frame.cmd.length === 0) return;
|
|
525
625
|
let admission;
|
|
526
626
|
try {
|
|
627
|
+
const frame = first ? parseInitialFrame(data).frame : parseFrame(data);
|
|
628
|
+
if (frame.cmd.length === 0) return;
|
|
527
629
|
admission = admitFrame(h, conn, frame);
|
|
528
630
|
} catch (error) {
|
|
529
631
|
const reason = violationReason(error);
|
|
530
632
|
if (reason === null) throw error;
|
|
531
|
-
|
|
532
|
-
reason: "protocol-error",
|
|
533
|
-
message: reason
|
|
534
|
-
} }));
|
|
535
|
-
closeConn(h, conn, 1008, reason);
|
|
633
|
+
violate(h, conn, reason);
|
|
536
634
|
return;
|
|
537
635
|
}
|
|
538
636
|
if (admission.type === "refused") sendTo(h, conn, JSON.stringify(admission.syn));
|
|
@@ -692,13 +790,15 @@ const connectAuthorized = (h, clientId, socket, options) => {
|
|
|
692
790
|
pending.open = false;
|
|
693
791
|
h.pendingConns.delete(pending);
|
|
694
792
|
if (pending.timer !== null) clearTimeout(pending.timer);
|
|
695
|
-
|
|
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
|
+
}
|
|
696
800
|
if (registration === null) return;
|
|
697
801
|
pending.connection = registration.connection;
|
|
698
|
-
if (frame.ctx.length || frame.cmd.length) message(h, registration.conn, JSON.stringify({
|
|
699
|
-
...frame.ctx.length ? { ctx: frame.ctx } : {},
|
|
700
|
-
...frame.cmd.length ? { cmd: frame.cmd } : {}
|
|
701
|
-
}));
|
|
702
802
|
for (const data of pending.queued.splice(0)) registration.connection.message(data);
|
|
703
803
|
}).catch((error) => {
|
|
704
804
|
if (pending.open) rejectPendingAuthorization(h, pending, error);
|
|
@@ -718,20 +818,25 @@ const gcClients = (h) => {
|
|
|
718
818
|
const oldest = [...h.clients.values()].sort((a, b) => a.touched - b.touched);
|
|
719
819
|
for (const rec of oldest) {
|
|
720
820
|
if (excess <= 0) break;
|
|
721
|
-
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;
|
|
722
822
|
h.clients.delete(rec.id);
|
|
723
823
|
changed = true;
|
|
724
824
|
excess -= 1;
|
|
725
825
|
}
|
|
726
826
|
if (changed) notifyClientRecords(h);
|
|
727
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
|
+
*/
|
|
728
833
|
const register = (h, clientId, socket, sawFrame = false, auth = void 0, selection = {
|
|
729
834
|
version: h.wireVersion.version,
|
|
730
835
|
protocols: [{
|
|
731
836
|
name: h.protocol.name,
|
|
732
837
|
version: h.protocol.version
|
|
733
838
|
}]
|
|
734
|
-
}) => {
|
|
839
|
+
}, frame = { cmd: [] }) => {
|
|
735
840
|
const refuse = (code, reason) => {
|
|
736
841
|
try {
|
|
737
842
|
socket.close(code, reason);
|
|
@@ -763,10 +868,8 @@ const register = (h, clientId, socket, sawFrame = false, auth = void 0, selectio
|
|
|
763
868
|
const rec = known ?? (() => {
|
|
764
869
|
const created = {
|
|
765
870
|
id: clientId,
|
|
766
|
-
lastSeq: 0,
|
|
767
871
|
touched: Date.now(),
|
|
768
|
-
|
|
769
|
-
inflight: /* @__PURE__ */ new Map(),
|
|
872
|
+
lanes: /* @__PURE__ */ new Map(),
|
|
770
873
|
principal: auth
|
|
771
874
|
};
|
|
772
875
|
h.clients.set(clientId, created);
|
|
@@ -778,39 +881,71 @@ const register = (h, clientId, socket, sawFrame = false, auth = void 0, selectio
|
|
|
778
881
|
clientId,
|
|
779
882
|
socket,
|
|
780
883
|
open: true,
|
|
884
|
+
ready: false,
|
|
781
885
|
sawFrame,
|
|
782
886
|
lastSentAt: 0,
|
|
783
887
|
lease: null,
|
|
784
888
|
auth,
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
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
|
|
791
897
|
};
|
|
898
|
+
conn.attach = attachOf(h, conn);
|
|
792
899
|
h.conns.add(conn);
|
|
793
900
|
gcClients(h);
|
|
794
|
-
notifyClientRecords(h);
|
|
795
901
|
ensureHeartbeat(h);
|
|
796
|
-
const
|
|
797
|
-
const
|
|
798
|
-
|
|
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({
|
|
799
923
|
op: "mount",
|
|
800
924
|
protocol: h.protocol.name,
|
|
801
925
|
main: true,
|
|
802
926
|
value,
|
|
803
927
|
...meta !== void 0 && { meta }
|
|
804
|
-
}
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
ops.push(...diffView(h, conn));
|
|
931
|
+
const answers = [...conn.pendingApplied.splice(0), ...conn.pendingCmds.splice(0)];
|
|
932
|
+
const hello = {
|
|
933
|
+
ops,
|
|
805
934
|
syn: {
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
935
|
+
version: selection.version,
|
|
936
|
+
protocols: selection.protocols.map((p) => ({
|
|
937
|
+
...p,
|
|
938
|
+
seq: p.record === "ephemeral" ? 0 : watermark(h, conn, p.name)
|
|
939
|
+
})),
|
|
809
940
|
...h.idleWire && { idle: true }
|
|
810
|
-
}
|
|
941
|
+
},
|
|
942
|
+
...answers.length > 0 && { cmd: answers }
|
|
811
943
|
};
|
|
812
|
-
|
|
944
|
+
conn.ready = true;
|
|
813
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));
|
|
814
949
|
return {
|
|
815
950
|
conn,
|
|
816
951
|
connection: {
|
|
@@ -837,6 +972,7 @@ const finish = (h, fin) => {
|
|
|
837
972
|
const resume = (h) => {
|
|
838
973
|
h.disposed = false;
|
|
839
974
|
h.stateHost?.setOnOp((op) => recordOp(h, op));
|
|
975
|
+
h.stateHost?.setOnDocuments(() => markViews(h));
|
|
840
976
|
};
|
|
841
977
|
const suspend = (h) => {
|
|
842
978
|
h.disposed = true;
|
|
@@ -845,16 +981,22 @@ const suspend = (h) => {
|
|
|
845
981
|
h.heartbeatTimer = null;
|
|
846
982
|
}
|
|
847
983
|
h.stateHost?.setOnOp(null);
|
|
984
|
+
h.stateHost?.setOnDocuments(null);
|
|
848
985
|
};
|
|
849
986
|
const restoreClientRecords = (records = []) => new Map(records.map((record) => [record.id, {
|
|
850
987
|
id: record.id,
|
|
851
|
-
lastSeq: record.lastSeq,
|
|
852
988
|
touched: record.touched,
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
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()
|
|
856
999
|
}])),
|
|
857
|
-
inflight: /* @__PURE__ */ new Map(),
|
|
858
1000
|
principal: record.principal
|
|
859
1001
|
}]));
|
|
860
1002
|
const useSocketHost = (transport, options, persistence) => {
|
|
@@ -880,6 +1022,7 @@ const useSocketHost = (transport, options, persistence) => {
|
|
|
880
1022
|
pendingOps: [],
|
|
881
1023
|
pendingApplied: /* @__PURE__ */ new Map(),
|
|
882
1024
|
pendingCmds: /* @__PURE__ */ new Map(),
|
|
1025
|
+
viewsDirty: false,
|
|
883
1026
|
idleWire: true,
|
|
884
1027
|
finished: null,
|
|
885
1028
|
disposed: false,
|
|
@@ -902,9 +1045,9 @@ const useSocketHost = (transport, options, persistence) => {
|
|
|
902
1045
|
});
|
|
903
1046
|
h.protocol = options?.protocol ?? {
|
|
904
1047
|
name: "default",
|
|
905
|
-
version: "2026-09-
|
|
1048
|
+
version: "2026-09-13"
|
|
906
1049
|
};
|
|
907
|
-
h.wireVersion = options?.wireVersion ?? { version: "2026-09-
|
|
1050
|
+
h.wireVersion = options?.wireVersion ?? { version: "2026-09-13" };
|
|
908
1051
|
setHeartbeatMs(h, options?.heartbeatIntervalMs ?? 15e3);
|
|
909
1052
|
h.pushback = options?.pushback;
|
|
910
1053
|
h.wsInitTimeoutMs = options?.wsInitTimeoutMs ?? 5e3;
|
|
@@ -936,9 +1079,10 @@ const useSocketHost = (transport, options, persistence) => {
|
|
|
936
1079
|
* Transport-agnostic WS binding over a statewire/host transport:
|
|
937
1080
|
* feed it sockets (anything with `send`/`close`) and raw text frames, and it
|
|
938
1081
|
* speaks the Statewire packet protocol — snapshot-first hello with `syn`,
|
|
939
|
-
*
|
|
940
|
-
* arrays fanned out to every attach of a client id
|
|
941
|
-
*
|
|
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.
|
|
942
1086
|
*/
|
|
943
1087
|
const StatewireSocketHost = (transport, options, persistence) => {
|
|
944
1088
|
const SocketHostRoot = () => {
|