statewire 0.18.1 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/connection.d.ts +8 -5
- package/dist/client/connection.d.ts.map +1 -1
- package/dist/client/connection.js +24 -13
- package/dist/client/connection.js.map +1 -1
- package/dist/client/session.d.ts +13 -15
- package/dist/client/session.d.ts.map +1 -1
- package/dist/client/session.js +296 -401
- package/dist/client/session.js.map +1 -1
- package/dist/client/timing.d.ts +2 -1
- package/dist/client/timing.d.ts.map +1 -1
- package/dist/client/timing.js +2 -1
- package/dist/client/timing.js.map +1 -1
- package/dist/host/engine.d.ts +2 -0
- package/dist/host/engine.d.ts.map +1 -1
- package/dist/host/engine.js +2 -0
- package/dist/host/engine.js.map +1 -1
- package/dist/host/host.d.ts +3 -1
- package/dist/host/host.d.ts.map +1 -1
- package/dist/host/host.js +38 -7
- package/dist/host/host.js.map +1 -1
- package/dist/host/http.d.ts.map +1 -1
- package/dist/host/http.js +9 -19
- package/dist/host/http.js.map +1 -1
- package/dist/host/socket.d.ts +48 -25
- package/dist/host/socket.d.ts.map +1 -1
- package/dist/host/socket.js +399 -266
- package/dist/host/socket.js.map +1 -1
- package/dist/host/synced.d.ts +9 -0
- package/dist/host/synced.d.ts.map +1 -0
- package/dist/host/synced.js +45 -0
- package/dist/host/synced.js.map +1 -0
- package/dist/host-internal.d.ts +20 -12
- package/dist/host-internal.d.ts.map +1 -1
- package/dist/host.d.ts +33 -9
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +2 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -1
- package/dist/statewire.d.ts +8 -20
- package/dist/statewire.d.ts.map +1 -1
- package/dist/statewire.js +0 -14
- package/dist/statewire.js.map +1 -1
- package/dist/storage.d.ts +2 -5
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +54 -89
- package/dist/storage.js.map +1 -1
- package/dist/transports/remote.d.ts.map +1 -1
- package/dist/transports/remote.js +1 -6
- package/dist/transports/remote.js.map +1 -1
- package/dist/transports/sse.d.ts +2 -5
- package/dist/transports/sse.d.ts.map +1 -1
- package/dist/transports/sse.js.map +1 -1
- package/dist/transports/testing.d.ts +7 -5
- package/dist/transports/testing.d.ts.map +1 -1
- package/dist/transports/testing.js +19 -18
- package/dist/transports/testing.js.map +1 -1
- package/dist/types.d.ts +3 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/wire/limits.d.ts +2 -2
- package/dist/wire/limits.js +2 -2
- package/dist/wire/limits.js.map +1 -1
- package/dist/wire/protocol.d.ts +6 -1
- package/dist/wire/protocol.d.ts.map +1 -1
- package/dist/wire/protocol.js +33 -12
- package/dist/wire/protocol.js.map +1 -1
- package/package.json +1 -1
- package/src/client/connection.ts +46 -33
- package/src/client/session.ts +442 -517
- package/src/client/timing.ts +2 -0
- package/src/host/engine.ts +3 -1
- package/src/host/host.ts +50 -3
- package/src/host/http.ts +23 -31
- package/src/host/socket.ts +560 -339
- package/src/host/synced.ts +60 -0
- package/src/host-internal.ts +21 -7
- package/src/host.ts +40 -8
- package/src/index.ts +1 -0
- package/src/statewire.ts +8 -37
- package/src/storage.ts +90 -157
- package/src/transports/remote.ts +0 -7
- package/src/transports/sse.ts +8 -3
- package/src/transports/testing.ts +34 -18
- package/src/types.ts +3 -5
- package/src/wire/limits.ts +1 -1
- package/src/wire/protocol.ts +40 -11
package/src/client/timing.ts
CHANGED
package/src/host/engine.ts
CHANGED
|
@@ -3,11 +3,13 @@
|
|
|
3
3
|
* message/payload travel to the client's rejected promise.
|
|
4
4
|
*/
|
|
5
5
|
export class StatewireReject extends Error {
|
|
6
|
+
readonly code?: string;
|
|
6
7
|
readonly payload?: unknown;
|
|
7
8
|
|
|
8
|
-
constructor(message: string, options?: { payload?: unknown }) {
|
|
9
|
+
constructor(message: string, options?: { code?: string; payload?: unknown }) {
|
|
9
10
|
super(message);
|
|
10
11
|
this.name = "StatewireReject";
|
|
12
|
+
if (options?.code !== undefined) this.code = options.code;
|
|
11
13
|
if (options?.payload !== undefined) this.payload = options.payload;
|
|
12
14
|
}
|
|
13
15
|
}
|
package/src/host/host.ts
CHANGED
|
@@ -16,6 +16,9 @@ const isDev = () =>
|
|
|
16
16
|
|
|
17
17
|
const stateHosts = new WeakMap<object, StatewireHost.StateHost>();
|
|
18
18
|
|
|
19
|
+
export const documentIdentity = (protocol: string, key?: readonly string[]) =>
|
|
20
|
+
JSON.stringify([protocol, key ?? null]);
|
|
21
|
+
|
|
19
22
|
/** Host-side accessor for a `useStatewireState` state object's source (`snapshot()` reads the committed state). */
|
|
20
23
|
export const getStateHost = (
|
|
21
24
|
state: object,
|
|
@@ -25,8 +28,10 @@ type StateCell = {
|
|
|
25
28
|
current: unknown;
|
|
26
29
|
draft: StateDraft<object> | null;
|
|
27
30
|
changed: StatewirePath[];
|
|
31
|
+
documents: Map<string, StatewireHost.Document>;
|
|
28
32
|
onOp: ((op: Statewire.Op) => void) | null;
|
|
29
33
|
onChange: (() => void) | null;
|
|
34
|
+
onDocuments: (() => void) | null;
|
|
30
35
|
ctl: StatewireHost.StateCtl;
|
|
31
36
|
host: StatewireHost.StateHost;
|
|
32
37
|
bump: () => void;
|
|
@@ -45,8 +50,10 @@ export const useStatewireState = <TState>(
|
|
|
45
50
|
current: sanitizeValue(init()),
|
|
46
51
|
draft: null,
|
|
47
52
|
changed: [[]],
|
|
53
|
+
documents: new Map(),
|
|
48
54
|
onOp: null,
|
|
49
55
|
onChange: null,
|
|
56
|
+
onDocuments: null,
|
|
50
57
|
bump: () => setVersion((v) => v + 1),
|
|
51
58
|
ctl: {
|
|
52
59
|
apply: (ops) => {
|
|
@@ -73,6 +80,38 @@ export const useStatewireState = <TState>(
|
|
|
73
80
|
c.changed = [];
|
|
74
81
|
return changed;
|
|
75
82
|
},
|
|
83
|
+
mount: (protocol, value, options) => {
|
|
84
|
+
const key = options?.key;
|
|
85
|
+
const main = options?.main ?? key === undefined;
|
|
86
|
+
if (main) {
|
|
87
|
+
for (const [id, doc] of c.documents) {
|
|
88
|
+
if (
|
|
89
|
+
doc.protocol === protocol &&
|
|
90
|
+
doc.main &&
|
|
91
|
+
id !== documentIdentity(protocol, key)
|
|
92
|
+
)
|
|
93
|
+
throw new Error(
|
|
94
|
+
`statewire: ${protocol} already has a main document`,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
c.documents.set(documentIdentity(protocol, key), {
|
|
99
|
+
protocol,
|
|
100
|
+
key: key === undefined ? null : [...key],
|
|
101
|
+
main,
|
|
102
|
+
value: sanitizeValue(value),
|
|
103
|
+
});
|
|
104
|
+
c.onDocuments?.();
|
|
105
|
+
},
|
|
106
|
+
unmount: (protocol, key) => {
|
|
107
|
+
if (!c.documents.delete(documentIdentity(protocol, key)))
|
|
108
|
+
throw new Error("statewire: document is not mounted");
|
|
109
|
+
c.onDocuments?.();
|
|
110
|
+
},
|
|
111
|
+
documents: () => c.documents,
|
|
112
|
+
setOnDocuments: (listener) => {
|
|
113
|
+
c.onDocuments = listener;
|
|
114
|
+
},
|
|
76
115
|
},
|
|
77
116
|
};
|
|
78
117
|
return c;
|
|
@@ -128,6 +167,7 @@ export type CommandsSubmission = {
|
|
|
128
167
|
params: readonly unknown[];
|
|
129
168
|
clientId?: string;
|
|
130
169
|
caller?: StatewireHost.Caller;
|
|
170
|
+
attach?: StatewireHost.Attach;
|
|
131
171
|
onApplied?: () => void;
|
|
132
172
|
onSettle?: (outcome: CommandsOutcome) => void;
|
|
133
173
|
};
|
|
@@ -178,13 +218,12 @@ type CommandsCell = {
|
|
|
178
218
|
|
|
179
219
|
const STALL_WARN_MS = 5000;
|
|
180
220
|
|
|
181
|
-
// In-process dispatch has no wire client:
|
|
221
|
+
// In-process dispatch has no wire client: trivially attached.
|
|
182
222
|
const LOCAL_CALLER: StatewireHost.Caller = Object.freeze({
|
|
183
223
|
get clientId(): string {
|
|
184
224
|
throw new Error("statewire: in-process dispatch has no client id");
|
|
185
225
|
},
|
|
186
226
|
auth: undefined,
|
|
187
|
-
context: null,
|
|
188
227
|
isConnected: true,
|
|
189
228
|
});
|
|
190
229
|
|
|
@@ -196,6 +235,7 @@ const toSendError = (error: unknown): StatewireSendError =>
|
|
|
196
235
|
fate: "rejected",
|
|
197
236
|
cause: "server",
|
|
198
237
|
message: error.message,
|
|
238
|
+
...(error.code !== undefined && { code: error.code }),
|
|
199
239
|
...(error.payload !== undefined && { payload: error.payload }),
|
|
200
240
|
})
|
|
201
241
|
: new StatewireSendError({
|
|
@@ -221,8 +261,8 @@ const settleInvocation = (
|
|
|
221
261
|
if (inv.settled) return;
|
|
222
262
|
inv.settled = true;
|
|
223
263
|
cell.active.delete(inv);
|
|
224
|
-
applyInvocation(cell, inv);
|
|
225
264
|
if (settled.type === "resolved") {
|
|
265
|
+
applyInvocation(cell, inv);
|
|
226
266
|
const outcome: CommandsOutcome =
|
|
227
267
|
settled.value === undefined
|
|
228
268
|
? { type: "accepted" }
|
|
@@ -231,6 +271,7 @@ const settleInvocation = (
|
|
|
231
271
|
inv.origin?.onSettle?.(outcome);
|
|
232
272
|
call.resolve(outcome.payload);
|
|
233
273
|
} else {
|
|
274
|
+
cell.listener?.onCovered?.(inv);
|
|
234
275
|
const error = toSendError(settled.error);
|
|
235
276
|
const outcome: CommandsOutcome = { type: "rejected", error };
|
|
236
277
|
cell.listener?.onSettle(inv, outcome);
|
|
@@ -299,8 +340,14 @@ const dispatch = (cell: CommandsCell, call: QueuedCall) => {
|
|
|
299
340
|
cell.active.set(inv, call);
|
|
300
341
|
cell.listener?.onStart(inv);
|
|
301
342
|
cell.listener?.onPosted?.(inv);
|
|
343
|
+
const attach = call.origin?.attach;
|
|
302
344
|
const ctx: StatewireHost.Ctx = {
|
|
303
345
|
caller: call.origin?.caller ?? LOCAL_CALLER,
|
|
346
|
+
get attach() {
|
|
347
|
+
if (attach === undefined)
|
|
348
|
+
throw new Error("statewire: in-process dispatch has no attach");
|
|
349
|
+
return attach;
|
|
350
|
+
},
|
|
304
351
|
signal: inv.controller.signal,
|
|
305
352
|
applied: () => {
|
|
306
353
|
if (!inv.settled) applyInvocation(cell, inv);
|
package/src/host/http.ts
CHANGED
|
@@ -141,6 +141,7 @@ const attachSse = (
|
|
|
141
141
|
clientId: string,
|
|
142
142
|
auth: unknown,
|
|
143
143
|
selection: StatewireProtocol.Selection,
|
|
144
|
+
frame?: ParsedFrame,
|
|
144
145
|
): SseAttachment => {
|
|
145
146
|
let controller: ReadableStreamDefaultController<Uint8Array>;
|
|
146
147
|
let connection: Registration["connection"] | null = null;
|
|
@@ -194,7 +195,15 @@ const attachSse = (
|
|
|
194
195
|
}
|
|
195
196
|
},
|
|
196
197
|
};
|
|
197
|
-
const registration = register(
|
|
198
|
+
const registration = register(
|
|
199
|
+
h,
|
|
200
|
+
clientId,
|
|
201
|
+
socket,
|
|
202
|
+
false,
|
|
203
|
+
auth,
|
|
204
|
+
selection,
|
|
205
|
+
frame,
|
|
206
|
+
);
|
|
198
207
|
connection = registration?.connection ?? null;
|
|
199
208
|
if (!open) connection?.close();
|
|
200
209
|
return {
|
|
@@ -220,24 +229,6 @@ const streamResponse = (
|
|
|
220
229
|
});
|
|
221
230
|
};
|
|
222
231
|
|
|
223
|
-
const admitAttachFrame = (
|
|
224
|
-
h: SocketHostState,
|
|
225
|
-
registration: Registration,
|
|
226
|
-
frame: ParsedFrame,
|
|
227
|
-
) => {
|
|
228
|
-
if (frame.ctx.length === 0 && frame.cmd.length === 0) return;
|
|
229
|
-
const admission = admitFrame(
|
|
230
|
-
h,
|
|
231
|
-
registration.rec,
|
|
232
|
-
frame,
|
|
233
|
-
registration.conn.auth,
|
|
234
|
-
registration.conn.selection,
|
|
235
|
-
);
|
|
236
|
-
if (admission.type === "refused") {
|
|
237
|
-
sendTo(h, registration.conn, JSON.stringify(admission.syn));
|
|
238
|
-
}
|
|
239
|
-
};
|
|
240
|
-
|
|
241
232
|
const getStream = async (
|
|
242
233
|
h: SocketHostState,
|
|
243
234
|
request: Request,
|
|
@@ -283,10 +274,7 @@ const postStream = async (
|
|
|
283
274
|
}
|
|
284
275
|
let frame: ParsedFrame;
|
|
285
276
|
try {
|
|
286
|
-
frame =
|
|
287
|
-
body === null && !raw.trim()
|
|
288
|
-
? { ctx: [], cmd: [] }
|
|
289
|
-
: parseFrameValue(body);
|
|
277
|
+
frame = body === null && !raw.trim() ? { cmd: [] } : parseFrameValue(body);
|
|
290
278
|
} catch (error) {
|
|
291
279
|
if (!(error instanceof Error) || error.name !== "StatewireViolation") {
|
|
292
280
|
throw error;
|
|
@@ -307,14 +295,16 @@ const postStream = async (
|
|
|
307
295
|
]);
|
|
308
296
|
if ("fin" in selection)
|
|
309
297
|
return Response.json(selection, { status: selection.fin.status });
|
|
310
|
-
|
|
311
|
-
if (attachment.registration === null) return streamResponse(attachment, null);
|
|
312
|
-
const lease = mintLease();
|
|
313
|
-
attachment.registration.conn.lease = lease;
|
|
298
|
+
let attachment: SseAttachment;
|
|
314
299
|
try {
|
|
315
|
-
|
|
300
|
+
attachment = attachSse(
|
|
301
|
+
h,
|
|
302
|
+
client.clientId,
|
|
303
|
+
authorized.auth,
|
|
304
|
+
selection,
|
|
305
|
+
frame,
|
|
306
|
+
);
|
|
316
307
|
} catch (error) {
|
|
317
|
-
closeConn(h, attachment.registration.conn, 1008, "invalid-context");
|
|
318
308
|
if (!(error instanceof Error) || error.name !== "StatewireViolation")
|
|
319
309
|
throw error;
|
|
320
310
|
return Response.json(
|
|
@@ -322,6 +312,9 @@ const postStream = async (
|
|
|
322
312
|
{ status: 400 },
|
|
323
313
|
);
|
|
324
314
|
}
|
|
315
|
+
if (attachment.registration === null) return streamResponse(attachment, null);
|
|
316
|
+
const lease = mintLease();
|
|
317
|
+
attachment.registration.conn.lease = lease;
|
|
325
318
|
return streamResponse(attachment, lease);
|
|
326
319
|
};
|
|
327
320
|
|
|
@@ -376,7 +369,6 @@ export const frames = async (
|
|
|
376
369
|
return Response.json({ error: "stale-lease" }, { status: 423 });
|
|
377
370
|
}
|
|
378
371
|
const clientId = leased.clientId;
|
|
379
|
-
const rec = h.clients.get(clientId)!;
|
|
380
372
|
const raw = await readFrameBody(request);
|
|
381
373
|
if (raw === null) {
|
|
382
374
|
return Response.json({ error: "frame body too large" }, { status: 413 });
|
|
@@ -395,7 +387,7 @@ export const frames = async (
|
|
|
395
387
|
}
|
|
396
388
|
let admission: ReturnType<typeof admitFrame>;
|
|
397
389
|
try {
|
|
398
|
-
admission = admitFrame(h,
|
|
390
|
+
admission = admitFrame(h, leased, frame);
|
|
399
391
|
} catch (error) {
|
|
400
392
|
if (!(error instanceof Error) || error.name !== "StatewireViolation")
|
|
401
393
|
throw error;
|