statewire 0.18.1 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/connection.d.ts +8 -5
- package/dist/client/connection.d.ts.map +1 -1
- package/dist/client/connection.js +24 -13
- package/dist/client/connection.js.map +1 -1
- package/dist/client/session.d.ts +13 -15
- package/dist/client/session.d.ts.map +1 -1
- package/dist/client/session.js +296 -401
- package/dist/client/session.js.map +1 -1
- package/dist/client/timing.d.ts +2 -1
- package/dist/client/timing.d.ts.map +1 -1
- package/dist/client/timing.js +2 -1
- package/dist/client/timing.js.map +1 -1
- package/dist/host/engine.d.ts +2 -0
- package/dist/host/engine.d.ts.map +1 -1
- package/dist/host/engine.js +2 -0
- package/dist/host/engine.js.map +1 -1
- package/dist/host/host.d.ts +3 -1
- package/dist/host/host.d.ts.map +1 -1
- package/dist/host/host.js +38 -7
- package/dist/host/host.js.map +1 -1
- package/dist/host/http.d.ts.map +1 -1
- package/dist/host/http.js +9 -19
- package/dist/host/http.js.map +1 -1
- package/dist/host/socket.d.ts +48 -25
- package/dist/host/socket.d.ts.map +1 -1
- package/dist/host/socket.js +399 -266
- package/dist/host/socket.js.map +1 -1
- package/dist/host/synced.d.ts +9 -0
- package/dist/host/synced.d.ts.map +1 -0
- package/dist/host/synced.js +45 -0
- package/dist/host/synced.js.map +1 -0
- package/dist/host-internal.d.ts +20 -12
- package/dist/host-internal.d.ts.map +1 -1
- package/dist/host.d.ts +33 -9
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +2 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -1
- package/dist/statewire.d.ts +8 -20
- package/dist/statewire.d.ts.map +1 -1
- package/dist/statewire.js +0 -14
- package/dist/statewire.js.map +1 -1
- package/dist/storage.d.ts +2 -5
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +54 -89
- package/dist/storage.js.map +1 -1
- package/dist/transports/remote.d.ts.map +1 -1
- package/dist/transports/remote.js +1 -6
- package/dist/transports/remote.js.map +1 -1
- package/dist/transports/sse.d.ts +2 -5
- package/dist/transports/sse.d.ts.map +1 -1
- package/dist/transports/sse.js.map +1 -1
- package/dist/transports/testing.d.ts +7 -5
- package/dist/transports/testing.d.ts.map +1 -1
- package/dist/transports/testing.js +19 -18
- package/dist/transports/testing.js.map +1 -1
- package/dist/types.d.ts +3 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/wire/limits.d.ts +2 -2
- package/dist/wire/limits.js +2 -2
- package/dist/wire/limits.js.map +1 -1
- package/dist/wire/protocol.d.ts +6 -1
- package/dist/wire/protocol.d.ts.map +1 -1
- package/dist/wire/protocol.js +33 -12
- package/dist/wire/protocol.js.map +1 -1
- package/package.json +1 -1
- package/src/client/connection.ts +46 -33
- package/src/client/session.ts +442 -517
- package/src/client/timing.ts +2 -0
- package/src/host/engine.ts +3 -1
- package/src/host/host.ts +50 -3
- package/src/host/http.ts +23 -31
- package/src/host/socket.ts +560 -339
- package/src/host/synced.ts +60 -0
- package/src/host-internal.ts +21 -7
- package/src/host.ts +40 -8
- package/src/index.ts +1 -0
- package/src/statewire.ts +8 -37
- package/src/storage.ts +90 -157
- package/src/transports/remote.ts +0 -7
- package/src/transports/sse.ts +8 -3
- package/src/transports/testing.ts +34 -18
- package/src/types.ts +3 -5
- package/src/wire/limits.ts +1 -1
- package/src/wire/protocol.ts +40 -11
package/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,26 +185,20 @@ 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
|
-
ctxNextSeq: number;
|
|
199
|
-
context?: {
|
|
200
|
-
ops: unknown[];
|
|
201
|
-
pending: {
|
|
202
|
-
seq: number;
|
|
203
|
-
ops: unknown[];
|
|
204
|
-
}[];
|
|
205
|
-
/** Recover the original wire's unpersisted single-document context after the next hello. */
|
|
206
|
-
recover?: "version-1";
|
|
207
|
-
};
|
|
208
191
|
commands: readonly SessionCommand[];
|
|
209
192
|
/** Terminal verdicts parked for the sharer that minted the seq (shared storage only). */
|
|
210
193
|
verdicts?: readonly SessionVerdict[];
|
|
211
194
|
/** Non-accepted sends retained for the user; survive record rebases and resets. */
|
|
212
195
|
failed?: readonly FailedCommand[];
|
|
213
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
|
+
};
|
|
214
202
|
/** A terminal verdict one sharer's connection carried, parked for the sharer awaiting the seq. */
|
|
215
203
|
type SessionVerdict = {
|
|
216
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":"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"}
|
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","ctxNextSeq","context","recover","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 lane clocks, and the commands the server has not marked durable. */\n export type SessionRecord = {\n clientId: string;\n nextSeq: number;\n ctxNextSeq: number;\n context?: {\n ops: unknown[];\n pending: { seq: number; ops: unknown[] }[];\n /** Recover the original wire's unpersisted single-document context after the next hello. */\n recover?: \"version-1\";\n };\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,5 +1,4 @@
|
|
|
1
|
-
import { negotiationHeaders, protocolToken } from "./wire/protocol.js";
|
|
2
|
-
import { applyDocumentOps } from "./wire/documents.js";
|
|
1
|
+
import { DEFAULT_PROTOCOL, negotiationHeaders, protocolToken } from "./wire/protocol.js";
|
|
3
2
|
import { wrapStorageError } from "./client/storageError.js";
|
|
4
3
|
import { useEffect, useMemo, useRef } from "@assistant-ui/tap/react-shim";
|
|
5
4
|
import { resource } from "@assistant-ui/tap";
|
|
@@ -22,7 +21,9 @@ const isSendFailure = (value) => {
|
|
|
22
21
|
}
|
|
23
22
|
};
|
|
24
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"]);
|
|
25
|
-
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);
|
|
26
27
|
const isProtocol = (value) => value === void 0 || typeof value === "string" && value !== "*" && protocolToken.test(value);
|
|
27
28
|
const isProducer = (value) => {
|
|
28
29
|
if (value === void 0) return true;
|
|
@@ -37,80 +38,49 @@ const isProducer = (value) => {
|
|
|
37
38
|
return false;
|
|
38
39
|
}
|
|
39
40
|
};
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
if (value["doc"] !== void 0 && (typeof value["doc"] !== "number" || !Number.isSafeInteger(value["doc"]) || value["doc"] < 0)) return false;
|
|
43
|
-
if (value["op"] === "mount") try {
|
|
44
|
-
applyDocumentOps({
|
|
45
|
-
mode: "mounted",
|
|
46
|
-
documents: /* @__PURE__ */ new Map()
|
|
47
|
-
}, [value], void 0, true);
|
|
48
|
-
return true;
|
|
49
|
-
} catch {
|
|
50
|
-
return false;
|
|
51
|
-
}
|
|
52
|
-
if (value["op"] === "unmount") return true;
|
|
53
|
-
if (![
|
|
54
|
-
"add",
|
|
55
|
-
"replace",
|
|
56
|
-
"remove"
|
|
57
|
-
].includes(value["op"])) return false;
|
|
58
|
-
return Array.isArray(value["path"]) && value["path"].every((part) => typeof part === "string" || typeof part === "number" && Number.isSafeInteger(part) && part >= 0) && (value["op"] === "remove" || "value" in value);
|
|
59
|
-
};
|
|
60
|
-
const isContext = (value) => {
|
|
61
|
-
if (value === void 0) return true;
|
|
62
|
-
if (!isRecord(value) || !Array.isArray(value["ops"]) || !Array.isArray(value["pending"])) return false;
|
|
63
|
-
if (value["recover"] !== void 0 && value["recover"] !== "version-1") return false;
|
|
64
|
-
try {
|
|
65
|
-
const documents = applyDocumentOps({
|
|
66
|
-
mode: "mounted",
|
|
67
|
-
documents: /* @__PURE__ */ new Map()
|
|
68
|
-
}, value["ops"], void 0, true);
|
|
69
|
-
for (const entry of value["pending"]) {
|
|
70
|
-
if (!isRecord(entry) || !isPositiveSafeInteger(entry["seq"]) || !Array.isArray(entry["ops"])) return false;
|
|
71
|
-
if (!entry["ops"].every(isContextOperation)) return false;
|
|
72
|
-
}
|
|
73
|
-
return documents.mode === "mounted";
|
|
74
|
-
} catch {
|
|
75
|
-
return false;
|
|
76
|
-
}
|
|
41
|
+
const invalid = () => {
|
|
42
|
+
throw new Error("statewire: invalid stored session record");
|
|
77
43
|
};
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
if (format !== void 0 && format !== 1) throw new Error("statewire: unsupported session storage version");
|
|
85
|
-
if (legacyProtocol === void 0 || format !== void 0) return value;
|
|
86
|
-
const produced = {
|
|
87
|
-
wire: { version: "2026-09-07" },
|
|
88
|
-
protocols: [legacyProtocol]
|
|
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 }
|
|
89
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
|
+
}
|
|
90
69
|
return {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
ops: [{
|
|
94
|
-
op: "mount",
|
|
95
|
-
protocol: legacyProtocol.name,
|
|
96
|
-
main: true,
|
|
97
|
-
value: {}
|
|
98
|
-
}],
|
|
99
|
-
pending: [],
|
|
100
|
-
recover: "version-1"
|
|
101
|
-
} } : {},
|
|
102
|
-
commands: value.commands.map((command) => command.produced === void 0 ? {
|
|
103
|
-
...command,
|
|
104
|
-
protocol: command.protocol ?? legacyProtocol.name,
|
|
105
|
-
produced
|
|
106
|
-
} : command),
|
|
107
|
-
...value.failed === void 0 ? {} : { failed: value.failed.map((entry) => ({
|
|
108
|
-
...entry,
|
|
109
|
-
protocol: entry.protocol ?? legacyProtocol.name,
|
|
110
|
-
produced: entry.produced ?? produced
|
|
111
|
-
})) }
|
|
70
|
+
clientId,
|
|
71
|
+
lanes
|
|
112
72
|
};
|
|
113
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);
|
|
114
84
|
const active = /* @__PURE__ */ new WeakMap();
|
|
115
85
|
const sweep = async (options, currentKey, quota = false) => {
|
|
116
86
|
const { backend, retention, namespace = "" } = options;
|
|
@@ -124,9 +94,11 @@ const sweep = async (options, currentKey, quota = false) => {
|
|
|
124
94
|
} catch {
|
|
125
95
|
return;
|
|
126
96
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
97
|
+
try {
|
|
98
|
+
if (!isEmpty(normalize(value, "default"))) return void 0;
|
|
99
|
+
} catch {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
130
102
|
const savedAt = value["savedAt"] ?? value["updatedAt"];
|
|
131
103
|
return typeof savedAt === "number" && Number.isFinite(savedAt) ? savedAt : void 0;
|
|
132
104
|
};
|
|
@@ -159,16 +131,10 @@ const sweep = async (options, currentKey, quota = false) => {
|
|
|
159
131
|
};
|
|
160
132
|
const isQuotaError = (error) => error instanceof Error && error.name === "QuotaExceededError";
|
|
161
133
|
const useKeyValueSessionStorage = (options) => {
|
|
162
|
-
const { backend, key, namespace = "",
|
|
134
|
+
const { backend, key, namespace = "", protocol = DEFAULT_PROTOCOL, migrate, retention: suppliedRetention, onError } = options;
|
|
163
135
|
const latestErrorHandler = useRef(onError);
|
|
164
136
|
latestErrorHandler.current = onError;
|
|
165
137
|
const reportError = useMemo(() => (error) => latestErrorHandler.current?.(error), []);
|
|
166
|
-
const legacyName = suppliedLegacyProtocol?.name;
|
|
167
|
-
const legacyVersion = suppliedLegacyProtocol?.version;
|
|
168
|
-
const legacyProtocol = useMemo(() => legacyName === void 0 ? void 0 : {
|
|
169
|
-
name: legacyName,
|
|
170
|
-
version: legacyVersion
|
|
171
|
-
}, [legacyName, legacyVersion]);
|
|
172
138
|
const retain = suppliedRetention !== void 0;
|
|
173
139
|
const maxAgeMs = suppliedRetention?.maxAgeMs;
|
|
174
140
|
const maxEntries = suppliedRetention?.maxEntries;
|
|
@@ -192,7 +158,6 @@ const useKeyValueSessionStorage = (options) => {
|
|
|
192
158
|
retention
|
|
193
159
|
]);
|
|
194
160
|
const storageKey = namespace + key;
|
|
195
|
-
if (legacyProtocol !== void 0) negotiationHeaders({ applicationProtocols: [legacyProtocol] });
|
|
196
161
|
if (retention !== void 0) {
|
|
197
162
|
if (!namespace) throw new Error("statewire: session retention requires a namespace");
|
|
198
163
|
if (!backend.getAllKeys) throw new Error("statewire: session retention requires getAllKeys");
|
|
@@ -219,7 +184,7 @@ const useKeyValueSessionStorage = (options) => {
|
|
|
219
184
|
};
|
|
220
185
|
if (state.asynchronous) {
|
|
221
186
|
state.ready = Promise.resolve(raw).then((value) => {
|
|
222
|
-
state.record = decode(value,
|
|
187
|
+
state.record = decode(value, protocol);
|
|
223
188
|
state.hydrated = true;
|
|
224
189
|
}).catch((cause_0) => {
|
|
225
190
|
const error_1 = wrapStorageError(cause_0);
|
|
@@ -229,7 +194,7 @@ const useKeyValueSessionStorage = (options) => {
|
|
|
229
194
|
});
|
|
230
195
|
state.ready.catch(() => {});
|
|
231
196
|
} else try {
|
|
232
|
-
state.record = decode(raw,
|
|
197
|
+
state.record = decode(raw, protocol);
|
|
233
198
|
} catch (cause_1) {
|
|
234
199
|
const error_2 = wrapStorageError(cause_1);
|
|
235
200
|
reportError(error_2);
|
|
@@ -239,7 +204,7 @@ const useKeyValueSessionStorage = (options) => {
|
|
|
239
204
|
}, [
|
|
240
205
|
backend,
|
|
241
206
|
storageKey,
|
|
242
|
-
|
|
207
|
+
protocol,
|
|
243
208
|
reportError
|
|
244
209
|
]);
|
|
245
210
|
useEffect(() => {
|
|
@@ -281,7 +246,7 @@ const useKeyValueSessionStorage = (options) => {
|
|
|
281
246
|
}
|
|
282
247
|
if (typeof raw_0 !== "string" && raw_0 !== null) throw new Error("statewire: storage backend changed from synchronous to asynchronous");
|
|
283
248
|
try {
|
|
284
|
-
return decode(raw_0,
|
|
249
|
+
return decode(raw_0, protocol);
|
|
285
250
|
} catch (cause_4) {
|
|
286
251
|
const error_5 = wrapStorageError(cause_4);
|
|
287
252
|
reportError(error_5);
|
|
@@ -294,7 +259,7 @@ const useKeyValueSessionStorage = (options) => {
|
|
|
294
259
|
if (!isSessionRecord(record)) throw new Error("statewire: invalid session record");
|
|
295
260
|
const encoded = JSON.stringify({
|
|
296
261
|
...record,
|
|
297
|
-
statewireStorageVersion:
|
|
262
|
+
statewireStorageVersion: 3,
|
|
298
263
|
savedAt: Date.now()
|
|
299
264
|
});
|
|
300
265
|
state_0.record = record;
|
|
@@ -356,7 +321,7 @@ const useKeyValueSessionStorage = (options) => {
|
|
|
356
321
|
state_0,
|
|
357
322
|
backend,
|
|
358
323
|
storageKey,
|
|
359
|
-
|
|
324
|
+
protocol,
|
|
360
325
|
migrate,
|
|
361
326
|
retention,
|
|
362
327
|
configured,
|