statewire 0.24.2 → 0.25.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/indexedDb.d.ts +1 -1
- package/dist/indexedDb.d.ts.map +1 -1
- package/dist/indexedDb.js +6 -5
- package/dist/indexedDb.js.map +1 -1
- package/dist/storage.d.ts +12 -4
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +55 -28
- package/dist/storage.js.map +1 -1
- package/dist/transports/testing.d.ts.map +1 -1
- package/dist/transports/testing.js +6 -2
- package/dist/transports/testing.js.map +1 -1
- package/package.json +1 -1
- package/src/indexedDb.ts +5 -4
- package/src/storage.ts +80 -26
- package/src/transports/testing.ts +15 -2
package/dist/indexedDb.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { KeyValueSessionStorage } from "./storage.js";
|
|
|
2
2
|
import type { StatewireClient } from "./statewire.js";
|
|
3
3
|
/** A key-value backend over one IndexedDB database; writes broadcast to other runtimes. */
|
|
4
4
|
export declare const indexedDbBackend: (database: string) => KeyValueSessionStorage.Backend;
|
|
5
|
-
/** Shared session storage on IndexedDB: sharers serialize through Web Locks and hear each other over a BroadcastChannel. */
|
|
5
|
+
/** Shared session storage on IndexedDB, keyed per principal and key: sharers serialize through Web Locks and hear each other over a BroadcastChannel. */
|
|
6
6
|
export declare const IndexedDbSession: import("@assistant-ui/tap").Resource<StatewireClient.SessionStorage, [IndexedDbSession.Options]>;
|
|
7
7
|
export declare namespace IndexedDbSession {
|
|
8
8
|
type Options = Omit<KeyValueSessionStorage.Options, "backend" | "namespace"> & {
|
package/dist/indexedDb.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"indexedDb.d.ts","sourceRoot":"","sources":["../src/indexedDb.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,sBAAsB,
|
|
1
|
+
{"version":3,"file":"indexedDb.d.ts","sourceRoot":"","sources":["../src/indexedDb.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,sBAAsB,EAAoB,qBAAkB;AACrE,OAAO,KAAK,EAAE,eAAe,EAAE,uBAAoB;AAanD,2FAA2F;AAC3F,eAAO,MAAM,gBAAgB,GAC3B,UAAU,MAAM,KACf,sBAAsB,CAAC,OAmEzB,CAAC;AAoEF,yJAAyJ;AACzJ,eAAO,MAAM,gBAAgB,kGAAgC,CAAC;AAE9D,yBAAiB,gBAAgB,CAAC;IAChC,KAAY,OAAO,GAAG,IAAI,CACxB,sBAAsB,CAAC,OAAO,EAC9B,SAAS,GAAG,WAAW,CACxB,GAAG;QACF,oDAAoD;QACpD,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,oCAAoC;QACpC,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB,CAAC;CACH"}
|
package/dist/indexedDb.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomId } from "./util/randomId.js";
|
|
2
|
-
import { KeyValueSessionStorage } from "./storage.js";
|
|
2
|
+
import { KeyValueSessionStorage, sessionNamespace } from "./storage.js";
|
|
3
3
|
import { useEffect, useState } from "@assistant-ui/tap/react-shim";
|
|
4
4
|
import { resource, useResource } from "@assistant-ui/tap";
|
|
5
5
|
import { c } from "@assistant-ui/tap/react-shim/compiler-runtime";
|
|
@@ -71,12 +71,12 @@ const holdLock = (name, mode = "exclusive") => {
|
|
|
71
71
|
};
|
|
72
72
|
const useIndexedDbSession = (t0) => {
|
|
73
73
|
const $ = c(22);
|
|
74
|
-
const { database: t1, key, namespace: t2, ...options } = t0;
|
|
74
|
+
const { database: t1, key, namespace: t2, principal, ...options } = t0;
|
|
75
75
|
const database = t1 === void 0 ? "statewire" : t1;
|
|
76
76
|
const namespace = t2 === void 0 ? "statewire-session:" : t2;
|
|
77
77
|
if (typeof navigator === "undefined" || navigator.locks === void 0) throw new Error("statewire: IndexedDbSession requires the Web Locks API (navigator.locks)");
|
|
78
78
|
const backend = indexedDbBackend(database);
|
|
79
|
-
const storageKey = namespace + key;
|
|
79
|
+
const storageKey = sessionNamespace(namespace, principal) + key;
|
|
80
80
|
const [tabId] = useState(randomId);
|
|
81
81
|
let t3;
|
|
82
82
|
let t4;
|
|
@@ -109,7 +109,8 @@ const useIndexedDbSession = (t0) => {
|
|
|
109
109
|
...options,
|
|
110
110
|
backend,
|
|
111
111
|
key,
|
|
112
|
-
namespace
|
|
112
|
+
namespace,
|
|
113
|
+
principal
|
|
113
114
|
}));
|
|
114
115
|
let t7;
|
|
115
116
|
let t8;
|
|
@@ -160,7 +161,7 @@ const useIndexedDbSession = (t0) => {
|
|
|
160
161
|
} else t11 = $[21];
|
|
161
162
|
return t11;
|
|
162
163
|
};
|
|
163
|
-
/** Shared session storage on IndexedDB: sharers serialize through Web Locks and hear each other over a BroadcastChannel. */
|
|
164
|
+
/** Shared session storage on IndexedDB, keyed per principal and key: sharers serialize through Web Locks and hear each other over a BroadcastChannel. */
|
|
164
165
|
const IndexedDbSession = resource(useIndexedDbSession);
|
|
165
166
|
function _temp() {}
|
|
166
167
|
function _temp2(id, signal) {
|
package/dist/indexedDb.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"indexedDb.js","names":["useEffect","useMemo","useState","resource","useResource","KeyValueSessionStorage","StatewireClient","randomId","STORE","request","req","IDBRequest","T","Promise","resolve","reject","onsuccess","result","onerror","error","backends","Map","Backend","indexedDbBackend","database","existing","get","indexedDB","Error","opened","IDBDatabase","db","open","onupgradeneeded","createObjectStore","run","mode","IDBTransactionMode","fn","store","IDBObjectStore","tx","transaction","committed","undefined","oncomplete","onabort","objectStore","channel","BroadcastChannel","announce","key","postMessage","backend","getItem","s","setItem","value","put","removeItem","delete","getAllKeys","withUnused","action","navigator","locks","ifAvailable","lock","inner","subscribe","listener","own","onmessage","event","MessageEvent","data","close","set","holdLock","name","LockMode","release","disposed","catch","useIndexedDbSession","t0","$","_c","t1","namespace","t2","options","storageKey","tabId","t3","t4","t5","t6","storage","t7","t8","fn_0","t9","id","whenGone","_temp2","t10","changed","t11","notifyLock","owner","IndexedDbSession","Options","Omit","_temp","signal","_c"],"sources":["../src/indexedDb.ts"],"sourcesContent":["import { useEffect, useMemo, useState } from \"react\";\nimport { resource, useResource } from \"@assistant-ui/tap\";\nimport { KeyValueSessionStorage } from \"./storage\";\nimport type { StatewireClient } from \"./statewire\";\nimport { randomId } from \"./util/randomId\";\n\nconst STORE = \"sessions\";\n\nconst request = <T>(req: IDBRequest<T>) =>\n new Promise<T>((resolve, reject) => {\n req.onsuccess = () => resolve(req.result);\n req.onerror = () => reject(req.error);\n });\n\nconst backends = new Map<string, KeyValueSessionStorage.Backend>();\n\n/** A key-value backend over one IndexedDB database; writes broadcast to other runtimes. */\nexport const indexedDbBackend = (\n database: string,\n): KeyValueSessionStorage.Backend => {\n const existing = backends.get(database);\n if (existing) return existing;\n if (typeof indexedDB === \"undefined\")\n throw new Error(\"statewire: IndexedDbSession requires indexedDB\");\n let opened: Promise<IDBDatabase> | undefined;\n const db = () =>\n (opened ??= (() => {\n const open = indexedDB.open(database, 1);\n open.onupgradeneeded = () => open.result.createObjectStore(STORE);\n return request(open);\n })());\n // Writes resolve once their transaction commits.\n const run = async <T>(\n mode: IDBTransactionMode,\n fn: (store: IDBObjectStore) => IDBRequest<T>,\n ) => {\n const tx = (await db()).transaction(STORE, mode);\n const committed =\n mode === \"readonly\"\n ? undefined\n : new Promise<void>((resolve, reject) => {\n tx.oncomplete = () => resolve();\n tx.onerror = () => reject(tx.error);\n tx.onabort = () => reject(tx.error);\n });\n const result = await request(fn(tx.objectStore(STORE)));\n await committed;\n return result;\n };\n const channel = new BroadcastChannel(`statewire-idb:${database}`);\n const announce = (key: string) => channel.postMessage(key);\n const backend: KeyValueSessionStorage.Backend = {\n getItem: async (key) =>\n ((await run(\"readonly\", (s) => s.get(key))) as string | undefined) ??\n null,\n setItem: async (key, value) => {\n await run(\"readwrite\", (s) => s.put(value, key));\n announce(key);\n },\n removeItem: async (key) => {\n await run(\"readwrite\", (s) => s.delete(key));\n announce(key);\n },\n getAllKeys: async () =>\n (await run(\"readonly\", (s) => s.getAllKeys())) as string[],\n withUnused: (key, action) =>\n navigator.locks.request(\n `statewire-active:${database}:${key}`,\n { ifAvailable: true },\n (lock) =>\n lock === null\n ? undefined\n : navigator.locks.request(\n `statewire-session:${database}:${key}`,\n { ifAvailable: true },\n (inner) => (inner === null ? undefined : action()),\n ),\n ),\n subscribe: (listener) => {\n const own = new BroadcastChannel(`statewire-idb:${database}`);\n own.onmessage = (event: MessageEvent<string>) => listener(event.data);\n return () => own.close();\n },\n };\n backends.set(database, backend);\n return backend;\n};\n\nconst holdLock = (name: string, mode: LockMode = \"exclusive\") => {\n let release: (() => void) | undefined;\n let disposed = false;\n void navigator.locks\n .request(name, { mode }, () => {\n if (disposed) return;\n return new Promise<void>((resolve) => {\n release = resolve;\n });\n })\n .catch(() => {});\n return () => {\n disposed = true;\n release?.();\n };\n};\n\nconst useIndexedDbSession = ({\n database = \"statewire\",\n key,\n namespace = \"statewire-session:\",\n ...options\n}: IndexedDbSession.Options): StatewireClient.SessionStorage => {\n if (typeof navigator === \"undefined\" || navigator.locks === undefined)\n throw new Error(\n \"statewire: IndexedDbSession requires the Web Locks API (navigator.locks)\",\n );\n const backend = indexedDbBackend(database);\n const storageKey = namespace + key;\n const [tabId] = useState(randomId);\n useEffect(() => holdLock(`statewire-tab:${tabId}`), [tabId]);\n useEffect(\n () => holdLock(`statewire-active:${database}:${storageKey}`, \"shared\"),\n [database, storageKey],\n );\n const storage = useResource(\n KeyValueSessionStorage({ ...options, backend, key, namespace }),\n );\n return useMemo(\n (): StatewireClient.SessionStorage => ({\n ...storage,\n lock: (fn) =>\n navigator.locks.request(\n `statewire-session:${database}:${storageKey}`,\n async () => fn(),\n ),\n notifyLock: (fn) =>\n navigator.locks.request(\n `statewire-notify:${database}:${storageKey}`,\n fn,\n ),\n owner: {\n id: tabId,\n whenGone: (id, signal) =>\n navigator.locks.request(`statewire-tab:${id}`, { signal }, () => {}),\n },\n subscribe: (listener) =>\n backend.subscribe!((changed) => {\n if (changed === storageKey) listener();\n }),\n }),\n [storage, backend, database, storageKey, tabId],\n );\n};\n\n/** Shared session storage on IndexedDB: sharers serialize through Web Locks and hear each other over a BroadcastChannel. */\nexport const IndexedDbSession = resource(useIndexedDbSession);\n\nexport namespace IndexedDbSession {\n export type Options = Omit<\n KeyValueSessionStorage.Options,\n \"backend\" | \"namespace\"\n > & {\n /** IndexedDB database name; default \"statewire\". */\n database?: string;\n /** Default \"statewire-session:\". */\n namespace?: string;\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"indexedDb.js","names":["useEffect","useMemo","useState","resource","useResource","KeyValueSessionStorage","sessionNamespace","StatewireClient","randomId","STORE","request","req","IDBRequest","T","Promise","resolve","reject","onsuccess","result","onerror","error","backends","Map","Backend","indexedDbBackend","database","existing","get","indexedDB","Error","opened","IDBDatabase","db","open","onupgradeneeded","createObjectStore","run","mode","IDBTransactionMode","fn","store","IDBObjectStore","tx","transaction","committed","undefined","oncomplete","onabort","objectStore","channel","BroadcastChannel","announce","key","postMessage","backend","getItem","s","setItem","value","put","removeItem","delete","getAllKeys","withUnused","action","navigator","locks","ifAvailable","lock","inner","subscribe","listener","own","onmessage","event","MessageEvent","data","close","set","holdLock","name","LockMode","release","disposed","catch","useIndexedDbSession","t0","$","_c","t1","namespace","t2","principal","options","storageKey","tabId","t3","t4","t5","t6","storage","t7","t8","fn_0","t9","id","whenGone","_temp2","t10","changed","t11","notifyLock","owner","IndexedDbSession","Options","Omit","_temp","signal","_c"],"sources":["../src/indexedDb.ts"],"sourcesContent":["import { useEffect, useMemo, useState } from \"react\";\nimport { resource, useResource } from \"@assistant-ui/tap\";\nimport { KeyValueSessionStorage, sessionNamespace } from \"./storage\";\nimport type { StatewireClient } from \"./statewire\";\nimport { randomId } from \"./util/randomId\";\n\nconst STORE = \"sessions\";\n\nconst request = <T>(req: IDBRequest<T>) =>\n new Promise<T>((resolve, reject) => {\n req.onsuccess = () => resolve(req.result);\n req.onerror = () => reject(req.error);\n });\n\nconst backends = new Map<string, KeyValueSessionStorage.Backend>();\n\n/** A key-value backend over one IndexedDB database; writes broadcast to other runtimes. */\nexport const indexedDbBackend = (\n database: string,\n): KeyValueSessionStorage.Backend => {\n const existing = backends.get(database);\n if (existing) return existing;\n if (typeof indexedDB === \"undefined\")\n throw new Error(\"statewire: IndexedDbSession requires indexedDB\");\n let opened: Promise<IDBDatabase> | undefined;\n const db = () =>\n (opened ??= (() => {\n const open = indexedDB.open(database, 1);\n open.onupgradeneeded = () => open.result.createObjectStore(STORE);\n return request(open);\n })());\n // Writes resolve once their transaction commits.\n const run = async <T>(\n mode: IDBTransactionMode,\n fn: (store: IDBObjectStore) => IDBRequest<T>,\n ) => {\n const tx = (await db()).transaction(STORE, mode);\n const committed =\n mode === \"readonly\"\n ? undefined\n : new Promise<void>((resolve, reject) => {\n tx.oncomplete = () => resolve();\n tx.onerror = () => reject(tx.error);\n tx.onabort = () => reject(tx.error);\n });\n const result = await request(fn(tx.objectStore(STORE)));\n await committed;\n return result;\n };\n const channel = new BroadcastChannel(`statewire-idb:${database}`);\n const announce = (key: string) => channel.postMessage(key);\n const backend: KeyValueSessionStorage.Backend = {\n getItem: async (key) =>\n ((await run(\"readonly\", (s) => s.get(key))) as string | undefined) ??\n null,\n setItem: async (key, value) => {\n await run(\"readwrite\", (s) => s.put(value, key));\n announce(key);\n },\n removeItem: async (key) => {\n await run(\"readwrite\", (s) => s.delete(key));\n announce(key);\n },\n getAllKeys: async () =>\n (await run(\"readonly\", (s) => s.getAllKeys())) as string[],\n withUnused: (key, action) =>\n navigator.locks.request(\n `statewire-active:${database}:${key}`,\n { ifAvailable: true },\n (lock) =>\n lock === null\n ? undefined\n : navigator.locks.request(\n `statewire-session:${database}:${key}`,\n { ifAvailable: true },\n (inner) => (inner === null ? undefined : action()),\n ),\n ),\n subscribe: (listener) => {\n const own = new BroadcastChannel(`statewire-idb:${database}`);\n own.onmessage = (event: MessageEvent<string>) => listener(event.data);\n return () => own.close();\n },\n };\n backends.set(database, backend);\n return backend;\n};\n\nconst holdLock = (name: string, mode: LockMode = \"exclusive\") => {\n let release: (() => void) | undefined;\n let disposed = false;\n void navigator.locks\n .request(name, { mode }, () => {\n if (disposed) return;\n return new Promise<void>((resolve) => {\n release = resolve;\n });\n })\n .catch(() => {});\n return () => {\n disposed = true;\n release?.();\n };\n};\n\nconst useIndexedDbSession = ({\n database = \"statewire\",\n key,\n namespace = \"statewire-session:\",\n principal,\n ...options\n}: IndexedDbSession.Options): StatewireClient.SessionStorage => {\n if (typeof navigator === \"undefined\" || navigator.locks === undefined)\n throw new Error(\n \"statewire: IndexedDbSession requires the Web Locks API (navigator.locks)\",\n );\n const backend = indexedDbBackend(database);\n const storageKey = sessionNamespace(namespace, principal) + key;\n const [tabId] = useState(randomId);\n useEffect(() => holdLock(`statewire-tab:${tabId}`), [tabId]);\n useEffect(\n () => holdLock(`statewire-active:${database}:${storageKey}`, \"shared\"),\n [database, storageKey],\n );\n const storage = useResource(\n KeyValueSessionStorage({ ...options, backend, key, namespace, principal }),\n );\n return useMemo(\n (): StatewireClient.SessionStorage => ({\n ...storage,\n lock: (fn) =>\n navigator.locks.request(\n `statewire-session:${database}:${storageKey}`,\n async () => fn(),\n ),\n notifyLock: (fn) =>\n navigator.locks.request(\n `statewire-notify:${database}:${storageKey}`,\n fn,\n ),\n owner: {\n id: tabId,\n whenGone: (id, signal) =>\n navigator.locks.request(`statewire-tab:${id}`, { signal }, () => {}),\n },\n subscribe: (listener) =>\n backend.subscribe!((changed) => {\n if (changed === storageKey) listener();\n }),\n }),\n [storage, backend, database, storageKey, tabId],\n );\n};\n\n/** Shared session storage on IndexedDB, keyed per principal and key: sharers serialize through Web Locks and hear each other over a BroadcastChannel. */\nexport const IndexedDbSession = resource(useIndexedDbSession);\n\nexport namespace IndexedDbSession {\n export type Options = Omit<\n KeyValueSessionStorage.Options,\n \"backend\" | \"namespace\"\n > & {\n /** IndexedDB database name; default \"statewire\". */\n database?: string;\n /** Default \"statewire-session:\". */\n namespace?: string;\n };\n}\n"],"mappings":"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"}
|
package/dist/storage.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { StatewireClient } from "./statewire.js";
|
|
2
|
-
|
|
2
|
+
/** The prefix of every record `KeyValueSessionStorage` keeps for `principal` under `namespace`. */
|
|
3
|
+
export declare const sessionNamespace: (namespace: string, principal: string) => string;
|
|
4
|
+
export declare const STORAGE_FORMAT = 6;
|
|
3
5
|
/** Persist sessions through a synchronous or asynchronous string storage backend. */
|
|
4
6
|
export declare const KeyValueSessionStorage: import("@assistant-ui/tap").Resource<StatewireClient.SessionStorage, [options: KeyValueSessionStorage.Options]>;
|
|
5
7
|
export declare namespace KeyValueSessionStorage {
|
|
@@ -23,6 +25,8 @@ export declare namespace KeyValueSessionStorage {
|
|
|
23
25
|
backend: Backend;
|
|
24
26
|
key: string;
|
|
25
27
|
namespace?: string;
|
|
28
|
+
/** The account whose session this storage holds; scopes the stored key, tags the record. */
|
|
29
|
+
principal: string;
|
|
26
30
|
/** The lane that sessions saved in a single-lane format lift into; defaults to the client's default protocol. */
|
|
27
31
|
protocol?: string;
|
|
28
32
|
migrate?: StatewireClient.SessionStorage["migrate"];
|
|
@@ -42,20 +46,24 @@ export declare namespace KeyValueSessionStorage {
|
|
|
42
46
|
onEvict?: (event: EvictEvent) => void;
|
|
43
47
|
};
|
|
44
48
|
}
|
|
45
|
-
/** Session storage in page memory, keyed by scope: the record and client identity survive remounts, not reloads. */
|
|
49
|
+
/** Session storage in page memory, keyed by principal and scope: the record and client identity survive remounts, not reloads. */
|
|
46
50
|
export declare const MemorySessionStorage: import("@assistant-ui/tap").Resource<StatewireClient.SessionStorage, [MemorySessionStorage.Options]>;
|
|
47
51
|
export declare namespace MemorySessionStorage {
|
|
48
52
|
type Options = {
|
|
53
|
+
/** The account whose session this storage holds. */
|
|
54
|
+
principal: string;
|
|
49
55
|
scope: string;
|
|
50
56
|
};
|
|
51
57
|
}
|
|
52
|
-
/** Reads the outboxes of every session under `namespace`, mounted or not. */
|
|
58
|
+
/** Reads the outboxes of every session `principal` keeps under `namespace`, mounted or not. */
|
|
53
59
|
export declare const readOutboxes: (backend: KeyValueSessionStorage.Backend, options: ReadOutboxes.Options) => Promise<readonly ReadOutboxes.Outbox[]>;
|
|
54
|
-
/** Calls `listener` with `readOutboxes` now and after every save under `namespace`; returns unsubscribe. */
|
|
60
|
+
/** Calls `listener` with `readOutboxes` now and after every save to `principal`'s sessions under `namespace`; returns unsubscribe. */
|
|
55
61
|
export declare const subscribeOutboxes: (backend: KeyValueSessionStorage.Backend, options: ReadOutboxes.Options, listener: (outboxes: readonly ReadOutboxes.Outbox[]) => void, onError: (error: unknown) => void) => (() => void);
|
|
56
62
|
export declare namespace ReadOutboxes {
|
|
57
63
|
type Options = {
|
|
58
64
|
namespace: string;
|
|
65
|
+
/** The account whose sessions to read. */
|
|
66
|
+
principal: string;
|
|
59
67
|
codec?: KeyValueSessionStorage.Codec;
|
|
60
68
|
/** The lane that single-lane formats lift into. */
|
|
61
69
|
protocol?: string;
|
package/dist/storage.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,eAAe,EAAE,uBAAoB;
|
|
1
|
+
{"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"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"}
|
package/dist/storage.js
CHANGED
|
@@ -70,6 +70,15 @@ const isProducer = (value) => {
|
|
|
70
70
|
const invalid = () => {
|
|
71
71
|
throw new Error("statewire: invalid stored session record");
|
|
72
72
|
};
|
|
73
|
+
const isPrincipal = (value) => typeof value === "string" && value !== "";
|
|
74
|
+
/** The prefix of every record `KeyValueSessionStorage` keeps for `principal` under `namespace`. */
|
|
75
|
+
const sessionNamespace = (namespace, principal) => {
|
|
76
|
+
if (!isPrincipal(principal)) throw new Error("statewire: storage requires a non-empty principal");
|
|
77
|
+
return `${namespace}${encodeURIComponent(principal)}:`;
|
|
78
|
+
};
|
|
79
|
+
const usePrincipal = (principal) => {
|
|
80
|
+
if (useRef(principal).current !== principal) throw new Error("statewire: principal changed; remount the account's thread/session");
|
|
81
|
+
};
|
|
73
82
|
const liftLegacy = (value, protocol) => {
|
|
74
83
|
const { clientId, nextSeq, commands, verdicts, failed } = value;
|
|
75
84
|
const lane = {
|
|
@@ -124,9 +133,15 @@ const upgrade = (record) => ({
|
|
|
124
133
|
}))
|
|
125
134
|
});
|
|
126
135
|
const isLegacyLane = (lane) => isLaneWith(lane, isSessionCommand, true);
|
|
127
|
-
const normalize = (parsed, protocol) => {
|
|
136
|
+
const normalize = (parsed, protocol, principal) => {
|
|
128
137
|
if (!isRecord(parsed)) return invalid();
|
|
129
|
-
const { statewireStorageVersion: format, savedAt: _savedAt, ...value } = parsed;
|
|
138
|
+
const { statewireStorageVersion: format, savedAt: _savedAt, principal: owner, ...value } = parsed;
|
|
139
|
+
if (format === 6) {
|
|
140
|
+
if (!isPrincipal(owner) || !isSessionRecord(value)) return invalid();
|
|
141
|
+
if (owner !== principal) throw new Error("statewire: stored session belongs to another principal");
|
|
142
|
+
return value;
|
|
143
|
+
}
|
|
144
|
+
if (owner !== void 0) return invalid();
|
|
130
145
|
if (format === 5) return isSessionRecord(value) ? value : invalid();
|
|
131
146
|
if (format === 4) return isRecordWith(value, isLegacyLane) ? upgrade(value) : invalid();
|
|
132
147
|
if (format === 3) return isRecordWith(value, (lane) => isLaneWith(lane, isLegacyCommand, true)) ? upgrade(identify(value)) : invalid();
|
|
@@ -134,9 +149,9 @@ const normalize = (parsed, protocol) => {
|
|
|
134
149
|
const { ctxNextSeq: _ctxNextSeq, context: _context, ...legacy } = value;
|
|
135
150
|
return upgrade(liftLegacy(legacy, protocol));
|
|
136
151
|
};
|
|
137
|
-
const STORAGE_FORMAT =
|
|
152
|
+
const STORAGE_FORMAT = 6;
|
|
138
153
|
const parse = async (raw, codec) => raw === null ? null : JSON.parse(codec === void 0 ? raw : await codec.decode(raw));
|
|
139
|
-
const decode = (raw, protocol) => raw === null ? null : normalize(JSON.parse(raw), protocol);
|
|
154
|
+
const decode = (raw, protocol, principal) => raw === null ? null : normalize(JSON.parse(raw), protocol, principal);
|
|
140
155
|
const isUnsent = (lane) => lane.commands.length > 0 || (lane.outbox?.length ?? 0) > 0;
|
|
141
156
|
const entryCount = (record) => record === null ? 0 : Object.values(record.lanes).reduce((sum, lane) => sum + lane.commands.length + (lane.outbox?.length ?? 0), 0);
|
|
142
157
|
const noticesOf = (record) => Object.entries(record.lanes).flatMap(([protocol, lane]) => (lane.inbox ?? []).map((n) => ({
|
|
@@ -162,8 +177,9 @@ const sweep = (options, currentKey, quota = false) => {
|
|
|
162
177
|
return run;
|
|
163
178
|
};
|
|
164
179
|
const sweepOnce = async (options, currentKey, quota = false) => {
|
|
165
|
-
const { backend, retention,
|
|
180
|
+
const { backend, retention, codec, onEvict, principal } = options;
|
|
166
181
|
if (!retention || !backend.getAllKeys) return;
|
|
182
|
+
const namespace = sessionNamespace(options.namespace ?? "", principal);
|
|
167
183
|
const candidate = async (raw) => {
|
|
168
184
|
let value;
|
|
169
185
|
try {
|
|
@@ -174,7 +190,7 @@ const sweepOnce = async (options, currentKey, quota = false) => {
|
|
|
174
190
|
if (value === null) return void 0;
|
|
175
191
|
let record;
|
|
176
192
|
try {
|
|
177
|
-
record = normalize(value, DEFAULT_PROTOCOL);
|
|
193
|
+
record = normalize(value, DEFAULT_PROTOCOL, principal);
|
|
178
194
|
} catch {
|
|
179
195
|
return;
|
|
180
196
|
}
|
|
@@ -245,7 +261,9 @@ const validLimit = (value, name) => {
|
|
|
245
261
|
if (value !== void 0 && (!Number.isSafeInteger(value) || value < 1)) throw new Error(`statewire: invalid session limit ${name}`);
|
|
246
262
|
};
|
|
247
263
|
const useKeyValueSessionStorage = (options) => {
|
|
248
|
-
const { backend, key, namespace = "", protocol = DEFAULT_PROTOCOL, migrate, retention: suppliedRetention, limits: suppliedLimits, codec, onError, onEvict } = options;
|
|
264
|
+
const { backend, key, namespace = "", principal, protocol = DEFAULT_PROTOCOL, migrate, retention: suppliedRetention, limits: suppliedLimits, codec, onError, onEvict } = options;
|
|
265
|
+
const scope = sessionNamespace(namespace, principal);
|
|
266
|
+
usePrincipal(principal);
|
|
249
267
|
const latest = useRef({
|
|
250
268
|
onError,
|
|
251
269
|
onEvict
|
|
@@ -278,6 +296,7 @@ const useKeyValueSessionStorage = (options) => {
|
|
|
278
296
|
backend,
|
|
279
297
|
key,
|
|
280
298
|
namespace,
|
|
299
|
+
principal,
|
|
281
300
|
onEvict: reportEvict,
|
|
282
301
|
...codec === void 0 ? {} : { codec },
|
|
283
302
|
...retention === void 0 ? {} : { retention }
|
|
@@ -285,11 +304,12 @@ const useKeyValueSessionStorage = (options) => {
|
|
|
285
304
|
backend,
|
|
286
305
|
key,
|
|
287
306
|
namespace,
|
|
307
|
+
principal,
|
|
288
308
|
retention,
|
|
289
309
|
codec,
|
|
290
310
|
reportEvict
|
|
291
311
|
]);
|
|
292
|
-
const storageKey =
|
|
312
|
+
const storageKey = scope + key;
|
|
293
313
|
if (retention !== void 0) {
|
|
294
314
|
if (!namespace) throw new Error("statewire: session retention requires a namespace");
|
|
295
315
|
if (!backend.getAllKeys) throw new Error("statewire: session retention requires getAllKeys");
|
|
@@ -324,7 +344,7 @@ const useKeyValueSessionStorage = (options) => {
|
|
|
324
344
|
state.ready = Promise.resolve(raw).then(async (value) => {
|
|
325
345
|
state.bytes = value?.length ?? 0;
|
|
326
346
|
const parsed = await parse(value, codec);
|
|
327
|
-
hydrate(parsed === null ? null : normalize(parsed, protocol));
|
|
347
|
+
hydrate(parsed === null ? null : normalize(parsed, protocol, principal));
|
|
328
348
|
}).catch((cause_0) => {
|
|
329
349
|
const error_1 = wrapStorageError(cause_0);
|
|
330
350
|
if (state.failure !== error_1) reportError(error_1);
|
|
@@ -334,7 +354,7 @@ const useKeyValueSessionStorage = (options) => {
|
|
|
334
354
|
state.ready.catch(() => {});
|
|
335
355
|
} else try {
|
|
336
356
|
state.bytes = raw?.length ?? 0;
|
|
337
|
-
hydrate(decode(raw, protocol));
|
|
357
|
+
hydrate(decode(raw, protocol, principal));
|
|
338
358
|
} catch (cause_1) {
|
|
339
359
|
const error_2 = wrapStorageError(cause_1);
|
|
340
360
|
reportError(error_2);
|
|
@@ -345,6 +365,7 @@ const useKeyValueSessionStorage = (options) => {
|
|
|
345
365
|
backend,
|
|
346
366
|
storageKey,
|
|
347
367
|
protocol,
|
|
368
|
+
principal,
|
|
348
369
|
codec,
|
|
349
370
|
reportError
|
|
350
371
|
]);
|
|
@@ -373,13 +394,13 @@ const useKeyValueSessionStorage = (options) => {
|
|
|
373
394
|
]);
|
|
374
395
|
useEffect(() => {
|
|
375
396
|
if (totalBytes === void 0) return;
|
|
376
|
-
otherBytes(backend,
|
|
397
|
+
otherBytes(backend, scope, storageKey).then((bytes) => {
|
|
377
398
|
state_0.others = bytes;
|
|
378
399
|
}, () => {});
|
|
379
400
|
}, [
|
|
380
401
|
state_0,
|
|
381
402
|
backend,
|
|
382
|
-
|
|
403
|
+
scope,
|
|
383
404
|
storageKey,
|
|
384
405
|
totalBytes
|
|
385
406
|
]);
|
|
@@ -412,7 +433,7 @@ const useKeyValueSessionStorage = (options) => {
|
|
|
412
433
|
const raw_0 = await backend.getItem(storageKey);
|
|
413
434
|
const parsed_0 = await parse(raw_0, codec);
|
|
414
435
|
state_0.bytes = raw_0?.length ?? 0;
|
|
415
|
-
state_0.record = parsed_0 === null ? null : normalize(parsed_0, protocol);
|
|
436
|
+
state_0.record = parsed_0 === null ? null : normalize(parsed_0, protocol, principal);
|
|
416
437
|
return state_0.record;
|
|
417
438
|
} catch (cause_3) {
|
|
418
439
|
const error_5 = wrapStorageError(cause_3);
|
|
@@ -434,7 +455,7 @@ const useKeyValueSessionStorage = (options) => {
|
|
|
434
455
|
}
|
|
435
456
|
if (typeof raw_1 !== "string" && raw_1 !== null) throw new Error("statewire: storage backend changed from synchronous to asynchronous");
|
|
436
457
|
try {
|
|
437
|
-
state_0.record = decode(raw_1, protocol);
|
|
458
|
+
state_0.record = decode(raw_1, protocol, principal);
|
|
438
459
|
return state_0.record;
|
|
439
460
|
} catch (cause_5) {
|
|
440
461
|
const error_7 = wrapStorageError(cause_5);
|
|
@@ -448,7 +469,8 @@ const useKeyValueSessionStorage = (options) => {
|
|
|
448
469
|
if (!isSessionRecord(record_1)) throw new Error("statewire: invalid session record");
|
|
449
470
|
const encoded_0 = JSON.stringify({
|
|
450
471
|
...record_1,
|
|
451
|
-
|
|
472
|
+
principal,
|
|
473
|
+
statewireStorageVersion: 6,
|
|
452
474
|
savedAt: Date.now()
|
|
453
475
|
});
|
|
454
476
|
checkLimits(record_1, encoded_0.length);
|
|
@@ -520,6 +542,7 @@ const useKeyValueSessionStorage = (options) => {
|
|
|
520
542
|
backend,
|
|
521
543
|
storageKey,
|
|
522
544
|
protocol,
|
|
545
|
+
principal,
|
|
523
546
|
migrate,
|
|
524
547
|
retention,
|
|
525
548
|
configured,
|
|
@@ -536,22 +559,24 @@ const KeyValueSessionStorage = resource(useKeyValueSessionStorage);
|
|
|
536
559
|
const pages = /* @__PURE__ */ new Map();
|
|
537
560
|
const useMemorySessionStorage = (t0) => {
|
|
538
561
|
const $ = c(2);
|
|
539
|
-
const { scope } = t0;
|
|
562
|
+
const { principal, scope } = t0;
|
|
563
|
+
const key = sessionNamespace("", principal) + scope;
|
|
564
|
+
usePrincipal(principal);
|
|
540
565
|
let t1;
|
|
541
|
-
if ($[0] !==
|
|
566
|
+
if ($[0] !== key) {
|
|
542
567
|
t1 = {
|
|
543
568
|
durable: false,
|
|
544
|
-
load: () => pages.get(
|
|
569
|
+
load: () => pages.get(key) ?? null,
|
|
545
570
|
save: (record) => {
|
|
546
|
-
pages.set(
|
|
571
|
+
pages.set(key, record);
|
|
547
572
|
}
|
|
548
573
|
};
|
|
549
|
-
$[0] =
|
|
574
|
+
$[0] = key;
|
|
550
575
|
$[1] = t1;
|
|
551
576
|
} else t1 = $[1];
|
|
552
577
|
return t1;
|
|
553
578
|
};
|
|
554
|
-
/** Session storage in page memory, keyed by scope: the record and client identity survive remounts, not reloads. */
|
|
579
|
+
/** Session storage in page memory, keyed by principal and scope: the record and client identity survive remounts, not reloads. */
|
|
555
580
|
const MemorySessionStorage = resource(useMemorySessionStorage);
|
|
556
581
|
const entriesOf = (record) => Object.entries(record.lanes).flatMap(([protocol, lane]) => {
|
|
557
582
|
const view = (c, state) => ({
|
|
@@ -568,17 +593,18 @@ const entriesOf = (record) => Object.entries(record.lanes).flatMap(([protocol, l
|
|
|
568
593
|
});
|
|
569
594
|
return [...[...lane.commands].sort((a, b) => a.seq - b.seq).map((c) => view(c, "sending")), ...(lane.outbox ?? []).map((c) => view(c, c.held ? "held" : "queued"))];
|
|
570
595
|
});
|
|
571
|
-
/** Reads the outboxes of every session under `namespace`, mounted or not. */
|
|
596
|
+
/** Reads the outboxes of every session `principal` keeps under `namespace`, mounted or not. */
|
|
572
597
|
const readOutboxes = async (backend, options) => {
|
|
573
598
|
if (!backend.getAllKeys) throw new Error("statewire: readOutboxes requires getAllKeys");
|
|
574
|
-
const { namespace, codec, protocol = DEFAULT_PROTOCOL } = options;
|
|
599
|
+
const { namespace, principal, codec, protocol = DEFAULT_PROTOCOL } = options;
|
|
575
600
|
if (!namespace) throw new Error("statewire: readOutboxes requires a namespace");
|
|
601
|
+
const scope = sessionNamespace(namespace, principal);
|
|
576
602
|
const result = [];
|
|
577
603
|
for (const key of await backend.getAllKeys()) {
|
|
578
|
-
if (!key.startsWith(
|
|
604
|
+
if (!key.startsWith(scope)) continue;
|
|
579
605
|
const parsed = await parse(await backend.getItem(key), codec);
|
|
580
606
|
if (parsed === null) continue;
|
|
581
|
-
const entries = entriesOf(normalize(parsed, protocol));
|
|
607
|
+
const entries = entriesOf(normalize(parsed, protocol, principal));
|
|
582
608
|
if (entries.length > 0) result.push({
|
|
583
609
|
key,
|
|
584
610
|
entries
|
|
@@ -586,8 +612,9 @@ const readOutboxes = async (backend, options) => {
|
|
|
586
612
|
}
|
|
587
613
|
return result;
|
|
588
614
|
};
|
|
589
|
-
/** Calls `listener` with `readOutboxes` now and after every save under `namespace`; returns unsubscribe. */
|
|
615
|
+
/** Calls `listener` with `readOutboxes` now and after every save to `principal`'s sessions under `namespace`; returns unsubscribe. */
|
|
590
616
|
const subscribeOutboxes = (backend, options, listener, onError) => {
|
|
617
|
+
const scope = sessionNamespace(options.namespace, options.principal);
|
|
591
618
|
let closed = false;
|
|
592
619
|
let reading = Promise.resolve();
|
|
593
620
|
const refresh = () => {
|
|
@@ -602,7 +629,7 @@ const subscribeOutboxes = (backend, options, listener, onError) => {
|
|
|
602
629
|
});
|
|
603
630
|
};
|
|
604
631
|
const onKey = (key) => {
|
|
605
|
-
if (key.startsWith(
|
|
632
|
+
if (key.startsWith(scope)) refresh();
|
|
606
633
|
};
|
|
607
634
|
const local = savers.get(backend) ?? /* @__PURE__ */ new Set();
|
|
608
635
|
savers.set(backend, local);
|
|
@@ -615,6 +642,6 @@ const subscribeOutboxes = (backend, options, listener, onError) => {
|
|
|
615
642
|
remote?.();
|
|
616
643
|
};
|
|
617
644
|
};
|
|
618
|
-
export { IndexedDbSession, KeyValueSessionStorage, MemorySessionStorage, STORAGE_FORMAT, indexedDbBackend, readOutboxes, subscribeOutboxes };
|
|
645
|
+
export { IndexedDbSession, KeyValueSessionStorage, MemorySessionStorage, STORAGE_FORMAT, indexedDbBackend, readOutboxes, sessionNamespace, subscribeOutboxes };
|
|
619
646
|
|
|
620
647
|
//# sourceMappingURL=storage.js.map
|
package/dist/storage.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"storage.js","names":["useEffect","useMemo","useRef","randomId","wrapStorageError","reviveValue","resource","StatewireClient","Statewire","StatewireProtocol","DEFAULT_PROTOCOL","MIN_SERVED_WIRE_VERSION","WIRE_VERSION","negotiationHeaders","protocolToken","isRecord","value","Record","Array","isArray","isOptionalString","undefined","isPositiveSafeInteger","Number","isSafeInteger","isStatement","isProtocol","isProducer","isIdentified","isFinite","isLegacyCommand","isSessionCommand","SessionCommand","isOutboxCommand","OutboxCommand","isLegacyOutboxCommand","isStoredSettled","StoredSettled","isSendFailure","isSessionVerdict","SessionVerdict","SendFailure","FailedCommand","protocol","produced","command","params","meta","failure","ts","seq","isFailedCommand","isArrayOf","is","item","every","isMark","mark","isLaneWith","isCommand","legacy","SessionLane","isSessionLane","isClientId","length","isRecordWith","isLane","lane","SessionRecord","Object","keys","values","isSessionRecord","identify","record","lanes","fromEntries","entries","map","commands","wire","version","id","Date","now","test","wireVersion","Range","applicationProtocols","Offer","invalid","Error","LegacyLane","outbox","Omit","hold","failed","LegacyRecord","clientId","liftLegacy","nextSeq","verdicts","legacyCommands","filter","_protocol","entry","target","upgrade","inbox","sort","a","b","f","method","held","const","localMeta","isLegacyLane","normalize","parsed","statewireStorageVersion","format","savedAt","_savedAt","ctxNextSeq","_ctxNextSeq","context","_context","STORAGE_FORMAT","parse","raw","codec","KeyValueSessionStorage","Codec","Promise","JSON","decode","isUnsent","entryCount","reduce","sum","noticesOf","CommandUpdate","flatMap","n","status","active","WeakMap","Backend","Map","savers","Set","key","announce","backend","listener","get","Candidate","sweeping","sweep","options","Options","currentKey","quota","run","resolve","catch","then","sweepOnce","set","retention","namespace","onEvict","getAllKeys","candidate","some","empty","noticed","startsWith","has","found","getItem","push","remove","reason","EvictEvent","again","removeItem","dropped","withUnused","expired","maxAgeMs","retained","quotaStart","Math","floor","index","overflow","maxEntries","isQuotaError","error","name","quotaError","limit","otherBytes","total","other","validLimit","useKeyValueSessionStorage","SessionStorage","migrate","suppliedRetention","limits","suppliedLimits","onError","latest","current","reportError","reportEvict","event","retain","outboxEntries","recordBytes","totalBytes","configured","storageKey","state","cause","bytes","others","ready","queue","pending","asynchronous","hydrated","hydrate","count","delete","checkLimits","persist","encoded","stored","encode","setItem","durable","read","load","save","stringify","finally","flush","action","subscribe","plain","pages","useMemorySessionStorage","t0","$","_c","scope","t1","MemorySessionStorage","entriesOf","OutboxEntry","view","c","OutboxState","structuredClone","readOutboxes","ReadOutboxes","Outbox","result","subscribeOutboxes","outboxes","closed","reading","refresh","onKey","local","add","remote","IndexedDbSession","indexedDbBackend","_c","reviveValue"],"sources":["../src/storage.ts"],"sourcesContent":["import { useEffect, useMemo, useRef } from \"react\";\nimport { randomId } from \"./util/randomId\";\nimport { wrapStorageError } from \"./client/storageError\";\nimport { reviveValue } from \"./wire/meta\";\nimport { resource } from \"@assistant-ui/tap\";\nimport type { StatewireClient } from \"./statewire\";\nimport type { Statewire } from \"./types\";\nimport type { StatewireProtocol } from \"./wire/protocol\";\nimport {\n DEFAULT_PROTOCOL,\n MIN_SERVED_WIRE_VERSION,\n WIRE_VERSION,\n negotiationHeaders,\n protocolToken,\n} from \"./wire/protocol\";\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === \"object\" && value !== null && !Array.isArray(value);\n\nconst isOptionalString = (value: unknown): boolean =>\n value === undefined || typeof value === \"string\";\n\nconst isPositiveSafeInteger = (value: unknown): value is number =>\n typeof value === \"number\" && Number.isSafeInteger(value) && value > 0;\n\nconst isStatement = (value: unknown): value is Record<string, unknown> =>\n isRecord(value) &&\n isProtocol(value[\"protocol\"]) &&\n isProducer(value[\"produced\"]) &&\n typeof value[\"method\"] === \"string\" &&\n Array.isArray(value[\"params\"]);\n\nconst isIdentified = (value: Record<string, unknown>) =>\n typeof value[\"id\"] === \"string\" &&\n value[\"id\"] !== \"\" &&\n typeof value[\"ts\"] === \"number\" &&\n Number.isFinite(value[\"ts\"]);\n\nconst isLegacyCommand = (value: unknown) =>\n isStatement(value) && isPositiveSafeInteger(value[\"seq\"]);\n\nconst isSessionCommand = (\n value: unknown,\n): value is StatewireClient.SessionCommand =>\n isLegacyCommand(value) &&\n isIdentified(value as Record<string, unknown>) &&\n isOptionalString((value as Record<string, unknown>)[\"owner\"]);\n\nconst isOutboxCommand = (\n value: unknown,\n): value is StatewireClient.OutboxCommand =>\n isStatement(value) &&\n isIdentified(value) &&\n (value[\"held\"] === undefined || value[\"held\"] === true) &&\n (value[\"released\"] === undefined || value[\"released\"] === true) &&\n isOptionalString(value[\"by\"]);\n\n// Format 4 outbox entries carried the hold as `hold: { meta }`.\nconst isLegacyOutboxCommand = (value: unknown) =>\n isStatement(value) &&\n isIdentified(value) &&\n (value[\"hold\"] === undefined || isRecord(value[\"hold\"])) &&\n (value[\"released\"] === undefined || value[\"released\"] === true) &&\n isOptionalString(value[\"by\"]);\n\nconst isStoredSettled = (\n value: unknown,\n): value is StatewireClient.StoredSettled =>\n isRecord(value) &&\n isIdentified(value) &&\n typeof value[\"method\"] === \"string\" &&\n Array.isArray(value[\"params\"]) &&\n (value[\"seq\"] === undefined || isPositiveSafeInteger(value[\"seq\"])) &&\n (value[\"failure\"] === undefined\n ? isRecord(value[\"result\"]) && Array.isArray(value[\"result\"][\"value\"])\n : value[\"result\"] === undefined && isSendFailure(value[\"failure\"]));\n\nconst isSessionVerdict = (\n value: unknown,\n): value is StatewireClient.SessionVerdict =>\n isRecord(value) &&\n isPositiveSafeInteger(value[\"seq\"]) &&\n isOptionalString(value[\"id\"]) &&\n (value[\"type\"] === \"result\" ||\n value[\"type\"] === \"rejected\" ||\n value[\"type\"] === \"failed\" ||\n value[\"type\"] === \"crashed\" ||\n value[\"type\"] === \"expired\") &&\n isOptionalString(value[\"code\"]) &&\n isOptionalString(value[\"message\"]);\n\nconst isSendFailure = (\n value: unknown,\n): value is StatewireClient.SendFailure => {\n if (!isRecord(value)) return false;\n switch (value[\"fate\"]) {\n case \"rejected\":\n return (\n value[\"cause\"] === \"server\" &&\n isOptionalString(value[\"code\"]) &&\n isOptionalString(value[\"message\"])\n );\n case \"failed\":\n return value[\"cause\"] === \"server\" && isOptionalString(value[\"message\"]);\n case \"crashed\":\n case \"expired\":\n return value[\"cause\"] === \"server\";\n case \"aborted\":\n return (\n (value[\"cause\"] === \"unmount\" ||\n value[\"cause\"] === \"stream-gone\" ||\n value[\"cause\"] === \"session-lost\" ||\n value[\"cause\"] === \"removed\" ||\n value[\"cause\"] === \"quota\") &&\n (value[\"predecessorLost\"] === undefined ||\n value[\"predecessorLost\"] === true)\n );\n case \"lost\":\n return (\n value[\"cause\"] === \"unmount\" ||\n value[\"cause\"] === \"stream-gone\" ||\n value[\"cause\"] === \"session-lost\"\n );\n default:\n return false;\n }\n};\n\ntype FailedCommand = {\n protocol: string;\n produced?: StatewireClient.SessionCommand[\"produced\"];\n command: string;\n params: unknown[];\n meta?: unknown;\n failure: StatewireClient.SendFailure;\n ts: number;\n seq: number;\n};\n\nconst isFailedCommand = (value: unknown): value is FailedCommand =>\n isRecord(value) &&\n isProtocol(value[\"protocol\"]) &&\n isProducer(value[\"produced\"]) &&\n typeof value[\"command\"] === \"string\" &&\n Array.isArray(value[\"params\"]) &&\n isSendFailure(value[\"failure\"]) &&\n typeof value[\"ts\"] === \"number\" &&\n Number.isFinite(value[\"ts\"]) &&\n isPositiveSafeInteger(value[\"seq\"]);\n\nconst isArrayOf = (value: unknown, is: (item: unknown) => boolean) =>\n value === undefined || (Array.isArray(value) && value.every(is));\n\nconst isMark = (mark: unknown) =>\n isRecord(mark) &&\n typeof mark[\"id\"] === \"string\" &&\n typeof mark[\"by\"] === \"string\" &&\n (mark[\"failure\"] === undefined || isSendFailure(mark[\"failure\"]));\n\nconst isLaneWith = (\n value: unknown,\n isCommand: (command: unknown) => boolean,\n legacy = false,\n): value is StatewireClient.SessionLane =>\n isRecord(value) &&\n isPositiveSafeInteger(value[\"nextSeq\"]) &&\n Array.isArray(value[\"commands\"]) &&\n value[\"commands\"].every(isCommand) &&\n isArrayOf(\n value[\"outbox\"],\n legacy ? isLegacyOutboxCommand : isOutboxCommand,\n ) &&\n isArrayOf(value[\"removed\"], isMark) &&\n isArrayOf(value[\"verdicts\"], isSessionVerdict) &&\n (legacy\n ? isArrayOf(value[\"failed\"], isFailedCommand)\n : value[\"failed\"] === undefined &&\n isArrayOf(value[\"inbox\"], isStoredSettled));\n\nconst isSessionLane = (value: unknown): value is StatewireClient.SessionLane =>\n isLaneWith(value, isSessionCommand);\n\nconst isClientId = (value: unknown): value is string =>\n typeof value === \"string\" && value.length > 0;\n\nconst isRecordWith = (\n value: unknown,\n isLane: (lane: unknown) => boolean,\n): value is StatewireClient.SessionRecord =>\n isRecord(value) &&\n isClientId(value[\"clientId\"]) &&\n isRecord(value[\"lanes\"]) &&\n Object.keys(value[\"lanes\"]).every(isProtocol) &&\n Object.values(value[\"lanes\"]).every(isLane);\n\nconst isSessionRecord = (\n value: unknown,\n): value is StatewireClient.SessionRecord => isRecordWith(value, isSessionLane);\n\n// Formats before 4 stamped commands without an id or an enqueue time.\nconst identify = (\n record: StatewireClient.SessionRecord,\n): StatewireClient.SessionRecord => ({\n ...record,\n lanes: Object.fromEntries(\n Object.entries(record.lanes).map(([protocol, lane]) => [\n protocol,\n {\n ...lane,\n commands: lane.commands.map((command) => ({\n ...command,\n // The statement shape is unchanged across the gap-fill contract.\n ...(command.produced?.wire.version === MIN_SERVED_WIRE_VERSION && {\n produced: { ...command.produced, wire: { version: WIRE_VERSION } },\n }),\n id: randomId(),\n ts: Date.now(),\n })),\n },\n ]),\n ),\n});\n\nconst isProtocol = (value: unknown) =>\n value === undefined ||\n (typeof value === \"string\" && value !== \"*\" && protocolToken.test(value));\n\nconst isProducer = (value: unknown) => {\n if (value === undefined) return true;\n if (!isRecord(value) || !isRecord(value[\"wire\"])) return false;\n try {\n negotiationHeaders({\n wireVersion: value[\"wire\"] as StatewireProtocol.Range,\n applicationProtocols: value[\"protocols\"] as\n | readonly StatewireProtocol.Offer[]\n | \"*\",\n });\n return value[\"protocols\"] !== undefined;\n } catch {\n return false;\n }\n};\n\nconst invalid = (): never => {\n throw new Error(\"statewire: invalid stored session record\");\n};\n\ntype LegacyLane = Record<string, unknown> & {\n commands: StatewireClient.SessionCommand[];\n outbox?: (Omit<StatewireClient.OutboxCommand, \"held\"> & {\n hold?: { meta?: unknown };\n })[];\n failed?: FailedCommand[];\n};\n\ntype LegacyRecord = { clientId: string; lanes: Record<string, LegacyLane> };\n\n// Formats 1 and 2 held one lane: it lifts under the primary protocol, failed\n// entries under their own.\nconst liftLegacy = (\n value: Record<string, unknown>,\n protocol: string,\n): LegacyRecord => {\n const { clientId, nextSeq, commands, verdicts, failed } = value;\n const lane = {\n nextSeq,\n commands: [],\n ...(verdicts !== undefined && { verdicts }),\n };\n if (!isClientId(clientId) || !isSessionLane(lane) || !Array.isArray(commands))\n return invalid();\n const legacyCommands = commands as (StatewireClient.SessionCommand & {\n protocol?: string;\n })[];\n if (!legacyCommands.every(isLegacyCommand)) return invalid();\n if (\n failed !== undefined &&\n (!Array.isArray(failed) || !failed.every(isFailedCommand))\n )\n return invalid();\n const lanes: Record<string, LegacyLane> = {\n [protocol]: {\n ...(lane as Record<string, unknown>),\n commands: legacyCommands\n .filter((command) => (command.protocol ?? protocol) === protocol)\n .map(({ protocol: _protocol, ...command }) => command),\n },\n };\n for (const entry of (failed ?? []) as FailedCommand[]) {\n const target = lanes[entry.protocol] ?? { nextSeq: 1, commands: [] };\n lanes[entry.protocol] = {\n ...target,\n failed: [...(target.failed ?? []), entry],\n };\n }\n return identify({ clientId, lanes } as never) as never;\n};\n\n// Format 5: holds are a flag with client-only localMeta; failed sends are\n// settled updates awaiting their onCommandChange ack.\nconst upgrade = (record: LegacyRecord): StatewireClient.SessionRecord => ({\n clientId: record.clientId,\n lanes: Object.fromEntries(\n Object.entries(record.lanes).map(([protocol, legacy]) => {\n const { outbox, failed, ...lane } = legacy;\n const inbox = [...(failed ?? [])]\n .sort((a, b) => a.ts - b.ts)\n .map((f): StatewireClient.StoredSettled => ({\n // Stable across re-migrations until the upgraded record is saved.\n id: `failed:${protocol}:${f.seq}:${f.ts}`,\n method: f.command,\n params: f.params,\n ...(f.meta !== undefined && { meta: f.meta }),\n ts: f.ts,\n failure: f.failure,\n }));\n return [\n protocol,\n {\n ...(lane as unknown as StatewireClient.SessionLane),\n ...(outbox?.length && {\n outbox: outbox.map(({ hold, ...command }) => ({\n ...command,\n ...(hold !== undefined && { held: true as const }),\n ...(hold?.meta !== undefined && { localMeta: hold.meta }),\n })),\n }),\n ...(inbox.length > 0 && { inbox }),\n },\n ];\n }),\n ),\n});\n\nconst isLegacyLane = (lane: unknown) =>\n isLaneWith(lane, isSessionCommand, true);\n\nconst normalize = (\n parsed: unknown,\n protocol: string,\n): StatewireClient.SessionRecord => {\n if (!isRecord(parsed)) return invalid();\n const {\n statewireStorageVersion: format,\n savedAt: _savedAt,\n ...value\n } = parsed;\n if (format === 5) return isSessionRecord(value) ? value : invalid();\n if (format === 4)\n return isRecordWith(value, isLegacyLane)\n ? upgrade(value as never)\n : invalid();\n if (format === 3)\n return isRecordWith(value, (lane) =>\n isLaneWith(lane, isLegacyCommand, true),\n )\n ? upgrade(identify(value) as never)\n : invalid();\n if (format !== undefined && format !== 1 && format !== 2)\n throw new Error(\"statewire: unsupported session storage version\");\n const { ctxNextSeq: _ctxNextSeq, context: _context, ...legacy } = value;\n return upgrade(liftLegacy(legacy, protocol));\n};\n\nexport const STORAGE_FORMAT = 5;\n\nconst parse = async (\n raw: string | null,\n codec: KeyValueSessionStorage.Codec | undefined,\n): Promise<unknown> =>\n raw === null\n ? null\n : JSON.parse(codec === undefined ? raw : await codec.decode(raw));\n\nconst decode = (\n raw: string | null,\n protocol: string,\n): StatewireClient.SessionRecord | null =>\n raw === null ? null : normalize(JSON.parse(raw), protocol);\n\nconst isUnsent = (lane: StatewireClient.SessionLane) =>\n lane.commands.length > 0 || (lane.outbox?.length ?? 0) > 0;\n\nconst entryCount = (record: StatewireClient.SessionRecord | null) =>\n record === null\n ? 0\n : Object.values(record.lanes).reduce(\n (sum, lane) => sum + lane.commands.length + (lane.outbox?.length ?? 0),\n 0,\n );\n\nconst noticesOf = (\n record: StatewireClient.SessionRecord,\n): Statewire.CommandUpdate[] =>\n Object.entries(record.lanes).flatMap(([protocol, lane]) =>\n (lane.inbox ?? []).map((n) => ({\n protocol,\n id: n.id,\n method: n.method,\n params: n.params as never[],\n ...(n.seq !== undefined && { seq: n.seq }),\n status: \"settled\" as const,\n ...(n.failure !== undefined && { failure: n.failure }),\n ...(n.localMeta !== undefined && { localMeta: n.localMeta }),\n })),\n );\n\nconst active = new WeakMap<\n KeyValueSessionStorage.Backend,\n Map<string, number>\n>();\n\n/** Same-runtime saves, per backend, for `subscribeOutboxes`. */\nconst savers = new WeakMap<\n KeyValueSessionStorage.Backend,\n Set<(key: string) => void>\n>();\n\nconst announce = (backend: KeyValueSessionStorage.Backend, key: string) => {\n for (const listener of savers.get(backend) ?? []) listener(key);\n};\n\ntype Candidate = {\n key: string;\n savedAt: number;\n record: StatewireClient.SessionRecord;\n};\n\nconst sweeping = new WeakMap<KeyValueSessionStorage.Backend, Promise<void>>();\n\n// Sweeps of one backend run one at a time.\nconst sweep = (\n options: KeyValueSessionStorage.Options,\n currentKey: string,\n quota = false,\n): Promise<void> => {\n const run = (sweeping.get(options.backend) ?? Promise.resolve())\n .catch(() => {})\n .then(() => sweepOnce(options, currentKey, quota));\n sweeping.set(options.backend, run);\n return run;\n};\n\n// Records with unsent commands are never evicted. Empty records go first;\n// records holding only failure notices go once past their age.\nconst sweepOnce = async (\n options: KeyValueSessionStorage.Options,\n currentKey: string,\n quota = false,\n) => {\n const { backend, retention, namespace = \"\", codec, onEvict } = options;\n if (!retention || !backend.getAllKeys) return;\n const candidate = async (raw: string | null) => {\n let value: unknown;\n try {\n value = await parse(raw, codec);\n } catch {\n return undefined;\n }\n if (value === null) return undefined;\n let record: StatewireClient.SessionRecord;\n try {\n record = normalize(value, DEFAULT_PROTOCOL);\n } catch {\n return undefined;\n }\n if (Object.values(record.lanes).some(isUnsent)) return undefined;\n const savedAt =\n (value as Record<string, unknown>)[\"savedAt\"] ??\n (value as Record<string, unknown>)[\"updatedAt\"];\n return typeof savedAt === \"number\" && Number.isFinite(savedAt)\n ? { savedAt, record }\n : undefined;\n };\n const empty: Candidate[] = [];\n const noticed: Candidate[] = [];\n for (const key of await backend.getAllKeys()) {\n if (\n !key.startsWith(namespace) ||\n key === currentKey ||\n active.get(backend)?.has(key)\n )\n continue;\n const found = await candidate(await backend.getItem(key));\n if (found === undefined) continue;\n (noticesOf(found.record).length > 0 ? noticed : empty).push({\n key,\n ...found,\n });\n }\n const remove = async (\n entry: Candidate,\n reason: KeyValueSessionStorage.EvictEvent[\"reason\"],\n ) => {\n const run = async () => {\n if (active.get(backend)?.has(entry.key)) return;\n const again = await candidate(await backend.getItem(entry.key));\n if (again?.savedAt !== entry.savedAt) return;\n if (active.get(backend)?.has(entry.key)) return;\n await backend.removeItem(entry.key);\n announce(backend, entry.key);\n onEvict?.({\n key: entry.key,\n reason,\n dropped: noticesOf(again.record),\n });\n };\n if (backend.withUnused) await backend.withUnused(entry.key, run);\n else await run();\n };\n const now = Date.now();\n const expired = (entry: Candidate) =>\n retention.maxAgeMs !== undefined &&\n now - entry.savedAt > retention.maxAgeMs;\n empty.sort((a, b) => b.savedAt - a.savedAt);\n let retained = 1;\n const quotaStart = Math.floor(empty.length / 2);\n for (const [index, entry] of empty.entries()) {\n const overflow =\n retention.maxEntries !== undefined && retained >= retention.maxEntries;\n if (expired(entry)) await remove(entry, \"expired\");\n else if (overflow) await remove(entry, \"overflow\");\n else if (quota && index >= quotaStart) await remove(entry, \"quota\");\n else retained++;\n }\n for (const entry of noticed)\n if (expired(entry)) await remove(entry, \"expired\");\n};\n\nconst isQuotaError = (error: unknown) =>\n error instanceof Error && error.name === \"QuotaExceededError\";\n\nconst quotaError = (limit: string) => {\n const error = new Error(`statewire: session ${limit} limit reached`);\n error.name = \"StatewireQuotaError\";\n return error;\n};\n\nconst otherBytes = async (\n backend: KeyValueSessionStorage.Backend,\n namespace: string,\n key: string,\n) => {\n let total = 0;\n for (const other of (await backend.getAllKeys?.()) ?? []) {\n if (other === key || !other.startsWith(namespace)) continue;\n total += (await backend.getItem(other))?.length ?? 0;\n }\n return total;\n};\n\nconst validLimit = (value: number | undefined, name: string) => {\n if (value !== undefined && (!Number.isSafeInteger(value) || value < 1))\n throw new Error(`statewire: invalid session limit ${name}`);\n};\n\nconst useKeyValueSessionStorage = (\n options: KeyValueSessionStorage.Options,\n): StatewireClient.SessionStorage => {\n const {\n backend,\n key,\n namespace = \"\",\n protocol = DEFAULT_PROTOCOL,\n migrate,\n retention: suppliedRetention,\n limits: suppliedLimits,\n codec,\n onError,\n onEvict,\n } = options;\n const latest = useRef({ onError, onEvict });\n latest.current = { onError, onEvict };\n const reportError = useMemo(\n () => (error: unknown) => latest.current.onError?.(error),\n [],\n );\n const reportEvict = useMemo(\n () => (event: KeyValueSessionStorage.EvictEvent) =>\n latest.current.onEvict?.(event),\n [],\n );\n const retain = suppliedRetention !== undefined;\n const maxAgeMs = suppliedRetention?.maxAgeMs;\n const maxEntries = suppliedRetention?.maxEntries;\n const retention = useMemo(\n () =>\n retain\n ? {\n ...(maxAgeMs === undefined ? {} : { maxAgeMs }),\n ...(maxEntries === undefined ? {} : { maxEntries }),\n }\n : undefined,\n [retain, maxAgeMs, maxEntries],\n );\n const outboxEntries = suppliedLimits?.outboxEntries;\n const recordBytes = suppliedLimits?.recordBytes;\n const totalBytes = suppliedLimits?.totalBytes;\n validLimit(outboxEntries, \"outboxEntries\");\n validLimit(recordBytes, \"recordBytes\");\n validLimit(totalBytes, \"totalBytes\");\n if (totalBytes !== undefined && !backend.getAllKeys)\n throw new Error(\"statewire: a totalBytes limit requires getAllKeys\");\n const configured = useMemo(\n () => ({\n backend,\n key,\n namespace,\n onEvict: reportEvict,\n ...(codec === undefined ? {} : { codec }),\n ...(retention === undefined ? {} : { retention }),\n }),\n [backend, key, namespace, retention, codec, reportEvict],\n );\n const storageKey = namespace + key;\n if (retention !== undefined) {\n if (!namespace)\n throw new Error(\"statewire: session retention requires a namespace\");\n if (!backend.getAllKeys)\n throw new Error(\"statewire: session retention requires getAllKeys\");\n if (\n retention.maxAgeMs !== undefined &&\n (!Number.isFinite(retention.maxAgeMs) || retention.maxAgeMs < 0)\n )\n throw new Error(\"statewire: invalid session retention maxAgeMs\");\n if (\n retention.maxEntries !== undefined &&\n (!Number.isSafeInteger(retention.maxEntries) || retention.maxEntries < 1)\n )\n throw new Error(\"statewire: invalid session retention maxEntries\");\n }\n const state = useMemo(() => {\n let raw: string | null | Promise<string | null>;\n try {\n raw = backend.getItem(storageKey);\n } catch (cause) {\n const error = wrapStorageError(cause);\n reportError(error);\n throw error;\n }\n const state = {\n record: null as StatewireClient.SessionRecord | null,\n bytes: 0,\n others: undefined as number | undefined,\n ready: undefined as Promise<void> | undefined,\n queue: Promise.resolve(),\n pending: 0,\n failure: undefined as unknown,\n asynchronous:\n codec !== undefined || (typeof raw !== \"string\" && raw !== null),\n hydrated: false,\n };\n const hydrate = (record: StatewireClient.SessionRecord | null) => {\n state.record = record;\n state.hydrated = true;\n };\n if (state.asynchronous) {\n state.ready = Promise.resolve(raw)\n .then(async (value) => {\n state.bytes = value?.length ?? 0;\n const parsed = await parse(value, codec);\n hydrate(parsed === null ? null : normalize(parsed, protocol));\n })\n .catch((cause: unknown) => {\n const error = wrapStorageError(cause);\n if (state.failure !== error) reportError(error);\n state.failure = error;\n throw error;\n });\n state.ready.catch(() => {});\n } else {\n try {\n state.bytes = (raw as string | null)?.length ?? 0;\n hydrate(decode(raw as string | null, protocol));\n } catch (cause) {\n const error = wrapStorageError(cause);\n reportError(error);\n throw error;\n }\n }\n return state;\n }, [backend, storageKey, protocol, codec, reportError]);\n\n useEffect(() => {\n const entries = active.get(backend) ?? new Map<string, number>();\n active.set(backend, entries);\n entries.set(storageKey, (entries.get(storageKey) ?? 0) + 1);\n return () => {\n const count = entries.get(storageKey)! - 1;\n if (count === 0) entries.delete(storageKey);\n else entries.set(storageKey, count);\n };\n }, [backend, storageKey]);\n\n useEffect(() => {\n if (retention !== undefined)\n void sweep(configured, storageKey).catch((cause: unknown) => {\n const error = wrapStorageError(cause);\n state.failure = error;\n reportError(error);\n });\n }, [state, configured, storageKey, retention, reportError]);\n\n useEffect(() => {\n if (totalBytes === undefined) return;\n void otherBytes(backend, namespace, storageKey).then(\n (bytes) => {\n state.others = bytes;\n },\n () => {},\n );\n }, [state, backend, namespace, storageKey, totalBytes]);\n\n return useMemo(() => {\n const checkLimits = (\n record: StatewireClient.SessionRecord,\n bytes: number,\n ) => {\n // Limits gate writes that add an entry; settling and stamping always land.\n if (entryCount(record) <= entryCount(state.record)) return;\n if (outboxEntries !== undefined && entryCount(record) > outboxEntries)\n throw quotaError(\"outboxEntries\");\n if (recordBytes !== undefined && bytes > recordBytes)\n throw quotaError(\"recordBytes\");\n if (\n totalBytes !== undefined &&\n state.others !== undefined &&\n state.others + bytes > totalBytes\n )\n throw quotaError(\"totalBytes\");\n };\n const persist = async (encoded: string) => {\n const stored =\n codec === undefined ? encoded : await codec.encode(encoded);\n try {\n await backend.setItem(storageKey, stored);\n } catch (error) {\n if (!isQuotaError(error)) throw error;\n if (retention !== undefined) await sweep(configured, storageKey, true);\n await backend.setItem(storageKey, stored);\n }\n announce(backend, storageKey);\n };\n return {\n durable: true,\n ...(state.ready === undefined ? {} : { ready: state.ready }),\n read: async () => {\n if (state.failure !== undefined) throw state.failure;\n await state.ready;\n await state.queue.catch(() => {});\n try {\n const raw = await backend.getItem(storageKey);\n const parsed = await parse(raw, codec);\n state.bytes = raw?.length ?? 0;\n state.record = parsed === null ? null : normalize(parsed, protocol);\n return state.record;\n } catch (cause) {\n const error = wrapStorageError(cause);\n reportError(error);\n throw error;\n }\n },\n load: () => {\n if (state.failure !== undefined) throw state.failure;\n if (!state.hydrated)\n throw new Error(\n \"statewire: await session storage readiness before loading\",\n );\n if (state.asynchronous || state.pending > 0) return state.record;\n let raw: string | null | Promise<string | null>;\n try {\n raw = backend.getItem(storageKey);\n } catch (cause) {\n const error = wrapStorageError(cause);\n reportError(error);\n throw error;\n }\n if (typeof raw !== \"string\" && raw !== null)\n throw new Error(\n \"statewire: storage backend changed from synchronous to asynchronous\",\n );\n try {\n state.record = decode(raw, protocol);\n return state.record;\n } catch (cause) {\n const error = wrapStorageError(cause);\n reportError(error);\n throw error;\n }\n },\n save: (record: StatewireClient.SessionRecord) => {\n if (state.failure !== undefined) throw state.failure;\n if (!state.hydrated)\n throw new Error(\n \"statewire: await session storage readiness before saving\",\n );\n if (!isSessionRecord(record))\n throw new Error(\"statewire: invalid session record\");\n const encoded = JSON.stringify({\n ...record,\n statewireStorageVersion: STORAGE_FORMAT,\n savedAt: Date.now(),\n });\n checkLimits(record, encoded.length);\n if (!state.asynchronous && state.pending === 0) {\n try {\n backend.setItem(storageKey, encoded) as void;\n } catch (error) {\n if (!isQuotaError(error)) {\n state.failure = wrapStorageError(error);\n reportError(state.failure);\n throw state.failure;\n }\n // A write that adds an entry is refused; the rest retry after the sweep.\n if (entryCount(record) > entryCount(state.record)) {\n if (retention !== undefined)\n void sweep(configured, storageKey, true).catch(() => {});\n throw quotaError(\"backend\");\n }\n state.record = record;\n state.bytes = encoded.length;\n state.pending++;\n state.queue = persist(encoded).finally(() => {\n state.pending--;\n });\n state.queue.catch((cause: unknown) => {\n const failure = wrapStorageError(cause);\n if (state.failure !== failure) reportError(failure);\n state.failure = failure;\n });\n return;\n }\n state.record = record;\n state.bytes = encoded.length;\n announce(backend, storageKey);\n return;\n }\n state.record = record;\n state.bytes = encoded.length;\n state.pending++;\n state.queue = state.queue\n .then(() => state.ready)\n .then(() => persist(encoded))\n .catch((cause: unknown) => {\n const error = wrapStorageError(cause);\n if (state.failure !== error) reportError(error);\n state.failure = error;\n throw error;\n })\n .finally(() => {\n state.pending--;\n });\n state.queue.catch(() => {});\n },\n flush: () => {\n if (state.failure !== undefined) throw state.failure;\n if (!state.asynchronous && state.pending === 0) return;\n return (async () => {\n await state.ready;\n for (;;) {\n const queue = state.queue;\n await queue;\n if (queue === state.queue) return;\n }\n })();\n },\n onEvict: ({ reason, dropped }) =>\n reportEvict({ key: storageKey, reason, dropped }),\n ...(migrate === undefined ? {} : { migrate }),\n } satisfies StatewireClient.SessionStorage;\n }, [\n state,\n backend,\n storageKey,\n protocol,\n migrate,\n retention,\n configured,\n codec,\n outboxEntries,\n recordBytes,\n totalBytes,\n reportError,\n reportEvict,\n ]);\n};\n\n/** Persist sessions through a synchronous or asynchronous string storage backend. */\nexport const KeyValueSessionStorage = resource(useKeyValueSessionStorage);\n\nexport namespace KeyValueSessionStorage {\n export type Backend = {\n getItem: (key: string) => string | null | Promise<string | null>;\n setItem: (key: string, value: string) => void | Promise<void>;\n removeItem: (key: string) => void | Promise<void>;\n getAllKeys?: () => readonly string[] | Promise<readonly string[]>;\n /** Runs cleanup under exclusive access, skipping keys in use by another runtime. */\n withUnused?: (key: string, action: () => Promise<void>) => Promise<void>;\n /** Notifies of keys other runtimes wrote; returns unsubscribe. */\n subscribe?: (listener: (key: string) => void) => () => void;\n };\n /** Transforms the stored string, e.g. encryption or compression. */\n export type Codec = {\n encode: (plain: string) => Promise<string>;\n decode: (stored: string) => Promise<string>;\n };\n export type EvictEvent = StatewireClient.EvictEvent;\n export type Options = {\n backend: Backend;\n key: string;\n namespace?: string;\n /** The lane that sessions saved in a single-lane format lift into; defaults to the client's default protocol. */\n protocol?: string;\n migrate?: StatewireClient.SessionStorage[\"migrate\"];\n retention?: { maxAgeMs?: number; maxEntries?: number };\n /** Off by default; a write that adds an entry past one rejects `{ fate: \"aborted\", cause: \"quota\" }`. */\n limits?: {\n outboxEntries?: number;\n recordBytes?: number;\n totalBytes?: number;\n };\n codec?: Codec;\n onError?: (error: unknown) => void;\n /** Reports evicted records and dropped failure notices. */\n onEvict?: (event: EvictEvent) => void;\n };\n}\n\nconst pages = new Map<string, StatewireClient.SessionRecord | null>();\n\nconst useMemorySessionStorage = ({\n scope,\n}: MemorySessionStorage.Options): StatewireClient.SessionStorage =>\n useMemo(\n () => ({\n durable: false,\n load: () => pages.get(scope) ?? null,\n save: (record) => {\n pages.set(scope, record);\n },\n }),\n [scope],\n );\n\n/** Session storage in page memory, keyed by scope: the record and client identity survive remounts, not reloads. */\nexport const MemorySessionStorage = resource(useMemorySessionStorage);\n\nexport namespace MemorySessionStorage {\n export type Options = { scope: string };\n}\n\nconst entriesOf = (\n record: StatewireClient.SessionRecord,\n): StatewireClient.OutboxEntry[] =>\n Object.entries(record.lanes).flatMap(([protocol, lane]) => {\n const view = (\n c: StatewireClient.OutboxCommand | StatewireClient.SessionCommand,\n state: StatewireClient.OutboxState,\n ): StatewireClient.OutboxEntry => ({\n id: c.id,\n protocol,\n command: {\n method: c.method,\n params:\n c.meta === undefined\n ? c.params\n : (reviveValue(structuredClone(c.params), c.meta) as unknown[]),\n },\n ts: c.ts,\n state,\n localMeta: c.localMeta,\n durable: true,\n });\n return [\n ...[...lane.commands]\n .sort((a, b) => a.seq - b.seq)\n .map((c) => view(c, \"sending\")),\n ...(lane.outbox ?? []).map((c) => view(c, c.held ? \"held\" : \"queued\")),\n ];\n });\n\n/** Reads the outboxes of every session under `namespace`, mounted or not. */\nexport const readOutboxes = async (\n backend: KeyValueSessionStorage.Backend,\n options: ReadOutboxes.Options,\n): Promise<readonly ReadOutboxes.Outbox[]> => {\n if (!backend.getAllKeys)\n throw new Error(\"statewire: readOutboxes requires getAllKeys\");\n const { namespace, codec, protocol = DEFAULT_PROTOCOL } = options;\n if (!namespace)\n throw new Error(\"statewire: readOutboxes requires a namespace\");\n const result: ReadOutboxes.Outbox[] = [];\n for (const key of await backend.getAllKeys()) {\n if (!key.startsWith(namespace)) continue;\n const parsed = await parse(await backend.getItem(key), codec);\n if (parsed === null) continue;\n const entries = entriesOf(normalize(parsed, protocol));\n if (entries.length > 0) result.push({ key, entries });\n }\n return result;\n};\n\n/** Calls `listener` with `readOutboxes` now and after every save under `namespace`; returns unsubscribe. */\nexport const subscribeOutboxes = (\n backend: KeyValueSessionStorage.Backend,\n options: ReadOutboxes.Options,\n listener: (outboxes: readonly ReadOutboxes.Outbox[]) => void,\n onError: (error: unknown) => void,\n): (() => void) => {\n let closed = false;\n let reading = Promise.resolve();\n const refresh = () => {\n reading = reading.then(async () => {\n if (closed) return;\n try {\n const outboxes = await readOutboxes(backend, options);\n if (!closed) listener(outboxes);\n } catch (error) {\n onError(error);\n }\n });\n };\n const onKey = (key: string) => {\n if (key.startsWith(options.namespace)) refresh();\n };\n const local = savers.get(backend) ?? new Set();\n savers.set(backend, local);\n local.add(onKey);\n const remote = backend.subscribe?.(onKey);\n refresh();\n return () => {\n closed = true;\n local.delete(onKey);\n remote?.();\n };\n};\n\nexport namespace ReadOutboxes {\n export type Options = {\n namespace: string;\n codec?: KeyValueSessionStorage.Codec;\n /** The lane that single-lane formats lift into. */\n protocol?: string;\n };\n export type Outbox = {\n key: string;\n entries: readonly StatewireClient.OutboxEntry[];\n };\n}\nexport { IndexedDbSession, indexedDbBackend } from \"./indexedDb\";\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"storage.js","names":["useEffect","useMemo","useRef","randomId","wrapStorageError","reviveValue","resource","StatewireClient","Statewire","StatewireProtocol","DEFAULT_PROTOCOL","MIN_SERVED_WIRE_VERSION","WIRE_VERSION","negotiationHeaders","protocolToken","isRecord","value","Record","Array","isArray","isOptionalString","undefined","isPositiveSafeInteger","Number","isSafeInteger","isStatement","isProtocol","isProducer","isIdentified","isFinite","isLegacyCommand","isSessionCommand","SessionCommand","isOutboxCommand","OutboxCommand","isLegacyOutboxCommand","isStoredSettled","StoredSettled","isSendFailure","isSessionVerdict","SessionVerdict","SendFailure","FailedCommand","protocol","produced","command","params","meta","failure","ts","seq","isFailedCommand","isArrayOf","is","item","every","isMark","mark","isLaneWith","isCommand","legacy","SessionLane","isSessionLane","isClientId","length","isRecordWith","isLane","lane","SessionRecord","Object","keys","values","isSessionRecord","identify","record","lanes","fromEntries","entries","map","commands","wire","version","id","Date","now","test","wireVersion","Range","applicationProtocols","Offer","invalid","Error","isPrincipal","sessionNamespace","namespace","principal","encodeURIComponent","usePrincipal","initial","current","LegacyLane","outbox","Omit","hold","failed","LegacyRecord","clientId","liftLegacy","nextSeq","verdicts","legacyCommands","filter","_protocol","entry","target","upgrade","inbox","sort","a","b","f","method","held","const","localMeta","isLegacyLane","normalize","parsed","statewireStorageVersion","format","savedAt","_savedAt","owner","ctxNextSeq","_ctxNextSeq","context","_context","STORAGE_FORMAT","parse","raw","codec","KeyValueSessionStorage","Codec","Promise","JSON","decode","isUnsent","entryCount","reduce","sum","noticesOf","CommandUpdate","flatMap","n","status","active","WeakMap","Backend","Map","savers","Set","key","announce","backend","listener","get","Candidate","sweeping","sweep","options","Options","currentKey","quota","run","resolve","catch","then","sweepOnce","set","retention","onEvict","getAllKeys","candidate","some","empty","noticed","startsWith","has","found","getItem","push","remove","reason","EvictEvent","again","removeItem","dropped","withUnused","expired","maxAgeMs","retained","quotaStart","Math","floor","index","overflow","maxEntries","isQuotaError","error","name","quotaError","limit","otherBytes","total","other","validLimit","useKeyValueSessionStorage","SessionStorage","migrate","suppliedRetention","limits","suppliedLimits","onError","scope","latest","reportError","reportEvict","event","retain","outboxEntries","recordBytes","totalBytes","configured","storageKey","state","cause","bytes","others","ready","queue","pending","asynchronous","hydrated","hydrate","count","delete","checkLimits","persist","encoded","stored","encode","setItem","durable","read","load","save","stringify","finally","flush","action","subscribe","plain","pages","useMemorySessionStorage","t0","$","_c","t1","MemorySessionStorage","entriesOf","OutboxEntry","view","c","OutboxState","structuredClone","readOutboxes","ReadOutboxes","Outbox","result","subscribeOutboxes","outboxes","closed","reading","refresh","onKey","local","add","remote","IndexedDbSession","indexedDbBackend","_c","reviveValue"],"sources":["../src/storage.ts"],"sourcesContent":["import { useEffect, useMemo, useRef } from \"react\";\nimport { randomId } from \"./util/randomId\";\nimport { wrapStorageError } from \"./client/storageError\";\nimport { reviveValue } from \"./wire/meta\";\nimport { resource } from \"@assistant-ui/tap\";\nimport type { StatewireClient } from \"./statewire\";\nimport type { Statewire } from \"./types\";\nimport type { StatewireProtocol } from \"./wire/protocol\";\nimport {\n DEFAULT_PROTOCOL,\n MIN_SERVED_WIRE_VERSION,\n WIRE_VERSION,\n negotiationHeaders,\n protocolToken,\n} from \"./wire/protocol\";\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === \"object\" && value !== null && !Array.isArray(value);\n\nconst isOptionalString = (value: unknown): boolean =>\n value === undefined || typeof value === \"string\";\n\nconst isPositiveSafeInteger = (value: unknown): value is number =>\n typeof value === \"number\" && Number.isSafeInteger(value) && value > 0;\n\nconst isStatement = (value: unknown): value is Record<string, unknown> =>\n isRecord(value) &&\n isProtocol(value[\"protocol\"]) &&\n isProducer(value[\"produced\"]) &&\n typeof value[\"method\"] === \"string\" &&\n Array.isArray(value[\"params\"]);\n\nconst isIdentified = (value: Record<string, unknown>) =>\n typeof value[\"id\"] === \"string\" &&\n value[\"id\"] !== \"\" &&\n typeof value[\"ts\"] === \"number\" &&\n Number.isFinite(value[\"ts\"]);\n\nconst isLegacyCommand = (value: unknown) =>\n isStatement(value) && isPositiveSafeInteger(value[\"seq\"]);\n\nconst isSessionCommand = (\n value: unknown,\n): value is StatewireClient.SessionCommand =>\n isLegacyCommand(value) &&\n isIdentified(value as Record<string, unknown>) &&\n isOptionalString((value as Record<string, unknown>)[\"owner\"]);\n\nconst isOutboxCommand = (\n value: unknown,\n): value is StatewireClient.OutboxCommand =>\n isStatement(value) &&\n isIdentified(value) &&\n (value[\"held\"] === undefined || value[\"held\"] === true) &&\n (value[\"released\"] === undefined || value[\"released\"] === true) &&\n isOptionalString(value[\"by\"]);\n\n// Format 4 outbox entries carried the hold as `hold: { meta }`.\nconst isLegacyOutboxCommand = (value: unknown) =>\n isStatement(value) &&\n isIdentified(value) &&\n (value[\"hold\"] === undefined || isRecord(value[\"hold\"])) &&\n (value[\"released\"] === undefined || value[\"released\"] === true) &&\n isOptionalString(value[\"by\"]);\n\nconst isStoredSettled = (\n value: unknown,\n): value is StatewireClient.StoredSettled =>\n isRecord(value) &&\n isIdentified(value) &&\n typeof value[\"method\"] === \"string\" &&\n Array.isArray(value[\"params\"]) &&\n (value[\"seq\"] === undefined || isPositiveSafeInteger(value[\"seq\"])) &&\n (value[\"failure\"] === undefined\n ? isRecord(value[\"result\"]) && Array.isArray(value[\"result\"][\"value\"])\n : value[\"result\"] === undefined && isSendFailure(value[\"failure\"]));\n\nconst isSessionVerdict = (\n value: unknown,\n): value is StatewireClient.SessionVerdict =>\n isRecord(value) &&\n isPositiveSafeInteger(value[\"seq\"]) &&\n isOptionalString(value[\"id\"]) &&\n (value[\"type\"] === \"result\" ||\n value[\"type\"] === \"rejected\" ||\n value[\"type\"] === \"failed\" ||\n value[\"type\"] === \"crashed\" ||\n value[\"type\"] === \"expired\") &&\n isOptionalString(value[\"code\"]) &&\n isOptionalString(value[\"message\"]);\n\nconst isSendFailure = (\n value: unknown,\n): value is StatewireClient.SendFailure => {\n if (!isRecord(value)) return false;\n switch (value[\"fate\"]) {\n case \"rejected\":\n return (\n value[\"cause\"] === \"server\" &&\n isOptionalString(value[\"code\"]) &&\n isOptionalString(value[\"message\"])\n );\n case \"failed\":\n return value[\"cause\"] === \"server\" && isOptionalString(value[\"message\"]);\n case \"crashed\":\n case \"expired\":\n return value[\"cause\"] === \"server\";\n case \"aborted\":\n return (\n (value[\"cause\"] === \"unmount\" ||\n value[\"cause\"] === \"stream-gone\" ||\n value[\"cause\"] === \"session-lost\" ||\n value[\"cause\"] === \"removed\" ||\n value[\"cause\"] === \"quota\") &&\n (value[\"predecessorLost\"] === undefined ||\n value[\"predecessorLost\"] === true)\n );\n case \"lost\":\n return (\n value[\"cause\"] === \"unmount\" ||\n value[\"cause\"] === \"stream-gone\" ||\n value[\"cause\"] === \"session-lost\"\n );\n default:\n return false;\n }\n};\n\ntype FailedCommand = {\n protocol: string;\n produced?: StatewireClient.SessionCommand[\"produced\"];\n command: string;\n params: unknown[];\n meta?: unknown;\n failure: StatewireClient.SendFailure;\n ts: number;\n seq: number;\n};\n\nconst isFailedCommand = (value: unknown): value is FailedCommand =>\n isRecord(value) &&\n isProtocol(value[\"protocol\"]) &&\n isProducer(value[\"produced\"]) &&\n typeof value[\"command\"] === \"string\" &&\n Array.isArray(value[\"params\"]) &&\n isSendFailure(value[\"failure\"]) &&\n typeof value[\"ts\"] === \"number\" &&\n Number.isFinite(value[\"ts\"]) &&\n isPositiveSafeInteger(value[\"seq\"]);\n\nconst isArrayOf = (value: unknown, is: (item: unknown) => boolean) =>\n value === undefined || (Array.isArray(value) && value.every(is));\n\nconst isMark = (mark: unknown) =>\n isRecord(mark) &&\n typeof mark[\"id\"] === \"string\" &&\n typeof mark[\"by\"] === \"string\" &&\n (mark[\"failure\"] === undefined || isSendFailure(mark[\"failure\"]));\n\nconst isLaneWith = (\n value: unknown,\n isCommand: (command: unknown) => boolean,\n legacy = false,\n): value is StatewireClient.SessionLane =>\n isRecord(value) &&\n isPositiveSafeInteger(value[\"nextSeq\"]) &&\n Array.isArray(value[\"commands\"]) &&\n value[\"commands\"].every(isCommand) &&\n isArrayOf(\n value[\"outbox\"],\n legacy ? isLegacyOutboxCommand : isOutboxCommand,\n ) &&\n isArrayOf(value[\"removed\"], isMark) &&\n isArrayOf(value[\"verdicts\"], isSessionVerdict) &&\n (legacy\n ? isArrayOf(value[\"failed\"], isFailedCommand)\n : value[\"failed\"] === undefined &&\n isArrayOf(value[\"inbox\"], isStoredSettled));\n\nconst isSessionLane = (value: unknown): value is StatewireClient.SessionLane =>\n isLaneWith(value, isSessionCommand);\n\nconst isClientId = (value: unknown): value is string =>\n typeof value === \"string\" && value.length > 0;\n\nconst isRecordWith = (\n value: unknown,\n isLane: (lane: unknown) => boolean,\n): value is StatewireClient.SessionRecord =>\n isRecord(value) &&\n isClientId(value[\"clientId\"]) &&\n isRecord(value[\"lanes\"]) &&\n Object.keys(value[\"lanes\"]).every(isProtocol) &&\n Object.values(value[\"lanes\"]).every(isLane);\n\nconst isSessionRecord = (\n value: unknown,\n): value is StatewireClient.SessionRecord => isRecordWith(value, isSessionLane);\n\n// Formats before 4 stamped commands without an id or an enqueue time.\nconst identify = (\n record: StatewireClient.SessionRecord,\n): StatewireClient.SessionRecord => ({\n ...record,\n lanes: Object.fromEntries(\n Object.entries(record.lanes).map(([protocol, lane]) => [\n protocol,\n {\n ...lane,\n commands: lane.commands.map((command) => ({\n ...command,\n // The statement shape is unchanged across the gap-fill contract.\n ...(command.produced?.wire.version === MIN_SERVED_WIRE_VERSION && {\n produced: { ...command.produced, wire: { version: WIRE_VERSION } },\n }),\n id: randomId(),\n ts: Date.now(),\n })),\n },\n ]),\n ),\n});\n\nconst isProtocol = (value: unknown) =>\n value === undefined ||\n (typeof value === \"string\" && value !== \"*\" && protocolToken.test(value));\n\nconst isProducer = (value: unknown) => {\n if (value === undefined) return true;\n if (!isRecord(value) || !isRecord(value[\"wire\"])) return false;\n try {\n negotiationHeaders({\n wireVersion: value[\"wire\"] as StatewireProtocol.Range,\n applicationProtocols: value[\"protocols\"] as\n | readonly StatewireProtocol.Offer[]\n | \"*\",\n });\n return value[\"protocols\"] !== undefined;\n } catch {\n return false;\n }\n};\n\nconst invalid = (): never => {\n throw new Error(\"statewire: invalid stored session record\");\n};\n\nconst isPrincipal = (value: unknown): value is string =>\n typeof value === \"string\" && value !== \"\";\n\n/** The prefix of every record `KeyValueSessionStorage` keeps for `principal` under `namespace`. */\nexport const sessionNamespace = (namespace: string, principal: string) => {\n if (!isPrincipal(principal))\n throw new Error(\"statewire: storage requires a non-empty principal\");\n return `${namespace}${encodeURIComponent(principal)}:`;\n};\n\nconst usePrincipal = (principal: string) => {\n const initial = useRef(principal);\n if (initial.current !== principal)\n throw new Error(\n \"statewire: principal changed; remount the account's thread/session\",\n );\n};\n\ntype LegacyLane = Record<string, unknown> & {\n commands: StatewireClient.SessionCommand[];\n outbox?: (Omit<StatewireClient.OutboxCommand, \"held\"> & {\n hold?: { meta?: unknown };\n })[];\n failed?: FailedCommand[];\n};\n\ntype LegacyRecord = { clientId: string; lanes: Record<string, LegacyLane> };\n\n// Formats 1 and 2 held one lane: it lifts under the primary protocol, failed\n// entries under their own.\nconst liftLegacy = (\n value: Record<string, unknown>,\n protocol: string,\n): LegacyRecord => {\n const { clientId, nextSeq, commands, verdicts, failed } = value;\n const lane = {\n nextSeq,\n commands: [],\n ...(verdicts !== undefined && { verdicts }),\n };\n if (!isClientId(clientId) || !isSessionLane(lane) || !Array.isArray(commands))\n return invalid();\n const legacyCommands = commands as (StatewireClient.SessionCommand & {\n protocol?: string;\n })[];\n if (!legacyCommands.every(isLegacyCommand)) return invalid();\n if (\n failed !== undefined &&\n (!Array.isArray(failed) || !failed.every(isFailedCommand))\n )\n return invalid();\n const lanes: Record<string, LegacyLane> = {\n [protocol]: {\n ...(lane as Record<string, unknown>),\n commands: legacyCommands\n .filter((command) => (command.protocol ?? protocol) === protocol)\n .map(({ protocol: _protocol, ...command }) => command),\n },\n };\n for (const entry of (failed ?? []) as FailedCommand[]) {\n const target = lanes[entry.protocol] ?? { nextSeq: 1, commands: [] };\n lanes[entry.protocol] = {\n ...target,\n failed: [...(target.failed ?? []), entry],\n };\n }\n return identify({ clientId, lanes } as never) as never;\n};\n\n// Format 5: holds are a flag with client-only localMeta; failed sends are\n// settled updates awaiting their onCommandChange ack.\nconst upgrade = (record: LegacyRecord): StatewireClient.SessionRecord => ({\n clientId: record.clientId,\n lanes: Object.fromEntries(\n Object.entries(record.lanes).map(([protocol, legacy]) => {\n const { outbox, failed, ...lane } = legacy;\n const inbox = [...(failed ?? [])]\n .sort((a, b) => a.ts - b.ts)\n .map((f): StatewireClient.StoredSettled => ({\n // Stable across re-migrations until the upgraded record is saved.\n id: `failed:${protocol}:${f.seq}:${f.ts}`,\n method: f.command,\n params: f.params,\n ...(f.meta !== undefined && { meta: f.meta }),\n ts: f.ts,\n failure: f.failure,\n }));\n return [\n protocol,\n {\n ...(lane as unknown as StatewireClient.SessionLane),\n ...(outbox?.length && {\n outbox: outbox.map(({ hold, ...command }) => ({\n ...command,\n ...(hold !== undefined && { held: true as const }),\n ...(hold?.meta !== undefined && { localMeta: hold.meta }),\n })),\n }),\n ...(inbox.length > 0 && { inbox }),\n },\n ];\n }),\n ),\n});\n\nconst isLegacyLane = (lane: unknown) =>\n isLaneWith(lane, isSessionCommand, true);\n\n// Format 6 records name their principal; earlier formats sit under a key the\n// principal already scopes.\nconst normalize = (\n parsed: unknown,\n protocol: string,\n principal: string,\n): StatewireClient.SessionRecord => {\n if (!isRecord(parsed)) return invalid();\n const {\n statewireStorageVersion: format,\n savedAt: _savedAt,\n principal: owner,\n ...value\n } = parsed;\n if (format === 6) {\n if (!isPrincipal(owner) || !isSessionRecord(value)) return invalid();\n if (owner !== principal)\n throw new Error(\"statewire: stored session belongs to another principal\");\n return value;\n }\n if (owner !== undefined) return invalid();\n if (format === 5) return isSessionRecord(value) ? value : invalid();\n if (format === 4)\n return isRecordWith(value, isLegacyLane)\n ? upgrade(value as never)\n : invalid();\n if (format === 3)\n return isRecordWith(value, (lane) =>\n isLaneWith(lane, isLegacyCommand, true),\n )\n ? upgrade(identify(value) as never)\n : invalid();\n if (format !== undefined && format !== 1 && format !== 2)\n throw new Error(\"statewire: unsupported session storage version\");\n const { ctxNextSeq: _ctxNextSeq, context: _context, ...legacy } = value;\n return upgrade(liftLegacy(legacy, protocol));\n};\n\nexport const STORAGE_FORMAT = 6;\n\nconst parse = async (\n raw: string | null,\n codec: KeyValueSessionStorage.Codec | undefined,\n): Promise<unknown> =>\n raw === null\n ? null\n : JSON.parse(codec === undefined ? raw : await codec.decode(raw));\n\nconst decode = (\n raw: string | null,\n protocol: string,\n principal: string,\n): StatewireClient.SessionRecord | null =>\n raw === null ? null : normalize(JSON.parse(raw), protocol, principal);\n\nconst isUnsent = (lane: StatewireClient.SessionLane) =>\n lane.commands.length > 0 || (lane.outbox?.length ?? 0) > 0;\n\nconst entryCount = (record: StatewireClient.SessionRecord | null) =>\n record === null\n ? 0\n : Object.values(record.lanes).reduce(\n (sum, lane) => sum + lane.commands.length + (lane.outbox?.length ?? 0),\n 0,\n );\n\nconst noticesOf = (\n record: StatewireClient.SessionRecord,\n): Statewire.CommandUpdate[] =>\n Object.entries(record.lanes).flatMap(([protocol, lane]) =>\n (lane.inbox ?? []).map((n) => ({\n protocol,\n id: n.id,\n method: n.method,\n params: n.params as never[],\n ...(n.seq !== undefined && { seq: n.seq }),\n status: \"settled\" as const,\n ...(n.failure !== undefined && { failure: n.failure }),\n ...(n.localMeta !== undefined && { localMeta: n.localMeta }),\n })),\n );\n\nconst active = new WeakMap<\n KeyValueSessionStorage.Backend,\n Map<string, number>\n>();\n\n/** Same-runtime saves, per backend, for `subscribeOutboxes`. */\nconst savers = new WeakMap<\n KeyValueSessionStorage.Backend,\n Set<(key: string) => void>\n>();\n\nconst announce = (backend: KeyValueSessionStorage.Backend, key: string) => {\n for (const listener of savers.get(backend) ?? []) listener(key);\n};\n\ntype Candidate = {\n key: string;\n savedAt: number;\n record: StatewireClient.SessionRecord;\n};\n\nconst sweeping = new WeakMap<KeyValueSessionStorage.Backend, Promise<void>>();\n\n// Sweeps of one backend run one at a time.\nconst sweep = (\n options: KeyValueSessionStorage.Options,\n currentKey: string,\n quota = false,\n): Promise<void> => {\n const run = (sweeping.get(options.backend) ?? Promise.resolve())\n .catch(() => {})\n .then(() => sweepOnce(options, currentKey, quota));\n sweeping.set(options.backend, run);\n return run;\n};\n\n// Records with unsent commands are never evicted. Empty records go first;\n// records holding only failure notices go once past their age.\nconst sweepOnce = async (\n options: KeyValueSessionStorage.Options,\n currentKey: string,\n quota = false,\n) => {\n const { backend, retention, codec, onEvict, principal } = options;\n if (!retention || !backend.getAllKeys) return;\n const namespace = sessionNamespace(options.namespace ?? \"\", principal);\n const candidate = async (raw: string | null) => {\n let value: unknown;\n try {\n value = await parse(raw, codec);\n } catch {\n return undefined;\n }\n if (value === null) return undefined;\n let record: StatewireClient.SessionRecord;\n try {\n record = normalize(value, DEFAULT_PROTOCOL, principal);\n } catch {\n return undefined;\n }\n if (Object.values(record.lanes).some(isUnsent)) return undefined;\n const savedAt =\n (value as Record<string, unknown>)[\"savedAt\"] ??\n (value as Record<string, unknown>)[\"updatedAt\"];\n return typeof savedAt === \"number\" && Number.isFinite(savedAt)\n ? { savedAt, record }\n : undefined;\n };\n const empty: Candidate[] = [];\n const noticed: Candidate[] = [];\n for (const key of await backend.getAllKeys()) {\n if (\n !key.startsWith(namespace) ||\n key === currentKey ||\n active.get(backend)?.has(key)\n )\n continue;\n const found = await candidate(await backend.getItem(key));\n if (found === undefined) continue;\n (noticesOf(found.record).length > 0 ? noticed : empty).push({\n key,\n ...found,\n });\n }\n const remove = async (\n entry: Candidate,\n reason: KeyValueSessionStorage.EvictEvent[\"reason\"],\n ) => {\n const run = async () => {\n if (active.get(backend)?.has(entry.key)) return;\n const again = await candidate(await backend.getItem(entry.key));\n if (again?.savedAt !== entry.savedAt) return;\n if (active.get(backend)?.has(entry.key)) return;\n await backend.removeItem(entry.key);\n announce(backend, entry.key);\n onEvict?.({\n key: entry.key,\n reason,\n dropped: noticesOf(again.record),\n });\n };\n if (backend.withUnused) await backend.withUnused(entry.key, run);\n else await run();\n };\n const now = Date.now();\n const expired = (entry: Candidate) =>\n retention.maxAgeMs !== undefined &&\n now - entry.savedAt > retention.maxAgeMs;\n empty.sort((a, b) => b.savedAt - a.savedAt);\n let retained = 1;\n const quotaStart = Math.floor(empty.length / 2);\n for (const [index, entry] of empty.entries()) {\n const overflow =\n retention.maxEntries !== undefined && retained >= retention.maxEntries;\n if (expired(entry)) await remove(entry, \"expired\");\n else if (overflow) await remove(entry, \"overflow\");\n else if (quota && index >= quotaStart) await remove(entry, \"quota\");\n else retained++;\n }\n for (const entry of noticed)\n if (expired(entry)) await remove(entry, \"expired\");\n};\n\nconst isQuotaError = (error: unknown) =>\n error instanceof Error && error.name === \"QuotaExceededError\";\n\nconst quotaError = (limit: string) => {\n const error = new Error(`statewire: session ${limit} limit reached`);\n error.name = \"StatewireQuotaError\";\n return error;\n};\n\nconst otherBytes = async (\n backend: KeyValueSessionStorage.Backend,\n namespace: string,\n key: string,\n) => {\n let total = 0;\n for (const other of (await backend.getAllKeys?.()) ?? []) {\n if (other === key || !other.startsWith(namespace)) continue;\n total += (await backend.getItem(other))?.length ?? 0;\n }\n return total;\n};\n\nconst validLimit = (value: number | undefined, name: string) => {\n if (value !== undefined && (!Number.isSafeInteger(value) || value < 1))\n throw new Error(`statewire: invalid session limit ${name}`);\n};\n\nconst useKeyValueSessionStorage = (\n options: KeyValueSessionStorage.Options,\n): StatewireClient.SessionStorage => {\n const {\n backend,\n key,\n namespace = \"\",\n principal,\n protocol = DEFAULT_PROTOCOL,\n migrate,\n retention: suppliedRetention,\n limits: suppliedLimits,\n codec,\n onError,\n onEvict,\n } = options;\n const scope = sessionNamespace(namespace, principal);\n usePrincipal(principal);\n const latest = useRef({ onError, onEvict });\n latest.current = { onError, onEvict };\n const reportError = useMemo(\n () => (error: unknown) => latest.current.onError?.(error),\n [],\n );\n const reportEvict = useMemo(\n () => (event: KeyValueSessionStorage.EvictEvent) =>\n latest.current.onEvict?.(event),\n [],\n );\n const retain = suppliedRetention !== undefined;\n const maxAgeMs = suppliedRetention?.maxAgeMs;\n const maxEntries = suppliedRetention?.maxEntries;\n const retention = useMemo(\n () =>\n retain\n ? {\n ...(maxAgeMs === undefined ? {} : { maxAgeMs }),\n ...(maxEntries === undefined ? {} : { maxEntries }),\n }\n : undefined,\n [retain, maxAgeMs, maxEntries],\n );\n const outboxEntries = suppliedLimits?.outboxEntries;\n const recordBytes = suppliedLimits?.recordBytes;\n const totalBytes = suppliedLimits?.totalBytes;\n validLimit(outboxEntries, \"outboxEntries\");\n validLimit(recordBytes, \"recordBytes\");\n validLimit(totalBytes, \"totalBytes\");\n if (totalBytes !== undefined && !backend.getAllKeys)\n throw new Error(\"statewire: a totalBytes limit requires getAllKeys\");\n const configured = useMemo(\n () => ({\n backend,\n key,\n namespace,\n principal,\n onEvict: reportEvict,\n ...(codec === undefined ? {} : { codec }),\n ...(retention === undefined ? {} : { retention }),\n }),\n [backend, key, namespace, principal, retention, codec, reportEvict],\n );\n const storageKey = scope + key;\n if (retention !== undefined) {\n if (!namespace)\n throw new Error(\"statewire: session retention requires a namespace\");\n if (!backend.getAllKeys)\n throw new Error(\"statewire: session retention requires getAllKeys\");\n if (\n retention.maxAgeMs !== undefined &&\n (!Number.isFinite(retention.maxAgeMs) || retention.maxAgeMs < 0)\n )\n throw new Error(\"statewire: invalid session retention maxAgeMs\");\n if (\n retention.maxEntries !== undefined &&\n (!Number.isSafeInteger(retention.maxEntries) || retention.maxEntries < 1)\n )\n throw new Error(\"statewire: invalid session retention maxEntries\");\n }\n const state = useMemo(() => {\n let raw: string | null | Promise<string | null>;\n try {\n raw = backend.getItem(storageKey);\n } catch (cause) {\n const error = wrapStorageError(cause);\n reportError(error);\n throw error;\n }\n const state = {\n record: null as StatewireClient.SessionRecord | null,\n bytes: 0,\n others: undefined as number | undefined,\n ready: undefined as Promise<void> | undefined,\n queue: Promise.resolve(),\n pending: 0,\n failure: undefined as unknown,\n asynchronous:\n codec !== undefined || (typeof raw !== \"string\" && raw !== null),\n hydrated: false,\n };\n const hydrate = (record: StatewireClient.SessionRecord | null) => {\n state.record = record;\n state.hydrated = true;\n };\n if (state.asynchronous) {\n state.ready = Promise.resolve(raw)\n .then(async (value) => {\n state.bytes = value?.length ?? 0;\n const parsed = await parse(value, codec);\n hydrate(\n parsed === null ? null : normalize(parsed, protocol, principal),\n );\n })\n .catch((cause: unknown) => {\n const error = wrapStorageError(cause);\n if (state.failure !== error) reportError(error);\n state.failure = error;\n throw error;\n });\n state.ready.catch(() => {});\n } else {\n try {\n state.bytes = (raw as string | null)?.length ?? 0;\n hydrate(decode(raw as string | null, protocol, principal));\n } catch (cause) {\n const error = wrapStorageError(cause);\n reportError(error);\n throw error;\n }\n }\n return state;\n }, [backend, storageKey, protocol, principal, codec, reportError]);\n\n useEffect(() => {\n const entries = active.get(backend) ?? new Map<string, number>();\n active.set(backend, entries);\n entries.set(storageKey, (entries.get(storageKey) ?? 0) + 1);\n return () => {\n const count = entries.get(storageKey)! - 1;\n if (count === 0) entries.delete(storageKey);\n else entries.set(storageKey, count);\n };\n }, [backend, storageKey]);\n\n useEffect(() => {\n if (retention !== undefined)\n void sweep(configured, storageKey).catch((cause: unknown) => {\n const error = wrapStorageError(cause);\n state.failure = error;\n reportError(error);\n });\n }, [state, configured, storageKey, retention, reportError]);\n\n useEffect(() => {\n if (totalBytes === undefined) return;\n void otherBytes(backend, scope, storageKey).then(\n (bytes) => {\n state.others = bytes;\n },\n () => {},\n );\n }, [state, backend, scope, storageKey, totalBytes]);\n\n return useMemo(() => {\n const checkLimits = (\n record: StatewireClient.SessionRecord,\n bytes: number,\n ) => {\n // Limits gate writes that add an entry; settling and stamping always land.\n if (entryCount(record) <= entryCount(state.record)) return;\n if (outboxEntries !== undefined && entryCount(record) > outboxEntries)\n throw quotaError(\"outboxEntries\");\n if (recordBytes !== undefined && bytes > recordBytes)\n throw quotaError(\"recordBytes\");\n if (\n totalBytes !== undefined &&\n state.others !== undefined &&\n state.others + bytes > totalBytes\n )\n throw quotaError(\"totalBytes\");\n };\n const persist = async (encoded: string) => {\n const stored =\n codec === undefined ? encoded : await codec.encode(encoded);\n try {\n await backend.setItem(storageKey, stored);\n } catch (error) {\n if (!isQuotaError(error)) throw error;\n if (retention !== undefined) await sweep(configured, storageKey, true);\n await backend.setItem(storageKey, stored);\n }\n announce(backend, storageKey);\n };\n return {\n durable: true,\n ...(state.ready === undefined ? {} : { ready: state.ready }),\n read: async () => {\n if (state.failure !== undefined) throw state.failure;\n await state.ready;\n await state.queue.catch(() => {});\n try {\n const raw = await backend.getItem(storageKey);\n const parsed = await parse(raw, codec);\n state.bytes = raw?.length ?? 0;\n state.record =\n parsed === null ? null : normalize(parsed, protocol, principal);\n return state.record;\n } catch (cause) {\n const error = wrapStorageError(cause);\n reportError(error);\n throw error;\n }\n },\n load: () => {\n if (state.failure !== undefined) throw state.failure;\n if (!state.hydrated)\n throw new Error(\n \"statewire: await session storage readiness before loading\",\n );\n if (state.asynchronous || state.pending > 0) return state.record;\n let raw: string | null | Promise<string | null>;\n try {\n raw = backend.getItem(storageKey);\n } catch (cause) {\n const error = wrapStorageError(cause);\n reportError(error);\n throw error;\n }\n if (typeof raw !== \"string\" && raw !== null)\n throw new Error(\n \"statewire: storage backend changed from synchronous to asynchronous\",\n );\n try {\n state.record = decode(raw, protocol, principal);\n return state.record;\n } catch (cause) {\n const error = wrapStorageError(cause);\n reportError(error);\n throw error;\n }\n },\n save: (record: StatewireClient.SessionRecord) => {\n if (state.failure !== undefined) throw state.failure;\n if (!state.hydrated)\n throw new Error(\n \"statewire: await session storage readiness before saving\",\n );\n if (!isSessionRecord(record))\n throw new Error(\"statewire: invalid session record\");\n const encoded = JSON.stringify({\n ...record,\n principal,\n statewireStorageVersion: STORAGE_FORMAT,\n savedAt: Date.now(),\n });\n checkLimits(record, encoded.length);\n if (!state.asynchronous && state.pending === 0) {\n try {\n backend.setItem(storageKey, encoded) as void;\n } catch (error) {\n if (!isQuotaError(error)) {\n state.failure = wrapStorageError(error);\n reportError(state.failure);\n throw state.failure;\n }\n // A write that adds an entry is refused; the rest retry after the sweep.\n if (entryCount(record) > entryCount(state.record)) {\n if (retention !== undefined)\n void sweep(configured, storageKey, true).catch(() => {});\n throw quotaError(\"backend\");\n }\n state.record = record;\n state.bytes = encoded.length;\n state.pending++;\n state.queue = persist(encoded).finally(() => {\n state.pending--;\n });\n state.queue.catch((cause: unknown) => {\n const failure = wrapStorageError(cause);\n if (state.failure !== failure) reportError(failure);\n state.failure = failure;\n });\n return;\n }\n state.record = record;\n state.bytes = encoded.length;\n announce(backend, storageKey);\n return;\n }\n state.record = record;\n state.bytes = encoded.length;\n state.pending++;\n state.queue = state.queue\n .then(() => state.ready)\n .then(() => persist(encoded))\n .catch((cause: unknown) => {\n const error = wrapStorageError(cause);\n if (state.failure !== error) reportError(error);\n state.failure = error;\n throw error;\n })\n .finally(() => {\n state.pending--;\n });\n state.queue.catch(() => {});\n },\n flush: () => {\n if (state.failure !== undefined) throw state.failure;\n if (!state.asynchronous && state.pending === 0) return;\n return (async () => {\n await state.ready;\n for (;;) {\n const queue = state.queue;\n await queue;\n if (queue === state.queue) return;\n }\n })();\n },\n onEvict: ({ reason, dropped }) =>\n reportEvict({ key: storageKey, reason, dropped }),\n ...(migrate === undefined ? {} : { migrate }),\n } satisfies StatewireClient.SessionStorage;\n }, [\n state,\n backend,\n storageKey,\n protocol,\n principal,\n migrate,\n retention,\n configured,\n codec,\n outboxEntries,\n recordBytes,\n totalBytes,\n reportError,\n reportEvict,\n ]);\n};\n\n/** Persist sessions through a synchronous or asynchronous string storage backend. */\nexport const KeyValueSessionStorage = resource(useKeyValueSessionStorage);\n\nexport namespace KeyValueSessionStorage {\n export type Backend = {\n getItem: (key: string) => string | null | Promise<string | null>;\n setItem: (key: string, value: string) => void | Promise<void>;\n removeItem: (key: string) => void | Promise<void>;\n getAllKeys?: () => readonly string[] | Promise<readonly string[]>;\n /** Runs cleanup under exclusive access, skipping keys in use by another runtime. */\n withUnused?: (key: string, action: () => Promise<void>) => Promise<void>;\n /** Notifies of keys other runtimes wrote; returns unsubscribe. */\n subscribe?: (listener: (key: string) => void) => () => void;\n };\n /** Transforms the stored string, e.g. encryption or compression. */\n export type Codec = {\n encode: (plain: string) => Promise<string>;\n decode: (stored: string) => Promise<string>;\n };\n export type EvictEvent = StatewireClient.EvictEvent;\n export type Options = {\n backend: Backend;\n key: string;\n namespace?: string;\n /** The account whose session this storage holds; scopes the stored key, tags the record. */\n principal: string;\n /** The lane that sessions saved in a single-lane format lift into; defaults to the client's default protocol. */\n protocol?: string;\n migrate?: StatewireClient.SessionStorage[\"migrate\"];\n retention?: { maxAgeMs?: number; maxEntries?: number };\n /** Off by default; a write that adds an entry past one rejects `{ fate: \"aborted\", cause: \"quota\" }`. */\n limits?: {\n outboxEntries?: number;\n recordBytes?: number;\n totalBytes?: number;\n };\n codec?: Codec;\n onError?: (error: unknown) => void;\n /** Reports evicted records and dropped failure notices. */\n onEvict?: (event: EvictEvent) => void;\n };\n}\n\nconst pages = new Map<string, StatewireClient.SessionRecord | null>();\n\nconst useMemorySessionStorage = ({\n principal,\n scope,\n}: MemorySessionStorage.Options): StatewireClient.SessionStorage => {\n const key = sessionNamespace(\"\", principal) + scope;\n usePrincipal(principal);\n return useMemo(\n () => ({\n durable: false,\n load: () => pages.get(key) ?? null,\n save: (record) => {\n pages.set(key, record);\n },\n }),\n [key],\n );\n};\n\n/** Session storage in page memory, keyed by principal and scope: the record and client identity survive remounts, not reloads. */\nexport const MemorySessionStorage = resource(useMemorySessionStorage);\n\nexport namespace MemorySessionStorage {\n export type Options = {\n /** The account whose session this storage holds. */\n principal: string;\n scope: string;\n };\n}\n\nconst entriesOf = (\n record: StatewireClient.SessionRecord,\n): StatewireClient.OutboxEntry[] =>\n Object.entries(record.lanes).flatMap(([protocol, lane]) => {\n const view = (\n c: StatewireClient.OutboxCommand | StatewireClient.SessionCommand,\n state: StatewireClient.OutboxState,\n ): StatewireClient.OutboxEntry => ({\n id: c.id,\n protocol,\n command: {\n method: c.method,\n params:\n c.meta === undefined\n ? c.params\n : (reviveValue(structuredClone(c.params), c.meta) as unknown[]),\n },\n ts: c.ts,\n state,\n localMeta: c.localMeta,\n durable: true,\n });\n return [\n ...[...lane.commands]\n .sort((a, b) => a.seq - b.seq)\n .map((c) => view(c, \"sending\")),\n ...(lane.outbox ?? []).map((c) => view(c, c.held ? \"held\" : \"queued\")),\n ];\n });\n\n/** Reads the outboxes of every session `principal` keeps under `namespace`, mounted or not. */\nexport const readOutboxes = async (\n backend: KeyValueSessionStorage.Backend,\n options: ReadOutboxes.Options,\n): Promise<readonly ReadOutboxes.Outbox[]> => {\n if (!backend.getAllKeys)\n throw new Error(\"statewire: readOutboxes requires getAllKeys\");\n const { namespace, principal, codec, protocol = DEFAULT_PROTOCOL } = options;\n if (!namespace)\n throw new Error(\"statewire: readOutboxes requires a namespace\");\n const scope = sessionNamespace(namespace, principal);\n const result: ReadOutboxes.Outbox[] = [];\n for (const key of await backend.getAllKeys()) {\n if (!key.startsWith(scope)) continue;\n const parsed = await parse(await backend.getItem(key), codec);\n if (parsed === null) continue;\n const entries = entriesOf(normalize(parsed, protocol, principal));\n if (entries.length > 0) result.push({ key, entries });\n }\n return result;\n};\n\n/** Calls `listener` with `readOutboxes` now and after every save to `principal`'s sessions under `namespace`; returns unsubscribe. */\nexport const subscribeOutboxes = (\n backend: KeyValueSessionStorage.Backend,\n options: ReadOutboxes.Options,\n listener: (outboxes: readonly ReadOutboxes.Outbox[]) => void,\n onError: (error: unknown) => void,\n): (() => void) => {\n const scope = sessionNamespace(options.namespace, options.principal);\n let closed = false;\n let reading = Promise.resolve();\n const refresh = () => {\n reading = reading.then(async () => {\n if (closed) return;\n try {\n const outboxes = await readOutboxes(backend, options);\n if (!closed) listener(outboxes);\n } catch (error) {\n onError(error);\n }\n });\n };\n const onKey = (key: string) => {\n if (key.startsWith(scope)) refresh();\n };\n const local = savers.get(backend) ?? new Set();\n savers.set(backend, local);\n local.add(onKey);\n const remote = backend.subscribe?.(onKey);\n refresh();\n return () => {\n closed = true;\n local.delete(onKey);\n remote?.();\n };\n};\n\nexport namespace ReadOutboxes {\n export type Options = {\n namespace: string;\n /** The account whose sessions to read. */\n principal: string;\n codec?: KeyValueSessionStorage.Codec;\n /** The lane that single-lane formats lift into. */\n protocol?: string;\n };\n export type Outbox = {\n key: string;\n entries: readonly StatewireClient.OutboxEntry[];\n };\n}\nexport { IndexedDbSession, indexedDbBackend } from \"./indexedDb\";\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../../src/transports/testing.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,KAAK,EAAE,iBAAiB,EAAE,gCAA6B;AAE9D,OAAO,KAAK,EAAE,cAAc,EAAE,iBAAc;AAC5C,OAAO,KAAK,EAAE,SAAS,EAAE,oBAAiB;
|
|
1
|
+
{"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../../src/transports/testing.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,KAAK,EAAE,iBAAiB,EAAE,gCAA6B;AAE9D,OAAO,KAAK,EAAE,cAAc,EAAE,iBAAc;AAC5C,OAAO,KAAK,EAAE,SAAS,EAAE,oBAAiB;AAU1C,8BAA2B;AAE3B,+EAA+E;AAC/E,eAAO,MAAM,IAAI,qBAAmD,CAAC;AAErE,OAAO,EAAE,eAAe,EAAE,mCAAgC;AAE1D,MAAM,MAAM,aAAa,CAAC,MAAM,IAAI;IAClC,iEAAiE;IACjE,SAAS,EAAE,eAAe,CAAC,SAAS,CAAC,MAAM,GAAG,SAAS,EAAE,GAAG,CAAC,CAAC,CAAC;IAC/D,4CAA4C;IAC5C,QAAQ,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,OAAO,EAAE,CAAA;KAAE,EAAE,CAAC;IACpE,QAAQ,EAAE,WAAW,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CAAC;IAC7C,2EAA2E;IAC3E,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3C,mDAAmD;IACnD,OAAO,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACxC,0DAA0D;IAC1D,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1D,mDAAmD;IACnD,YAAY,EAAE,CAAC,QAAQ,EAAE,iBAAiB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7D,2EAA2E;IAC3E,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5D,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;CAC5B,CAAC;AASF,eAAO,MAAM,mBAAmB,GAAI,MAAM,EACxC,cAAc,MAAM,EACpB,iBAAoB,EACpB,UAAS;IAAE,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CAAO,KAC9C,aAAa,CAAC,MAAM,CA+ItB,CAAC"}
|
|
@@ -48,7 +48,11 @@ const createMockTransport = (initialState, protocol = "default", options = {}) =
|
|
|
48
48
|
if (!clients.has(id)) clients.set(id, {});
|
|
49
49
|
const rec = clients.get(id);
|
|
50
50
|
const ephemeral = ephemeralLanes();
|
|
51
|
-
const
|
|
51
|
+
const negotiation = parseNegotiation(headers);
|
|
52
|
+
const offers = negotiation.applicationProtocols;
|
|
53
|
+
const offered = negotiation.wireVersion;
|
|
54
|
+
const wireVersion = offered.version < "2026-11-02" ? offered.version : WIRE_VERSION;
|
|
55
|
+
if (wireVersion < "2026-11-01" || offered.minVersion !== void 0 && wireVersion < offered.minVersion) throw new Error("mock wire version was not offered");
|
|
52
56
|
const applicationVersion = offers === "*" ? DEFAULT_PROTOCOL_VERSION : offers.find((offer) => offer.name === protocol)?.version;
|
|
53
57
|
if (applicationVersion === void 0) throw new Error("mock protocol was not offered");
|
|
54
58
|
for (const lane of ephemeral) delete rec[lane];
|
|
@@ -63,7 +67,7 @@ const createMockTransport = (initialState, protocol = "default", options = {}) =
|
|
|
63
67
|
value: state
|
|
64
68
|
}],
|
|
65
69
|
syn: {
|
|
66
|
-
version:
|
|
70
|
+
version: wireVersion,
|
|
67
71
|
protocols: [{
|
|
68
72
|
name: protocol,
|
|
69
73
|
version: applicationVersion,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"testing.js","names":["ResourceElement","StatewireEnvelope","StatewireHttp","StatewireFrame","Statewire","openSSEStream","sseFrame","FakeStreamHandle","DEFAULT_PROTOCOL_VERSION","WIRE_VERSION","parseNegotiation","tick","Promise","r","setTimeout","applyOptimistic","MockTransport","transport","TState","commands","protocol","method","params","contexts","NonNullable","setState","state","applied","seq","settle","payload","sendEnvelope","envelope","finish","reason","message","clientId","attached","mockCount","createMockTransport","initialState","options","ephemeral","ephemeralLanes","map","child","finished","clients","Map","Record","streams","Set","stream","send","admit","id","frame","rec","get","push","ctx","statement","cmd","fakeFetch","input","RequestInfo","URL","init","RequestInit","Response","TypeError","url","String","headers","Headers","endsWith","has","set","offers","applicationProtocols","applicationVersion","find","offer","name","version","undefined","Error","lane","JSON","parse","body","handle","ops","op","main","value","syn","protocols","record","const","signal","json","status","fetch","next","path","type","fin","close","size"],"sources":["../../src/transports/testing.ts"],"sourcesContent":["import type { ResourceElement } from \"@assistant-ui/tap\";\nimport type { StatewireEnvelope } from \"../client/connection\";\nimport { StatewireHttp } from \"./sse\";\nimport type { StatewireFrame } from \"./sse\";\nimport type { Statewire } from \"../types\";\nimport { openSSEStream, sseFrame } from \"./fake-sse\";\nimport type { FakeStreamHandle } from \"./fake-sse\";\nimport {\n DEFAULT_PROTOCOL_VERSION,\n WIRE_VERSION,\n parseNegotiation,\n} from \"../wire/protocol\";\n\nexport * from \"./fixtures\";\n\n/** Let queued microtasks and op batches drain through the statewire client. */\nexport const tick = () => new Promise<void>((r) => setTimeout(r, 0));\n\nexport { applyOptimistic } from \"../util/recordMutations\";\n\nexport type MockTransport<TState> = {\n /** Pass as the `transport:` option of useStatewire / Harness. */\n transport: ResourceElement<Statewire<TState | undefined, any>>;\n /** Captured statements in arrival order. */\n commands: { protocol: string; method: string; params: unknown[] }[];\n contexts: NonNullable<StatewireFrame[\"ctx\"]>;\n /** Replace the replicated state and broadcast it as a root snapshot op. */\n setState: (state: TState) => Promise<void>;\n /** Deliver a bare ack cmd packet on the stream. */\n applied: (seq: number) => Promise<void>;\n /** Deliver a terminal result cmd packet on the stream. */\n settle: (seq: number, payload?: unknown) => Promise<void>;\n /** Deliver an arbitrary envelope on the stream. */\n sendEnvelope: (envelope: StatewireEnvelope) => Promise<void>;\n /** End the stream with a fin; further attaches fail like a dead server. */\n finish: (reason: string, message?: string) => Promise<void>;\n readonly clientId: string;\n readonly attached: boolean;\n};\n\n/**\n * A mock statewire backend: serves `initialState` as the attach snapshot,\n * records submitted frames (receipt only — acks and results are delivered\n * from test code), and exposes the wire as a transport ResourceElement.\n */\nlet mockCount = 0;\n\nexport const createMockTransport = <TState>(\n initialState: TState,\n protocol = \"default\",\n options: { ephemeral?: readonly string[] } = {},\n): MockTransport<TState> => {\n // Read per attach: a test can grow the lane set between connects.\n const ephemeralLanes = () =>\n (options.ephemeral ?? []).map((child) => `${protocol}/${child}`);\n let state: unknown = initialState;\n let clientId = \"\";\n let finished = false;\n const clients = new Map<string, Record<string, number>>();\n const streams = new Set<FakeStreamHandle>();\n const commands: MockTransport<unknown>[\"commands\"] = [];\n const contexts: MockTransport<unknown>[\"contexts\"] = [];\n\n const sendEnvelope = (envelope: StatewireEnvelope) => {\n for (const stream of streams) stream.send(sseFrame(envelope));\n return tick();\n };\n\n const admit = (id: string, frame: StatewireFrame) => {\n const rec = clients.get(id)!;\n contexts.push(...(frame.ctx ?? []));\n for (const statement of frame.cmd ?? []) {\n if (statement.seq <= (rec[statement.protocol] ?? 0)) continue;\n rec[statement.protocol] = statement.seq;\n commands.push({\n protocol: statement.protocol,\n method: statement.method,\n params: statement.params,\n });\n }\n };\n\n const fakeFetch = (async (\n input: RequestInfo | URL,\n init?: RequestInit,\n ): Promise<Response> => {\n if (finished) throw new TypeError(\"fetch failed (finished)\");\n const url = String(input);\n const headers = new Headers(init?.headers);\n const id = headers.get(\"Statewire-Client-Id\") ?? \"\";\n if (url.endsWith(\"/stream\")) {\n clientId = id;\n if (!clients.has(id)) clients.set(id, {});\n const rec = clients.get(id)!;\n const ephemeral = ephemeralLanes();\n const offers = parseNegotiation(headers).applicationProtocols;\n const applicationVersion =\n offers === \"*\"\n ? DEFAULT_PROTOCOL_VERSION\n : offers.find((offer) => offer.name === protocol)?.version;\n if (applicationVersion === undefined)\n throw new Error(\"mock protocol was not offered\");\n for (const lane of ephemeral) delete rec[lane];\n const frame = JSON.parse(String(init?.body ?? \"{}\")) as StatewireFrame;\n admit(id, frame);\n return openSSEStream(\n streams,\n id,\n (handle) => {\n handle.send(\n sseFrame({\n ops: [{ op: \"mount\", protocol, main: true, value: state }],\n syn: {\n version: WIRE_VERSION,\n protocols: [\n {\n name: protocol,\n version: applicationVersion,\n seq: rec[protocol] ?? 0,\n },\n ...ephemeral.map((name) => ({\n name,\n version: applicationVersion,\n record: \"ephemeral\" as const,\n seq: 0,\n })),\n ],\n },\n }),\n );\n },\n init?.signal ?? undefined,\n { \"Statewire-Lease\": \"mock-lease\" },\n );\n }\n if (url.endsWith(\"/frames\")) {\n admit(clientId, JSON.parse(String(init?.body ?? \"{}\")) as StatewireFrame);\n return Response.json({});\n }\n return new Response(null, { status: 404 });\n }) as typeof fetch;\n\n return {\n transport: StatewireHttp({\n url: `http://mock-${++mockCount}/api`,\n fetch: fakeFetch,\n }),\n commands,\n contexts,\n setState: (next) => {\n state = next;\n return sendEnvelope({ ops: [{ op: \"replace\", path: [], value: next }] });\n },\n applied: (seq) =>\n sendEnvelope({ cmd: [{ protocol, seq, type: \"applied\" }] }),\n settle: (seq, payload) =>\n sendEnvelope({\n cmd: [\n {\n protocol,\n seq,\n type: \"result\",\n ...(payload !== undefined && { payload }),\n },\n ],\n }),\n sendEnvelope,\n finish: async (reason, message) => {\n await sendEnvelope({\n fin: { reason, ...(message !== undefined && { message }) },\n });\n finished = true;\n for (const stream of [...streams]) stream.close();\n await tick();\n },\n get clientId() {\n return clientId;\n },\n get attached() {\n return streams.size > 0;\n },\n };\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"testing.js","names":["ResourceElement","StatewireEnvelope","StatewireHttp","StatewireFrame","Statewire","openSSEStream","sseFrame","FakeStreamHandle","DEFAULT_PROTOCOL_VERSION","GAP_FILL_VERSION","WIRE_VERSION","parseNegotiation","tick","Promise","r","setTimeout","applyOptimistic","MockTransport","transport","TState","commands","protocol","method","params","contexts","NonNullable","setState","state","applied","seq","settle","payload","sendEnvelope","envelope","finish","reason","message","clientId","attached","mockCount","createMockTransport","initialState","options","ephemeral","ephemeralLanes","map","child","finished","clients","Map","Record","streams","Set","stream","send","admit","id","frame","rec","get","push","ctx","statement","cmd","fakeFetch","input","RequestInfo","URL","init","RequestInit","Response","TypeError","url","String","headers","Headers","endsWith","has","set","negotiation","offers","applicationProtocols","offered","wireVersion","version","minVersion","undefined","Error","applicationVersion","find","offer","name","lane","JSON","parse","body","handle","ops","op","main","value","syn","protocols","record","const","signal","json","status","fetch","next","path","type","fin","close","size"],"sources":["../../src/transports/testing.ts"],"sourcesContent":["import type { ResourceElement } from \"@assistant-ui/tap\";\nimport type { StatewireEnvelope } from \"../client/connection\";\nimport { StatewireHttp } from \"./sse\";\nimport type { StatewireFrame } from \"./sse\";\nimport type { Statewire } from \"../types\";\nimport { openSSEStream, sseFrame } from \"./fake-sse\";\nimport type { FakeStreamHandle } from \"./fake-sse\";\nimport {\n DEFAULT_PROTOCOL_VERSION,\n GAP_FILL_VERSION,\n WIRE_VERSION,\n parseNegotiation,\n} from \"../wire/protocol\";\n\nexport * from \"./fixtures\";\n\n/** Let queued microtasks and op batches drain through the statewire client. */\nexport const tick = () => new Promise<void>((r) => setTimeout(r, 0));\n\nexport { applyOptimistic } from \"../util/recordMutations\";\n\nexport type MockTransport<TState> = {\n /** Pass as the `transport:` option of useStatewire / Harness. */\n transport: ResourceElement<Statewire<TState | undefined, any>>;\n /** Captured statements in arrival order. */\n commands: { protocol: string; method: string; params: unknown[] }[];\n contexts: NonNullable<StatewireFrame[\"ctx\"]>;\n /** Replace the replicated state and broadcast it as a root snapshot op. */\n setState: (state: TState) => Promise<void>;\n /** Deliver a bare ack cmd packet on the stream. */\n applied: (seq: number) => Promise<void>;\n /** Deliver a terminal result cmd packet on the stream. */\n settle: (seq: number, payload?: unknown) => Promise<void>;\n /** Deliver an arbitrary envelope on the stream. */\n sendEnvelope: (envelope: StatewireEnvelope) => Promise<void>;\n /** End the stream with a fin; further attaches fail like a dead server. */\n finish: (reason: string, message?: string) => Promise<void>;\n readonly clientId: string;\n readonly attached: boolean;\n};\n\n/**\n * A mock statewire backend: serves `initialState` as the attach snapshot,\n * records submitted frames (receipt only — acks and results are delivered\n * from test code), and exposes the wire as a transport ResourceElement.\n */\nlet mockCount = 0;\n\nexport const createMockTransport = <TState>(\n initialState: TState,\n protocol = \"default\",\n options: { ephemeral?: readonly string[] } = {},\n): MockTransport<TState> => {\n // Read per attach: a test can grow the lane set between connects.\n const ephemeralLanes = () =>\n (options.ephemeral ?? []).map((child) => `${protocol}/${child}`);\n let state: unknown = initialState;\n let clientId = \"\";\n let finished = false;\n const clients = new Map<string, Record<string, number>>();\n const streams = new Set<FakeStreamHandle>();\n const commands: MockTransport<unknown>[\"commands\"] = [];\n const contexts: MockTransport<unknown>[\"contexts\"] = [];\n\n const sendEnvelope = (envelope: StatewireEnvelope) => {\n for (const stream of streams) stream.send(sseFrame(envelope));\n return tick();\n };\n\n const admit = (id: string, frame: StatewireFrame) => {\n const rec = clients.get(id)!;\n contexts.push(...(frame.ctx ?? []));\n for (const statement of frame.cmd ?? []) {\n if (statement.seq <= (rec[statement.protocol] ?? 0)) continue;\n rec[statement.protocol] = statement.seq;\n commands.push({\n protocol: statement.protocol,\n method: statement.method,\n params: statement.params,\n });\n }\n };\n\n const fakeFetch = (async (\n input: RequestInfo | URL,\n init?: RequestInit,\n ): Promise<Response> => {\n if (finished) throw new TypeError(\"fetch failed (finished)\");\n const url = String(input);\n const headers = new Headers(init?.headers);\n const id = headers.get(\"Statewire-Client-Id\") ?? \"\";\n if (url.endsWith(\"/stream\")) {\n clientId = id;\n if (!clients.has(id)) clients.set(id, {});\n const rec = clients.get(id)!;\n const ephemeral = ephemeralLanes();\n const negotiation = parseNegotiation(headers);\n const offers = negotiation.applicationProtocols;\n // The newest wire this mock speaks that the client offered. It speaks\n // GAP_FILL_VERSION and newer: it skips a re-sent seq instead of\n // re-emitting its verdict, as older wires require.\n const offered = negotiation.wireVersion;\n const wireVersion =\n offered.version < WIRE_VERSION ? offered.version : WIRE_VERSION;\n if (\n wireVersion < GAP_FILL_VERSION ||\n (offered.minVersion !== undefined && wireVersion < offered.minVersion)\n )\n throw new Error(\"mock wire version was not offered\");\n const applicationVersion =\n offers === \"*\"\n ? DEFAULT_PROTOCOL_VERSION\n : offers.find((offer) => offer.name === protocol)?.version;\n if (applicationVersion === undefined)\n throw new Error(\"mock protocol was not offered\");\n for (const lane of ephemeral) delete rec[lane];\n const frame = JSON.parse(String(init?.body ?? \"{}\")) as StatewireFrame;\n admit(id, frame);\n return openSSEStream(\n streams,\n id,\n (handle) => {\n handle.send(\n sseFrame({\n ops: [{ op: \"mount\", protocol, main: true, value: state }],\n syn: {\n version: wireVersion,\n protocols: [\n {\n name: protocol,\n version: applicationVersion,\n seq: rec[protocol] ?? 0,\n },\n ...ephemeral.map((name) => ({\n name,\n version: applicationVersion,\n record: \"ephemeral\" as const,\n seq: 0,\n })),\n ],\n },\n }),\n );\n },\n init?.signal ?? undefined,\n { \"Statewire-Lease\": \"mock-lease\" },\n );\n }\n if (url.endsWith(\"/frames\")) {\n admit(clientId, JSON.parse(String(init?.body ?? \"{}\")) as StatewireFrame);\n return Response.json({});\n }\n return new Response(null, { status: 404 });\n }) as typeof fetch;\n\n return {\n transport: StatewireHttp({\n url: `http://mock-${++mockCount}/api`,\n fetch: fakeFetch,\n }),\n commands,\n contexts,\n setState: (next) => {\n state = next;\n return sendEnvelope({ ops: [{ op: \"replace\", path: [], value: next }] });\n },\n applied: (seq) =>\n sendEnvelope({ cmd: [{ protocol, seq, type: \"applied\" }] }),\n settle: (seq, payload) =>\n sendEnvelope({\n cmd: [\n {\n protocol,\n seq,\n type: \"result\",\n ...(payload !== undefined && { payload }),\n },\n ],\n }),\n sendEnvelope,\n finish: async (reason, message) => {\n await sendEnvelope({\n fin: { reason, ...(message !== undefined && { message }) },\n });\n finished = true;\n for (const stream of [...streams]) stream.close();\n await tick();\n },\n get clientId() {\n return clientId;\n },\n get attached() {\n return streams.size > 0;\n },\n };\n};\n"],"mappings":"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"}
|
package/package.json
CHANGED
package/src/indexedDb.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useEffect, useMemo, useState } from "react";
|
|
2
2
|
import { resource, useResource } from "@assistant-ui/tap";
|
|
3
|
-
import { KeyValueSessionStorage } from "./storage";
|
|
3
|
+
import { KeyValueSessionStorage, sessionNamespace } from "./storage";
|
|
4
4
|
import type { StatewireClient } from "./statewire";
|
|
5
5
|
import { randomId } from "./util/randomId";
|
|
6
6
|
|
|
@@ -107,6 +107,7 @@ const useIndexedDbSession = ({
|
|
|
107
107
|
database = "statewire",
|
|
108
108
|
key,
|
|
109
109
|
namespace = "statewire-session:",
|
|
110
|
+
principal,
|
|
110
111
|
...options
|
|
111
112
|
}: IndexedDbSession.Options): StatewireClient.SessionStorage => {
|
|
112
113
|
if (typeof navigator === "undefined" || navigator.locks === undefined)
|
|
@@ -114,7 +115,7 @@ const useIndexedDbSession = ({
|
|
|
114
115
|
"statewire: IndexedDbSession requires the Web Locks API (navigator.locks)",
|
|
115
116
|
);
|
|
116
117
|
const backend = indexedDbBackend(database);
|
|
117
|
-
const storageKey = namespace + key;
|
|
118
|
+
const storageKey = sessionNamespace(namespace, principal) + key;
|
|
118
119
|
const [tabId] = useState(randomId);
|
|
119
120
|
useEffect(() => holdLock(`statewire-tab:${tabId}`), [tabId]);
|
|
120
121
|
useEffect(
|
|
@@ -122,7 +123,7 @@ const useIndexedDbSession = ({
|
|
|
122
123
|
[database, storageKey],
|
|
123
124
|
);
|
|
124
125
|
const storage = useResource(
|
|
125
|
-
KeyValueSessionStorage({ ...options, backend, key, namespace }),
|
|
126
|
+
KeyValueSessionStorage({ ...options, backend, key, namespace, principal }),
|
|
126
127
|
);
|
|
127
128
|
return useMemo(
|
|
128
129
|
(): StatewireClient.SessionStorage => ({
|
|
@@ -151,7 +152,7 @@ const useIndexedDbSession = ({
|
|
|
151
152
|
);
|
|
152
153
|
};
|
|
153
154
|
|
|
154
|
-
/** Shared session storage on IndexedDB: sharers serialize through Web Locks and hear each other over a BroadcastChannel. */
|
|
155
|
+
/** Shared session storage on IndexedDB, keyed per principal and key: sharers serialize through Web Locks and hear each other over a BroadcastChannel. */
|
|
155
156
|
export const IndexedDbSession = resource(useIndexedDbSession);
|
|
156
157
|
|
|
157
158
|
export namespace IndexedDbSession {
|
package/src/storage.ts
CHANGED
|
@@ -245,6 +245,24 @@ const invalid = (): never => {
|
|
|
245
245
|
throw new Error("statewire: invalid stored session record");
|
|
246
246
|
};
|
|
247
247
|
|
|
248
|
+
const isPrincipal = (value: unknown): value is string =>
|
|
249
|
+
typeof value === "string" && value !== "";
|
|
250
|
+
|
|
251
|
+
/** The prefix of every record `KeyValueSessionStorage` keeps for `principal` under `namespace`. */
|
|
252
|
+
export const sessionNamespace = (namespace: string, principal: string) => {
|
|
253
|
+
if (!isPrincipal(principal))
|
|
254
|
+
throw new Error("statewire: storage requires a non-empty principal");
|
|
255
|
+
return `${namespace}${encodeURIComponent(principal)}:`;
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
const usePrincipal = (principal: string) => {
|
|
259
|
+
const initial = useRef(principal);
|
|
260
|
+
if (initial.current !== principal)
|
|
261
|
+
throw new Error(
|
|
262
|
+
"statewire: principal changed; remount the account's thread/session",
|
|
263
|
+
);
|
|
264
|
+
};
|
|
265
|
+
|
|
248
266
|
type LegacyLane = Record<string, unknown> & {
|
|
249
267
|
commands: StatewireClient.SessionCommand[];
|
|
250
268
|
outbox?: (Omit<StatewireClient.OutboxCommand, "held"> & {
|
|
@@ -335,16 +353,27 @@ const upgrade = (record: LegacyRecord): StatewireClient.SessionRecord => ({
|
|
|
335
353
|
const isLegacyLane = (lane: unknown) =>
|
|
336
354
|
isLaneWith(lane, isSessionCommand, true);
|
|
337
355
|
|
|
356
|
+
// Format 6 records name their principal; earlier formats sit under a key the
|
|
357
|
+
// principal already scopes.
|
|
338
358
|
const normalize = (
|
|
339
359
|
parsed: unknown,
|
|
340
360
|
protocol: string,
|
|
361
|
+
principal: string,
|
|
341
362
|
): StatewireClient.SessionRecord => {
|
|
342
363
|
if (!isRecord(parsed)) return invalid();
|
|
343
364
|
const {
|
|
344
365
|
statewireStorageVersion: format,
|
|
345
366
|
savedAt: _savedAt,
|
|
367
|
+
principal: owner,
|
|
346
368
|
...value
|
|
347
369
|
} = parsed;
|
|
370
|
+
if (format === 6) {
|
|
371
|
+
if (!isPrincipal(owner) || !isSessionRecord(value)) return invalid();
|
|
372
|
+
if (owner !== principal)
|
|
373
|
+
throw new Error("statewire: stored session belongs to another principal");
|
|
374
|
+
return value;
|
|
375
|
+
}
|
|
376
|
+
if (owner !== undefined) return invalid();
|
|
348
377
|
if (format === 5) return isSessionRecord(value) ? value : invalid();
|
|
349
378
|
if (format === 4)
|
|
350
379
|
return isRecordWith(value, isLegacyLane)
|
|
@@ -362,7 +391,7 @@ const normalize = (
|
|
|
362
391
|
return upgrade(liftLegacy(legacy, protocol));
|
|
363
392
|
};
|
|
364
393
|
|
|
365
|
-
export const STORAGE_FORMAT =
|
|
394
|
+
export const STORAGE_FORMAT = 6;
|
|
366
395
|
|
|
367
396
|
const parse = async (
|
|
368
397
|
raw: string | null,
|
|
@@ -375,8 +404,9 @@ const parse = async (
|
|
|
375
404
|
const decode = (
|
|
376
405
|
raw: string | null,
|
|
377
406
|
protocol: string,
|
|
407
|
+
principal: string,
|
|
378
408
|
): StatewireClient.SessionRecord | null =>
|
|
379
|
-
raw === null ? null : normalize(JSON.parse(raw), protocol);
|
|
409
|
+
raw === null ? null : normalize(JSON.parse(raw), protocol, principal);
|
|
380
410
|
|
|
381
411
|
const isUnsent = (lane: StatewireClient.SessionLane) =>
|
|
382
412
|
lane.commands.length > 0 || (lane.outbox?.length ?? 0) > 0;
|
|
@@ -448,8 +478,9 @@ const sweepOnce = async (
|
|
|
448
478
|
currentKey: string,
|
|
449
479
|
quota = false,
|
|
450
480
|
) => {
|
|
451
|
-
const { backend, retention,
|
|
481
|
+
const { backend, retention, codec, onEvict, principal } = options;
|
|
452
482
|
if (!retention || !backend.getAllKeys) return;
|
|
483
|
+
const namespace = sessionNamespace(options.namespace ?? "", principal);
|
|
453
484
|
const candidate = async (raw: string | null) => {
|
|
454
485
|
let value: unknown;
|
|
455
486
|
try {
|
|
@@ -460,7 +491,7 @@ const sweepOnce = async (
|
|
|
460
491
|
if (value === null) return undefined;
|
|
461
492
|
let record: StatewireClient.SessionRecord;
|
|
462
493
|
try {
|
|
463
|
-
record = normalize(value, DEFAULT_PROTOCOL);
|
|
494
|
+
record = normalize(value, DEFAULT_PROTOCOL, principal);
|
|
464
495
|
} catch {
|
|
465
496
|
return undefined;
|
|
466
497
|
}
|
|
@@ -561,6 +592,7 @@ const useKeyValueSessionStorage = (
|
|
|
561
592
|
backend,
|
|
562
593
|
key,
|
|
563
594
|
namespace = "",
|
|
595
|
+
principal,
|
|
564
596
|
protocol = DEFAULT_PROTOCOL,
|
|
565
597
|
migrate,
|
|
566
598
|
retention: suppliedRetention,
|
|
@@ -569,6 +601,8 @@ const useKeyValueSessionStorage = (
|
|
|
569
601
|
onError,
|
|
570
602
|
onEvict,
|
|
571
603
|
} = options;
|
|
604
|
+
const scope = sessionNamespace(namespace, principal);
|
|
605
|
+
usePrincipal(principal);
|
|
572
606
|
const latest = useRef({ onError, onEvict });
|
|
573
607
|
latest.current = { onError, onEvict };
|
|
574
608
|
const reportError = useMemo(
|
|
@@ -606,13 +640,14 @@ const useKeyValueSessionStorage = (
|
|
|
606
640
|
backend,
|
|
607
641
|
key,
|
|
608
642
|
namespace,
|
|
643
|
+
principal,
|
|
609
644
|
onEvict: reportEvict,
|
|
610
645
|
...(codec === undefined ? {} : { codec }),
|
|
611
646
|
...(retention === undefined ? {} : { retention }),
|
|
612
647
|
}),
|
|
613
|
-
[backend, key, namespace, retention, codec, reportEvict],
|
|
648
|
+
[backend, key, namespace, principal, retention, codec, reportEvict],
|
|
614
649
|
);
|
|
615
|
-
const storageKey =
|
|
650
|
+
const storageKey = scope + key;
|
|
616
651
|
if (retention !== undefined) {
|
|
617
652
|
if (!namespace)
|
|
618
653
|
throw new Error("statewire: session retention requires a namespace");
|
|
@@ -659,7 +694,9 @@ const useKeyValueSessionStorage = (
|
|
|
659
694
|
.then(async (value) => {
|
|
660
695
|
state.bytes = value?.length ?? 0;
|
|
661
696
|
const parsed = await parse(value, codec);
|
|
662
|
-
hydrate(
|
|
697
|
+
hydrate(
|
|
698
|
+
parsed === null ? null : normalize(parsed, protocol, principal),
|
|
699
|
+
);
|
|
663
700
|
})
|
|
664
701
|
.catch((cause: unknown) => {
|
|
665
702
|
const error = wrapStorageError(cause);
|
|
@@ -671,7 +708,7 @@ const useKeyValueSessionStorage = (
|
|
|
671
708
|
} else {
|
|
672
709
|
try {
|
|
673
710
|
state.bytes = (raw as string | null)?.length ?? 0;
|
|
674
|
-
hydrate(decode(raw as string | null, protocol));
|
|
711
|
+
hydrate(decode(raw as string | null, protocol, principal));
|
|
675
712
|
} catch (cause) {
|
|
676
713
|
const error = wrapStorageError(cause);
|
|
677
714
|
reportError(error);
|
|
@@ -679,7 +716,7 @@ const useKeyValueSessionStorage = (
|
|
|
679
716
|
}
|
|
680
717
|
}
|
|
681
718
|
return state;
|
|
682
|
-
}, [backend, storageKey, protocol, codec, reportError]);
|
|
719
|
+
}, [backend, storageKey, protocol, principal, codec, reportError]);
|
|
683
720
|
|
|
684
721
|
useEffect(() => {
|
|
685
722
|
const entries = active.get(backend) ?? new Map<string, number>();
|
|
@@ -703,13 +740,13 @@ const useKeyValueSessionStorage = (
|
|
|
703
740
|
|
|
704
741
|
useEffect(() => {
|
|
705
742
|
if (totalBytes === undefined) return;
|
|
706
|
-
void otherBytes(backend,
|
|
743
|
+
void otherBytes(backend, scope, storageKey).then(
|
|
707
744
|
(bytes) => {
|
|
708
745
|
state.others = bytes;
|
|
709
746
|
},
|
|
710
747
|
() => {},
|
|
711
748
|
);
|
|
712
|
-
}, [state, backend,
|
|
749
|
+
}, [state, backend, scope, storageKey, totalBytes]);
|
|
713
750
|
|
|
714
751
|
return useMemo(() => {
|
|
715
752
|
const checkLimits = (
|
|
@@ -752,7 +789,8 @@ const useKeyValueSessionStorage = (
|
|
|
752
789
|
const raw = await backend.getItem(storageKey);
|
|
753
790
|
const parsed = await parse(raw, codec);
|
|
754
791
|
state.bytes = raw?.length ?? 0;
|
|
755
|
-
state.record =
|
|
792
|
+
state.record =
|
|
793
|
+
parsed === null ? null : normalize(parsed, protocol, principal);
|
|
756
794
|
return state.record;
|
|
757
795
|
} catch (cause) {
|
|
758
796
|
const error = wrapStorageError(cause);
|
|
@@ -780,7 +818,7 @@ const useKeyValueSessionStorage = (
|
|
|
780
818
|
"statewire: storage backend changed from synchronous to asynchronous",
|
|
781
819
|
);
|
|
782
820
|
try {
|
|
783
|
-
state.record = decode(raw, protocol);
|
|
821
|
+
state.record = decode(raw, protocol, principal);
|
|
784
822
|
return state.record;
|
|
785
823
|
} catch (cause) {
|
|
786
824
|
const error = wrapStorageError(cause);
|
|
@@ -798,6 +836,7 @@ const useKeyValueSessionStorage = (
|
|
|
798
836
|
throw new Error("statewire: invalid session record");
|
|
799
837
|
const encoded = JSON.stringify({
|
|
800
838
|
...record,
|
|
839
|
+
principal,
|
|
801
840
|
statewireStorageVersion: STORAGE_FORMAT,
|
|
802
841
|
savedAt: Date.now(),
|
|
803
842
|
});
|
|
@@ -873,6 +912,7 @@ const useKeyValueSessionStorage = (
|
|
|
873
912
|
backend,
|
|
874
913
|
storageKey,
|
|
875
914
|
protocol,
|
|
915
|
+
principal,
|
|
876
916
|
migrate,
|
|
877
917
|
retention,
|
|
878
918
|
configured,
|
|
@@ -909,6 +949,8 @@ export namespace KeyValueSessionStorage {
|
|
|
909
949
|
backend: Backend;
|
|
910
950
|
key: string;
|
|
911
951
|
namespace?: string;
|
|
952
|
+
/** The account whose session this storage holds; scopes the stored key, tags the record. */
|
|
953
|
+
principal: string;
|
|
912
954
|
/** The lane that sessions saved in a single-lane format lift into; defaults to the client's default protocol. */
|
|
913
955
|
protocol?: string;
|
|
914
956
|
migrate?: StatewireClient.SessionStorage["migrate"];
|
|
@@ -929,24 +971,32 @@ export namespace KeyValueSessionStorage {
|
|
|
929
971
|
const pages = new Map<string, StatewireClient.SessionRecord | null>();
|
|
930
972
|
|
|
931
973
|
const useMemorySessionStorage = ({
|
|
974
|
+
principal,
|
|
932
975
|
scope,
|
|
933
|
-
}: MemorySessionStorage.Options): StatewireClient.SessionStorage =>
|
|
934
|
-
|
|
976
|
+
}: MemorySessionStorage.Options): StatewireClient.SessionStorage => {
|
|
977
|
+
const key = sessionNamespace("", principal) + scope;
|
|
978
|
+
usePrincipal(principal);
|
|
979
|
+
return useMemo(
|
|
935
980
|
() => ({
|
|
936
981
|
durable: false,
|
|
937
|
-
load: () => pages.get(
|
|
982
|
+
load: () => pages.get(key) ?? null,
|
|
938
983
|
save: (record) => {
|
|
939
|
-
pages.set(
|
|
984
|
+
pages.set(key, record);
|
|
940
985
|
},
|
|
941
986
|
}),
|
|
942
|
-
[
|
|
987
|
+
[key],
|
|
943
988
|
);
|
|
989
|
+
};
|
|
944
990
|
|
|
945
|
-
/** Session storage in page memory, keyed by scope: the record and client identity survive remounts, not reloads. */
|
|
991
|
+
/** Session storage in page memory, keyed by principal and scope: the record and client identity survive remounts, not reloads. */
|
|
946
992
|
export const MemorySessionStorage = resource(useMemorySessionStorage);
|
|
947
993
|
|
|
948
994
|
export namespace MemorySessionStorage {
|
|
949
|
-
export type Options = {
|
|
995
|
+
export type Options = {
|
|
996
|
+
/** The account whose session this storage holds. */
|
|
997
|
+
principal: string;
|
|
998
|
+
scope: string;
|
|
999
|
+
};
|
|
950
1000
|
}
|
|
951
1001
|
|
|
952
1002
|
const entriesOf = (
|
|
@@ -979,34 +1029,36 @@ const entriesOf = (
|
|
|
979
1029
|
];
|
|
980
1030
|
});
|
|
981
1031
|
|
|
982
|
-
/** Reads the outboxes of every session under `namespace`, mounted or not. */
|
|
1032
|
+
/** Reads the outboxes of every session `principal` keeps under `namespace`, mounted or not. */
|
|
983
1033
|
export const readOutboxes = async (
|
|
984
1034
|
backend: KeyValueSessionStorage.Backend,
|
|
985
1035
|
options: ReadOutboxes.Options,
|
|
986
1036
|
): Promise<readonly ReadOutboxes.Outbox[]> => {
|
|
987
1037
|
if (!backend.getAllKeys)
|
|
988
1038
|
throw new Error("statewire: readOutboxes requires getAllKeys");
|
|
989
|
-
const { namespace, codec, protocol = DEFAULT_PROTOCOL } = options;
|
|
1039
|
+
const { namespace, principal, codec, protocol = DEFAULT_PROTOCOL } = options;
|
|
990
1040
|
if (!namespace)
|
|
991
1041
|
throw new Error("statewire: readOutboxes requires a namespace");
|
|
1042
|
+
const scope = sessionNamespace(namespace, principal);
|
|
992
1043
|
const result: ReadOutboxes.Outbox[] = [];
|
|
993
1044
|
for (const key of await backend.getAllKeys()) {
|
|
994
|
-
if (!key.startsWith(
|
|
1045
|
+
if (!key.startsWith(scope)) continue;
|
|
995
1046
|
const parsed = await parse(await backend.getItem(key), codec);
|
|
996
1047
|
if (parsed === null) continue;
|
|
997
|
-
const entries = entriesOf(normalize(parsed, protocol));
|
|
1048
|
+
const entries = entriesOf(normalize(parsed, protocol, principal));
|
|
998
1049
|
if (entries.length > 0) result.push({ key, entries });
|
|
999
1050
|
}
|
|
1000
1051
|
return result;
|
|
1001
1052
|
};
|
|
1002
1053
|
|
|
1003
|
-
/** Calls `listener` with `readOutboxes` now and after every save under `namespace`; returns unsubscribe. */
|
|
1054
|
+
/** Calls `listener` with `readOutboxes` now and after every save to `principal`'s sessions under `namespace`; returns unsubscribe. */
|
|
1004
1055
|
export const subscribeOutboxes = (
|
|
1005
1056
|
backend: KeyValueSessionStorage.Backend,
|
|
1006
1057
|
options: ReadOutboxes.Options,
|
|
1007
1058
|
listener: (outboxes: readonly ReadOutboxes.Outbox[]) => void,
|
|
1008
1059
|
onError: (error: unknown) => void,
|
|
1009
1060
|
): (() => void) => {
|
|
1061
|
+
const scope = sessionNamespace(options.namespace, options.principal);
|
|
1010
1062
|
let closed = false;
|
|
1011
1063
|
let reading = Promise.resolve();
|
|
1012
1064
|
const refresh = () => {
|
|
@@ -1021,7 +1073,7 @@ export const subscribeOutboxes = (
|
|
|
1021
1073
|
});
|
|
1022
1074
|
};
|
|
1023
1075
|
const onKey = (key: string) => {
|
|
1024
|
-
if (key.startsWith(
|
|
1076
|
+
if (key.startsWith(scope)) refresh();
|
|
1025
1077
|
};
|
|
1026
1078
|
const local = savers.get(backend) ?? new Set();
|
|
1027
1079
|
savers.set(backend, local);
|
|
@@ -1038,6 +1090,8 @@ export const subscribeOutboxes = (
|
|
|
1038
1090
|
export namespace ReadOutboxes {
|
|
1039
1091
|
export type Options = {
|
|
1040
1092
|
namespace: string;
|
|
1093
|
+
/** The account whose sessions to read. */
|
|
1094
|
+
principal: string;
|
|
1041
1095
|
codec?: KeyValueSessionStorage.Codec;
|
|
1042
1096
|
/** The lane that single-lane formats lift into. */
|
|
1043
1097
|
protocol?: string;
|
|
@@ -7,6 +7,7 @@ import { openSSEStream, sseFrame } from "./fake-sse";
|
|
|
7
7
|
import type { FakeStreamHandle } from "./fake-sse";
|
|
8
8
|
import {
|
|
9
9
|
DEFAULT_PROTOCOL_VERSION,
|
|
10
|
+
GAP_FILL_VERSION,
|
|
10
11
|
WIRE_VERSION,
|
|
11
12
|
parseNegotiation,
|
|
12
13
|
} from "../wire/protocol";
|
|
@@ -93,7 +94,19 @@ export const createMockTransport = <TState>(
|
|
|
93
94
|
if (!clients.has(id)) clients.set(id, {});
|
|
94
95
|
const rec = clients.get(id)!;
|
|
95
96
|
const ephemeral = ephemeralLanes();
|
|
96
|
-
const
|
|
97
|
+
const negotiation = parseNegotiation(headers);
|
|
98
|
+
const offers = negotiation.applicationProtocols;
|
|
99
|
+
// The newest wire this mock speaks that the client offered. It speaks
|
|
100
|
+
// GAP_FILL_VERSION and newer: it skips a re-sent seq instead of
|
|
101
|
+
// re-emitting its verdict, as older wires require.
|
|
102
|
+
const offered = negotiation.wireVersion;
|
|
103
|
+
const wireVersion =
|
|
104
|
+
offered.version < WIRE_VERSION ? offered.version : WIRE_VERSION;
|
|
105
|
+
if (
|
|
106
|
+
wireVersion < GAP_FILL_VERSION ||
|
|
107
|
+
(offered.minVersion !== undefined && wireVersion < offered.minVersion)
|
|
108
|
+
)
|
|
109
|
+
throw new Error("mock wire version was not offered");
|
|
97
110
|
const applicationVersion =
|
|
98
111
|
offers === "*"
|
|
99
112
|
? DEFAULT_PROTOCOL_VERSION
|
|
@@ -111,7 +124,7 @@ export const createMockTransport = <TState>(
|
|
|
111
124
|
sseFrame({
|
|
112
125
|
ops: [{ op: "mount", protocol, main: true, value: state }],
|
|
113
126
|
syn: {
|
|
114
|
-
version:
|
|
127
|
+
version: wireVersion,
|
|
115
128
|
protocols: [
|
|
116
129
|
{
|
|
117
130
|
name: protocol,
|