statewire 0.0.1 → 0.0.2
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 +8 -25
- package/dist/client/session.d.ts.map +1 -1
- package/dist/client/session.js +56 -36
- 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 +18 -35
- package/dist/host/host.d.ts.map +1 -1
- package/dist/host/host.js +81 -53
- 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 +459 -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 +44 -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 +94 -10
- package/dist/statewire.d.ts.map +1 -1
- package/dist/statewire.js +23 -35
- 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 +5 -24
- package/dist/transports/remote.js.map +1 -1
- package/dist/transports/sse.d.ts +10 -20
- package/dist/transports/sse.d.ts.map +1 -1
- package/dist/transports/sse.js +42 -41
- package/dist/transports/sse.js.map +1 -1
- package/dist/transports/testing.d.ts +4 -3
- 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 +57 -60
- package/dist/transports/ws.js.map +1 -1
- package/dist/types.d.ts +53 -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 +87 -70
- 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 +107 -106
- package/src/host/socket.ts +542 -0
- package/src/host-internal.ts +28 -0
- package/src/host.ts +61 -0
- package/src/index.ts +2 -62
- package/src/statewire.ts +154 -61
- package/src/transports/fake-sse.ts +51 -0
- package/src/transports/fixtures.ts +60 -0
- package/src/transports/remote.ts +12 -54
- package/src/transports/sse.ts +68 -81
- package/src/transports/testing.ts +27 -59
- package/src/transports/ws.ts +76 -92
- package/src/types.ts +50 -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.
|
|
@@ -47,7 +48,7 @@ type Pending = {
|
|
|
47
48
|
|
|
48
49
|
type Outcome =
|
|
49
50
|
| { ok: true; payload: unknown }
|
|
50
|
-
| { ok: false; failure:
|
|
51
|
+
| { ok: false; failure: StatewireClient.SendFailure };
|
|
51
52
|
|
|
52
53
|
const isResolved = (state: PendingState) =>
|
|
53
54
|
state.at === "settled-live" || state.at === "done";
|
|
@@ -55,6 +56,15 @@ const isResolved = (state: PendingState) =>
|
|
|
55
56
|
const isStateSettled = (state: PendingState) =>
|
|
56
57
|
state.at === "awaiting-result" || state.at === "done";
|
|
57
58
|
|
|
59
|
+
// The pump admits an entry as trying before sending; only the pump itself
|
|
60
|
+
// requeues, and a done entry is never still pending.
|
|
61
|
+
const inFlight = (state: PendingState) => {
|
|
62
|
+
if (state.at === "queued" || state.at === "done") {
|
|
63
|
+
throw new Error(`statewire: ${state.at} entry observed after a send`);
|
|
64
|
+
}
|
|
65
|
+
return state;
|
|
66
|
+
};
|
|
67
|
+
|
|
58
68
|
const legalTransitions: Record<PendingState["at"], PendingState["at"][]> = {
|
|
59
69
|
queued: ["queued", "trying", "awaiting-result", "settled-live", "done"],
|
|
60
70
|
// trying -> queued: an answered unknown-client proves the seq was never admitted.
|
|
@@ -122,10 +132,11 @@ const carried = (
|
|
|
122
132
|
? { posted: state.at === "posted", failedAttempts: state.failedAttempts }
|
|
123
133
|
: { posted: state.posted, failedAttempts: state.failedAttempts };
|
|
124
134
|
|
|
125
|
-
const newClientId = () =>
|
|
126
|
-
`c-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
135
|
+
const newClientId = () => `c-${randomId()}`;
|
|
127
136
|
|
|
128
|
-
const rejectedSend = (
|
|
137
|
+
const rejectedSend = (
|
|
138
|
+
failure: StatewireClient.SendFailure,
|
|
139
|
+
): Promise<unknown> => {
|
|
129
140
|
const result = Promise.reject<unknown>(new StatewireSendError(failure));
|
|
130
141
|
result.catch(() => {});
|
|
131
142
|
return result;
|
|
@@ -135,6 +146,7 @@ type AttachState = "detached" | "attached" | "finished";
|
|
|
135
146
|
|
|
136
147
|
type Session = {
|
|
137
148
|
record: StatewireSessionRecord;
|
|
149
|
+
ctx: StatewireRemoteProps;
|
|
138
150
|
recordPersisted: boolean;
|
|
139
151
|
store: StatewireSessionStore;
|
|
140
152
|
lastAck: number;
|
|
@@ -142,7 +154,7 @@ type Session = {
|
|
|
142
154
|
canonical: unknown;
|
|
143
155
|
lastPublished: unknown;
|
|
144
156
|
changed: StatewirePath[];
|
|
145
|
-
source:
|
|
157
|
+
source: Statewire.StateSource;
|
|
146
158
|
attach: AttachState;
|
|
147
159
|
attachEpoch: number;
|
|
148
160
|
running: boolean;
|
|
@@ -156,29 +168,20 @@ type Session = {
|
|
|
156
168
|
commands: Record<string, (...params: unknown[]) => Promise<unknown>>;
|
|
157
169
|
};
|
|
158
170
|
|
|
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 => {
|
|
171
|
+
const createSession = (options: StatewireSessionStoreOptions): Session => {
|
|
177
172
|
const store = options.sessionStore ?? createMemorySessionStore();
|
|
178
173
|
const stored = store.load();
|
|
179
174
|
const record = stored ?? { clientId: newClientId(), nextSeq: 1 };
|
|
180
175
|
const s: Session = {
|
|
181
176
|
record,
|
|
177
|
+
ctx: {
|
|
178
|
+
get clientId() {
|
|
179
|
+
return s.record.clientId;
|
|
180
|
+
},
|
|
181
|
+
onSnapshot: (args) => handleSnapshot(s, args),
|
|
182
|
+
onOps: (args) => handleOps(s, args),
|
|
183
|
+
onFinish: (args) => handleFinish(s, args),
|
|
184
|
+
},
|
|
182
185
|
recordPersisted: stored !== null,
|
|
183
186
|
store,
|
|
184
187
|
lastAck: -1,
|
|
@@ -187,7 +190,7 @@ const createSession = (options: SessionOptions): Session => {
|
|
|
187
190
|
lastPublished: undefined,
|
|
188
191
|
changed: [],
|
|
189
192
|
source: {
|
|
190
|
-
|
|
193
|
+
snapshot: () => s.canonical,
|
|
191
194
|
takeChanged: () => {
|
|
192
195
|
const changed = s.changed;
|
|
193
196
|
s.changed = [];
|
|
@@ -207,7 +210,7 @@ const createSession = (options: SessionOptions): Session => {
|
|
|
207
210
|
notify: () => {},
|
|
208
211
|
emitEvent: () => {},
|
|
209
212
|
send: (command) => sendCommand(s, command),
|
|
210
|
-
commands:
|
|
213
|
+
commands: createMethodProxy((method, params) => s.send({ method, params })),
|
|
211
214
|
};
|
|
212
215
|
bindCommandsHost(s.commands, {
|
|
213
216
|
setListener: (listener) => {
|
|
@@ -231,12 +234,25 @@ const wake = (s: Session) => {
|
|
|
231
234
|
|
|
232
235
|
const waitForWake = (s: Session) => new Promise<void>((r) => s.waiters.push(r));
|
|
233
236
|
|
|
237
|
+
const sleepUntilWake = (s: Session, delayMs: number) =>
|
|
238
|
+
new Promise<void>((resolve) => {
|
|
239
|
+
const waiter = () => {
|
|
240
|
+
clearTimeout(timer);
|
|
241
|
+
resolve();
|
|
242
|
+
};
|
|
243
|
+
const timer = setTimeout(() => {
|
|
244
|
+
s.waiters = s.waiters.filter((w) => w !== waiter);
|
|
245
|
+
resolve();
|
|
246
|
+
}, delayMs);
|
|
247
|
+
s.waiters.push(waiter);
|
|
248
|
+
});
|
|
249
|
+
|
|
234
250
|
const settleAll = (
|
|
235
251
|
s: Session,
|
|
236
252
|
detach: "evicted" | "unmount" | "gone" | "superseded",
|
|
237
253
|
verdict?: {
|
|
238
254
|
entry: Pending;
|
|
239
|
-
failure:
|
|
255
|
+
failure: StatewireClient.SendFailure;
|
|
240
256
|
},
|
|
241
257
|
) => {
|
|
242
258
|
const cause = (
|
|
@@ -248,7 +264,7 @@ const settleAll = (
|
|
|
248
264
|
} as const
|
|
249
265
|
)[detach];
|
|
250
266
|
const batch = s.pending;
|
|
251
|
-
const detachFailure = (entry: Pending):
|
|
267
|
+
const detachFailure = (entry: Pending): StatewireClient.SendFailure => {
|
|
252
268
|
const state = entry.state;
|
|
253
269
|
if (state.at === "awaiting-result")
|
|
254
270
|
return { fate: "result-unavailable", cause };
|
|
@@ -271,7 +287,7 @@ const evict = (
|
|
|
271
287
|
s: Session,
|
|
272
288
|
verdict?: {
|
|
273
289
|
entry: Pending;
|
|
274
|
-
failure:
|
|
290
|
+
failure: StatewireClient.SendFailure;
|
|
275
291
|
},
|
|
276
292
|
) => {
|
|
277
293
|
settleAll(s, "evicted", verdict);
|
|
@@ -347,12 +363,16 @@ const applyResponse = (s: Session, response: StatewireResponse) => {
|
|
|
347
363
|
? {
|
|
348
364
|
fate: "invalid-params",
|
|
349
365
|
cause: "server",
|
|
350
|
-
message:
|
|
366
|
+
message:
|
|
367
|
+
response.message ??
|
|
368
|
+
protocolError("rejected response missing message"),
|
|
351
369
|
}
|
|
352
370
|
: {
|
|
353
371
|
fate: "rejected",
|
|
354
372
|
cause: "server",
|
|
355
|
-
message:
|
|
373
|
+
message:
|
|
374
|
+
response.message ??
|
|
375
|
+
protocolError("rejected response missing message"),
|
|
356
376
|
...(response.payload !== undefined && {
|
|
357
377
|
payload: response.payload,
|
|
358
378
|
}),
|
|
@@ -382,12 +402,9 @@ const ingestOps = (s: Session, ops: unknown[]) => {
|
|
|
382
402
|
return applied.events;
|
|
383
403
|
};
|
|
384
404
|
|
|
385
|
-
const publishIngested = (
|
|
386
|
-
s: Session,
|
|
387
|
-
events: { event: unknown; state: unknown }[],
|
|
388
|
-
) => {
|
|
405
|
+
const publishIngested = (s: Session, events: unknown[]) => {
|
|
389
406
|
publish(s);
|
|
390
|
-
for (const
|
|
407
|
+
for (const event of events) s.emitEvent(event);
|
|
391
408
|
wake(s);
|
|
392
409
|
};
|
|
393
410
|
|
|
@@ -459,7 +476,7 @@ const handleOps = (
|
|
|
459
476
|
};
|
|
460
477
|
|
|
461
478
|
const handleFinish = (s: Session, { reason }: { reason: string }) => {
|
|
462
|
-
if (reason === "
|
|
479
|
+
if (reason === "evicted" || reason === "error") return;
|
|
463
480
|
s.attach = "finished";
|
|
464
481
|
settleAll(s, reason === "superseded" ? "superseded" : "gone");
|
|
465
482
|
publish(s);
|
|
@@ -504,18 +521,12 @@ const pump = async (s: Session) => {
|
|
|
504
521
|
} catch (error) {
|
|
505
522
|
if (!s.running || isFinished(s)) return;
|
|
506
523
|
if (!s.pending.includes(entry)) continue;
|
|
507
|
-
const state = entry.state;
|
|
508
|
-
if (state.at === "queued" || state.at === "done") continue;
|
|
524
|
+
const state = inFlight(entry.state);
|
|
509
525
|
transition(s, entry, {
|
|
510
526
|
...state,
|
|
511
527
|
failedAttempts: state.failedAttempts + 1,
|
|
512
528
|
});
|
|
513
|
-
|
|
514
|
-
const delay =
|
|
515
|
-
typeof retryAfterMs === "number"
|
|
516
|
-
? retryAfterMs
|
|
517
|
-
: backoff(state.failedAttempts + 1);
|
|
518
|
-
await new Promise((r) => setTimeout(r, delay));
|
|
529
|
+
await sleepUntilWake(s, retryDelay(error, state.failedAttempts + 1));
|
|
519
530
|
continue;
|
|
520
531
|
}
|
|
521
532
|
if (!s.running) return;
|
|
@@ -541,7 +552,6 @@ const pump = async (s: Session) => {
|
|
|
541
552
|
publish(s);
|
|
542
553
|
}
|
|
543
554
|
} else if (result.type === "rejected") {
|
|
544
|
-
const { message, payload } = result;
|
|
545
555
|
transition(
|
|
546
556
|
s,
|
|
547
557
|
entry,
|
|
@@ -551,8 +561,7 @@ const pump = async (s: Session) => {
|
|
|
551
561
|
failure: {
|
|
552
562
|
fate: "rejected",
|
|
553
563
|
cause: "server",
|
|
554
|
-
message,
|
|
555
|
-
...(payload !== undefined && { payload }),
|
|
564
|
+
message: result.message,
|
|
556
565
|
},
|
|
557
566
|
},
|
|
558
567
|
);
|
|
@@ -589,16 +598,14 @@ const pump = async (s: Session) => {
|
|
|
589
598
|
);
|
|
590
599
|
// Every attempt answered means the server never saw it; a thrown
|
|
591
600
|
// attempt may have left the machine unanswered, so the verdict is "lost".
|
|
592
|
-
const state = entry.state;
|
|
593
|
-
const failure:
|
|
601
|
+
const state = inFlight(entry.state);
|
|
602
|
+
const failure: StatewireClient.SendFailure =
|
|
594
603
|
state.at === "awaiting-result"
|
|
595
604
|
? { fate: "result-unavailable", cause: "session-lost" }
|
|
596
|
-
:
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
cause: "session-lost",
|
|
601
|
-
};
|
|
605
|
+
: {
|
|
606
|
+
fate: state.failedAttempts > 0 ? "lost" : "aborted",
|
|
607
|
+
cause: "session-lost",
|
|
608
|
+
};
|
|
602
609
|
evict(s, { entry, failure });
|
|
603
610
|
s.transport?.connection.reconnect();
|
|
604
611
|
}
|
|
@@ -668,12 +675,22 @@ const sendCommand = (
|
|
|
668
675
|
return result;
|
|
669
676
|
};
|
|
670
677
|
|
|
671
|
-
export
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
678
|
+
export const useStatewireSession = (
|
|
679
|
+
options: StatewireSessionStoreOptions,
|
|
680
|
+
emit: (event: unknown) => void,
|
|
681
|
+
): Session => {
|
|
682
|
+
const [s] = useState(() => createSession(options));
|
|
683
|
+
useEffect(() => {
|
|
684
|
+
s.emitEvent = emit;
|
|
685
|
+
return () => {
|
|
686
|
+
s.emitEvent = () => {};
|
|
687
|
+
};
|
|
688
|
+
}, [s, emit]);
|
|
689
|
+
useEffect(() => {
|
|
690
|
+
start(s);
|
|
691
|
+
return () => stop(s);
|
|
692
|
+
}, [s]);
|
|
693
|
+
return s;
|
|
679
694
|
};
|
|
695
|
+
|
|
696
|
+
export { 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
|
+
};
|