statewire 0.0.1 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -13
- package/dist/client/changes.d.ts +2 -2
- package/dist/client/changes.d.ts.map +1 -1
- package/dist/client/changes.js +3 -10
- package/dist/client/changes.js.map +1 -1
- package/dist/client/connection.d.ts +18 -39
- package/dist/client/connection.d.ts.map +1 -1
- package/dist/client/connection.js +61 -54
- package/dist/client/connection.js.map +1 -1
- package/dist/client/core.d.ts +7 -11
- package/dist/client/core.d.ts.map +1 -1
- package/dist/client/core.js +58 -55
- package/dist/client/core.js.map +1 -1
- package/dist/client/session.d.ts +12 -26
- package/dist/client/session.d.ts.map +1 -1
- package/dist/client/session.js +70 -43
- package/dist/client/session.js.map +1 -1
- package/dist/client/sessionStore.d.ts +1 -1
- package/dist/client/sessionStore.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 +5 -1
- package/dist/client/timing.js.map +1 -1
- package/dist/host/adapter.d.ts +8 -0
- package/dist/host/adapter.d.ts.map +1 -0
- package/dist/host/adapter.js +23 -0
- package/dist/host/adapter.js.map +1 -0
- package/dist/host/draft.d.ts +21 -7
- package/dist/host/draft.d.ts.map +1 -1
- package/dist/host/draft.js +190 -103
- package/dist/host/draft.js.map +1 -1
- package/dist/host/host.d.ts +19 -35
- package/dist/host/host.d.ts.map +1 -1
- package/dist/host/host.js +86 -54
- package/dist/host/host.js.map +1 -1
- package/dist/host/socket.d.ts +14 -0
- package/dist/host/socket.d.ts.map +1 -0
- package/dist/host/socket.js +511 -0
- package/dist/host/socket.js.map +1 -0
- package/dist/host-internal.d.ts +27 -0
- package/dist/host-internal.d.ts.map +1 -0
- package/dist/host-internal.js +3 -0
- package/dist/host.d.ts +54 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +4 -0
- package/dist/index.d.ts +5 -28
- package/dist/index.js +2 -3
- package/dist/statewire.d.ts +97 -10
- package/dist/statewire.d.ts.map +1 -1
- package/dist/statewire.js +32 -36
- package/dist/statewire.js.map +1 -1
- package/dist/transports/fake-sse.d.ts +12 -0
- package/dist/transports/fake-sse.d.ts.map +1 -0
- package/dist/transports/fake-sse.js +39 -0
- package/dist/transports/fake-sse.js.map +1 -0
- package/dist/transports/fixtures.d.ts +33 -0
- package/dist/transports/fixtures.d.ts.map +1 -0
- package/dist/transports/fixtures.js +50 -0
- package/dist/transports/fixtures.js.map +1 -0
- package/dist/transports/remote.d.ts +6 -13
- package/dist/transports/remote.d.ts.map +1 -1
- package/dist/transports/remote.js +7 -25
- package/dist/transports/remote.js.map +1 -1
- package/dist/transports/sse.d.ts +32 -20
- package/dist/transports/sse.d.ts.map +1 -1
- package/dist/transports/sse.js +57 -46
- package/dist/transports/sse.js.map +1 -1
- package/dist/transports/testing.d.ts +8 -6
- package/dist/transports/testing.d.ts.map +1 -1
- package/dist/transports/testing.js +19 -46
- package/dist/transports/testing.js.map +1 -1
- package/dist/transports/ws.d.ts +4 -13
- package/dist/transports/ws.d.ts.map +1 -1
- package/dist/transports/ws.js +59 -63
- package/dist/transports/ws.js.map +1 -1
- package/dist/types.d.ts +55 -100
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/util/draftTraps.d.ts +6 -0
- package/dist/util/draftTraps.d.ts.map +1 -0
- package/dist/util/draftTraps.js +25 -0
- package/dist/util/draftTraps.js.map +1 -0
- package/dist/util/macrotask.d.ts +5 -0
- package/dist/util/macrotask.d.ts.map +1 -0
- package/dist/util/macrotask.js +25 -0
- package/dist/util/macrotask.js.map +1 -0
- package/dist/util/methodProxy.d.ts +5 -0
- package/dist/util/methodProxy.d.ts.map +1 -0
- package/dist/util/methodProxy.js +17 -0
- package/dist/util/methodProxy.js.map +1 -0
- package/dist/util/randomId.d.ts +5 -0
- package/dist/util/randomId.d.ts.map +1 -0
- package/dist/util/randomId.js +6 -0
- package/dist/util/randomId.js.map +1 -0
- package/dist/{client/optimistic.d.ts → util/recordMutations.d.ts} +2 -2
- package/dist/util/recordMutations.d.ts.map +1 -0
- package/dist/{client/optimistic.js → util/recordMutations.js} +5 -24
- package/dist/util/recordMutations.js.map +1 -0
- package/dist/util/statePath.d.ts +14 -0
- package/dist/util/statePath.d.ts.map +1 -0
- package/dist/util/statePath.js +31 -0
- package/dist/util/statePath.js.map +1 -0
- package/dist/wire/meta.d.ts +6 -1
- package/dist/wire/meta.d.ts.map +1 -1
- package/dist/wire/meta.js +30 -28
- package/dist/wire/meta.js.map +1 -1
- package/dist/wire/ops.d.ts +5 -26
- package/dist/wire/ops.d.ts.map +1 -1
- package/dist/wire/ops.js +67 -80
- package/dist/wire/ops.js.map +1 -1
- package/package.json +15 -11
- package/src/client/changes.ts +5 -15
- package/src/client/connection.ts +87 -113
- package/src/client/core.ts +57 -91
- package/src/client/session.ts +129 -90
- package/src/client/sessionStore.ts +1 -1
- package/src/client/timing.ts +6 -0
- package/src/host/adapter.ts +33 -0
- package/src/host/draft.ts +293 -125
- package/src/host/host.ts +122 -110
- package/src/host/socket.ts +618 -0
- package/src/host-internal.ts +28 -0
- package/src/host.ts +72 -0
- package/src/index.ts +2 -62
- package/src/statewire.ts +168 -61
- package/src/transports/fake-sse.ts +51 -0
- package/src/transports/fixtures.ts +60 -0
- package/src/transports/remote.ts +14 -51
- package/src/transports/sse.ts +87 -89
- package/src/transports/testing.ts +33 -63
- package/src/transports/ws.ts +78 -100
- package/src/types.ts +52 -131
- package/src/util/draftTraps.ts +32 -0
- package/src/util/macrotask.ts +25 -0
- package/src/util/methodProxy.ts +16 -0
- package/src/util/randomId.ts +4 -0
- package/src/{client/optimistic.ts → util/recordMutations.ts} +0 -0
- package/src/util/statePath.ts +61 -0
- package/src/wire/meta.ts +42 -47
- package/src/wire/ops.ts +107 -106
- package/LICENSE +0 -21
- package/dist/client/optimistic.d.ts.map +0 -1
- package/dist/client/optimistic.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/server.d.ts +0 -15
- package/dist/server.d.ts.map +0 -1
- package/dist/server.js +0 -3
- package/src/server.ts +0 -32
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
import { StatewireSendError } from "../types.js";
|
|
2
|
+
import { getCommandsHost, getEmitHost, getStateHost } from "./host.js";
|
|
3
|
+
import { encodeValue, reviveValue } from "../wire/meta.js";
|
|
4
|
+
import { createMacrotaskPoster } from "../util/macrotask.js";
|
|
5
|
+
import { randomId } from "../util/randomId.js";
|
|
6
|
+
import { c } from "@assistant-ui/tap/react-shim/compiler-runtime";
|
|
7
|
+
import { useEffect, useState } from "@assistant-ui/tap/react-shim";
|
|
8
|
+
import { createTapRoot, flushTapSync, useResource } from "@assistant-ui/tap";
|
|
9
|
+
//#region src/host/socket.ts
|
|
10
|
+
const CLIENT_ID_RE = /^[A-Za-z0-9._~-]{1,256}$/;
|
|
11
|
+
const RESULT_TTL_MS = 3e4;
|
|
12
|
+
const RESULT_LIMIT = 256;
|
|
13
|
+
const parseFrame = (raw) => {
|
|
14
|
+
let frame;
|
|
15
|
+
try {
|
|
16
|
+
frame = JSON.parse(raw);
|
|
17
|
+
} catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
if (typeof frame !== "object" || frame === null || Array.isArray(frame)) return null;
|
|
21
|
+
const { method, seq, params, meta, context } = frame;
|
|
22
|
+
if (method === void 0 && context === void 0) return null;
|
|
23
|
+
if (typeof seq !== "number" || !Number.isInteger(seq) || seq < 1) return null;
|
|
24
|
+
if (method === void 0) return {
|
|
25
|
+
seq,
|
|
26
|
+
params: [],
|
|
27
|
+
...context !== void 0 && { context }
|
|
28
|
+
};
|
|
29
|
+
if (typeof method !== "string") return null;
|
|
30
|
+
if (!Array.isArray(params)) return null;
|
|
31
|
+
if (meta !== void 0) try {
|
|
32
|
+
reviveValue(params, meta);
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
method,
|
|
38
|
+
seq,
|
|
39
|
+
params,
|
|
40
|
+
...context !== void 0 && { context }
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
/** v1 context submissions are exactly one root replace with an object value. */
|
|
44
|
+
const contextValue = (context) => {
|
|
45
|
+
if (!Array.isArray(context) || context.length !== 1) throw new Error("context must be a single-op array");
|
|
46
|
+
const op = context[0];
|
|
47
|
+
if (typeof op !== "object" || op === null || op["op"] !== "replace" || !Array.isArray(op["path"]) || op["path"].length !== 0) throw new Error("context op must be a root replace");
|
|
48
|
+
const value = op["value"];
|
|
49
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("context value must be an object");
|
|
50
|
+
return value;
|
|
51
|
+
};
|
|
52
|
+
const encodePayload = (payload) => {
|
|
53
|
+
if (payload === void 0) return {};
|
|
54
|
+
const { value, meta } = encodeValue(payload);
|
|
55
|
+
return {
|
|
56
|
+
payload: value,
|
|
57
|
+
...meta !== void 0 && { meta }
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
const responseOf = (adm, outcome) => {
|
|
61
|
+
if (outcome.type === "accepted") return {
|
|
62
|
+
seq: adm.seq,
|
|
63
|
+
type: "accepted",
|
|
64
|
+
...encodePayload(outcome.payload)
|
|
65
|
+
};
|
|
66
|
+
const failure = outcome.error instanceof StatewireSendError ? outcome.error.failure : null;
|
|
67
|
+
switch (failure?.fate) {
|
|
68
|
+
case "unknown-command": return {
|
|
69
|
+
seq: adm.seq,
|
|
70
|
+
type: "rejected",
|
|
71
|
+
code: "unknown-command",
|
|
72
|
+
message: `unknown method: '${adm.method}'`
|
|
73
|
+
};
|
|
74
|
+
case "invalid-params": return {
|
|
75
|
+
seq: adm.seq,
|
|
76
|
+
type: "rejected",
|
|
77
|
+
code: "invalid-params",
|
|
78
|
+
message: failure.message
|
|
79
|
+
};
|
|
80
|
+
case "crashed": return {
|
|
81
|
+
seq: adm.seq,
|
|
82
|
+
type: "crashed"
|
|
83
|
+
};
|
|
84
|
+
case "rejected": return {
|
|
85
|
+
seq: adm.seq,
|
|
86
|
+
type: "rejected",
|
|
87
|
+
message: failure.message,
|
|
88
|
+
...encodePayload(failure.payload)
|
|
89
|
+
};
|
|
90
|
+
default: return {
|
|
91
|
+
seq: adm.seq,
|
|
92
|
+
type: "rejected",
|
|
93
|
+
message: outcome.error instanceof Error ? outcome.error.message : String(outcome.error)
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
const createSocketHost = (options) => {
|
|
98
|
+
const heartbeatMs = options?.heartbeatIntervalMs ?? 15e3;
|
|
99
|
+
const clients = /* @__PURE__ */ new Map();
|
|
100
|
+
const conns = /* @__PURE__ */ new Set();
|
|
101
|
+
let pendingOps = [];
|
|
102
|
+
let dirtyAcks = /* @__PURE__ */ new Set();
|
|
103
|
+
let pendingRes = /* @__PURE__ */ new Map();
|
|
104
|
+
let inbox = Promise.resolve();
|
|
105
|
+
let finished = null;
|
|
106
|
+
let disposed = false;
|
|
107
|
+
let flushScheduled = false;
|
|
108
|
+
let heartbeatTimer = null;
|
|
109
|
+
let stateHost = null;
|
|
110
|
+
let commandsHost = null;
|
|
111
|
+
let emitHost = null;
|
|
112
|
+
let current = null;
|
|
113
|
+
const post = createMacrotaskPoster(() => {
|
|
114
|
+
flushScheduled = false;
|
|
115
|
+
flush();
|
|
116
|
+
});
|
|
117
|
+
const scheduleFlush = () => {
|
|
118
|
+
if (flushScheduled || disposed) return;
|
|
119
|
+
flushScheduled = true;
|
|
120
|
+
post();
|
|
121
|
+
};
|
|
122
|
+
const recordOp = (op) => {
|
|
123
|
+
if (op.op === "replace" || op.op === "add" || op.op === "event") {
|
|
124
|
+
const { value, meta } = encodeValue(op.value);
|
|
125
|
+
pendingOps.push({
|
|
126
|
+
...op,
|
|
127
|
+
value,
|
|
128
|
+
...meta !== void 0 && { meta }
|
|
129
|
+
});
|
|
130
|
+
} else pendingOps.push(op);
|
|
131
|
+
scheduleFlush();
|
|
132
|
+
};
|
|
133
|
+
const attach = (next) => {
|
|
134
|
+
if (next === current) return;
|
|
135
|
+
current = next;
|
|
136
|
+
const nextState = typeof next.state === "object" && next.state !== null ? getStateHost(next.state) : void 0;
|
|
137
|
+
const nextCommands = getCommandsHost(next.commands);
|
|
138
|
+
const nextEmit = getEmitHost(next.emit);
|
|
139
|
+
if (!nextState || !nextCommands?.submit || !nextEmit) throw new Error("StatewireSocketHost requires a statewire/host transport (useStatewireState with object state, useStatewireCommands, useStatewireEmit)");
|
|
140
|
+
if (nextState !== stateHost) {
|
|
141
|
+
stateHost?.setOnOp(null);
|
|
142
|
+
stateHost = nextState;
|
|
143
|
+
stateHost.setOnOp(recordOp);
|
|
144
|
+
}
|
|
145
|
+
commandsHost = nextCommands;
|
|
146
|
+
if (nextEmit !== emitHost) {
|
|
147
|
+
emitHost?.setSink(null);
|
|
148
|
+
emitHost = nextEmit;
|
|
149
|
+
emitHost.setSink((event) => recordOp({
|
|
150
|
+
op: "event",
|
|
151
|
+
value: event
|
|
152
|
+
}));
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
const closeConn = (conn, code, reason) => {
|
|
156
|
+
if (!conn.open) return;
|
|
157
|
+
conn.open = false;
|
|
158
|
+
conns.delete(conn);
|
|
159
|
+
try {
|
|
160
|
+
conn.socket.close(code, reason);
|
|
161
|
+
} catch {}
|
|
162
|
+
if (conns.size === 0 && heartbeatTimer !== null) {
|
|
163
|
+
clearInterval(heartbeatTimer);
|
|
164
|
+
heartbeatTimer = null;
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
const sendTo = (conn, data) => {
|
|
168
|
+
if (!conn.open) return;
|
|
169
|
+
conn.lastSentAt = Date.now();
|
|
170
|
+
try {
|
|
171
|
+
conn.socket.send(data);
|
|
172
|
+
} catch {
|
|
173
|
+
closeConn(conn);
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
const ensureHeartbeat = () => {
|
|
177
|
+
if (heartbeatTimer !== null || heartbeatMs <= 0) return;
|
|
178
|
+
heartbeatTimer = setInterval(() => {
|
|
179
|
+
const now = Date.now();
|
|
180
|
+
for (const conn of [...conns]) if (now - conn.lastSentAt >= heartbeatMs) sendTo(conn, "{}");
|
|
181
|
+
}, heartbeatMs);
|
|
182
|
+
heartbeatTimer.unref?.();
|
|
183
|
+
};
|
|
184
|
+
const flush = (fin) => {
|
|
185
|
+
flushTapSync(() => {});
|
|
186
|
+
stateHost?.takeChanged();
|
|
187
|
+
for (const rec of clients.values()) for (const adm of rec.inflight.values()) if (adm.acked && !adm.settled && !adm.pendingSent) {
|
|
188
|
+
adm.pendingSent = true;
|
|
189
|
+
queueRes(rec.id, {
|
|
190
|
+
seq: adm.seq,
|
|
191
|
+
type: "pending"
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
if (pendingOps.length === 0 && dirtyAcks.size === 0 && pendingRes.size === 0 && fin === void 0) return;
|
|
195
|
+
const ops = pendingOps;
|
|
196
|
+
const acks = dirtyAcks;
|
|
197
|
+
const res = pendingRes;
|
|
198
|
+
pendingOps = [];
|
|
199
|
+
dirtyAcks = /* @__PURE__ */ new Set();
|
|
200
|
+
pendingRes = /* @__PURE__ */ new Map();
|
|
201
|
+
for (const conn of [...conns]) {
|
|
202
|
+
const envelope = {};
|
|
203
|
+
if (ops.length > 0) envelope.ops = ops;
|
|
204
|
+
const clientRes = res.get(conn.clientId);
|
|
205
|
+
if (clientRes !== void 0) envelope.res = clientRes;
|
|
206
|
+
if (acks.has(conn.clientId)) {
|
|
207
|
+
const rec = clients.get(conn.clientId);
|
|
208
|
+
if (rec !== void 0) envelope.ack = rec.lastSeq;
|
|
209
|
+
}
|
|
210
|
+
if (fin !== void 0) envelope.fin = fin;
|
|
211
|
+
if (Object.keys(envelope).length === 0) continue;
|
|
212
|
+
sendTo(conn, JSON.stringify(envelope));
|
|
213
|
+
if (fin !== void 0) closeConn(conn);
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
const queueRes = (clientId, response) => {
|
|
217
|
+
const list = pendingRes.get(clientId);
|
|
218
|
+
if (list) list.push(response);
|
|
219
|
+
else pendingRes.set(clientId, [response]);
|
|
220
|
+
};
|
|
221
|
+
const advance = (rec, seq) => {
|
|
222
|
+
rec.lastSeq = seq;
|
|
223
|
+
dirtyAcks.add(rec.id);
|
|
224
|
+
};
|
|
225
|
+
const retain = (rec, response) => {
|
|
226
|
+
const now = Date.now();
|
|
227
|
+
rec.results.set(response.seq, {
|
|
228
|
+
ts: now,
|
|
229
|
+
res: response
|
|
230
|
+
});
|
|
231
|
+
const newest = Math.max(...rec.results.keys());
|
|
232
|
+
for (const [seq, entry] of rec.results) if (seq !== newest && now - entry.ts > RESULT_TTL_MS) rec.results.delete(seq);
|
|
233
|
+
while (rec.results.size > RESULT_LIMIT) rec.results.delete(Math.min(...rec.results.keys()));
|
|
234
|
+
};
|
|
235
|
+
const duplicateAnswer = (rec, seq) => {
|
|
236
|
+
const cached = rec.results.get(seq);
|
|
237
|
+
if (cached) {
|
|
238
|
+
if (seq === Math.max(...rec.results.keys()) || Date.now() - cached.ts <= RESULT_TTL_MS) return cached.res;
|
|
239
|
+
rec.results.delete(seq);
|
|
240
|
+
}
|
|
241
|
+
if (rec.inflight.has(seq)) return {
|
|
242
|
+
seq,
|
|
243
|
+
type: "pending"
|
|
244
|
+
};
|
|
245
|
+
return {
|
|
246
|
+
seq,
|
|
247
|
+
type: "result-unavailable"
|
|
248
|
+
};
|
|
249
|
+
};
|
|
250
|
+
const settleResponse = (rec, adm, response) => {
|
|
251
|
+
if (adm.settled) return;
|
|
252
|
+
adm.settled = true;
|
|
253
|
+
rec.inflight.delete(adm.seq);
|
|
254
|
+
retain(rec, response);
|
|
255
|
+
if (!adm.acked) {
|
|
256
|
+
adm.acked = true;
|
|
257
|
+
advance(rec, adm.seq);
|
|
258
|
+
}
|
|
259
|
+
if (adm.pendingSent || response.type !== "accepted" || response.payload !== void 0) queueRes(rec.id, response);
|
|
260
|
+
scheduleFlush();
|
|
261
|
+
adm.release();
|
|
262
|
+
};
|
|
263
|
+
const settleWire = (rec, adm, outcome) => settleResponse(rec, adm, responseOf(adm, outcome));
|
|
264
|
+
const callerOf = (rec) => ({
|
|
265
|
+
get context() {
|
|
266
|
+
return rec.context;
|
|
267
|
+
},
|
|
268
|
+
get isConnected() {
|
|
269
|
+
if (clients.get(rec.id) !== rec) return false;
|
|
270
|
+
for (const conn of conns) if (conn.clientId === rec.id && conn.open) return true;
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
const admit = (conn, frame) => {
|
|
275
|
+
if (!conn.open || disposed || finished !== null) return;
|
|
276
|
+
const rec = clients.get(conn.clientId);
|
|
277
|
+
if (frame.seq <= rec.lastSeq || rec.inflight.has(frame.seq)) {
|
|
278
|
+
queueRes(rec.id, duplicateAnswer(rec, frame.seq));
|
|
279
|
+
scheduleFlush();
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
if (frame.seq > rec.lastSeq + 1) {
|
|
283
|
+
closeConn(conn, 1008, `seq gap: expected ${rec.lastSeq + 1}, got ${frame.seq}`);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
const adm = {
|
|
287
|
+
seq: frame.seq,
|
|
288
|
+
method: frame.method ?? "",
|
|
289
|
+
acked: false,
|
|
290
|
+
pendingSent: false,
|
|
291
|
+
settled: false,
|
|
292
|
+
release: () => {}
|
|
293
|
+
};
|
|
294
|
+
if (frame.context !== void 0) {
|
|
295
|
+
let value;
|
|
296
|
+
try {
|
|
297
|
+
value = contextValue(frame.context);
|
|
298
|
+
} catch (error) {
|
|
299
|
+
rec.inflight.set(frame.seq, adm);
|
|
300
|
+
settleResponse(rec, adm, {
|
|
301
|
+
seq: frame.seq,
|
|
302
|
+
type: "rejected",
|
|
303
|
+
code: "invalid-context",
|
|
304
|
+
message: `invalid context: ${error.message}`
|
|
305
|
+
});
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
rec.context = value;
|
|
309
|
+
if (frame.method === void 0) {
|
|
310
|
+
rec.inflight.set(frame.seq, adm);
|
|
311
|
+
settleResponse(rec, adm, {
|
|
312
|
+
seq: frame.seq,
|
|
313
|
+
type: "accepted"
|
|
314
|
+
});
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
const released = new Promise((resolve) => {
|
|
319
|
+
adm.release = resolve;
|
|
320
|
+
});
|
|
321
|
+
rec.inflight.set(frame.seq, adm);
|
|
322
|
+
commandsHost.submit({
|
|
323
|
+
method: frame.method,
|
|
324
|
+
params: frame.params,
|
|
325
|
+
clientId: rec.id,
|
|
326
|
+
caller: callerOf(rec),
|
|
327
|
+
onAck: () => {
|
|
328
|
+
if (adm.settled || adm.acked) return;
|
|
329
|
+
adm.acked = true;
|
|
330
|
+
advance(rec, adm.seq);
|
|
331
|
+
scheduleFlush();
|
|
332
|
+
adm.release();
|
|
333
|
+
},
|
|
334
|
+
onSettle: (outcome) => settleWire(rec, adm, outcome)
|
|
335
|
+
}).catch((error) => {
|
|
336
|
+
if (!adm.settled) settleWire(rec, adm, {
|
|
337
|
+
type: "rejected",
|
|
338
|
+
error
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
return released;
|
|
342
|
+
};
|
|
343
|
+
const message = (conn, data) => {
|
|
344
|
+
if (!conn.open || disposed || finished !== null) return;
|
|
345
|
+
const frame = parseFrame(data);
|
|
346
|
+
if (frame === null) {
|
|
347
|
+
closeConn(conn, 1008, "invalid frame");
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
inbox = inbox.then(() => admit(conn, frame));
|
|
351
|
+
};
|
|
352
|
+
const connect = (clientId, socket) => {
|
|
353
|
+
const dead = {
|
|
354
|
+
clientId,
|
|
355
|
+
message: () => {},
|
|
356
|
+
close: () => {}
|
|
357
|
+
};
|
|
358
|
+
const refuse = (code, reason) => {
|
|
359
|
+
try {
|
|
360
|
+
socket.close(code, reason);
|
|
361
|
+
} catch {}
|
|
362
|
+
return dead;
|
|
363
|
+
};
|
|
364
|
+
if (disposed) return refuse(1001, "disposed");
|
|
365
|
+
if (!CLIENT_ID_RE.test(clientId)) return refuse(1008, "invalid client id");
|
|
366
|
+
flush();
|
|
367
|
+
if (finished !== null) {
|
|
368
|
+
try {
|
|
369
|
+
socket.send(JSON.stringify({ fin: finished }));
|
|
370
|
+
} catch {}
|
|
371
|
+
return refuse(1e3, "finished");
|
|
372
|
+
}
|
|
373
|
+
const known = clients.has(clientId);
|
|
374
|
+
const rec = known ? clients.get(clientId) : (() => {
|
|
375
|
+
const created = {
|
|
376
|
+
id: clientId,
|
|
377
|
+
lastSeq: 0,
|
|
378
|
+
context: null,
|
|
379
|
+
results: /* @__PURE__ */ new Map(),
|
|
380
|
+
inflight: /* @__PURE__ */ new Map()
|
|
381
|
+
};
|
|
382
|
+
clients.set(clientId, created);
|
|
383
|
+
return created;
|
|
384
|
+
})();
|
|
385
|
+
for (const other of [...conns]) {
|
|
386
|
+
if (other.clientId !== clientId) continue;
|
|
387
|
+
sendTo(other, JSON.stringify({ fin: { reason: "superseded" } }));
|
|
388
|
+
closeConn(other);
|
|
389
|
+
}
|
|
390
|
+
const conn = {
|
|
391
|
+
clientId,
|
|
392
|
+
socket,
|
|
393
|
+
open: true,
|
|
394
|
+
lastSentAt: 0
|
|
395
|
+
};
|
|
396
|
+
conns.add(conn);
|
|
397
|
+
ensureHeartbeat();
|
|
398
|
+
const { value, meta } = encodeValue(stateHost.snapshot());
|
|
399
|
+
const snapshot = {
|
|
400
|
+
ops: [{
|
|
401
|
+
op: "replace",
|
|
402
|
+
path: [],
|
|
403
|
+
value,
|
|
404
|
+
...meta !== void 0 && { meta }
|
|
405
|
+
}],
|
|
406
|
+
syn: {
|
|
407
|
+
lastSeq: known ? rec.lastSeq : -1,
|
|
408
|
+
lease: randomId()
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
sendTo(conn, JSON.stringify(snapshot));
|
|
412
|
+
return {
|
|
413
|
+
clientId,
|
|
414
|
+
message: (data) => message(conn, data),
|
|
415
|
+
close: () => closeConn(conn)
|
|
416
|
+
};
|
|
417
|
+
};
|
|
418
|
+
const finish = (fin) => {
|
|
419
|
+
if (finished !== null || disposed) return;
|
|
420
|
+
finished = fin;
|
|
421
|
+
flush(fin);
|
|
422
|
+
};
|
|
423
|
+
return {
|
|
424
|
+
connect,
|
|
425
|
+
finish,
|
|
426
|
+
attach,
|
|
427
|
+
resume: () => {
|
|
428
|
+
disposed = false;
|
|
429
|
+
stateHost?.setOnOp(recordOp);
|
|
430
|
+
emitHost?.setSink((event) => recordOp({
|
|
431
|
+
op: "event",
|
|
432
|
+
value: event
|
|
433
|
+
}));
|
|
434
|
+
},
|
|
435
|
+
suspend: () => {
|
|
436
|
+
disposed = true;
|
|
437
|
+
if (heartbeatTimer !== null) {
|
|
438
|
+
clearInterval(heartbeatTimer);
|
|
439
|
+
heartbeatTimer = null;
|
|
440
|
+
}
|
|
441
|
+
stateHost?.setOnOp(null);
|
|
442
|
+
emitHost?.setSink(null);
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
};
|
|
446
|
+
const useSocketHost = (transport, options) => {
|
|
447
|
+
const $ = c(8);
|
|
448
|
+
let t0;
|
|
449
|
+
if ($[0] !== options) {
|
|
450
|
+
t0 = () => createSocketHost(options);
|
|
451
|
+
$[0] = options;
|
|
452
|
+
$[1] = t0;
|
|
453
|
+
} else t0 = $[1];
|
|
454
|
+
const [h] = useState(t0);
|
|
455
|
+
const value = useResource(transport);
|
|
456
|
+
h.attach(value);
|
|
457
|
+
let t1;
|
|
458
|
+
if ($[2] !== h || $[3] !== value) {
|
|
459
|
+
t1 = () => {
|
|
460
|
+
h.attach(value);
|
|
461
|
+
};
|
|
462
|
+
$[2] = h;
|
|
463
|
+
$[3] = value;
|
|
464
|
+
$[4] = t1;
|
|
465
|
+
} else t1 = $[4];
|
|
466
|
+
useEffect(t1);
|
|
467
|
+
let t2;
|
|
468
|
+
let t3;
|
|
469
|
+
if ($[5] !== h) {
|
|
470
|
+
t2 = () => {
|
|
471
|
+
h.resume();
|
|
472
|
+
return () => h.suspend();
|
|
473
|
+
};
|
|
474
|
+
t3 = [h];
|
|
475
|
+
$[5] = h;
|
|
476
|
+
$[6] = t2;
|
|
477
|
+
$[7] = t3;
|
|
478
|
+
} else {
|
|
479
|
+
t2 = $[6];
|
|
480
|
+
t3 = $[7];
|
|
481
|
+
}
|
|
482
|
+
useEffect(t2, t3);
|
|
483
|
+
return h;
|
|
484
|
+
};
|
|
485
|
+
/**
|
|
486
|
+
* Transport-agnostic WS binding over a statewire/host transport:
|
|
487
|
+
* feed it sockets (anything with `send`/`close`) and raw text frames, and it
|
|
488
|
+
* speaks the Statewire envelope protocol — snapshot-first attach with `syn`,
|
|
489
|
+
* delta/ack/res envelopes, per-client-id supersede, and `fin` on finish.
|
|
490
|
+
*/
|
|
491
|
+
const StatewireSocketHost = (transport, options) => {
|
|
492
|
+
const SocketHostRoot = () => {
|
|
493
|
+
return useSocketHost(transport, options);
|
|
494
|
+
};
|
|
495
|
+
const root = createTapRoot(SocketHostRoot);
|
|
496
|
+
let disposed = false;
|
|
497
|
+
return {
|
|
498
|
+
connect: (clientId, socket) => flushTapSync(() => root.getValue().connect(clientId, socket)),
|
|
499
|
+
finish: (fin) => flushTapSync(() => root.getValue().finish(fin)),
|
|
500
|
+
dispose: () => {
|
|
501
|
+
if (disposed) return;
|
|
502
|
+
disposed = true;
|
|
503
|
+
flushTapSync(() => root.getValue().finish({ reason: "evicted" }));
|
|
504
|
+
root.unmount();
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
};
|
|
508
|
+
//#endregion
|
|
509
|
+
export { StatewireSocketHost };
|
|
510
|
+
|
|
511
|
+
//# sourceMappingURL=socket.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"socket.js","names":["useEffect","useState","createTapRoot","flushTapSync","useResource","ResourceElement","getCommandsHost","getEmitHost","getStateHost","CommandsHost","CommandsOutcome","StatewireSendError","Statewire","StatewireHost","StatewireHostInternal","encodeValue","reviveValue","createMacrotaskPoster","randomId","StatewireMetaEntry","StatewirePath","StatewireEnvelope","StatewireResponse","CLIENT_ID_RE","RESULT_TTL_MS","RESULT_LIMIT","WireOp","op","path","value","meta","Admission","seq","method","acked","pendingSent","settled","release","ClientRec","id","lastSeq","context","Record","results","Map","ts","res","inflight","Conn","clientId","socket","Socket","open","lastSentAt","Frame","params","parseFrame","raw","frame","JSON","parse","Array","isArray","undefined","Number","isInteger","contextValue","length","Error","encodePayload","payload","responseOf","adm","outcome","type","failure","error","fate","code","message","String","createSocketHost","options","SocketHostOptions","heartbeatMs","heartbeatIntervalMs","clients","conns","Set","pendingOps","dirtyAcks","pendingRes","inbox","Promise","resolve","finished","Fin","disposed","flushScheduled","heartbeatTimer","ReturnType","setInterval","stateHost","StateHost","commandsHost","emitHost","current","post","flush","scheduleFlush","recordOp","push","attach","next","nextState","state","nextCommands","commands","nextEmit","emit","submit","setOnOp","setSink","event","closeConn","conn","reason","delete","close","size","clearInterval","sendTo","data","Date","now","send","ensureHeartbeat","unref","fin","takeChanged","rec","values","queueRes","ops","acks","envelope","clientRes","get","has","ack","Object","keys","stringify","response","list","set","advance","add","retain","newest","Math","max","entry","min","duplicateAnswer","cached","settleResponse","settleWire","callerOf","Caller","isConnected","admit","released","caller","onAck","onSettle","catch","then","connect","SocketConnection","dead","refuse","test","known","created","other","snapshot","syn","lease","finish","resume","suspend","useSocketHost","transport","$","_c","t0","h","t1","t2","t3","StatewireSocketHost","SocketHost","SocketHostRoot","root","getValue","dispose","unmount"],"sources":["../../src/host/socket.ts"],"sourcesContent":["import { useEffect, useState } from \"react\";\nimport { createTapRoot, flushTapSync, useResource } from \"@assistant-ui/tap\";\nimport type { ResourceElement } from \"@assistant-ui/tap\";\nimport { getCommandsHost, getEmitHost, getStateHost } from \"./host\";\nimport type { CommandsHost, CommandsOutcome } from \"./host\";\nimport { StatewireSendError } from \"../types\";\nimport type { Statewire } from \"../types\";\nimport type { StatewireHost } from \"../host\";\nimport type { StatewireHostInternal } from \"../host-internal\";\nimport { encodeValue, reviveValue } from \"../wire/meta\";\nimport { createMacrotaskPoster } from \"../util/macrotask\";\nimport { randomId } from \"../util/randomId\";\nimport type { StatewireMetaEntry } from \"../wire/meta\";\nimport type { StatewirePath } from \"../wire/ops\";\nimport type {\n StatewireEnvelope,\n StatewireResponse,\n} from \"../client/connection\";\n\nconst CLIENT_ID_RE = /^[A-Za-z0-9._~-]{1,256}$/;\nconst RESULT_TTL_MS = 30_000;\nconst RESULT_LIMIT = 256;\n\ntype WireOp = {\n op: string;\n path?: StatewirePath;\n value?: unknown;\n meta?: StatewireMetaEntry[];\n};\n\ntype Admission = {\n seq: number;\n method: string;\n acked: boolean;\n pendingSent: boolean;\n settled: boolean;\n release: () => void;\n};\n\ntype ClientRec = {\n id: string;\n lastSeq: number;\n context: Record<string, unknown> | null;\n results: Map<number, { ts: number; res: StatewireResponse }>;\n inflight: Map<number, Admission>;\n};\n\ntype Conn = {\n clientId: string;\n socket: StatewireHostInternal.Socket;\n open: boolean;\n lastSentAt: number;\n};\n\ntype Frame = {\n method?: string;\n seq: number;\n params: unknown[];\n context?: unknown;\n};\n\nconst parseFrame = (raw: string): Frame | null => {\n let frame: unknown;\n try {\n frame = JSON.parse(raw);\n } catch {\n return null;\n }\n if (typeof frame !== \"object\" || frame === null || Array.isArray(frame)) {\n return null;\n }\n const { method, seq, params, meta, context } = frame as Record<\n string,\n unknown\n >;\n if (method === undefined && context === undefined) return null;\n if (typeof seq !== \"number\" || !Number.isInteger(seq) || seq < 1) return null;\n if (method === undefined) {\n return { seq, params: [], ...(context !== undefined && { context }) };\n }\n if (typeof method !== \"string\") return null;\n if (!Array.isArray(params)) return null;\n if (meta !== undefined) {\n try {\n reviveValue(params, meta);\n } catch {\n return null;\n }\n }\n return { method, seq, params, ...(context !== undefined && { context }) };\n};\n\n/** v1 context submissions are exactly one root replace with an object value. */\nconst contextValue = (context: unknown): Record<string, unknown> => {\n if (!Array.isArray(context) || context.length !== 1) {\n throw new Error(\"context must be a single-op array\");\n }\n const op = context[0] as Record<string, unknown> | null;\n if (\n typeof op !== \"object\" ||\n op === null ||\n op[\"op\"] !== \"replace\" ||\n !Array.isArray(op[\"path\"]) ||\n op[\"path\"].length !== 0\n ) {\n throw new Error(\"context op must be a root replace\");\n }\n const value = op[\"value\"];\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n throw new Error(\"context value must be an object\");\n }\n return value as Record<string, unknown>;\n};\n\nconst encodePayload = (\n payload: unknown,\n): { payload?: unknown; meta?: StatewireMetaEntry[] } => {\n if (payload === undefined) return {};\n const { value, meta } = encodeValue(payload);\n return { payload: value, ...(meta !== undefined && { meta }) };\n};\n\nconst responseOf = (\n adm: Admission,\n outcome: CommandsOutcome,\n): StatewireResponse => {\n if (outcome.type === \"accepted\") {\n return {\n seq: adm.seq,\n type: \"accepted\",\n ...encodePayload(outcome.payload),\n };\n }\n const failure =\n outcome.error instanceof StatewireSendError ? outcome.error.failure : null;\n switch (failure?.fate) {\n case \"unknown-command\":\n return {\n seq: adm.seq,\n type: \"rejected\",\n code: \"unknown-command\",\n message: `unknown method: '${adm.method}'`,\n };\n case \"invalid-params\":\n return {\n seq: adm.seq,\n type: \"rejected\",\n code: \"invalid-params\",\n message: failure.message,\n };\n case \"crashed\":\n return { seq: adm.seq, type: \"crashed\" };\n case \"rejected\":\n return {\n seq: adm.seq,\n type: \"rejected\",\n message: failure.message,\n ...encodePayload(failure.payload),\n };\n default:\n return {\n seq: adm.seq,\n type: \"rejected\",\n message:\n outcome.error instanceof Error\n ? outcome.error.message\n : String(outcome.error),\n };\n }\n};\n\nconst createSocketHost = (\n options?: StatewireHostInternal.SocketHostOptions,\n) => {\n const heartbeatMs = options?.heartbeatIntervalMs ?? 15_000;\n\n const clients = new Map<string, ClientRec>();\n const conns = new Set<Conn>();\n let pendingOps: WireOp[] = [];\n let dirtyAcks = new Set<string>();\n let pendingRes = new Map<string, StatewireResponse[]>();\n let inbox: Promise<void> = Promise.resolve();\n let finished: StatewireHost.Fin | null = null;\n let disposed = false;\n let flushScheduled = false;\n let heartbeatTimer: ReturnType<typeof setInterval> | null = null;\n\n let stateHost: StatewireHost.StateHost | null = null;\n let commandsHost: CommandsHost | null = null;\n let emitHost: ReturnType<typeof getEmitHost> | null = null;\n let current: Statewire<unknown> | null = null;\n\n const post = createMacrotaskPoster(() => {\n flushScheduled = false;\n flush();\n });\n const scheduleFlush = () => {\n if (flushScheduled || disposed) return;\n flushScheduled = true;\n post();\n };\n\n const recordOp = (op: {\n op: string;\n path?: StatewirePath;\n value?: unknown;\n }) => {\n if (op.op === \"replace\" || op.op === \"add\" || op.op === \"event\") {\n const { value, meta } = encodeValue(op.value);\n pendingOps.push({ ...op, value, ...(meta !== undefined && { meta }) });\n } else {\n pendingOps.push(op);\n }\n scheduleFlush();\n };\n\n const attach = (next: Statewire<unknown>) => {\n if (next === current) return;\n current = next;\n const nextState =\n typeof next.state === \"object\" && next.state !== null\n ? getStateHost(next.state)\n : undefined;\n const nextCommands = getCommandsHost(next.commands);\n const nextEmit = getEmitHost(next.emit);\n if (!nextState || !nextCommands?.submit || !nextEmit) {\n throw new Error(\n \"StatewireSocketHost requires a statewire/host transport (useStatewireState with object state, useStatewireCommands, useStatewireEmit)\",\n );\n }\n if (nextState !== stateHost) {\n stateHost?.setOnOp(null);\n stateHost = nextState;\n stateHost.setOnOp(recordOp);\n }\n commandsHost = nextCommands;\n if (nextEmit !== emitHost) {\n emitHost?.setSink(null);\n emitHost = nextEmit;\n emitHost.setSink((event) => recordOp({ op: \"event\", value: event }));\n }\n };\n\n const closeConn = (conn: Conn, code?: number, reason?: string) => {\n if (!conn.open) return;\n conn.open = false;\n conns.delete(conn);\n try {\n conn.socket.close(code, reason);\n } catch {\n /* peer already gone */\n }\n if (conns.size === 0 && heartbeatTimer !== null) {\n clearInterval(heartbeatTimer);\n heartbeatTimer = null;\n }\n };\n\n const sendTo = (conn: Conn, data: string) => {\n if (!conn.open) return;\n conn.lastSentAt = Date.now();\n try {\n conn.socket.send(data);\n } catch {\n closeConn(conn);\n }\n };\n\n const ensureHeartbeat = () => {\n if (heartbeatTimer !== null || heartbeatMs <= 0) return;\n heartbeatTimer = setInterval(() => {\n const now = Date.now();\n for (const conn of [...conns]) {\n if (now - conn.lastSentAt >= heartbeatMs) sendTo(conn, \"{}\");\n }\n }, heartbeatMs);\n (heartbeatTimer as unknown as { unref?: () => void }).unref?.();\n };\n\n const flush = (fin?: StatewireHost.Fin) => {\n flushTapSync(() => {});\n stateHost?.takeChanged();\n for (const rec of clients.values()) {\n for (const adm of rec.inflight.values()) {\n if (adm.acked && !adm.settled && !adm.pendingSent) {\n adm.pendingSent = true;\n queueRes(rec.id, { seq: adm.seq, type: \"pending\" });\n }\n }\n }\n if (\n pendingOps.length === 0 &&\n dirtyAcks.size === 0 &&\n pendingRes.size === 0 &&\n fin === undefined\n ) {\n return;\n }\n const ops = pendingOps;\n const acks = dirtyAcks;\n const res = pendingRes;\n pendingOps = [];\n dirtyAcks = new Set();\n pendingRes = new Map();\n for (const conn of [...conns]) {\n const envelope: StatewireEnvelope = {};\n if (ops.length > 0) envelope.ops = ops;\n const clientRes = res.get(conn.clientId);\n if (clientRes !== undefined) envelope.res = clientRes;\n if (acks.has(conn.clientId)) {\n const rec = clients.get(conn.clientId);\n if (rec !== undefined) envelope.ack = rec.lastSeq;\n }\n if (fin !== undefined) envelope.fin = fin;\n if (Object.keys(envelope).length === 0) continue;\n sendTo(conn, JSON.stringify(envelope));\n if (fin !== undefined) closeConn(conn);\n }\n };\n\n const queueRes = (clientId: string, response: StatewireResponse) => {\n const list = pendingRes.get(clientId);\n if (list) list.push(response);\n else pendingRes.set(clientId, [response]);\n };\n\n const advance = (rec: ClientRec, seq: number) => {\n rec.lastSeq = seq;\n dirtyAcks.add(rec.id);\n };\n\n const retain = (rec: ClientRec, response: StatewireResponse) => {\n const now = Date.now();\n rec.results.set(response.seq, { ts: now, res: response });\n const newest = Math.max(...rec.results.keys());\n for (const [seq, entry] of rec.results) {\n if (seq !== newest && now - entry.ts > RESULT_TTL_MS) {\n rec.results.delete(seq);\n }\n }\n while (rec.results.size > RESULT_LIMIT) {\n rec.results.delete(Math.min(...rec.results.keys()));\n }\n };\n\n const duplicateAnswer = (rec: ClientRec, seq: number): StatewireResponse => {\n const cached = rec.results.get(seq);\n if (cached) {\n const newest = Math.max(...rec.results.keys());\n if (seq === newest || Date.now() - cached.ts <= RESULT_TTL_MS) {\n return cached.res;\n }\n rec.results.delete(seq);\n }\n if (rec.inflight.has(seq)) return { seq, type: \"pending\" };\n return { seq, type: \"result-unavailable\" };\n };\n\n const settleResponse = (\n rec: ClientRec,\n adm: Admission,\n response: StatewireResponse,\n ) => {\n if (adm.settled) return;\n adm.settled = true;\n rec.inflight.delete(adm.seq);\n retain(rec, response);\n if (!adm.acked) {\n adm.acked = true;\n advance(rec, adm.seq);\n }\n if (\n adm.pendingSent ||\n response.type !== \"accepted\" ||\n response.payload !== undefined\n ) {\n queueRes(rec.id, response);\n }\n scheduleFlush();\n adm.release();\n };\n\n const settleWire = (\n rec: ClientRec,\n adm: Admission,\n outcome: CommandsOutcome,\n ) => settleResponse(rec, adm, responseOf(adm, outcome));\n\n const callerOf = (rec: ClientRec): StatewireHost.Caller => ({\n get context() {\n return rec.context;\n },\n get isConnected() {\n if (clients.get(rec.id) !== rec) return false;\n for (const conn of conns) {\n if (conn.clientId === rec.id && conn.open) return true;\n }\n return false;\n },\n });\n\n const admit = (conn: Conn, frame: Frame): Promise<void> | void => {\n if (!conn.open || disposed || finished !== null) return;\n const rec = clients.get(conn.clientId)!;\n if (frame.seq <= rec.lastSeq || rec.inflight.has(frame.seq)) {\n queueRes(rec.id, duplicateAnswer(rec, frame.seq));\n scheduleFlush();\n return;\n }\n if (frame.seq > rec.lastSeq + 1) {\n closeConn(\n conn,\n 1008,\n `seq gap: expected ${rec.lastSeq + 1}, got ${frame.seq}`,\n );\n return;\n }\n const adm: Admission = {\n seq: frame.seq,\n method: frame.method ?? \"\",\n acked: false,\n pendingSent: false,\n settled: false,\n release: () => {},\n };\n if (frame.context !== undefined) {\n let value: Record<string, unknown>;\n try {\n value = contextValue(frame.context);\n } catch (error) {\n rec.inflight.set(frame.seq, adm);\n settleResponse(rec, adm, {\n seq: frame.seq,\n type: \"rejected\",\n code: \"invalid-context\",\n message: `invalid context: ${(error as Error).message}`,\n });\n return;\n }\n rec.context = value;\n if (frame.method === undefined) {\n rec.inflight.set(frame.seq, adm);\n settleResponse(rec, adm, { seq: frame.seq, type: \"accepted\" });\n return;\n }\n }\n const released = new Promise<void>((resolve) => {\n adm.release = resolve;\n });\n rec.inflight.set(frame.seq, adm);\n commandsHost!.submit!({\n method: frame.method!,\n params: frame.params,\n clientId: rec.id,\n caller: callerOf(rec),\n onAck: () => {\n if (adm.settled || adm.acked) return;\n adm.acked = true;\n advance(rec, adm.seq);\n scheduleFlush();\n adm.release();\n },\n onSettle: (outcome) => settleWire(rec, adm, outcome),\n }).catch((error: unknown) => {\n if (!adm.settled) settleWire(rec, adm, { type: \"rejected\", error });\n });\n return released;\n };\n\n const message = (conn: Conn, data: string) => {\n if (!conn.open || disposed || finished !== null) return;\n const frame = parseFrame(data);\n if (frame === null) {\n closeConn(conn, 1008, \"invalid frame\");\n return;\n }\n inbox = inbox.then(() => admit(conn, frame));\n };\n\n const connect = (\n clientId: string,\n socket: StatewireHostInternal.Socket,\n ): StatewireHostInternal.SocketConnection => {\n const dead: StatewireHostInternal.SocketConnection = {\n clientId,\n message: () => {},\n close: () => {},\n };\n const refuse = (code: number, reason: string) => {\n try {\n socket.close(code, reason);\n } catch {\n /* peer already gone */\n }\n return dead;\n };\n if (disposed) return refuse(1001, \"disposed\");\n if (!CLIENT_ID_RE.test(clientId)) return refuse(1008, \"invalid client id\");\n flush();\n if (finished !== null) {\n try {\n socket.send(JSON.stringify({ fin: finished }));\n } catch {\n /* peer already gone */\n }\n return refuse(1000, \"finished\");\n }\n const known = clients.has(clientId);\n const rec = known\n ? clients.get(clientId)!\n : (() => {\n const created: ClientRec = {\n id: clientId,\n lastSeq: 0,\n context: null,\n results: new Map(),\n inflight: new Map(),\n };\n clients.set(clientId, created);\n return created;\n })();\n for (const other of [...conns]) {\n if (other.clientId !== clientId) continue;\n sendTo(other, JSON.stringify({ fin: { reason: \"superseded\" } }));\n closeConn(other);\n }\n const conn: Conn = { clientId, socket, open: true, lastSentAt: 0 };\n conns.add(conn);\n ensureHeartbeat();\n const { value, meta } = encodeValue(stateHost!.snapshot());\n const snapshot: StatewireEnvelope = {\n ops: [\n {\n op: \"replace\",\n path: [],\n value,\n ...(meta !== undefined && { meta }),\n },\n ],\n syn: { lastSeq: known ? rec.lastSeq : -1, lease: randomId() },\n };\n sendTo(conn, JSON.stringify(snapshot));\n return {\n clientId,\n message: (data) => message(conn, data),\n close: () => closeConn(conn),\n };\n };\n\n const finish = (fin: StatewireHost.Fin) => {\n if (finished !== null || disposed) return;\n finished = fin;\n flush(fin);\n };\n\n return {\n connect,\n finish,\n attach,\n resume: () => {\n disposed = false;\n stateHost?.setOnOp(recordOp);\n emitHost?.setSink((event) => recordOp({ op: \"event\", value: event }));\n },\n suspend: () => {\n disposed = true;\n if (heartbeatTimer !== null) {\n clearInterval(heartbeatTimer);\n heartbeatTimer = null;\n }\n stateHost?.setOnOp(null);\n emitHost?.setSink(null);\n },\n };\n};\n\nconst useSocketHost = (\n transport: ResourceElement<Statewire>,\n options?: StatewireHostInternal.SocketHostOptions,\n): Pick<StatewireHostInternal.SocketHost, \"connect\" | \"finish\"> => {\n const [h] = useState(() => createSocketHost(options));\n const value = useResource(transport) as Statewire<unknown>;\n h.attach(value);\n useEffect(() => {\n h.attach(value);\n });\n useEffect(() => {\n h.resume();\n return () => h.suspend();\n }, [h]);\n return h;\n};\n\n/**\n * Transport-agnostic WS binding over a statewire/host transport:\n * feed it sockets (anything with `send`/`close`) and raw text frames, and it\n * speaks the Statewire envelope protocol — snapshot-first attach with `syn`,\n * delta/ack/res envelopes, per-client-id supersede, and `fin` on finish.\n */\nexport const StatewireSocketHost = (\n transport: ResourceElement<Statewire>,\n options?: StatewireHostInternal.SocketHostOptions,\n): StatewireHostInternal.SocketHost => {\n const SocketHostRoot = () => useSocketHost(transport, options);\n const root = createTapRoot(SocketHostRoot);\n let disposed = false;\n return {\n connect: (clientId, socket) =>\n flushTapSync(() => root.getValue().connect(clientId, socket)),\n finish: (fin) => flushTapSync(() => root.getValue().finish(fin)),\n dispose: () => {\n if (disposed) return;\n disposed = true;\n flushTapSync(() => root.getValue().finish({ reason: \"evicted\" }));\n root.unmount();\n },\n };\n};\n"],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { StatewireHost } from "./host.js";
|
|
2
|
+
import { StatewireSocketHost } from "./host/socket.js";
|
|
3
|
+
import { clientIdFromUrl, textFrameOrClose } from "./host/adapter.js";
|
|
4
|
+
//#region src/host-internal.d.ts
|
|
5
|
+
declare namespace StatewireHostInternal {
|
|
6
|
+
type Socket = {
|
|
7
|
+
send: (data: string) => void;
|
|
8
|
+
close: (code?: number, reason?: string) => void;
|
|
9
|
+
};
|
|
10
|
+
type SocketConnection = {
|
|
11
|
+
readonly clientId: string;
|
|
12
|
+
message: (data: string) => void;
|
|
13
|
+
close: () => void;
|
|
14
|
+
};
|
|
15
|
+
type SocketHost = {
|
|
16
|
+
connect: (clientId: string, socket: Socket) => SocketConnection;
|
|
17
|
+
finish: (fin: StatewireHost.Fin) => void;
|
|
18
|
+
dispose: () => void;
|
|
19
|
+
};
|
|
20
|
+
type SocketHostOptions = {
|
|
21
|
+
heartbeatIntervalMs?: number;
|
|
22
|
+
};
|
|
23
|
+
type Fin = StatewireHost.Fin;
|
|
24
|
+
}
|
|
25
|
+
//#endregion
|
|
26
|
+
export { StatewireHostInternal, StatewireSocketHost, clientIdFromUrl, textFrameOrClose };
|
|
27
|
+
//# sourceMappingURL=host-internal.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"host-internal.d.ts","names":[],"sources":["../src/host-internal.ts"],"mappings":";;;;kBAIiB;OACH;IACV,OAAO;IACP,QAAQ,eAAe;;OAGb;aACD;IACT,UAAU;IACV;;OAGU;IACV,UAAU,kBAAkB,QAAQ,WAAW;IAC/C,SAAS,KAAK,cAAc;IAC5B;;OAGU;IACV;;OAGU,MAAM,cAAc"}
|
package/dist/host.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { ServerOp, append } from "./host/draft.js";
|
|
2
|
+
import { StatewireReject } from "./host/engine.js";
|
|
3
|
+
import { CommandExecution, getStateHost, useStatewireCommands, useStatewireEmit, useStatewireState } from "./host/host.js";
|
|
4
|
+
import { Statewire } from "./types.js";
|
|
5
|
+
//#region src/host.d.ts
|
|
6
|
+
declare const executionResult: unique symbol;
|
|
7
|
+
type UnwrapExecution<V> = V extends StatewireHost.Execution<infer T> ? T : V;
|
|
8
|
+
declare namespace StatewireHost {
|
|
9
|
+
type StateCtl<TState = unknown> = {
|
|
10
|
+
/** Apply server-vocabulary ops (root replace = whole-state swap). */
|
|
11
|
+
apply: (ops: readonly Statewire.Op[]) => void;
|
|
12
|
+
/** The committed plain state — immutably replaced per op, so a captured value is a stable snapshot. */
|
|
13
|
+
snapshot: () => TState;
|
|
14
|
+
};
|
|
15
|
+
type StateHost = Statewire.StateSource & {
|
|
16
|
+
/** Synchronous, per locally-recorded op; the host owns batching. */
|
|
17
|
+
setOnOp: (listener: ((op: ServerOp) => void) | null) => void;
|
|
18
|
+
};
|
|
19
|
+
type StateSource = Statewire.StateSource;
|
|
20
|
+
/**
|
|
21
|
+
* Handle a command handler returns to keep the command open past its own
|
|
22
|
+
* return: `ack()` releases the client's optimistic overlay / ack watermark,
|
|
23
|
+
* `resolve`/`reject` settle the command (and imply ack). `ack` is idempotent
|
|
24
|
+
* and a no-op after settle.
|
|
25
|
+
*/
|
|
26
|
+
type Execution<T = void> = {
|
|
27
|
+
readonly [executionResult]: (value: T) => void;
|
|
28
|
+
};
|
|
29
|
+
type ExecutionResolvers<T = void> = {
|
|
30
|
+
ack: () => void;
|
|
31
|
+
resolve: (value: T) => void;
|
|
32
|
+
reject: (error: unknown) => void;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* The submitting client, bound as `this` of a command handler. Holdable
|
|
36
|
+
* across a long run: `context` reads the client's current ambient context
|
|
37
|
+
* live (null until the client sends one), `isConnected` reports whether the
|
|
38
|
+
* client has an active stream attachment.
|
|
39
|
+
*/
|
|
40
|
+
type Caller = {
|
|
41
|
+
readonly context: Record<string, unknown> | null;
|
|
42
|
+
readonly isConnected: boolean;
|
|
43
|
+
};
|
|
44
|
+
type HandledCommands<THandlers extends Statewire.Commands> = { [K in keyof THandlers]: (...args: Parameters<THandlers[K]>) => UnwrapExecution<Awaited<ReturnType<THandlers[K]>>>; };
|
|
45
|
+
type Fin = {
|
|
46
|
+
reason: "evicted" | "error" | "gone" | "superseded" | (string & {});
|
|
47
|
+
message?: string;
|
|
48
|
+
payload?: unknown;
|
|
49
|
+
meta?: unknown;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
//#endregion
|
|
53
|
+
export { CommandExecution, StatewireHost, StatewireReject, append, getStateHost, useStatewireCommands, useStatewireEmit, useStatewireState };
|
|
54
|
+
//# sourceMappingURL=host.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"host.d.ts","names":[],"sources":["../src/host.ts"],"mappings":";;;;;cAac;KAET,gBAAgB,KAAK,UAAU,cAAc,gBAAgB,KAAK,IAAI;kBAE1D;OACH,SAAS;;IAEnB,QAAQ,cAAc,UAAU;;IAEhC,gBAAgB;;OAGN,YAAY,UAAU;;IAEhC,UAAU,YAAY,IAAI;;OAGhB,cAAc,UAAU;;;;;;;OAQxB,UAAU;cACV,mBAAmB,OAAO;;OAG1B,mBAAmB;IAC7B;IACA,UAAU,OAAO;IACjB,SAAS;;;;;;;;OASC;aACD,SAAS;aACT;;OAGC,gBAAgB,kBAAkB,UAAU,eACrD,WAAW,gBACP,MAAM,WAAW,UAAU,QAC3B,gBAAgB,QAAQ,WAAW,UAAU;OAGxC;IACV;IACA;IACA;IACA"}
|