statewire 0.18.2 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/connection.d.ts +8 -5
- package/dist/client/connection.d.ts.map +1 -1
- package/dist/client/connection.js +24 -13
- package/dist/client/connection.js.map +1 -1
- package/dist/client/session.d.ts +13 -15
- package/dist/client/session.d.ts.map +1 -1
- package/dist/client/session.js +291 -256
- package/dist/client/session.js.map +1 -1
- package/dist/client/timing.d.ts +2 -1
- package/dist/client/timing.d.ts.map +1 -1
- package/dist/client/timing.js +2 -1
- package/dist/client/timing.js.map +1 -1
- package/dist/host/engine.d.ts +2 -0
- package/dist/host/engine.d.ts.map +1 -1
- package/dist/host/engine.js +2 -0
- package/dist/host/engine.js.map +1 -1
- package/dist/host/host.d.ts +3 -1
- package/dist/host/host.d.ts.map +1 -1
- package/dist/host/host.js +38 -7
- package/dist/host/host.js.map +1 -1
- package/dist/host/http.d.ts.map +1 -1
- package/dist/host/http.js +8 -17
- package/dist/host/http.js.map +1 -1
- package/dist/host/socket.d.ts +47 -23
- package/dist/host/socket.d.ts.map +1 -1
- package/dist/host/socket.js +389 -245
- package/dist/host/socket.js.map +1 -1
- package/dist/host/synced.d.ts +9 -0
- package/dist/host/synced.d.ts.map +1 -0
- package/dist/host/synced.js +45 -0
- package/dist/host/synced.js.map +1 -0
- package/dist/host-internal.d.ts +20 -8
- package/dist/host-internal.d.ts.map +1 -1
- package/dist/host.d.ts +33 -9
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +2 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -1
- package/dist/statewire.d.ts +8 -10
- package/dist/statewire.d.ts.map +1 -1
- package/dist/statewire.js +0 -14
- package/dist/statewire.js.map +1 -1
- package/dist/storage.d.ts +2 -5
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +55 -41
- package/dist/storage.js.map +1 -1
- package/dist/transports/remote.d.ts.map +1 -1
- package/dist/transports/remote.js +1 -6
- package/dist/transports/remote.js.map +1 -1
- package/dist/transports/sse.d.ts +2 -5
- package/dist/transports/sse.d.ts.map +1 -1
- package/dist/transports/sse.js.map +1 -1
- package/dist/transports/testing.d.ts +7 -5
- package/dist/transports/testing.d.ts.map +1 -1
- package/dist/transports/testing.js +19 -19
- package/dist/transports/testing.js.map +1 -1
- package/dist/types.d.ts +3 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/wire/limits.d.ts +2 -2
- package/dist/wire/limits.js +2 -2
- package/dist/wire/limits.js.map +1 -1
- package/dist/wire/protocol.d.ts +6 -1
- package/dist/wire/protocol.d.ts.map +1 -1
- package/dist/wire/protocol.js +33 -12
- package/dist/wire/protocol.js.map +1 -1
- package/package.json +1 -1
- package/src/client/connection.ts +46 -33
- package/src/client/session.ts +434 -365
- package/src/client/timing.ts +2 -0
- package/src/host/engine.ts +3 -1
- package/src/host/host.ts +50 -3
- package/src/host/http.ts +22 -23
- package/src/host/socket.ts +550 -321
- package/src/host/synced.ts +60 -0
- package/src/host-internal.ts +21 -3
- package/src/host.ts +40 -8
- package/src/index.ts +1 -0
- package/src/statewire.ts +8 -30
- package/src/storage.ts +96 -79
- package/src/transports/remote.ts +0 -7
- package/src/transports/sse.ts +8 -3
- package/src/transports/testing.ts +34 -19
- package/src/types.ts +3 -5
- package/src/wire/limits.ts +1 -1
- package/src/wire/protocol.ts +40 -11
package/dist/host.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { StatewireReject } from "./host/engine.js";
|
|
|
2
2
|
import { useStatewireWork } from "./host/work.js";
|
|
3
3
|
import { append } from "./host/draft.js";
|
|
4
4
|
import { getStateHost, useStatewireCommands, useStatewireState } from "./host/host.js";
|
|
5
|
+
import { syncedDocument } from "./host/synced.js";
|
|
5
6
|
import { Statewire } from "./types.js";
|
|
6
7
|
//#region src/host.d.ts
|
|
7
8
|
declare namespace StatewireHost {
|
|
@@ -16,19 +17,35 @@ declare namespace StatewireHost {
|
|
|
16
17
|
/** The committed plain state — immutably replaced per op, so a captured value is a stable snapshot. */
|
|
17
18
|
snapshot: () => TState;
|
|
18
19
|
};
|
|
20
|
+
type Document = {
|
|
21
|
+
protocol: string;
|
|
22
|
+
key: readonly string[] | null;
|
|
23
|
+
main: boolean;
|
|
24
|
+
value: unknown;
|
|
25
|
+
};
|
|
19
26
|
type StateHost = Statewire.StateSource & {
|
|
20
27
|
/** Synchronous, per locally-recorded op; the host owns batching. */
|
|
21
28
|
setOnOp: (listener: ((op: Statewire.Op) => void) | null) => void;
|
|
29
|
+
/** Mount or replace the shared document `(protocol, key)`; keyed documents reach an attach only once it `include`s them. */
|
|
30
|
+
mount: (protocol: string, value: unknown, options?: {
|
|
31
|
+
key?: readonly string[];
|
|
32
|
+
main?: boolean;
|
|
33
|
+
}) => void;
|
|
34
|
+
unmount: (protocol: string, key?: readonly string[]) => void;
|
|
35
|
+
documents: () => ReadonlyMap<string, Document>;
|
|
36
|
+
setOnDocuments: (listener: (() => void) | null) => void;
|
|
22
37
|
};
|
|
23
38
|
/**
|
|
24
39
|
* Per-invocation handle bound as `this` of a command handler: `caller` is
|
|
25
|
-
* the submitting client, `
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
40
|
+
* the submitting client, `attach` the admitting attach's document view
|
|
41
|
+
* (in-process dispatch has none — reading it throws), `applied()` states the
|
|
42
|
+
* broadcast state now contains the command's effects — the client drops its
|
|
43
|
+
* optimistic overlay — before the handler settles (idempotent, a no-op after
|
|
44
|
+
* settle). The command settles when the handler's returned promise does.
|
|
29
45
|
*/
|
|
30
46
|
type Ctx = {
|
|
31
47
|
readonly caller: Caller;
|
|
48
|
+
readonly attach: Attach;
|
|
32
49
|
readonly signal: AbortSignal;
|
|
33
50
|
readonly applied: () => void;
|
|
34
51
|
};
|
|
@@ -37,16 +54,23 @@ declare namespace StatewireHost {
|
|
|
37
54
|
* across a long run: `clientId` is the client's `Statewire-Client-Id`
|
|
38
55
|
* (in-process dispatch has none — reading it throws), `auth` is the value
|
|
39
56
|
* `authorize` returned for the attach that admitted the command (undefined
|
|
40
|
-
* for a void `authorize` and for in-process dispatch), `
|
|
41
|
-
*
|
|
42
|
-
* `isConnected` reports whether the client has an active stream attachment.
|
|
57
|
+
* for a void `authorize` and for in-process dispatch), `isConnected`
|
|
58
|
+
* reports whether the client has an active stream attachment.
|
|
43
59
|
*/
|
|
44
60
|
type Caller = {
|
|
45
61
|
readonly clientId: string;
|
|
46
62
|
readonly auth: unknown;
|
|
47
|
-
readonly context: Record<string, unknown> | null;
|
|
48
63
|
readonly isConnected: boolean;
|
|
49
64
|
};
|
|
65
|
+
/** The admitting attach's view: its ephemeral documents plus the shared documents included for it. */
|
|
66
|
+
type Attach = {
|
|
67
|
+
document: (protocol: string, key?: readonly string[]) => unknown;
|
|
68
|
+
/** `protocol` must be ephemeral; the encoded value is bounded by `MAX_EPHEMERAL_DOCUMENT_BYTES`. */
|
|
69
|
+
mount: (protocol: string, value: unknown, key?: readonly string[], main?: boolean) => void;
|
|
70
|
+
unmount: (protocol: string, key?: readonly string[]) => void;
|
|
71
|
+
include: (protocol: string, key: readonly string[]) => void;
|
|
72
|
+
exclude: (protocol: string, key: readonly string[]) => void;
|
|
73
|
+
};
|
|
50
74
|
type HandledCommands<THandlers extends Statewire.Commands> = { [K in keyof THandlers]: (...args: Parameters<THandlers[K]>) => Awaited<ReturnType<THandlers[K]>>; };
|
|
51
75
|
type Fin = {
|
|
52
76
|
reason: "reconnect" | "error" | "protocol-error" | "forbidden" | "unauthorized" | "gone" | "idle" | (string & {});
|
|
@@ -59,5 +83,5 @@ declare namespace StatewireHost {
|
|
|
59
83
|
};
|
|
60
84
|
}
|
|
61
85
|
//#endregion
|
|
62
|
-
export { StatewireHost, StatewireReject, append, getStateHost, useStatewireCommands, useStatewireState, useStatewireWork };
|
|
86
|
+
export { StatewireHost, StatewireReject, append, getStateHost, syncedDocument, useStatewireCommands, useStatewireState, useStatewireWork };
|
|
63
87
|
//# sourceMappingURL=host.d.ts.map
|
package/dist/host.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"host.d.ts","names":[],"sources":["../src/host.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"host.d.ts","names":[],"sources":["../src/host.ts"],"mappings":";;;;;;;kBAYiB;OACH;aACD;IACT,QAAQ,GAAG,MAAM,QAAQ,OAAO,QAAQ;IACxC,WAAW;;OAGD,SAAS;;IAEnB,QAAQ,cAAc,UAAU;;IAEhC,gBAAgB;;OAGN;IACV;IACA;IACA;IACA;;OAGU,YAAY,UAAU;;IAEhC,UAAU,YAAY,IAAI,UAAU;;IAEpC,QACE,kBACA,gBACA;MAAY;MAAyB;;IAEvC,UAAU,kBAAkB;IAC5B,iBAAiB,oBAAoB;IACrC,iBAAiB;;;;;;;;;;OAWP;aACD,QAAQ;aACR,QAAQ;aACR,QAAQ;aACR;;;;;;;;;;OAWC;aACD;aACA;aACA;;;OAIC;IACV,WAAW,kBAAkB;;IAE7B,QACE,kBACA,gBACA,yBACA;IAEF,UAAU,kBAAkB;IAC5B,UAAU,kBAAkB;IAC5B,UAAU,kBAAkB;;OAGlB,gBAAgB,kBAAkB,UAAU,eACrD,WAAW,gBACP,MAAM,WAAW,UAAU,QAC3B,QAAQ,WAAW,UAAU;OAGxB;IACV;IASA;IACA;IACA;IACA;IACA;IACA"}
|
package/dist/host.js
CHANGED
|
@@ -2,4 +2,5 @@ import { StatewireReject } from "./host/engine.js";
|
|
|
2
2
|
import { append } from "./host/draft.js";
|
|
3
3
|
import { getStateHost, useStatewireCommands, useStatewireState } from "./host/host.js";
|
|
4
4
|
import { useStatewireWork } from "./host/work.js";
|
|
5
|
-
|
|
5
|
+
import { syncedDocument } from "./host/synced.js";
|
|
6
|
+
export { StatewireReject, append, getStateHost, syncedDocument, useStatewireCommands, useStatewireState, useStatewireWork };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { StatewireDocuments } from "./wire/documents.js";
|
|
2
|
-
import { StatewireProtocol } from "./wire/protocol.js";
|
|
2
|
+
import { DEFAULT_PROTOCOL, StatewireProtocol } from "./wire/protocol.js";
|
|
3
3
|
import { StatewireHttp } from "./transports/sse.js";
|
|
4
4
|
import { StatewireClient, StatewireResource, useStatewire } from "./statewire.js";
|
|
5
5
|
import { Statewire, StatewireSendError } from "./types.js";
|
|
6
6
|
import { StatewireDocument, decodeDocument, encodeDocument } from "./document.js";
|
|
7
7
|
import { StatewireWebsocket } from "./transports/ws.js";
|
|
8
|
-
export { type Statewire, StatewireClient, type StatewireDocument, type StatewireDocuments, StatewireHttp, type StatewireProtocol, StatewireResource, StatewireSendError, StatewireWebsocket, decodeDocument, encodeDocument, useStatewire };
|
|
8
|
+
export { DEFAULT_PROTOCOL, type Statewire, StatewireClient, type StatewireDocument, type StatewireDocuments, StatewireHttp, type StatewireProtocol, StatewireResource, StatewireSendError, StatewireWebsocket, decodeDocument, encodeDocument, useStatewire };
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { decodeDocument, encodeDocument } from "./document.js";
|
|
2
|
+
import { DEFAULT_PROTOCOL } from "./wire/protocol.js";
|
|
2
3
|
import { StatewireSendError } from "./types.js";
|
|
3
4
|
import { StatewireClient, StatewireResource, useStatewire } from "./statewire.js";
|
|
4
5
|
import { StatewireHttp } from "./transports/sse.js";
|
|
5
6
|
import { StatewireWebsocket } from "./transports/ws.js";
|
|
6
|
-
export { StatewireClient, StatewireHttp, StatewireResource, StatewireSendError, StatewireWebsocket, decodeDocument, encodeDocument, useStatewire };
|
|
7
|
+
export { DEFAULT_PROTOCOL, StatewireClient, StatewireHttp, StatewireResource, StatewireSendError, StatewireWebsocket, decodeDocument, encodeDocument, useStatewire };
|
package/dist/statewire.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { StatewireDocuments } from "./wire/documents.js";
|
|
2
|
-
import { StatewireProtocol } from "./wire/protocol.js";
|
|
3
2
|
import { StatewireHost } from "./host.js";
|
|
3
|
+
import { StatewireProtocol } from "./wire/protocol.js";
|
|
4
4
|
import { TERMINAL } from "./client/connection.js";
|
|
5
5
|
import { Statewire } from "./types.js";
|
|
6
6
|
import { ResourceElement } from "@assistant-ui/tap";
|
|
@@ -17,9 +17,7 @@ declare class StatewireClient<TState, TCommands extends Statewire.Commands = Sta
|
|
|
17
17
|
get documents(): StatewireDocuments.Source | undefined;
|
|
18
18
|
get selection(): StatewireProtocol.Selection | undefined;
|
|
19
19
|
invoke(protocol: string, method: string, params: readonly unknown[]): Promise<unknown>;
|
|
20
|
-
updateContext(ops: readonly unknown[]): void;
|
|
21
20
|
get isBusy(): boolean;
|
|
22
|
-
setContext(value: Record<string, unknown>): void;
|
|
23
21
|
get failed(): readonly StatewireClient.FailedCommand[];
|
|
24
22
|
retryFailed(entry: StatewireClient.FailedCommand): Promise<unknown>;
|
|
25
23
|
discardFailed(entry: StatewireClient.FailedCommand): void;
|
|
@@ -59,15 +57,12 @@ declare namespace StatewireClient {
|
|
|
59
57
|
documents: StatewireDocuments.Source | undefined;
|
|
60
58
|
selection: StatewireProtocol.Selection | undefined;
|
|
61
59
|
invoke: (protocol: string, method: string, params: readonly unknown[]) => Promise<unknown>;
|
|
62
|
-
updateContext: (ops: readonly unknown[]) => void;
|
|
63
60
|
/** Identity-stable across renders; reads the current canonical state without subscribing the caller. */
|
|
64
61
|
getState: () => TState;
|
|
65
62
|
connection: Connection;
|
|
66
63
|
commands: Statewire.CommandsProxy<TCommands>;
|
|
67
64
|
/** Negation of the server's broadcast idle level (a transport without idle is always busy). */
|
|
68
65
|
isBusy: boolean;
|
|
69
|
-
/** Replace this client's ambient context on the server; throws on transports without a client context channel. */
|
|
70
|
-
setContext: (value: Record<string, unknown>) => void;
|
|
71
66
|
/** Non-accepted sends retained in the session record; reload with it. */
|
|
72
67
|
failed: readonly FailedCommand[];
|
|
73
68
|
/** Re-enqueues the entry's command on fresh clocks and removes the entry. */
|
|
@@ -182,7 +177,6 @@ declare namespace StatewireClient {
|
|
|
182
177
|
/** A command as persisted while un-durable: its lane seq plus the wire statement. */
|
|
183
178
|
type SessionCommand = {
|
|
184
179
|
seq: number;
|
|
185
|
-
protocol: string;
|
|
186
180
|
produced?: {
|
|
187
181
|
wire: StatewireProtocol.Range;
|
|
188
182
|
protocols: readonly StatewireProtocol.Offer[] | "*";
|
|
@@ -191,9 +185,8 @@ declare namespace StatewireClient {
|
|
|
191
185
|
params: unknown[];
|
|
192
186
|
meta?: unknown;
|
|
193
187
|
};
|
|
194
|
-
/**
|
|
195
|
-
type
|
|
196
|
-
clientId: string;
|
|
188
|
+
/** A durable lane: its cmd clock and the commands the server has not marked durable. */
|
|
189
|
+
type SessionLane = {
|
|
197
190
|
nextSeq: number;
|
|
198
191
|
commands: readonly SessionCommand[];
|
|
199
192
|
/** Terminal verdicts parked for the sharer that minted the seq (shared storage only). */
|
|
@@ -201,6 +194,11 @@ declare namespace StatewireClient {
|
|
|
201
194
|
/** Non-accepted sends retained for the user; survive record rebases and resets. */
|
|
202
195
|
failed?: readonly FailedCommand[];
|
|
203
196
|
};
|
|
197
|
+
/** The persisted session: the client identity plus one lane per durable protocol. */
|
|
198
|
+
type SessionRecord = {
|
|
199
|
+
clientId: string;
|
|
200
|
+
lanes: Readonly<Record<string, SessionLane>>;
|
|
201
|
+
};
|
|
204
202
|
/** A terminal verdict one sharer's connection carried, parked for the sharer awaiting the seq. */
|
|
205
203
|
type SessionVerdict = {
|
|
206
204
|
seq: number;
|
package/dist/statewire.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"statewire.d.ts","names":[],"sources":["../src/statewire.ts"],"mappings":";;;;;;;cAiDa,eACX,QACA,kBAAkB,UAAU,WAAW,UAAU,UAEjD,SAAS,gBAAgB,QAAQ,QAAQ,eACxC,gBAAgB,QAAQ,QAAQ;
|
|
1
|
+
{"version":3,"file":"statewire.d.ts","names":[],"sources":["../src/statewire.ts"],"mappings":";;;;;;;cAiDa,eACX,QACA,kBAAkB,UAAU,WAAW,UAAU,UAEjD,SAAS,gBAAgB,QAAQ,QAAQ,eACxC,gBAAgB,QAAQ,QAAQ;cAwFtB,oBACX,QACA,kBAAkB,UAAU,WAAW,UAAU,UAEjD,SAAS,gBAAgB,QAAQ,QAAQ,eACxC,gBAAgB,gBAAgB,QAAQ,QAAQ;cAGtC,gBACX,QACA,kBAAkB,UAAU,WAAW,UAAU;mBAEhC;UACT;EAEI,YAAA,SAAS,gBAAgB,QAAQ,QAAQ;MAKjD,SAAS;MAIT,cAAc,gBAAgB;MAI9B,YAAY,UAAU,cAAc;MAIpC,aAAa,mBAAmB;MAGhC,aAAa,kBAAkB;EAGnC,OACE,kBACA,gBACA,6BACC;MAGC;MAIA,mBAAmB,gBAAgB;EAIvC,YAAY,OAAO,gBAAgB,gBAAgB;EAInD,cAAc,OAAO,gBAAgB;EAIrC,UAAU;EAIV;;kBAOe;OACH,QACV,QACA,kBAAkB,UAAU,WAAW,UAAU;IAEjD;IACA,cAAc,SAAS;IACvB,cAAc,kBAAkB;IAChC,gCAAgC,kBAAkB;IAClD,WAAW,gBAAgB,UAAU,QAAQ;;IAE7C;MAEM,UAAU,gBAAgB;MAC1B,YAAY,gBAAgB,mBAAmB;;;IAIrD,eAAe;IACf,aAAa,WAAW,YAAY,SAAS;IAC7C,cAAc,YAAY,YAAY;IACtC,iBACE,OAAO,YAAY,SACnB;MAAO,SAAS,UAAU;;IAE5B,mBACE,QAAQ,UAAU,cAAc,YAChC;MAAO,OAAO,YAAY;;IAE5B,sBAAsB,YAAY;IAClC,WAAW,OAAO;;IAElB,kBACE,OAAO,YAAY,SACnB,OAAO;;OAIC,QACV,QACA,kBAAkB,UAAU,WAAW,UAAU;;IAGjD,OAAO;IACP,WAAW,mBAAmB;IAC9B,WAAW,kBAAkB;IAC7B,SACE,kBACA,gBACA,+BACG;;IAEL,gBAAgB;IAChB,YAAY;IACZ,UAAU,UAAU,cAAc;;IAElC;;IAEA,iBAAiB;;IAEjB,cAAc,OAAO,kBAAkB;;IAEvC,gBAAgB,OAAO;;;OAIb;IACN;IAAmB;IAA6B;;IAChD;IAAsB;;IACtB;IAAqB;;IAErB;IACA;IACA;;IAEA;IACA;IACA;IACA;IACA;;IAGA;IACA;IACA;IAMA;IACA;;OAEM,aAAa;IACvB,QAAQ;IACR;;OAEU,mBAAmB;;;;;OAMnB,WACV,QACA,kBAAkB,UAAU,WAAW,UAAU,aAEjD,OAAO,QACP,SAAS,UAAU,QAAQ,YAC3B,KAAK;OAGK;IACV;IACA;IACA;;;;;;;OAQU,YAAY,WAAW,OAAO;;;;;;;;;;;;;OAc9B;IAEN;IACA;IACA;IACA;IACA;;IAEA;IAAgB;IAAiB;;IACjC;IAAiB;;IACjB;IAAiB;;IAEjB;IACA;IACA;;IAGA;IACA;;;OAIM,iBACV,SAAS;IACP;IACA;IACA;IACA;IACA;;;OAKQ;aACD;aACA,WAAW;aACX;aACA;aACA;aACA,SAAS;aACT;aACA;;;OAIC;IACV;IACA;MACE,MAAM,kBAAkB;MACxB,oBAAoB,kBAAkB;;IAExC;IACA;IACA;;;OAIU;IACV;IACA,mBAAmB;;IAEnB,oBAAoB;;IAEpB,kBAAkB;;;OAIR;IACV;IACA,OAAO,SAAS,eAAe;;;OAIrB;IACV;IACA,cAAc;IACd;IACA;IACA;IACA;;;OAIU;;IAEV,QAAQ;;IAER,qBAAqB;IACrB,YAAY;IACZ,OAAO,QAAQ;;IAEf,WACE,QAAQ,eACR,SAAS,YAAY,gCAClB;;IAEL,QAAQ,GAAG,UAAU,IAAI,QAAQ,OAAO,QAAQ;;IAEhD,aAAa;;;OAIH,kBAAkB;IAAU,MAAM,cAAc;;OAEhD,gBACR,gBACE;IAAO;QAAyB,cAAc,QAAQ;OAEhD,cAAc,kBAAkB;IAC1C,eAAe;IACf;;IAEA;IACA,UAAU;IACV,eAAe,WAAW;IAC1B,gBAAgB;IAChB,YAAY,YAAY;IACxB,WAAW,OAAO;;IAElB;;OAGU,mBAAmB,kBAAkB;IAC/C,eAAe;IACf;;IAEA;IACA,UAAU;IACV;IACA,QACI,gCACO,yBAAyB,QAAQ;IAC5C,iBAAiB,aAAa,kCAAkC;IAChE,gBAAgB;IAChB,YAAY,YAAY;IACxB,WAAW,OAAO;;IAElB"}
|
package/dist/statewire.js
CHANGED
|
@@ -41,18 +41,10 @@ const useStatewire = (options) => {
|
|
|
41
41
|
if (!value.invoke) throw new Error("statewire: transport does not support protocol commands");
|
|
42
42
|
return value.invoke(protocol_0, method, params);
|
|
43
43
|
},
|
|
44
|
-
updateContext: (ops) => {
|
|
45
|
-
if (!value.updateContext) throw new Error("statewire: transport does not support context documents");
|
|
46
|
-
value.updateContext(ops);
|
|
47
|
-
},
|
|
48
44
|
getState: core.getState,
|
|
49
45
|
connection,
|
|
50
46
|
commands: core.commands,
|
|
51
47
|
isBusy,
|
|
52
|
-
setContext: (contextValue) => {
|
|
53
|
-
if (!value.setContext) throw new Error("statewire: this transport has no client context channel");
|
|
54
|
-
value.setContext(contextValue);
|
|
55
|
-
},
|
|
56
48
|
failed,
|
|
57
49
|
retryFailed: (entry) => failures().retry(entry),
|
|
58
50
|
discardFailed: (entry_0) => failures().discard(entry_0)
|
|
@@ -92,15 +84,9 @@ var StatewireClient = class {
|
|
|
92
84
|
invoke(protocol, method, params) {
|
|
93
85
|
return this.root.getValue().invoke(protocol, method, params);
|
|
94
86
|
}
|
|
95
|
-
updateContext(ops) {
|
|
96
|
-
this.root.getValue().updateContext(ops);
|
|
97
|
-
}
|
|
98
87
|
get isBusy() {
|
|
99
88
|
return this.root.getValue().isBusy;
|
|
100
89
|
}
|
|
101
|
-
setContext(value) {
|
|
102
|
-
this.root.getValue().setContext(value);
|
|
103
|
-
}
|
|
104
90
|
get failed() {
|
|
105
91
|
return this.root.getValue().failed;
|
|
106
92
|
}
|
package/dist/statewire.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"statewire.js","names":["StatewireDocuments","StatewireProtocol","StatewireHost","useMemo","useRef","useState","useSyncExternalStore","createTapRoot","flushTapSync","resource","useContextProvider","useResource","useResources","withKey","ResourceElement","CONNECTED_CONNECTION","useStatewireCore","Statewire","TERMINAL","DocumentStorageContext","ProtocolContext","SessionStorageContext","useSessionTransport","transport","session","documents","protocol","StatewireClient","SessionStorage","Storage","Options","SessionTransport","useStatewire","Commands","options","TState","TCommands","Helpers","latest","current","storage","undefined","wireVersion","applicationProtocols","keyPrefixes","value","core","subscribe","listener","state","getState","isBusy","getBusy","failed","getFailed","connection","failures","NonNullable","Error","selection","invoke","method","params","updateContext","ops","commands","CommandsProxy","setContext","contextValue","Record","retryFailed","entry","FailedCommand","retry","discardFailed","discard","StatewireResource","root","disposed","constructor","ClientRoot","getValue","Connection","Source","Selection","Promise","dispose","unmount","Readonly","Range","Offer","initialState","optimistic","Optimistic","materialize","Materialize","onStateChange","ctx","changes","Changes","onCommandChange","update","CommandUpdate","onConnectionChange","onError","error","ConnectionError","failureCovered","ConnectionState","status","reason","degraded","attempt","cause","code","nextRetryAt","lastError","message","payload","reconnect","ConnectionStatus","draft","command","Command","OptimisticContext","ts","seq","clientId","SendFailure","fate","predecessorLost","OnSessionLost","pending","ReadonlyArray","produced","SessionCommand","meta","failure","wire","protocols","SessionRecord","nextSeq","verdicts","SessionVerdict","type","ready","flush","load","save","record","migrate","lock","fn","T","fin","Fin","HeadersOption","HeadersInit","staleAuth","HttpOptions","sessionStore","url","isNew","headers","fetch","globalThis","onSessionLost","onChange","onDone","WebsocketOptions","query","webSocket","WebSocket"],"sources":["../src/statewire.ts"],"sourcesContent":["import type { StatewireDocuments } from \"./wire/documents\";\nimport type { StatewireProtocol } from \"./wire/protocol\";\nimport type { StatewireHost } from \"./host\";\nimport { useMemo, useRef, useState, useSyncExternalStore } from \"react\";\nimport {\n createTapRoot,\n flushTapSync,\n resource,\n useContextProvider,\n useResource,\n useResources,\n withKey,\n} from \"@assistant-ui/tap\";\nimport type { ResourceElement } from \"@assistant-ui/tap\";\nimport { CONNECTED_CONNECTION, useStatewireCore } from \"./client/core\";\nimport type { Statewire } from \"./types\";\nimport type { TERMINAL } from \"./client/connection\";\nimport {\n DocumentStorageContext,\n ProtocolContext,\n SessionStorageContext,\n} from \"./client/sessionStore\";\n\nconst useSessionTransport = ({\n transport,\n session,\n documents,\n protocol,\n}: {\n transport: ResourceElement<Statewire<any, any>>;\n session: StatewireClient.SessionStorage | undefined;\n documents: StatewireDocuments.Storage | undefined;\n protocol: StatewireProtocol.Options;\n}): Statewire<unknown> =>\n useContextProvider(SessionStorageContext, session, () =>\n // oxlint-disable-next-line react-hooks/rules-of-hooks\n useContextProvider(DocumentStorageContext, documents, () =>\n // oxlint-disable-next-line react-hooks/rules-of-hooks\n useContextProvider(ProtocolContext, protocol, () =>\n // oxlint-disable-next-line react-hooks/rules-of-hooks\n useResource(transport),\n ),\n ),\n );\n\n// The transport owns the session; it reads the storage off the context this\n// wrapper's own fiber provides, so plain-React callers get a provider too.\nconst SessionTransport = resource(useSessionTransport);\n\nexport const useStatewire = <\n TState,\n TCommands extends Statewire.Commands = Statewire.Commands,\n>(\n options: StatewireClient.Options<TState, TCommands>,\n): StatewireClient.Helpers<TState, TCommands> => {\n const latest = useRef(options);\n latest.current = options;\n\n const session = useResources(\n options.storage?.session !== undefined\n ? [withKey(\"session\", options.storage.session)]\n : [],\n )[0];\n const documents = useResources(\n options.storage?.documents === undefined\n ? []\n : [withKey(\"documents\", options.storage.documents)],\n )[0];\n const protocol: StatewireProtocol.Options = {\n ...(options.protocol !== undefined && { protocol: options.protocol }),\n ...(options.wireVersion !== undefined && {\n wireVersion: options.wireVersion,\n }),\n ...(options.applicationProtocols !== undefined && {\n applicationProtocols: options.applicationProtocols,\n }),\n ...(options.keyPrefixes !== undefined && {\n keyPrefixes: options.keyPrefixes,\n }),\n };\n const value = useResource(\n SessionTransport({\n transport: options.transport,\n session,\n documents,\n protocol,\n }),\n );\n const core = useStatewireCore(() => latest.current, value);\n const [subscribe] = useState(\n () => (listener: () => void) =>\n core.subscribe(() => flushTapSync(listener)),\n );\n\n const state = useSyncExternalStore(\n subscribe,\n core.getState,\n core.getState,\n ) as TState;\n const isBusy = useSyncExternalStore(subscribe, core.getBusy, core.getBusy);\n const failed = useSyncExternalStore(\n subscribe,\n core.getFailed,\n core.getFailed,\n );\n const connection = value.connection ?? CONNECTED_CONNECTION;\n\n return useMemo(() => {\n const failures = (): NonNullable<typeof value.failures> => {\n if (!value.failures) {\n throw new Error(\"statewire: this transport has no failed-send lane\");\n }\n return value.failures;\n };\n return {\n state,\n documents: value.documents,\n selection: value.selection,\n invoke: (\n protocol: string,\n method: string,\n params: readonly unknown[],\n ) => {\n if (!value.invoke)\n throw new Error(\n \"statewire: transport does not support protocol commands\",\n );\n return value.invoke(protocol, method, params);\n },\n updateContext: (ops: readonly unknown[]) => {\n if (!value.updateContext)\n throw new Error(\n \"statewire: transport does not support context documents\",\n );\n value.updateContext(ops);\n },\n getState: core.getState as () => TState,\n connection,\n commands: core.commands as Statewire.CommandsProxy<TCommands>,\n isBusy,\n setContext: (contextValue: Record<string, unknown>) => {\n if (!value.setContext) {\n throw new Error(\n \"statewire: this transport has no client context channel\",\n );\n }\n value.setContext(contextValue);\n },\n failed,\n retryFailed: (entry: StatewireClient.FailedCommand) =>\n failures().retry(entry),\n discardFailed: (entry: StatewireClient.FailedCommand) =>\n failures().discard(entry),\n };\n }, [state, isBusy, failed, connection, core, value]);\n};\n\nexport const StatewireResource = <\n TState,\n TCommands extends Statewire.Commands = Statewire.Commands,\n>(\n options: StatewireClient.Options<TState, TCommands>,\n): ResourceElement<StatewireClient.Helpers<TState, TCommands>> =>\n resource(useStatewire<TState, TCommands>)(options);\n\nexport class StatewireClient<\n TState,\n TCommands extends Statewire.Commands = Statewire.Commands,\n> {\n private readonly root;\n private disposed = false;\n\n constructor(options: StatewireClient.Options<TState, TCommands>) {\n const ClientRoot = () => useStatewire<TState, TCommands>(options);\n this.root = createTapRoot(ClientRoot);\n }\n\n get state(): TState {\n return this.root.getValue().getState();\n }\n\n get connection(): StatewireClient.Connection {\n return this.root.getValue().connection;\n }\n\n get commands(): Statewire.CommandsProxy<TCommands> {\n return this.root.getValue().commands;\n }\n\n get documents(): StatewireDocuments.Source | undefined {\n return this.root.getValue().documents;\n }\n get selection(): StatewireProtocol.Selection | undefined {\n return this.root.getValue().selection;\n }\n invoke(\n protocol: string,\n method: string,\n params: readonly unknown[],\n ): Promise<unknown> {\n return this.root.getValue().invoke(protocol, method, params);\n }\n updateContext(ops: readonly unknown[]): void {\n this.root.getValue().updateContext(ops);\n }\n\n get isBusy(): boolean {\n return this.root.getValue().isBusy;\n }\n\n setContext(value: Record<string, unknown>): void {\n this.root.getValue().setContext(value);\n }\n\n get failed(): readonly StatewireClient.FailedCommand[] {\n return this.root.getValue().failed;\n }\n\n retryFailed(entry: StatewireClient.FailedCommand): Promise<unknown> {\n return this.root.getValue().retryFailed(entry);\n }\n\n discardFailed(entry: StatewireClient.FailedCommand): void {\n this.root.getValue().discardFailed(entry);\n }\n\n subscribe(listener: () => void): () => void {\n return this.root.subscribe(listener);\n }\n\n dispose(): void {\n if (this.disposed) return;\n this.disposed = true;\n this.root.unmount();\n }\n}\n\nexport namespace StatewireClient {\n export type Options<\n TState,\n TCommands extends Statewire.Commands = Statewire.Commands,\n > = {\n protocol?: string;\n keyPrefixes?: Readonly<Record<string, readonly string[]>>;\n wireVersion?: StatewireProtocol.Range;\n applicationProtocols?: readonly StatewireProtocol.Offer[] | \"*\";\n transport: ResourceElement<Statewire<TState, TCommands>>;\n /** Durable session: the mounted storage holds this client's lane identity and its un-durable commands, which resend on the next attach. */\n storage?:\n | {\n session?: ResourceElement<SessionStorage> | undefined;\n documents?: ResourceElement<StatewireDocuments.Storage> | undefined;\n }\n | undefined;\n /** Served as the canonical state until the attach snapshot; own commands fold over it. */\n initialState?: TState;\n optimistic?: Optimistic<NonNullable<TState>, TCommands>;\n materialize?: Materialize<NonNullable<TState>>;\n onStateChange?: (\n state: NonNullable<TState>,\n ctx: { changes: Statewire.Changes },\n ) => void;\n onCommandChange?: (\n update: Statewire.CommandUpdate<TCommands>,\n ctx: { state: NonNullable<TState> },\n ) => void;\n onConnectionChange?: (connection: Connection) => void;\n onError?: (error: ConnectionError) => void;\n /** Drops a retained failed entry once a canonical snapshot shows its effect landed; checked against each new snapshot. */\n failureCovered?: (\n state: NonNullable<TState>,\n entry: FailedCommand,\n ) => boolean;\n };\n\n export type Helpers<\n TState,\n TCommands extends Statewire.Commands = Statewire.Commands,\n > = {\n /** The statewire's state type — remote statewires include `undefined` until the first attach snapshot applies. */\n state: TState;\n documents: StatewireDocuments.Source | undefined;\n selection: StatewireProtocol.Selection | undefined;\n invoke: (\n protocol: string,\n method: string,\n params: readonly unknown[],\n ) => Promise<unknown>;\n updateContext: (ops: readonly unknown[]) => void;\n /** Identity-stable across renders; reads the current canonical state without subscribing the caller. */\n getState: () => TState;\n connection: Connection;\n commands: Statewire.CommandsProxy<TCommands>;\n /** Negation of the server's broadcast idle level (a transport without idle is always busy). */\n isBusy: boolean;\n /** Replace this client's ambient context on the server; throws on transports without a client context channel. */\n setContext: (value: Record<string, unknown>) => void;\n /** Non-accepted sends retained in the session record; reload with it. */\n failed: readonly FailedCommand[];\n /** Re-enqueues the entry's command on fresh clocks and removes the entry. */\n retryFailed: (entry: FailedCommand) => Promise<unknown>;\n /** Removes the entry. */\n discardFailed: (entry: FailedCommand) => void;\n };\n\n /** Connection lifecycle and whether its current state warrants a problem indicator. */\n export type ConnectionState =\n | { status: \"standby\"; reason: \"deferred\" | \"idle\"; degraded: false }\n | { status: \"connecting\"; degraded: false }\n | { status: \"connected\"; degraded: false }\n | {\n status: \"retrying\";\n degraded: true;\n attempt: number;\n /** Backoff details are absent while the retry dial is in flight. */\n cause?: \"dropped\" | \"reconnect\" | \"error\" | \"unauthorized\";\n code?: string;\n nextRetryAt?: number;\n lastError?: string;\n message?: string;\n }\n | {\n status: \"stopped\";\n degraded: true;\n reason:\n | \"gone\"\n | \"forbidden\"\n | \"protocol-error\"\n | \"unauthorized\"\n | \"storage-error\";\n message?: string;\n payload?: unknown;\n };\n export type Connection = ConnectionState & {\n error?: ConnectionError;\n reconnect: () => void;\n };\n export type ConnectionStatus = Connection[\"status\"];\n\n /**\n * Mutate the draft in place; the client records the next state plus the exact\n * changed paths. Re-run against the new base on every rebase.\n */\n export type Optimistic<\n TState,\n TCommands extends Statewire.Commands = Statewire.Commands,\n > = (\n draft: TState,\n command: Statewire.Command<TCommands>,\n ctx: OptimisticContext,\n ) => void;\n\n export type OptimisticContext = {\n ts: number;\n seq: number;\n clientId?: string;\n };\n\n /**\n * Mutate the draft to make server-accepted, still-uncommitted intents in\n * the state concrete (e.g. dispatch records into messages). Runs after the\n * optimistic folds on every published state.\n */\n export type Materialize<TState> = (draft: TState) => void;\n\n /**\n * fate: \"aborted\" = never sent, \"rejected\" = the server refused it\n * (`code` \"unknown-command\" | \"invalid-params\" for the static rejects,\n * code-less for handler rejects), \"lost\" = sent but no verdict will ever\n * arrive — the record is gone (a proven seq gap) or the channel died: it\n * may or may not have run, \"failed\" = the handler ran and threw,\n * \"crashed\" = the handler died involuntarily, \"expired\" = settled —\n * result, rejected, failed, or crashed — but the verdict outlived the\n * server's retention.\n * cause is which channel settled it: \"server\" = the server's answer;\n * the rest are channel deaths the client settled locally.\n */\n export type SendFailure =\n | {\n fate: \"rejected\";\n cause: \"server\";\n code?: string;\n message?: string;\n payload?: unknown;\n }\n | { fate: \"failed\"; cause: \"server\"; message?: string }\n | { fate: \"crashed\"; cause: \"server\" }\n | { fate: \"expired\"; cause: \"server\" }\n | {\n fate: \"aborted\";\n cause: \"unmount\" | \"stream-gone\" | \"session-lost\";\n predecessorLost?: true;\n }\n | {\n fate: \"lost\";\n cause: \"unmount\" | \"stream-gone\" | \"session-lost\";\n };\n\n /** Held commands a proven record gap settled `{fate: \"lost\", cause: \"session-lost\"}`; return \"requeue\" to re-enqueue them on the reset clocks. */\n export type OnSessionLost = (\n pending: ReadonlyArray<{\n method: string;\n params: readonly unknown[];\n ts: number;\n seq: number;\n clientId?: string;\n }>,\n ) => \"requeue\" | \"drop\";\n\n /** A non-accepted send retained for the user: the wire statement, its failure, and its original clocks. */\n export type FailedCommand = {\n readonly protocol: string;\n readonly produced?: SessionCommand[\"produced\"];\n readonly command: string;\n readonly params: readonly unknown[];\n readonly meta?: unknown;\n readonly failure: SendFailure;\n readonly ts: number;\n readonly seq: number;\n };\n\n /** A command as persisted while un-durable: its lane seq plus the wire statement. */\n export type SessionCommand = {\n seq: number;\n protocol: string;\n produced?: {\n wire: StatewireProtocol.Range;\n protocols: readonly StatewireProtocol.Offer[] | \"*\";\n };\n method: string;\n params: unknown[];\n meta?: unknown;\n };\n\n /** The persisted session: the lane identity, the cmd clock, and the commands the server has not marked durable. */\n export type SessionRecord = {\n clientId: string;\n nextSeq: number;\n commands: readonly SessionCommand[];\n /** Terminal verdicts parked for the sharer that minted the seq (shared storage only). */\n verdicts?: readonly SessionVerdict[];\n /** Non-accepted sends retained for the user; survive record rebases and resets. */\n failed?: readonly FailedCommand[];\n };\n\n /** A terminal verdict one sharer's connection carried, parked for the sharer awaiting the seq. */\n export type SessionVerdict = {\n seq: number;\n type: (typeof TERMINAL)[number];\n code?: string;\n message?: string;\n payload?: unknown;\n meta?: unknown;\n };\n\n /** Session storage seam with optional asynchronous hydration and persistence. */\n export type SessionStorage = {\n /** Resolves when a synchronous load can return the hydrated record. */\n ready?: Promise<void>;\n /** Resolves after all queued saves are persisted; awaited before sending statements. */\n flush?: () => void | Promise<void>;\n load: () => SessionRecord | null;\n save: (record: SessionRecord) => void;\n /** Upgrade incompatible stored commands; null discards the old record and starts a new client identity. */\n migrate?: (\n record: SessionRecord,\n current: NonNullable<SessionCommand[\"produced\"]>,\n ) => SessionRecord | null;\n /** Serializes record mutations across sharers; presence marks the storage shared. */\n lock?: <T>(fn: () => T | Promise<T>) => Promise<T>;\n /** Notifies of record saves by other sharers; returns unsubscribe. Required alongside `lock`. */\n subscribe?: (listener: () => void) => () => void;\n };\n\n /** Static headers, or a resolver invoked per request/attach; `staleAuth` is true right after an auth rejection. */\n export type ConnectionError = Error & { fin?: StatewireHost.Fin };\n\n export type HeadersOption =\n | HeadersInit\n | ((ctx: { staleAuth: boolean }) => HeadersInit | Promise<HeadersInit>);\n\n export type HttpOptions = StatewireProtocol.Options & {\n sessionStore?: SessionStorage | undefined;\n url: string;\n /** The session does not exist on the server yet: the first attach waits for the first command send (or an explicit `reconnect()`). */\n isNew?: boolean;\n headers?: HeadersOption;\n fetch?: typeof globalThis.fetch;\n onSessionLost?: OnSessionLost;\n onChange?: (connection: Connection) => void;\n onError?: (error: ConnectionError) => void;\n /** The broadcast idle flag's rising edge: the instance went quiet. */\n onDone?: () => void;\n };\n\n export type WebsocketOptions = StatewireProtocol.Options & {\n sessionStore?: SessionStorage | undefined;\n url: string;\n /** The session does not exist on the server yet: the first attach waits for the first command send (or an explicit `reconnect()`). */\n isNew?: boolean;\n headers?: HeadersOption;\n protocols?: string | string[];\n query?:\n | Record<string, string>\n | (() => Record<string, string> | Promise<Record<string, string>>);\n webSocket?: new (url: string, protocols?: string | string[]) => WebSocket;\n onSessionLost?: OnSessionLost;\n onChange?: (connection: Connection) => void;\n onError?: (error: ConnectionError) => void;\n /** The broadcast idle flag's rising edge: the instance went quiet. */\n onDone?: () => void;\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"statewire.js","names":["StatewireDocuments","StatewireProtocol","StatewireHost","useMemo","useRef","useState","useSyncExternalStore","createTapRoot","flushTapSync","resource","useContextProvider","useResource","useResources","withKey","ResourceElement","CONNECTED_CONNECTION","useStatewireCore","Statewire","TERMINAL","DocumentStorageContext","ProtocolContext","SessionStorageContext","useSessionTransport","transport","session","documents","protocol","StatewireClient","SessionStorage","Storage","Options","SessionTransport","useStatewire","Commands","options","TState","TCommands","Helpers","latest","current","storage","undefined","wireVersion","applicationProtocols","keyPrefixes","value","core","subscribe","listener","state","getState","isBusy","getBusy","failed","getFailed","connection","failures","NonNullable","Error","selection","invoke","method","params","commands","CommandsProxy","retryFailed","entry","FailedCommand","retry","discardFailed","discard","StatewireResource","root","disposed","constructor","ClientRoot","getValue","Connection","Source","Selection","Promise","dispose","unmount","Readonly","Record","Range","Offer","initialState","optimistic","Optimistic","materialize","Materialize","onStateChange","ctx","changes","Changes","onCommandChange","update","CommandUpdate","onConnectionChange","onError","error","ConnectionError","failureCovered","ConnectionState","status","reason","degraded","attempt","cause","code","nextRetryAt","lastError","message","payload","reconnect","ConnectionStatus","draft","command","Command","OptimisticContext","ts","seq","clientId","SendFailure","fate","predecessorLost","OnSessionLost","pending","ReadonlyArray","produced","SessionCommand","meta","failure","wire","protocols","SessionLane","nextSeq","verdicts","SessionVerdict","SessionRecord","lanes","type","ready","flush","load","save","record","migrate","lock","fn","T","fin","Fin","HeadersOption","HeadersInit","staleAuth","HttpOptions","sessionStore","url","isNew","headers","fetch","globalThis","onSessionLost","onChange","onDone","WebsocketOptions","query","webSocket","WebSocket"],"sources":["../src/statewire.ts"],"sourcesContent":["import type { StatewireDocuments } from \"./wire/documents\";\nimport type { StatewireProtocol } from \"./wire/protocol\";\nimport type { StatewireHost } from \"./host\";\nimport { useMemo, useRef, useState, useSyncExternalStore } from \"react\";\nimport {\n createTapRoot,\n flushTapSync,\n resource,\n useContextProvider,\n useResource,\n useResources,\n withKey,\n} from \"@assistant-ui/tap\";\nimport type { ResourceElement } from \"@assistant-ui/tap\";\nimport { CONNECTED_CONNECTION, useStatewireCore } from \"./client/core\";\nimport type { Statewire } from \"./types\";\nimport type { TERMINAL } from \"./client/connection\";\nimport {\n DocumentStorageContext,\n ProtocolContext,\n SessionStorageContext,\n} from \"./client/sessionStore\";\n\nconst useSessionTransport = ({\n transport,\n session,\n documents,\n protocol,\n}: {\n transport: ResourceElement<Statewire<any, any>>;\n session: StatewireClient.SessionStorage | undefined;\n documents: StatewireDocuments.Storage | undefined;\n protocol: StatewireProtocol.Options;\n}): Statewire<unknown> =>\n useContextProvider(SessionStorageContext, session, () =>\n // oxlint-disable-next-line react-hooks/rules-of-hooks\n useContextProvider(DocumentStorageContext, documents, () =>\n // oxlint-disable-next-line react-hooks/rules-of-hooks\n useContextProvider(ProtocolContext, protocol, () =>\n // oxlint-disable-next-line react-hooks/rules-of-hooks\n useResource(transport),\n ),\n ),\n );\n\n// The transport owns the session; it reads the storage off the context this\n// wrapper's own fiber provides, so plain-React callers get a provider too.\nconst SessionTransport = resource(useSessionTransport);\n\nexport const useStatewire = <\n TState,\n TCommands extends Statewire.Commands = Statewire.Commands,\n>(\n options: StatewireClient.Options<TState, TCommands>,\n): StatewireClient.Helpers<TState, TCommands> => {\n const latest = useRef(options);\n latest.current = options;\n\n const session = useResources(\n options.storage?.session !== undefined\n ? [withKey(\"session\", options.storage.session)]\n : [],\n )[0];\n const documents = useResources(\n options.storage?.documents === undefined\n ? []\n : [withKey(\"documents\", options.storage.documents)],\n )[0];\n const protocol: StatewireProtocol.Options = {\n ...(options.protocol !== undefined && { protocol: options.protocol }),\n ...(options.wireVersion !== undefined && {\n wireVersion: options.wireVersion,\n }),\n ...(options.applicationProtocols !== undefined && {\n applicationProtocols: options.applicationProtocols,\n }),\n ...(options.keyPrefixes !== undefined && {\n keyPrefixes: options.keyPrefixes,\n }),\n };\n const value = useResource(\n SessionTransport({\n transport: options.transport,\n session,\n documents,\n protocol,\n }),\n );\n const core = useStatewireCore(() => latest.current, value);\n const [subscribe] = useState(\n () => (listener: () => void) =>\n core.subscribe(() => flushTapSync(listener)),\n );\n\n const state = useSyncExternalStore(\n subscribe,\n core.getState,\n core.getState,\n ) as TState;\n const isBusy = useSyncExternalStore(subscribe, core.getBusy, core.getBusy);\n const failed = useSyncExternalStore(\n subscribe,\n core.getFailed,\n core.getFailed,\n );\n const connection = value.connection ?? CONNECTED_CONNECTION;\n\n return useMemo(() => {\n const failures = (): NonNullable<typeof value.failures> => {\n if (!value.failures) {\n throw new Error(\"statewire: this transport has no failed-send lane\");\n }\n return value.failures;\n };\n return {\n state,\n documents: value.documents,\n selection: value.selection,\n invoke: (\n protocol: string,\n method: string,\n params: readonly unknown[],\n ) => {\n if (!value.invoke)\n throw new Error(\n \"statewire: transport does not support protocol commands\",\n );\n return value.invoke(protocol, method, params);\n },\n getState: core.getState as () => TState,\n connection,\n commands: core.commands as Statewire.CommandsProxy<TCommands>,\n isBusy,\n failed,\n retryFailed: (entry: StatewireClient.FailedCommand) =>\n failures().retry(entry),\n discardFailed: (entry: StatewireClient.FailedCommand) =>\n failures().discard(entry),\n };\n }, [state, isBusy, failed, connection, core, value]);\n};\n\nexport const StatewireResource = <\n TState,\n TCommands extends Statewire.Commands = Statewire.Commands,\n>(\n options: StatewireClient.Options<TState, TCommands>,\n): ResourceElement<StatewireClient.Helpers<TState, TCommands>> =>\n resource(useStatewire<TState, TCommands>)(options);\n\nexport class StatewireClient<\n TState,\n TCommands extends Statewire.Commands = Statewire.Commands,\n> {\n private readonly root;\n private disposed = false;\n\n constructor(options: StatewireClient.Options<TState, TCommands>) {\n const ClientRoot = () => useStatewire<TState, TCommands>(options);\n this.root = createTapRoot(ClientRoot);\n }\n\n get state(): TState {\n return this.root.getValue().getState();\n }\n\n get connection(): StatewireClient.Connection {\n return this.root.getValue().connection;\n }\n\n get commands(): Statewire.CommandsProxy<TCommands> {\n return this.root.getValue().commands;\n }\n\n get documents(): StatewireDocuments.Source | undefined {\n return this.root.getValue().documents;\n }\n get selection(): StatewireProtocol.Selection | undefined {\n return this.root.getValue().selection;\n }\n invoke(\n protocol: string,\n method: string,\n params: readonly unknown[],\n ): Promise<unknown> {\n return this.root.getValue().invoke(protocol, method, params);\n }\n get isBusy(): boolean {\n return this.root.getValue().isBusy;\n }\n\n get failed(): readonly StatewireClient.FailedCommand[] {\n return this.root.getValue().failed;\n }\n\n retryFailed(entry: StatewireClient.FailedCommand): Promise<unknown> {\n return this.root.getValue().retryFailed(entry);\n }\n\n discardFailed(entry: StatewireClient.FailedCommand): void {\n this.root.getValue().discardFailed(entry);\n }\n\n subscribe(listener: () => void): () => void {\n return this.root.subscribe(listener);\n }\n\n dispose(): void {\n if (this.disposed) return;\n this.disposed = true;\n this.root.unmount();\n }\n}\n\nexport namespace StatewireClient {\n export type Options<\n TState,\n TCommands extends Statewire.Commands = Statewire.Commands,\n > = {\n protocol?: string;\n keyPrefixes?: Readonly<Record<string, readonly string[]>>;\n wireVersion?: StatewireProtocol.Range;\n applicationProtocols?: readonly StatewireProtocol.Offer[] | \"*\";\n transport: ResourceElement<Statewire<TState, TCommands>>;\n /** Durable session: the mounted storage holds this client's lane identity and its un-durable commands, which resend on the next attach. */\n storage?:\n | {\n session?: ResourceElement<SessionStorage> | undefined;\n documents?: ResourceElement<StatewireDocuments.Storage> | undefined;\n }\n | undefined;\n /** Served as the canonical state until the attach snapshot; own commands fold over it. */\n initialState?: TState;\n optimistic?: Optimistic<NonNullable<TState>, TCommands>;\n materialize?: Materialize<NonNullable<TState>>;\n onStateChange?: (\n state: NonNullable<TState>,\n ctx: { changes: Statewire.Changes },\n ) => void;\n onCommandChange?: (\n update: Statewire.CommandUpdate<TCommands>,\n ctx: { state: NonNullable<TState> },\n ) => void;\n onConnectionChange?: (connection: Connection) => void;\n onError?: (error: ConnectionError) => void;\n /** Drops a retained failed entry once a canonical snapshot shows its effect landed; checked against each new snapshot. */\n failureCovered?: (\n state: NonNullable<TState>,\n entry: FailedCommand,\n ) => boolean;\n };\n\n export type Helpers<\n TState,\n TCommands extends Statewire.Commands = Statewire.Commands,\n > = {\n /** The statewire's state type — remote statewires include `undefined` until the first attach snapshot applies. */\n state: TState;\n documents: StatewireDocuments.Source | undefined;\n selection: StatewireProtocol.Selection | undefined;\n invoke: (\n protocol: string,\n method: string,\n params: readonly unknown[],\n ) => Promise<unknown>;\n /** Identity-stable across renders; reads the current canonical state without subscribing the caller. */\n getState: () => TState;\n connection: Connection;\n commands: Statewire.CommandsProxy<TCommands>;\n /** Negation of the server's broadcast idle level (a transport without idle is always busy). */\n isBusy: boolean;\n /** Non-accepted sends retained in the session record; reload with it. */\n failed: readonly FailedCommand[];\n /** Re-enqueues the entry's command on fresh clocks and removes the entry. */\n retryFailed: (entry: FailedCommand) => Promise<unknown>;\n /** Removes the entry. */\n discardFailed: (entry: FailedCommand) => void;\n };\n\n /** Connection lifecycle and whether its current state warrants a problem indicator. */\n export type ConnectionState =\n | { status: \"standby\"; reason: \"deferred\" | \"idle\"; degraded: false }\n | { status: \"connecting\"; degraded: false }\n | { status: \"connected\"; degraded: false }\n | {\n status: \"retrying\";\n degraded: true;\n attempt: number;\n /** Backoff details are absent while the retry dial is in flight. */\n cause?: \"dropped\" | \"reconnect\" | \"error\" | \"unauthorized\";\n code?: string;\n nextRetryAt?: number;\n lastError?: string;\n message?: string;\n }\n | {\n status: \"stopped\";\n degraded: true;\n reason:\n | \"gone\"\n | \"forbidden\"\n | \"protocol-error\"\n | \"unauthorized\"\n | \"storage-error\";\n message?: string;\n payload?: unknown;\n };\n export type Connection = ConnectionState & {\n error?: ConnectionError;\n reconnect: () => void;\n };\n export type ConnectionStatus = Connection[\"status\"];\n\n /**\n * Mutate the draft in place; the client records the next state plus the exact\n * changed paths. Re-run against the new base on every rebase.\n */\n export type Optimistic<\n TState,\n TCommands extends Statewire.Commands = Statewire.Commands,\n > = (\n draft: TState,\n command: Statewire.Command<TCommands>,\n ctx: OptimisticContext,\n ) => void;\n\n export type OptimisticContext = {\n ts: number;\n seq: number;\n clientId?: string;\n };\n\n /**\n * Mutate the draft to make server-accepted, still-uncommitted intents in\n * the state concrete (e.g. dispatch records into messages). Runs after the\n * optimistic folds on every published state.\n */\n export type Materialize<TState> = (draft: TState) => void;\n\n /**\n * fate: \"aborted\" = never sent, \"rejected\" = the server refused it\n * (`code` \"unknown-command\" | \"invalid-params\" for the static rejects,\n * code-less for handler rejects), \"lost\" = sent but no verdict will ever\n * arrive — the record is gone (a proven seq gap) or the channel died: it\n * may or may not have run, \"failed\" = the handler ran and threw,\n * \"crashed\" = the handler died involuntarily, \"expired\" = settled —\n * result, rejected, failed, or crashed — but the verdict outlived the\n * server's retention.\n * cause is which channel settled it: \"server\" = the server's answer;\n * the rest are channel deaths the client settled locally.\n */\n export type SendFailure =\n | {\n fate: \"rejected\";\n cause: \"server\";\n code?: string;\n message?: string;\n payload?: unknown;\n }\n | { fate: \"failed\"; cause: \"server\"; message?: string }\n | { fate: \"crashed\"; cause: \"server\" }\n | { fate: \"expired\"; cause: \"server\" }\n | {\n fate: \"aborted\";\n cause: \"unmount\" | \"stream-gone\" | \"session-lost\";\n predecessorLost?: true;\n }\n | {\n fate: \"lost\";\n cause: \"unmount\" | \"stream-gone\" | \"session-lost\";\n };\n\n /** Held commands a proven record gap settled `{fate: \"lost\", cause: \"session-lost\"}`; return \"requeue\" to re-enqueue them on the reset clocks. */\n export type OnSessionLost = (\n pending: ReadonlyArray<{\n method: string;\n params: readonly unknown[];\n ts: number;\n seq: number;\n clientId?: string;\n }>,\n ) => \"requeue\" | \"drop\";\n\n /** A non-accepted send retained for the user: the wire statement, its failure, and its original clocks. */\n export type FailedCommand = {\n readonly protocol: string;\n readonly produced?: SessionCommand[\"produced\"];\n readonly command: string;\n readonly params: readonly unknown[];\n readonly meta?: unknown;\n readonly failure: SendFailure;\n readonly ts: number;\n readonly seq: number;\n };\n\n /** A command as persisted while un-durable: its lane seq plus the wire statement. */\n export type SessionCommand = {\n seq: number;\n produced?: {\n wire: StatewireProtocol.Range;\n protocols: readonly StatewireProtocol.Offer[] | \"*\";\n };\n method: string;\n params: unknown[];\n meta?: unknown;\n };\n\n /** A durable lane: its cmd clock and the commands the server has not marked durable. */\n export type SessionLane = {\n nextSeq: number;\n commands: readonly SessionCommand[];\n /** Terminal verdicts parked for the sharer that minted the seq (shared storage only). */\n verdicts?: readonly SessionVerdict[];\n /** Non-accepted sends retained for the user; survive record rebases and resets. */\n failed?: readonly FailedCommand[];\n };\n\n /** The persisted session: the client identity plus one lane per durable protocol. */\n export type SessionRecord = {\n clientId: string;\n lanes: Readonly<Record<string, SessionLane>>;\n };\n\n /** A terminal verdict one sharer's connection carried, parked for the sharer awaiting the seq. */\n export type SessionVerdict = {\n seq: number;\n type: (typeof TERMINAL)[number];\n code?: string;\n message?: string;\n payload?: unknown;\n meta?: unknown;\n };\n\n /** Session storage seam with optional asynchronous hydration and persistence. */\n export type SessionStorage = {\n /** Resolves when a synchronous load can return the hydrated record. */\n ready?: Promise<void>;\n /** Resolves after all queued saves are persisted; awaited before sending statements. */\n flush?: () => void | Promise<void>;\n load: () => SessionRecord | null;\n save: (record: SessionRecord) => void;\n /** Upgrade incompatible stored commands; null discards the old record and starts a new client identity. */\n migrate?: (\n record: SessionRecord,\n current: NonNullable<SessionCommand[\"produced\"]>,\n ) => SessionRecord | null;\n /** Serializes record mutations across sharers; presence marks the storage shared. */\n lock?: <T>(fn: () => T | Promise<T>) => Promise<T>;\n /** Notifies of record saves by other sharers; returns unsubscribe. Required alongside `lock`. */\n subscribe?: (listener: () => void) => () => void;\n };\n\n /** Static headers, or a resolver invoked per request/attach; `staleAuth` is true right after an auth rejection. */\n export type ConnectionError = Error & { fin?: StatewireHost.Fin };\n\n export type HeadersOption =\n | HeadersInit\n | ((ctx: { staleAuth: boolean }) => HeadersInit | Promise<HeadersInit>);\n\n export type HttpOptions = StatewireProtocol.Options & {\n sessionStore?: SessionStorage | undefined;\n url: string;\n /** The session does not exist on the server yet: the first attach waits for the first command send (or an explicit `reconnect()`). */\n isNew?: boolean;\n headers?: HeadersOption;\n fetch?: typeof globalThis.fetch;\n onSessionLost?: OnSessionLost;\n onChange?: (connection: Connection) => void;\n onError?: (error: ConnectionError) => void;\n /** The broadcast idle flag's rising edge: the instance went quiet. */\n onDone?: () => void;\n };\n\n export type WebsocketOptions = StatewireProtocol.Options & {\n sessionStore?: SessionStorage | undefined;\n url: string;\n /** The session does not exist on the server yet: the first attach waits for the first command send (or an explicit `reconnect()`). */\n isNew?: boolean;\n headers?: HeadersOption;\n protocols?: string | string[];\n query?:\n | Record<string, string>\n | (() => Record<string, string> | Promise<Record<string, string>>);\n webSocket?: new (url: string, protocols?: string | string[]) => WebSocket;\n onSessionLost?: OnSessionLost;\n onChange?: (connection: Connection) => void;\n onError?: (error: ConnectionError) => void;\n /** The broadcast idle flag's rising edge: the instance went quiet. */\n onDone?: () => void;\n };\n}\n"],"mappings":"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"}
|
package/dist/storage.d.ts
CHANGED
|
@@ -15,11 +15,8 @@ declare namespace KeyValueSessionStorage {
|
|
|
15
15
|
backend: Backend;
|
|
16
16
|
key: string;
|
|
17
17
|
namespace?: string;
|
|
18
|
-
/** The
|
|
19
|
-
|
|
20
|
-
name: string;
|
|
21
|
-
version: string;
|
|
22
|
-
};
|
|
18
|
+
/** The lane that sessions saved in a single-lane format lift into; defaults to the client's default protocol. */
|
|
19
|
+
protocol?: string;
|
|
23
20
|
migrate?: StatewireClient.SessionStorage["migrate"];
|
|
24
21
|
retention?: {
|
|
25
22
|
maxAgeMs?: number;
|
package/dist/storage.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"storage.d.ts","names":[],"sources":["../src/storage.ts"],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"storage.d.ts","names":[],"sources":["../src/storage.ts"],"mappings":";;;cAggBa,oDAAsB,SAAA,gBAAA,iBAAA,SAAA,uBAAA;kBAElB;OACH;IACV,UAAU,gCAAgC;IAC1C,UAAU,aAAa,yBAAyB;IAChD,aAAa,uBAAuB;IACpC,uCAAuC;;IAEvC,cAAc,aAAa,cAAc,kBAAkB;;OAEjD;IACV,SAAS;IACT;IACA;;IAEA;IACA,UAAU,gBAAgB;IAC1B;MAAc;MAAmB;;IACjC,WAAW"}
|
package/dist/storage.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { negotiationHeaders, protocolToken } from "./wire/protocol.js";
|
|
1
|
+
import { DEFAULT_PROTOCOL, negotiationHeaders, protocolToken } from "./wire/protocol.js";
|
|
2
2
|
import { wrapStorageError } from "./client/storageError.js";
|
|
3
3
|
import { useEffect, useMemo, useRef } from "@assistant-ui/tap/react-shim";
|
|
4
4
|
import { resource } from "@assistant-ui/tap";
|
|
@@ -21,7 +21,9 @@ const isSendFailure = (value) => {
|
|
|
21
21
|
}
|
|
22
22
|
};
|
|
23
23
|
const isFailedCommand = (value) => isRecord(value) && isProtocol(value["protocol"]) && isProducer(value["produced"]) && typeof value["command"] === "string" && Array.isArray(value["params"]) && isSendFailure(value["failure"]) && typeof value["ts"] === "number" && Number.isFinite(value["ts"]) && isPositiveSafeInteger(value["seq"]);
|
|
24
|
-
const
|
|
24
|
+
const isSessionLane = (value) => isRecord(value) && isPositiveSafeInteger(value["nextSeq"]) && Array.isArray(value["commands"]) && value["commands"].every(isSessionCommand) && (value["verdicts"] === void 0 || Array.isArray(value["verdicts"]) && value["verdicts"].every(isSessionVerdict)) && (value["failed"] === void 0 || Array.isArray(value["failed"]) && value["failed"].every(isFailedCommand));
|
|
25
|
+
const isClientId = (value) => typeof value === "string" && value.length > 0;
|
|
26
|
+
const isSessionRecord = (value) => isRecord(value) && isClientId(value["clientId"]) && isRecord(value["lanes"]) && Object.keys(value["lanes"]).every(isProtocol) && Object.values(value["lanes"]).every(isSessionLane);
|
|
25
27
|
const isProtocol = (value) => value === void 0 || typeof value === "string" && value !== "*" && protocolToken.test(value);
|
|
26
28
|
const isProducer = (value) => {
|
|
27
29
|
if (value === void 0) return true;
|
|
@@ -36,32 +38,49 @@ const isProducer = (value) => {
|
|
|
36
38
|
return false;
|
|
37
39
|
}
|
|
38
40
|
};
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
wire: { version: "2026-09-07" },
|
|
49
|
-
protocols: [legacyProtocol]
|
|
41
|
+
const invalid = () => {
|
|
42
|
+
throw new Error("statewire: invalid stored session record");
|
|
43
|
+
};
|
|
44
|
+
const liftLegacy = (value, protocol) => {
|
|
45
|
+
const { clientId, nextSeq, commands, verdicts, failed } = value;
|
|
46
|
+
const lane = {
|
|
47
|
+
nextSeq,
|
|
48
|
+
commands: [],
|
|
49
|
+
...verdicts !== void 0 && { verdicts }
|
|
50
50
|
};
|
|
51
|
+
if (!isClientId(clientId) || !isSessionLane(lane) || !Array.isArray(commands)) return invalid();
|
|
52
|
+
const legacyCommands = commands;
|
|
53
|
+
if (!legacyCommands.every(isSessionCommand)) return invalid();
|
|
54
|
+
if (failed !== void 0 && (!Array.isArray(failed) || !failed.every(isFailedCommand))) return invalid();
|
|
55
|
+
const lanes = { [protocol]: {
|
|
56
|
+
...lane,
|
|
57
|
+
commands: legacyCommands.filter((command) => (command.protocol ?? protocol) === protocol).map(({ protocol: _protocol, ...command }) => command)
|
|
58
|
+
} };
|
|
59
|
+
for (const entry of failed ?? []) {
|
|
60
|
+
const target = lanes[entry.protocol] ?? {
|
|
61
|
+
nextSeq: 1,
|
|
62
|
+
commands: []
|
|
63
|
+
};
|
|
64
|
+
lanes[entry.protocol] = {
|
|
65
|
+
...target,
|
|
66
|
+
failed: [...target.failed ?? [], entry]
|
|
67
|
+
};
|
|
68
|
+
}
|
|
51
69
|
return {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
...command,
|
|
55
|
-
protocol: command.protocol ?? legacyProtocol.name,
|
|
56
|
-
produced
|
|
57
|
-
} : command),
|
|
58
|
-
...value.failed === void 0 ? {} : { failed: value.failed.map((entry) => ({
|
|
59
|
-
...entry,
|
|
60
|
-
protocol: entry.protocol ?? legacyProtocol.name,
|
|
61
|
-
produced: entry.produced ?? produced
|
|
62
|
-
})) }
|
|
70
|
+
clientId,
|
|
71
|
+
lanes
|
|
63
72
|
};
|
|
64
73
|
};
|
|
74
|
+
const normalize = (parsed, protocol) => {
|
|
75
|
+
if (!isRecord(parsed)) return invalid();
|
|
76
|
+
const { statewireStorageVersion: format, savedAt: _savedAt, ...value } = parsed;
|
|
77
|
+
if (format === 3) return isSessionRecord(value) ? value : invalid();
|
|
78
|
+
if (format !== void 0 && format !== 1 && format !== 2) throw new Error("statewire: unsupported session storage version");
|
|
79
|
+
const { ctxNextSeq: _ctxNextSeq, context: _context, ...legacy } = value;
|
|
80
|
+
return liftLegacy(legacy, protocol);
|
|
81
|
+
};
|
|
82
|
+
const decode = (raw, protocol) => raw === null ? null : normalize(JSON.parse(raw), protocol);
|
|
83
|
+
const isEmpty = (record) => Object.values(record.lanes).every((lane) => lane.commands.length === 0 && !lane.failed?.length);
|
|
65
84
|
const active = /* @__PURE__ */ new WeakMap();
|
|
66
85
|
const sweep = async (options, currentKey, quota = false) => {
|
|
67
86
|
const { backend, retention, namespace = "" } = options;
|
|
@@ -75,9 +94,11 @@ const sweep = async (options, currentKey, quota = false) => {
|
|
|
75
94
|
} catch {
|
|
76
95
|
return;
|
|
77
96
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
97
|
+
try {
|
|
98
|
+
if (!isEmpty(normalize(value, "default"))) return void 0;
|
|
99
|
+
} catch {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
81
102
|
const savedAt = value["savedAt"] ?? value["updatedAt"];
|
|
82
103
|
return typeof savedAt === "number" && Number.isFinite(savedAt) ? savedAt : void 0;
|
|
83
104
|
};
|
|
@@ -110,16 +131,10 @@ const sweep = async (options, currentKey, quota = false) => {
|
|
|
110
131
|
};
|
|
111
132
|
const isQuotaError = (error) => error instanceof Error && error.name === "QuotaExceededError";
|
|
112
133
|
const useKeyValueSessionStorage = (options) => {
|
|
113
|
-
const { backend, key, namespace = "",
|
|
134
|
+
const { backend, key, namespace = "", protocol = DEFAULT_PROTOCOL, migrate, retention: suppliedRetention, onError } = options;
|
|
114
135
|
const latestErrorHandler = useRef(onError);
|
|
115
136
|
latestErrorHandler.current = onError;
|
|
116
137
|
const reportError = useMemo(() => (error) => latestErrorHandler.current?.(error), []);
|
|
117
|
-
const legacyName = suppliedLegacyProtocol?.name;
|
|
118
|
-
const legacyVersion = suppliedLegacyProtocol?.version;
|
|
119
|
-
const legacyProtocol = useMemo(() => legacyName === void 0 ? void 0 : {
|
|
120
|
-
name: legacyName,
|
|
121
|
-
version: legacyVersion
|
|
122
|
-
}, [legacyName, legacyVersion]);
|
|
123
138
|
const retain = suppliedRetention !== void 0;
|
|
124
139
|
const maxAgeMs = suppliedRetention?.maxAgeMs;
|
|
125
140
|
const maxEntries = suppliedRetention?.maxEntries;
|
|
@@ -143,7 +158,6 @@ const useKeyValueSessionStorage = (options) => {
|
|
|
143
158
|
retention
|
|
144
159
|
]);
|
|
145
160
|
const storageKey = namespace + key;
|
|
146
|
-
if (legacyProtocol !== void 0) negotiationHeaders({ applicationProtocols: [legacyProtocol] });
|
|
147
161
|
if (retention !== void 0) {
|
|
148
162
|
if (!namespace) throw new Error("statewire: session retention requires a namespace");
|
|
149
163
|
if (!backend.getAllKeys) throw new Error("statewire: session retention requires getAllKeys");
|
|
@@ -170,7 +184,7 @@ const useKeyValueSessionStorage = (options) => {
|
|
|
170
184
|
};
|
|
171
185
|
if (state.asynchronous) {
|
|
172
186
|
state.ready = Promise.resolve(raw).then((value) => {
|
|
173
|
-
state.record = decode(value,
|
|
187
|
+
state.record = decode(value, protocol);
|
|
174
188
|
state.hydrated = true;
|
|
175
189
|
}).catch((cause_0) => {
|
|
176
190
|
const error_1 = wrapStorageError(cause_0);
|
|
@@ -180,7 +194,7 @@ const useKeyValueSessionStorage = (options) => {
|
|
|
180
194
|
});
|
|
181
195
|
state.ready.catch(() => {});
|
|
182
196
|
} else try {
|
|
183
|
-
state.record = decode(raw,
|
|
197
|
+
state.record = decode(raw, protocol);
|
|
184
198
|
} catch (cause_1) {
|
|
185
199
|
const error_2 = wrapStorageError(cause_1);
|
|
186
200
|
reportError(error_2);
|
|
@@ -190,7 +204,7 @@ const useKeyValueSessionStorage = (options) => {
|
|
|
190
204
|
}, [
|
|
191
205
|
backend,
|
|
192
206
|
storageKey,
|
|
193
|
-
|
|
207
|
+
protocol,
|
|
194
208
|
reportError
|
|
195
209
|
]);
|
|
196
210
|
useEffect(() => {
|
|
@@ -232,7 +246,7 @@ const useKeyValueSessionStorage = (options) => {
|
|
|
232
246
|
}
|
|
233
247
|
if (typeof raw_0 !== "string" && raw_0 !== null) throw new Error("statewire: storage backend changed from synchronous to asynchronous");
|
|
234
248
|
try {
|
|
235
|
-
return decode(raw_0,
|
|
249
|
+
return decode(raw_0, protocol);
|
|
236
250
|
} catch (cause_4) {
|
|
237
251
|
const error_5 = wrapStorageError(cause_4);
|
|
238
252
|
reportError(error_5);
|
|
@@ -245,7 +259,7 @@ const useKeyValueSessionStorage = (options) => {
|
|
|
245
259
|
if (!isSessionRecord(record)) throw new Error("statewire: invalid session record");
|
|
246
260
|
const encoded = JSON.stringify({
|
|
247
261
|
...record,
|
|
248
|
-
statewireStorageVersion:
|
|
262
|
+
statewireStorageVersion: 3,
|
|
249
263
|
savedAt: Date.now()
|
|
250
264
|
});
|
|
251
265
|
state_0.record = record;
|
|
@@ -307,7 +321,7 @@ const useKeyValueSessionStorage = (options) => {
|
|
|
307
321
|
state_0,
|
|
308
322
|
backend,
|
|
309
323
|
storageKey,
|
|
310
|
-
|
|
324
|
+
protocol,
|
|
311
325
|
migrate,
|
|
312
326
|
retention,
|
|
313
327
|
configured,
|