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
package/src/client/session.ts
CHANGED
|
@@ -1,27 +1,28 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
1
2
|
import { applyOps } from "../wire/ops";
|
|
2
3
|
import type { StatewirePath } from "../wire/ops";
|
|
3
4
|
import type { StatewireResponse } from "./connection";
|
|
4
5
|
import { bindCommandsHost } from "../host/host";
|
|
5
6
|
import type { CommandsInvocation, CommandsListener } from "../host/host";
|
|
6
|
-
import type {
|
|
7
|
+
import type { Statewire } from "../types";
|
|
8
|
+
import type { StatewireClient } from "../statewire";
|
|
7
9
|
import { createMemorySessionStore } from "./sessionStore";
|
|
8
10
|
import type {
|
|
9
11
|
StatewireSessionRecord,
|
|
10
12
|
StatewireSessionStore,
|
|
13
|
+
StatewireSessionStoreOptions,
|
|
11
14
|
} from "./sessionStore";
|
|
12
|
-
import {
|
|
15
|
+
import { retryDelay } from "./timing";
|
|
16
|
+
import { createMethodProxy } from "../util/methodProxy";
|
|
17
|
+
import { randomId } from "../util/randomId";
|
|
13
18
|
import { StatewireSendError } from "../types";
|
|
14
|
-
import type { SendFailure as StatewireSendFailure } from "../types";
|
|
15
19
|
import type {
|
|
16
20
|
RemoteLink,
|
|
21
|
+
StatewireRemoteProps,
|
|
17
22
|
StatewireSubmitResult,
|
|
18
23
|
StatewireWireCommand,
|
|
19
24
|
} from "../transports/sse";
|
|
20
25
|
|
|
21
|
-
export type SessionOptions = {
|
|
22
|
-
sessionStore?: StatewireSessionStore | undefined;
|
|
23
|
-
};
|
|
24
|
-
|
|
25
26
|
/**
|
|
26
27
|
* queued/trying/posted: overlay live, result pending. awaiting-result: acked
|
|
27
28
|
* (overlay gone) but the result rides a late res; `posted: false` re-sends.
|
|
@@ -39,7 +40,8 @@ type PendingState =
|
|
|
39
40
|
type Pending = {
|
|
40
41
|
readonly seq: number;
|
|
41
42
|
readonly command: StatewireWireCommand;
|
|
42
|
-
|
|
43
|
+
/** null for context submissions — they carry no command lifecycle. */
|
|
44
|
+
readonly inv: CommandsInvocation | null;
|
|
43
45
|
state: PendingState;
|
|
44
46
|
resolve: (payload: unknown) => void;
|
|
45
47
|
reject: (error: StatewireSendError) => void;
|
|
@@ -47,7 +49,7 @@ type Pending = {
|
|
|
47
49
|
|
|
48
50
|
type Outcome =
|
|
49
51
|
| { ok: true; payload: unknown }
|
|
50
|
-
| { ok: false; failure:
|
|
52
|
+
| { ok: false; failure: StatewireClient.SendFailure };
|
|
51
53
|
|
|
52
54
|
const isResolved = (state: PendingState) =>
|
|
53
55
|
state.at === "settled-live" || state.at === "done";
|
|
@@ -55,6 +57,15 @@ const isResolved = (state: PendingState) =>
|
|
|
55
57
|
const isStateSettled = (state: PendingState) =>
|
|
56
58
|
state.at === "awaiting-result" || state.at === "done";
|
|
57
59
|
|
|
60
|
+
// The pump admits an entry as trying before sending; only the pump itself
|
|
61
|
+
// requeues, and a done entry is never still pending.
|
|
62
|
+
const inFlight = (state: PendingState) => {
|
|
63
|
+
if (state.at === "queued" || state.at === "done") {
|
|
64
|
+
throw new Error(`statewire: ${state.at} entry observed after a send`);
|
|
65
|
+
}
|
|
66
|
+
return state;
|
|
67
|
+
};
|
|
68
|
+
|
|
58
69
|
const legalTransitions: Record<PendingState["at"], PendingState["at"][]> = {
|
|
59
70
|
queued: ["queued", "trying", "awaiting-result", "settled-live", "done"],
|
|
60
71
|
// trying -> queued: an answered unknown-client proves the seq was never admitted.
|
|
@@ -92,20 +103,22 @@ const transition = (
|
|
|
92
103
|
}
|
|
93
104
|
const covers = !isStateSettled(prev) && isStateSettled(next);
|
|
94
105
|
entry.state = next;
|
|
95
|
-
if (covers) s.lifecycle?.onCovered?.(entry.inv);
|
|
106
|
+
if (covers && entry.inv) s.lifecycle?.onCovered?.(entry.inv);
|
|
96
107
|
if (settles && outcome !== undefined) {
|
|
97
108
|
if (outcome.ok) {
|
|
98
109
|
entry.resolve(outcome.payload);
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
110
|
+
if (entry.inv)
|
|
111
|
+
s.lifecycle?.onSettle(entry.inv, {
|
|
112
|
+
type: "accepted",
|
|
113
|
+
payload: outcome.payload,
|
|
114
|
+
});
|
|
103
115
|
} else {
|
|
104
116
|
entry.reject(new StatewireSendError(outcome.failure));
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
117
|
+
if (entry.inv)
|
|
118
|
+
s.lifecycle?.onSettle(entry.inv, {
|
|
119
|
+
type: "rejected",
|
|
120
|
+
error: new StatewireSendError(outcome.failure),
|
|
121
|
+
});
|
|
109
122
|
}
|
|
110
123
|
}
|
|
111
124
|
if (next.at === "done") {
|
|
@@ -122,10 +135,11 @@ const carried = (
|
|
|
122
135
|
? { posted: state.at === "posted", failedAttempts: state.failedAttempts }
|
|
123
136
|
: { posted: state.posted, failedAttempts: state.failedAttempts };
|
|
124
137
|
|
|
125
|
-
const newClientId = () =>
|
|
126
|
-
`c-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
138
|
+
const newClientId = () => `c-${randomId()}`;
|
|
127
139
|
|
|
128
|
-
const rejectedSend = (
|
|
140
|
+
const rejectedSend = (
|
|
141
|
+
failure: StatewireClient.SendFailure,
|
|
142
|
+
): Promise<unknown> => {
|
|
129
143
|
const result = Promise.reject<unknown>(new StatewireSendError(failure));
|
|
130
144
|
result.catch(() => {});
|
|
131
145
|
return result;
|
|
@@ -135,14 +149,16 @@ type AttachState = "detached" | "attached" | "finished";
|
|
|
135
149
|
|
|
136
150
|
type Session = {
|
|
137
151
|
record: StatewireSessionRecord;
|
|
152
|
+
ctx: StatewireRemoteProps;
|
|
138
153
|
recordPersisted: boolean;
|
|
139
154
|
store: StatewireSessionStore;
|
|
140
155
|
lastAck: number;
|
|
156
|
+
context: Record<string, unknown> | undefined;
|
|
141
157
|
pending: Pending[];
|
|
142
158
|
canonical: unknown;
|
|
143
159
|
lastPublished: unknown;
|
|
144
160
|
changed: StatewirePath[];
|
|
145
|
-
source:
|
|
161
|
+
source: Statewire.StateSource;
|
|
146
162
|
attach: AttachState;
|
|
147
163
|
attachEpoch: number;
|
|
148
164
|
running: boolean;
|
|
@@ -156,38 +172,30 @@ type Session = {
|
|
|
156
172
|
commands: Record<string, (...params: unknown[]) => Promise<unknown>>;
|
|
157
173
|
};
|
|
158
174
|
|
|
159
|
-
const
|
|
160
|
-
send: (command: StatewireWireCommand) => Promise<unknown>,
|
|
161
|
-
): Session["commands"] => {
|
|
162
|
-
const memo = new Map<string, (...params: unknown[]) => Promise<unknown>>();
|
|
163
|
-
return new Proxy({} as Session["commands"], {
|
|
164
|
-
get: (_target, method) => {
|
|
165
|
-
if (typeof method !== "string") return undefined;
|
|
166
|
-
let fn = memo.get(method);
|
|
167
|
-
if (fn === undefined) {
|
|
168
|
-
fn = (...params: unknown[]) => send({ method, params });
|
|
169
|
-
memo.set(method, fn);
|
|
170
|
-
}
|
|
171
|
-
return fn;
|
|
172
|
-
},
|
|
173
|
-
});
|
|
174
|
-
};
|
|
175
|
-
|
|
176
|
-
const createSession = (options: SessionOptions): Session => {
|
|
175
|
+
const createSession = (options: StatewireSessionStoreOptions): Session => {
|
|
177
176
|
const store = options.sessionStore ?? createMemorySessionStore();
|
|
178
177
|
const stored = store.load();
|
|
179
178
|
const record = stored ?? { clientId: newClientId(), nextSeq: 1 };
|
|
180
179
|
const s: Session = {
|
|
181
180
|
record,
|
|
181
|
+
ctx: {
|
|
182
|
+
get clientId() {
|
|
183
|
+
return s.record.clientId;
|
|
184
|
+
},
|
|
185
|
+
onSnapshot: (args) => handleSnapshot(s, args),
|
|
186
|
+
onOps: (args) => handleOps(s, args),
|
|
187
|
+
onFinish: (args) => handleFinish(s, args),
|
|
188
|
+
},
|
|
182
189
|
recordPersisted: stored !== null,
|
|
183
190
|
store,
|
|
184
191
|
lastAck: -1,
|
|
192
|
+
context: undefined,
|
|
185
193
|
pending: [],
|
|
186
194
|
canonical: undefined,
|
|
187
195
|
lastPublished: undefined,
|
|
188
196
|
changed: [],
|
|
189
197
|
source: {
|
|
190
|
-
|
|
198
|
+
snapshot: () => s.canonical,
|
|
191
199
|
takeChanged: () => {
|
|
192
200
|
const changed = s.changed;
|
|
193
201
|
s.changed = [];
|
|
@@ -207,7 +215,7 @@ const createSession = (options: SessionOptions): Session => {
|
|
|
207
215
|
notify: () => {},
|
|
208
216
|
emitEvent: () => {},
|
|
209
217
|
send: (command) => sendCommand(s, command),
|
|
210
|
-
commands:
|
|
218
|
+
commands: createMethodProxy((method, params) => s.send({ method, params })),
|
|
211
219
|
};
|
|
212
220
|
bindCommandsHost(s.commands, {
|
|
213
221
|
setListener: (listener) => {
|
|
@@ -231,12 +239,25 @@ const wake = (s: Session) => {
|
|
|
231
239
|
|
|
232
240
|
const waitForWake = (s: Session) => new Promise<void>((r) => s.waiters.push(r));
|
|
233
241
|
|
|
242
|
+
const sleepUntilWake = (s: Session, delayMs: number) =>
|
|
243
|
+
new Promise<void>((resolve) => {
|
|
244
|
+
const waiter = () => {
|
|
245
|
+
clearTimeout(timer);
|
|
246
|
+
resolve();
|
|
247
|
+
};
|
|
248
|
+
const timer = setTimeout(() => {
|
|
249
|
+
s.waiters = s.waiters.filter((w) => w !== waiter);
|
|
250
|
+
resolve();
|
|
251
|
+
}, delayMs);
|
|
252
|
+
s.waiters.push(waiter);
|
|
253
|
+
});
|
|
254
|
+
|
|
234
255
|
const settleAll = (
|
|
235
256
|
s: Session,
|
|
236
257
|
detach: "evicted" | "unmount" | "gone" | "superseded",
|
|
237
258
|
verdict?: {
|
|
238
259
|
entry: Pending;
|
|
239
|
-
failure:
|
|
260
|
+
failure: StatewireClient.SendFailure;
|
|
240
261
|
},
|
|
241
262
|
) => {
|
|
242
263
|
const cause = (
|
|
@@ -248,7 +269,7 @@ const settleAll = (
|
|
|
248
269
|
} as const
|
|
249
270
|
)[detach];
|
|
250
271
|
const batch = s.pending;
|
|
251
|
-
const detachFailure = (entry: Pending):
|
|
272
|
+
const detachFailure = (entry: Pending): StatewireClient.SendFailure => {
|
|
252
273
|
const state = entry.state;
|
|
253
274
|
if (state.at === "awaiting-result")
|
|
254
275
|
return { fate: "result-unavailable", cause };
|
|
@@ -271,13 +292,16 @@ const evict = (
|
|
|
271
292
|
s: Session,
|
|
272
293
|
verdict?: {
|
|
273
294
|
entry: Pending;
|
|
274
|
-
failure:
|
|
295
|
+
failure: StatewireClient.SendFailure;
|
|
275
296
|
},
|
|
276
297
|
) => {
|
|
277
298
|
settleAll(s, "evicted", verdict);
|
|
278
299
|
s.record = { clientId: newClientId(), nextSeq: 1 };
|
|
279
300
|
s.store.save(s.record);
|
|
280
301
|
s.attach = "detached";
|
|
302
|
+
// The server dropped this client's record and its stored context with it:
|
|
303
|
+
// the new session opens with a fresh full root replace.
|
|
304
|
+
if (s.context !== undefined) void s.send({ context: s.context });
|
|
281
305
|
publish(s);
|
|
282
306
|
wake(s);
|
|
283
307
|
};
|
|
@@ -285,7 +309,7 @@ const evict = (
|
|
|
285
309
|
const noteAcked = (s: Session, seq: number) => {
|
|
286
310
|
for (const entry of s.pending) {
|
|
287
311
|
if (entry.seq > seq || isStateSettled(entry.state)) continue;
|
|
288
|
-
s.lifecycle?.onAck(entry.inv);
|
|
312
|
+
if (entry.inv) s.lifecycle?.onAck(entry.inv);
|
|
289
313
|
}
|
|
290
314
|
};
|
|
291
315
|
|
|
@@ -347,12 +371,16 @@ const applyResponse = (s: Session, response: StatewireResponse) => {
|
|
|
347
371
|
? {
|
|
348
372
|
fate: "invalid-params",
|
|
349
373
|
cause: "server",
|
|
350
|
-
message:
|
|
374
|
+
message:
|
|
375
|
+
response.message ??
|
|
376
|
+
protocolError("rejected response missing message"),
|
|
351
377
|
}
|
|
352
378
|
: {
|
|
353
379
|
fate: "rejected",
|
|
354
380
|
cause: "server",
|
|
355
|
-
message:
|
|
381
|
+
message:
|
|
382
|
+
response.message ??
|
|
383
|
+
protocolError("rejected response missing message"),
|
|
356
384
|
...(response.payload !== undefined && {
|
|
357
385
|
payload: response.payload,
|
|
358
386
|
}),
|
|
@@ -382,12 +410,9 @@ const ingestOps = (s: Session, ops: unknown[]) => {
|
|
|
382
410
|
return applied.events;
|
|
383
411
|
};
|
|
384
412
|
|
|
385
|
-
const publishIngested = (
|
|
386
|
-
s: Session,
|
|
387
|
-
events: { event: unknown; state: unknown }[],
|
|
388
|
-
) => {
|
|
413
|
+
const publishIngested = (s: Session, events: unknown[]) => {
|
|
389
414
|
publish(s);
|
|
390
|
-
for (const
|
|
415
|
+
for (const event of events) s.emitEvent(event);
|
|
391
416
|
wake(s);
|
|
392
417
|
};
|
|
393
418
|
|
|
@@ -459,7 +484,7 @@ const handleOps = (
|
|
|
459
484
|
};
|
|
460
485
|
|
|
461
486
|
const handleFinish = (s: Session, { reason }: { reason: string }) => {
|
|
462
|
-
if (reason === "
|
|
487
|
+
if (reason === "evicted" || reason === "error") return;
|
|
463
488
|
s.attach = "finished";
|
|
464
489
|
settleAll(s, reason === "superseded" ? "superseded" : "gone");
|
|
465
490
|
publish(s);
|
|
@@ -504,18 +529,12 @@ const pump = async (s: Session) => {
|
|
|
504
529
|
} catch (error) {
|
|
505
530
|
if (!s.running || isFinished(s)) return;
|
|
506
531
|
if (!s.pending.includes(entry)) continue;
|
|
507
|
-
const state = entry.state;
|
|
508
|
-
if (state.at === "queued" || state.at === "done") continue;
|
|
532
|
+
const state = inFlight(entry.state);
|
|
509
533
|
transition(s, entry, {
|
|
510
534
|
...state,
|
|
511
535
|
failedAttempts: state.failedAttempts + 1,
|
|
512
536
|
});
|
|
513
|
-
|
|
514
|
-
const delay =
|
|
515
|
-
typeof retryAfterMs === "number"
|
|
516
|
-
? retryAfterMs
|
|
517
|
-
: backoff(state.failedAttempts + 1);
|
|
518
|
-
await new Promise((r) => setTimeout(r, delay));
|
|
537
|
+
await sleepUntilWake(s, retryDelay(error, state.failedAttempts + 1));
|
|
519
538
|
continue;
|
|
520
539
|
}
|
|
521
540
|
if (!s.running) return;
|
|
@@ -535,13 +554,12 @@ const pump = async (s: Session) => {
|
|
|
535
554
|
) {
|
|
536
555
|
transition(s, entry, { ...state, posted: true });
|
|
537
556
|
}
|
|
538
|
-
s.lifecycle?.onPosted?.(entry.inv);
|
|
557
|
+
if (entry.inv) s.lifecycle?.onPosted?.(entry.inv);
|
|
539
558
|
if (response !== undefined) {
|
|
540
559
|
applyResponse(s, response);
|
|
541
560
|
publish(s);
|
|
542
561
|
}
|
|
543
562
|
} else if (result.type === "rejected") {
|
|
544
|
-
const { message, payload } = result;
|
|
545
563
|
transition(
|
|
546
564
|
s,
|
|
547
565
|
entry,
|
|
@@ -551,8 +569,7 @@ const pump = async (s: Session) => {
|
|
|
551
569
|
failure: {
|
|
552
570
|
fate: "rejected",
|
|
553
571
|
cause: "server",
|
|
554
|
-
message,
|
|
555
|
-
...(payload !== undefined && { payload }),
|
|
572
|
+
message: result.message,
|
|
556
573
|
},
|
|
557
574
|
},
|
|
558
575
|
);
|
|
@@ -589,16 +606,14 @@ const pump = async (s: Session) => {
|
|
|
589
606
|
);
|
|
590
607
|
// Every attempt answered means the server never saw it; a thrown
|
|
591
608
|
// attempt may have left the machine unanswered, so the verdict is "lost".
|
|
592
|
-
const state = entry.state;
|
|
593
|
-
const failure:
|
|
609
|
+
const state = inFlight(entry.state);
|
|
610
|
+
const failure: StatewireClient.SendFailure =
|
|
594
611
|
state.at === "awaiting-result"
|
|
595
612
|
? { fate: "result-unavailable", cause: "session-lost" }
|
|
596
|
-
:
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
cause: "session-lost",
|
|
601
|
-
};
|
|
613
|
+
: {
|
|
614
|
+
fate: state.failedAttempts > 0 ? "lost" : "aborted",
|
|
615
|
+
cause: "session-lost",
|
|
616
|
+
};
|
|
602
617
|
evict(s, { entry, failure });
|
|
603
618
|
s.transport?.connection.reconnect();
|
|
604
619
|
}
|
|
@@ -646,13 +661,16 @@ const sendCommand = (
|
|
|
646
661
|
const entry: Pending = {
|
|
647
662
|
seq,
|
|
648
663
|
command,
|
|
649
|
-
inv:
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
664
|
+
inv:
|
|
665
|
+
command.method === undefined
|
|
666
|
+
? null
|
|
667
|
+
: {
|
|
668
|
+
method: command.method,
|
|
669
|
+
params: command.params,
|
|
670
|
+
ts: Date.now(),
|
|
671
|
+
seq,
|
|
672
|
+
clientId: s.record.clientId,
|
|
673
|
+
},
|
|
656
674
|
state: { at: "queued", pendingMarked: false },
|
|
657
675
|
resolve: () => {},
|
|
658
676
|
reject: () => {},
|
|
@@ -663,17 +681,38 @@ const sendCommand = (
|
|
|
663
681
|
});
|
|
664
682
|
result.catch(() => {});
|
|
665
683
|
s.pending.push(entry);
|
|
666
|
-
s.lifecycle?.onStart(entry.inv);
|
|
684
|
+
if (entry.inv) s.lifecycle?.onStart(entry.inv);
|
|
667
685
|
void pump(s);
|
|
668
686
|
return result;
|
|
669
687
|
};
|
|
670
688
|
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
689
|
+
const setContext = (
|
|
690
|
+
s: Session,
|
|
691
|
+
value: Record<string, unknown>,
|
|
692
|
+
): Promise<unknown> => {
|
|
693
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
694
|
+
throw new Error("statewire: context must be a plain object");
|
|
695
|
+
}
|
|
696
|
+
s.context = value;
|
|
697
|
+
return s.send({ context: value });
|
|
698
|
+
};
|
|
699
|
+
|
|
700
|
+
export const useStatewireSession = (
|
|
701
|
+
options: StatewireSessionStoreOptions,
|
|
702
|
+
emit: (event: unknown) => void,
|
|
703
|
+
): Session => {
|
|
704
|
+
const [s] = useState(() => createSession(options));
|
|
705
|
+
useEffect(() => {
|
|
706
|
+
s.emitEvent = emit;
|
|
707
|
+
return () => {
|
|
708
|
+
s.emitEvent = () => {};
|
|
709
|
+
};
|
|
710
|
+
}, [s, emit]);
|
|
711
|
+
useEffect(() => {
|
|
712
|
+
start(s);
|
|
713
|
+
return () => stop(s);
|
|
714
|
+
}, [s]);
|
|
715
|
+
return s;
|
|
679
716
|
};
|
|
717
|
+
|
|
718
|
+
export { setContext, setTransport };
|
|
@@ -9,7 +9,7 @@ export type StatewireSessionStore = {
|
|
|
9
9
|
};
|
|
10
10
|
|
|
11
11
|
export type StatewireSessionStoreOptions = {
|
|
12
|
-
sessionStore?: StatewireSessionStore;
|
|
12
|
+
sessionStore?: StatewireSessionStore | undefined;
|
|
13
13
|
};
|
|
14
14
|
|
|
15
15
|
export const createMemorySessionStore = (): StatewireSessionStore => {
|
package/src/client/timing.ts
CHANGED
|
@@ -6,3 +6,9 @@ export const timing = {
|
|
|
6
6
|
|
|
7
7
|
export const backoff = (attempt: number) =>
|
|
8
8
|
Math.min(timing.backoffBaseMs * 2 ** (attempt - 1), timing.backoffMaxMs);
|
|
9
|
+
|
|
10
|
+
export const retryDelay = (error: unknown, attempt: number): number => {
|
|
11
|
+
const retryAfterMs = (error as { retryAfterMs?: unknown } | undefined)
|
|
12
|
+
?.retryAfterMs;
|
|
13
|
+
return typeof retryAfterMs === "number" ? retryAfterMs : backoff(attempt);
|
|
14
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { StatewireHostInternal } from "../host-internal";
|
|
2
|
+
|
|
3
|
+
export const CLIENT_QUERY_PARAM = "client";
|
|
4
|
+
|
|
5
|
+
export const clientIdFromUrl = (url: string | null | undefined): string => {
|
|
6
|
+
if (!url) {
|
|
7
|
+
throw new Error("statewire: cannot resolve client id: missing url");
|
|
8
|
+
}
|
|
9
|
+
let parsed: URL;
|
|
10
|
+
try {
|
|
11
|
+
parsed = new URL(url, "http://statewire");
|
|
12
|
+
} catch {
|
|
13
|
+
throw new Error(
|
|
14
|
+
`statewire: cannot resolve client id: unparseable url "${url}"`,
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
const clientId = parsed.searchParams.get(CLIENT_QUERY_PARAM);
|
|
18
|
+
if (!clientId) {
|
|
19
|
+
throw new Error(
|
|
20
|
+
`statewire: url is missing the "${CLIENT_QUERY_PARAM}" query parameter`,
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
return clientId;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const textFrameOrClose = (
|
|
27
|
+
socket: Pick<StatewireHostInternal.Socket, "close">,
|
|
28
|
+
data: unknown,
|
|
29
|
+
): string | null => {
|
|
30
|
+
if (typeof data === "string") return data;
|
|
31
|
+
socket.close(1008, "text frames only");
|
|
32
|
+
return null;
|
|
33
|
+
};
|