statewire 0.23.0 → 0.24.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/core.d.ts +4 -3
- package/dist/client/core.d.ts.map +1 -1
- package/dist/client/core.js +56 -45
- package/dist/client/core.js.map +1 -1
- package/dist/client/session.d.ts +56 -12
- package/dist/client/session.d.ts.map +1 -1
- package/dist/client/session.js +632 -260
- package/dist/client/session.js.map +1 -1
- package/dist/client/sessionStore.d.ts +1 -0
- package/dist/client/sessionStore.d.ts.map +1 -1
- package/dist/client/sessionStore.js +1 -0
- package/dist/client/sessionStore.js.map +1 -1
- package/dist/host/host.d.ts +6 -5
- package/dist/host/host.d.ts.map +1 -1
- package/dist/host/host.js.map +1 -1
- package/dist/indexedDb.d.ts +15 -0
- package/dist/indexedDb.d.ts.map +1 -0
- package/dist/indexedDb.js +171 -0
- package/dist/indexedDb.js.map +1 -0
- package/dist/statewire.d.ts +117 -59
- package/dist/statewire.d.ts.map +1 -1
- package/dist/statewire.js +13 -25
- package/dist/statewire.js.map +1 -1
- package/dist/storage.d.ts +42 -0
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +387 -125
- package/dist/storage.js.map +1 -1
- package/dist/transports/remote.d.ts.map +1 -1
- package/dist/transports/remote.js +7 -9
- package/dist/transports/remote.js.map +1 -1
- package/dist/types.d.ts +6 -7
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +2 -1
- package/src/client/core.ts +81 -64
- package/src/client/session.ts +886 -424
- package/src/client/sessionStore.ts +2 -0
- package/src/host/host.ts +10 -3
- package/src/indexedDb.ts +167 -0
- package/src/statewire.ts +133 -86
- package/src/storage.ts +548 -112
- package/src/transports/remote.ts +13 -14
- package/src/types.ts +6 -7
package/dist/storage.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { DEFAULT_PROTOCOL, negotiationHeaders, protocolToken } from "./wire/protocol.js";
|
|
2
|
+
import { reviveDocumentValue } from "./document.js";
|
|
2
3
|
import { wrapStorageError } from "./client/storageError.js";
|
|
3
4
|
import { randomId } from "./util/randomId.js";
|
|
5
|
+
import { IndexedDbSession, indexedDbBackend } from "./indexedDb.js";
|
|
4
6
|
import { useEffect, useMemo, useRef } from "@assistant-ui/tap/react-shim";
|
|
5
7
|
import { resource } from "@assistant-ui/tap";
|
|
8
|
+
import { c } from "@assistant-ui/tap/react-shim/compiler-runtime";
|
|
6
9
|
// src/storage.ts
|
|
7
10
|
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8
11
|
const isOptionalString = (value) => value === void 0 || typeof value === "string";
|
|
@@ -11,7 +14,9 @@ const isStatement = (value) => isRecord(value) && isProtocol(value["protocol"])
|
|
|
11
14
|
const isIdentified = (value) => typeof value["id"] === "string" && value["id"] !== "" && typeof value["ts"] === "number" && Number.isFinite(value["ts"]);
|
|
12
15
|
const isLegacyCommand = (value) => isStatement(value) && isPositiveSafeInteger(value["seq"]);
|
|
13
16
|
const isSessionCommand = (value) => isLegacyCommand(value) && isIdentified(value) && isOptionalString(value["owner"]);
|
|
14
|
-
const isOutboxCommand = (value) => isStatement(value) && isIdentified(value) && (value["
|
|
17
|
+
const isOutboxCommand = (value) => isStatement(value) && isIdentified(value) && (value["held"] === void 0 || value["held"] === true) && (value["released"] === void 0 || value["released"] === true) && isOptionalString(value["by"]);
|
|
18
|
+
const isLegacyOutboxCommand = (value) => isStatement(value) && isIdentified(value) && (value["hold"] === void 0 || isRecord(value["hold"])) && (value["released"] === void 0 || value["released"] === true) && isOptionalString(value["by"]);
|
|
19
|
+
const isStoredSettled = (value) => isRecord(value) && isIdentified(value) && typeof value["method"] === "string" && Array.isArray(value["params"]) && (value["seq"] === void 0 || isPositiveSafeInteger(value["seq"])) && (value["failure"] === void 0 ? isRecord(value["result"]) && Array.isArray(value["result"]["value"]) : value["result"] === void 0 && isSendFailure(value["failure"]));
|
|
15
20
|
const isSessionVerdict = (value) => isRecord(value) && isPositiveSafeInteger(value["seq"]) && isOptionalString(value["id"]) && (value["type"] === "result" || value["type"] === "rejected" || value["type"] === "failed" || value["type"] === "crashed" || value["type"] === "expired") && isOptionalString(value["code"]) && isOptionalString(value["message"]);
|
|
16
21
|
const isSendFailure = (value) => {
|
|
17
22
|
if (!isRecord(value)) return false;
|
|
@@ -20,13 +25,15 @@ const isSendFailure = (value) => {
|
|
|
20
25
|
case "failed": return value["cause"] === "server" && isOptionalString(value["message"]);
|
|
21
26
|
case "crashed":
|
|
22
27
|
case "expired": return value["cause"] === "server";
|
|
23
|
-
case "aborted": return (value["cause"] === "unmount" || value["cause"] === "stream-gone" || value["cause"] === "session-lost" || value["cause"] === "removed") && (value["predecessorLost"] === void 0 || value["predecessorLost"] === true);
|
|
28
|
+
case "aborted": return (value["cause"] === "unmount" || value["cause"] === "stream-gone" || value["cause"] === "session-lost" || value["cause"] === "removed" || value["cause"] === "quota") && (value["predecessorLost"] === void 0 || value["predecessorLost"] === true);
|
|
24
29
|
case "lost": return value["cause"] === "unmount" || value["cause"] === "stream-gone" || value["cause"] === "session-lost";
|
|
25
30
|
default: return false;
|
|
26
31
|
}
|
|
27
32
|
};
|
|
28
33
|
const isFailedCommand = (value) => isRecord(value) && isProtocol(value["protocol"]) && isProducer(value["produced"]) && typeof value["command"] === "string" && Array.isArray(value["params"]) && isSendFailure(value["failure"]) && typeof value["ts"] === "number" && Number.isFinite(value["ts"]) && isPositiveSafeInteger(value["seq"]);
|
|
29
|
-
const
|
|
34
|
+
const isArrayOf = (value, is) => value === void 0 || Array.isArray(value) && value.every(is);
|
|
35
|
+
const isMark = (mark) => isRecord(mark) && typeof mark["id"] === "string" && typeof mark["by"] === "string" && (mark["failure"] === void 0 || isSendFailure(mark["failure"]));
|
|
36
|
+
const isLaneWith = (value, isCommand, legacy = false) => isRecord(value) && isPositiveSafeInteger(value["nextSeq"]) && Array.isArray(value["commands"]) && value["commands"].every(isCommand) && isArrayOf(value["outbox"], legacy ? isLegacyOutboxCommand : isOutboxCommand) && isArrayOf(value["removed"], isMark) && isArrayOf(value["verdicts"], isSessionVerdict) && (legacy ? isArrayOf(value["failed"], isFailedCommand) : value["failed"] === void 0 && isArrayOf(value["inbox"], isStoredSettled));
|
|
30
37
|
const isSessionLane = (value) => isLaneWith(value, isSessionCommand);
|
|
31
38
|
const isClientId = (value) => typeof value === "string" && value.length > 0;
|
|
32
39
|
const isRecordWith = (value, isLane) => isRecord(value) && isClientId(value["clientId"]) && isRecord(value["lanes"]) && Object.keys(value["lanes"]).every(isProtocol) && Object.values(value["lanes"]).every(isLane);
|
|
@@ -93,71 +100,162 @@ const liftLegacy = (value, protocol) => {
|
|
|
93
100
|
lanes
|
|
94
101
|
});
|
|
95
102
|
};
|
|
103
|
+
const upgrade = (record) => ({
|
|
104
|
+
clientId: record.clientId,
|
|
105
|
+
lanes: Object.fromEntries(Object.entries(record.lanes).map(([protocol, legacy]) => {
|
|
106
|
+
const { outbox, failed, ...lane } = legacy;
|
|
107
|
+
const inbox = [...failed ?? []].sort((a, b) => a.ts - b.ts).map((f) => ({
|
|
108
|
+
id: `failed:${protocol}:${f.seq}:${f.ts}`,
|
|
109
|
+
method: f.command,
|
|
110
|
+
params: f.params,
|
|
111
|
+
...f.meta !== void 0 && { meta: f.meta },
|
|
112
|
+
ts: f.ts,
|
|
113
|
+
failure: f.failure
|
|
114
|
+
}));
|
|
115
|
+
return [protocol, {
|
|
116
|
+
...lane,
|
|
117
|
+
...outbox?.length && { outbox: outbox.map(({ hold, ...command }) => ({
|
|
118
|
+
...command,
|
|
119
|
+
...hold !== void 0 && { held: true },
|
|
120
|
+
...hold?.meta !== void 0 && { localMeta: hold.meta }
|
|
121
|
+
})) },
|
|
122
|
+
...inbox.length > 0 && { inbox }
|
|
123
|
+
}];
|
|
124
|
+
}))
|
|
125
|
+
});
|
|
126
|
+
const isLegacyLane = (lane) => isLaneWith(lane, isSessionCommand, true);
|
|
96
127
|
const normalize = (parsed, protocol) => {
|
|
97
128
|
if (!isRecord(parsed)) return invalid();
|
|
98
129
|
const { statewireStorageVersion: format, savedAt: _savedAt, ...value } = parsed;
|
|
99
|
-
if (format ===
|
|
100
|
-
if (format ===
|
|
130
|
+
if (format === 5) return isSessionRecord(value) ? value : invalid();
|
|
131
|
+
if (format === 4) return isRecordWith(value, isLegacyLane) ? upgrade(value) : invalid();
|
|
132
|
+
if (format === 3) return isRecordWith(value, (lane) => isLaneWith(lane, isLegacyCommand, true)) ? upgrade(identify(value)) : invalid();
|
|
101
133
|
if (format !== void 0 && format !== 1 && format !== 2) throw new Error("statewire: unsupported session storage version");
|
|
102
134
|
const { ctxNextSeq: _ctxNextSeq, context: _context, ...legacy } = value;
|
|
103
|
-
return liftLegacy(legacy, protocol);
|
|
135
|
+
return upgrade(liftLegacy(legacy, protocol));
|
|
104
136
|
};
|
|
137
|
+
const STORAGE_FORMAT = 5;
|
|
138
|
+
const parse = async (raw, codec) => raw === null ? null : JSON.parse(codec === void 0 ? raw : await codec.decode(raw));
|
|
105
139
|
const decode = (raw, protocol) => raw === null ? null : normalize(JSON.parse(raw), protocol);
|
|
106
|
-
const
|
|
140
|
+
const isUnsent = (lane) => lane.commands.length > 0 || (lane.outbox?.length ?? 0) > 0;
|
|
141
|
+
const entryCount = (record) => record === null ? 0 : Object.values(record.lanes).reduce((sum, lane) => sum + lane.commands.length + (lane.outbox?.length ?? 0), 0);
|
|
142
|
+
const noticesOf = (record) => Object.entries(record.lanes).flatMap(([protocol, lane]) => (lane.inbox ?? []).map((n) => ({
|
|
143
|
+
protocol,
|
|
144
|
+
id: n.id,
|
|
145
|
+
method: n.method,
|
|
146
|
+
params: n.params,
|
|
147
|
+
...n.seq !== void 0 && { seq: n.seq },
|
|
148
|
+
status: "settled",
|
|
149
|
+
...n.failure !== void 0 && { failure: n.failure },
|
|
150
|
+
...n.localMeta !== void 0 && { localMeta: n.localMeta }
|
|
151
|
+
})));
|
|
107
152
|
const active = /* @__PURE__ */ new WeakMap();
|
|
108
|
-
|
|
109
|
-
|
|
153
|
+
/** Same-runtime saves, per backend, for `subscribeOutboxes`. */
|
|
154
|
+
const savers = /* @__PURE__ */ new WeakMap();
|
|
155
|
+
const announce = (backend, key) => {
|
|
156
|
+
for (const listener of savers.get(backend) ?? []) listener(key);
|
|
157
|
+
};
|
|
158
|
+
const sweeping = /* @__PURE__ */ new WeakMap();
|
|
159
|
+
const sweep = (options, currentKey, quota = false) => {
|
|
160
|
+
const run = (sweeping.get(options.backend) ?? Promise.resolve()).catch(() => {}).then(() => sweepOnce(options, currentKey, quota));
|
|
161
|
+
sweeping.set(options.backend, run);
|
|
162
|
+
return run;
|
|
163
|
+
};
|
|
164
|
+
const sweepOnce = async (options, currentKey, quota = false) => {
|
|
165
|
+
const { backend, retention, namespace = "", codec, onEvict } = options;
|
|
110
166
|
if (!retention || !backend.getAllKeys) return;
|
|
111
|
-
const
|
|
112
|
-
const candidate = (raw) => {
|
|
113
|
-
if (raw === null) return void 0;
|
|
167
|
+
const candidate = async (raw) => {
|
|
114
168
|
let value;
|
|
115
169
|
try {
|
|
116
|
-
value =
|
|
170
|
+
value = await parse(raw, codec);
|
|
117
171
|
} catch {
|
|
118
172
|
return;
|
|
119
173
|
}
|
|
174
|
+
if (value === null) return void 0;
|
|
175
|
+
let record;
|
|
120
176
|
try {
|
|
121
|
-
|
|
177
|
+
record = normalize(value, DEFAULT_PROTOCOL);
|
|
122
178
|
} catch {
|
|
123
179
|
return;
|
|
124
180
|
}
|
|
181
|
+
if (Object.values(record.lanes).some(isUnsent)) return void 0;
|
|
125
182
|
const savedAt = value["savedAt"] ?? value["updatedAt"];
|
|
126
|
-
return typeof savedAt === "number" && Number.isFinite(savedAt) ?
|
|
183
|
+
return typeof savedAt === "number" && Number.isFinite(savedAt) ? {
|
|
184
|
+
savedAt,
|
|
185
|
+
record
|
|
186
|
+
} : void 0;
|
|
127
187
|
};
|
|
188
|
+
const empty = [];
|
|
189
|
+
const noticed = [];
|
|
128
190
|
for (const key of await backend.getAllKeys()) {
|
|
129
191
|
if (!key.startsWith(namespace) || key === currentKey || active.get(backend)?.has(key)) continue;
|
|
130
|
-
const
|
|
131
|
-
if (
|
|
192
|
+
const found = await candidate(await backend.getItem(key));
|
|
193
|
+
if (found === void 0) continue;
|
|
194
|
+
(noticesOf(found.record).length > 0 ? noticed : empty).push({
|
|
132
195
|
key,
|
|
133
|
-
|
|
196
|
+
...found
|
|
134
197
|
});
|
|
135
198
|
}
|
|
136
|
-
|
|
199
|
+
const remove = async (entry, reason) => {
|
|
200
|
+
const run = async () => {
|
|
201
|
+
if (active.get(backend)?.has(entry.key)) return;
|
|
202
|
+
const again = await candidate(await backend.getItem(entry.key));
|
|
203
|
+
if (again?.savedAt !== entry.savedAt) return;
|
|
204
|
+
if (active.get(backend)?.has(entry.key)) return;
|
|
205
|
+
await backend.removeItem(entry.key);
|
|
206
|
+
announce(backend, entry.key);
|
|
207
|
+
onEvict?.({
|
|
208
|
+
key: entry.key,
|
|
209
|
+
reason,
|
|
210
|
+
dropped: noticesOf(again.record)
|
|
211
|
+
});
|
|
212
|
+
};
|
|
213
|
+
if (backend.withUnused) await backend.withUnused(entry.key, run);
|
|
214
|
+
else await run();
|
|
215
|
+
};
|
|
137
216
|
const now = Date.now();
|
|
217
|
+
const expired = (entry) => retention.maxAgeMs !== void 0 && now - entry.savedAt > retention.maxAgeMs;
|
|
218
|
+
empty.sort((a, b) => b.savedAt - a.savedAt);
|
|
138
219
|
let retained = 1;
|
|
139
|
-
const quotaStart = Math.floor(
|
|
140
|
-
for (const [index, entry] of
|
|
141
|
-
const expired = retention.maxAgeMs !== void 0 && now - entry.savedAt > retention.maxAgeMs;
|
|
220
|
+
const quotaStart = Math.floor(empty.length / 2);
|
|
221
|
+
for (const [index, entry] of empty.entries()) {
|
|
142
222
|
const overflow = retention.maxEntries !== void 0 && retained >= retention.maxEntries;
|
|
143
|
-
if (expired
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
if (active.get(backend)?.has(entry.key)) return;
|
|
148
|
-
await backend.removeItem(entry.key);
|
|
149
|
-
};
|
|
150
|
-
if (backend.withUnused) await backend.withUnused(entry.key, remove);
|
|
151
|
-
else await remove();
|
|
152
|
-
} else retained++;
|
|
223
|
+
if (expired(entry)) await remove(entry, "expired");
|
|
224
|
+
else if (overflow) await remove(entry, "overflow");
|
|
225
|
+
else if (quota && index >= quotaStart) await remove(entry, "quota");
|
|
226
|
+
else retained++;
|
|
153
227
|
}
|
|
228
|
+
for (const entry of noticed) if (expired(entry)) await remove(entry, "expired");
|
|
154
229
|
};
|
|
155
230
|
const isQuotaError = (error) => error instanceof Error && error.name === "QuotaExceededError";
|
|
231
|
+
const quotaError = (limit) => {
|
|
232
|
+
const error = /* @__PURE__ */ new Error(`statewire: session ${limit} limit reached`);
|
|
233
|
+
error.name = "StatewireQuotaError";
|
|
234
|
+
return error;
|
|
235
|
+
};
|
|
236
|
+
const otherBytes = async (backend, namespace, key) => {
|
|
237
|
+
let total = 0;
|
|
238
|
+
for (const other of await backend.getAllKeys?.() ?? []) {
|
|
239
|
+
if (other === key || !other.startsWith(namespace)) continue;
|
|
240
|
+
total += (await backend.getItem(other))?.length ?? 0;
|
|
241
|
+
}
|
|
242
|
+
return total;
|
|
243
|
+
};
|
|
244
|
+
const validLimit = (value, name) => {
|
|
245
|
+
if (value !== void 0 && (!Number.isSafeInteger(value) || value < 1)) throw new Error(`statewire: invalid session limit ${name}`);
|
|
246
|
+
};
|
|
156
247
|
const useKeyValueSessionStorage = (options) => {
|
|
157
|
-
const { backend, key, namespace = "", protocol = DEFAULT_PROTOCOL, migrate, retention: suppliedRetention, onError } = options;
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
248
|
+
const { backend, key, namespace = "", protocol = DEFAULT_PROTOCOL, migrate, retention: suppliedRetention, limits: suppliedLimits, codec, onError, onEvict } = options;
|
|
249
|
+
const latest = useRef({
|
|
250
|
+
onError,
|
|
251
|
+
onEvict
|
|
252
|
+
});
|
|
253
|
+
latest.current = {
|
|
254
|
+
onError,
|
|
255
|
+
onEvict
|
|
256
|
+
};
|
|
257
|
+
const reportError = useMemo(() => (error) => latest.current.onError?.(error), []);
|
|
258
|
+
const reportEvict = useMemo(() => (event) => latest.current.onEvict?.(event), []);
|
|
161
259
|
const retain = suppliedRetention !== void 0;
|
|
162
260
|
const maxAgeMs = suppliedRetention?.maxAgeMs;
|
|
163
261
|
const maxEntries = suppliedRetention?.maxEntries;
|
|
@@ -169,16 +267,27 @@ const useKeyValueSessionStorage = (options) => {
|
|
|
169
267
|
maxAgeMs,
|
|
170
268
|
maxEntries
|
|
171
269
|
]);
|
|
270
|
+
const outboxEntries = suppliedLimits?.outboxEntries;
|
|
271
|
+
const recordBytes = suppliedLimits?.recordBytes;
|
|
272
|
+
const totalBytes = suppliedLimits?.totalBytes;
|
|
273
|
+
validLimit(outboxEntries, "outboxEntries");
|
|
274
|
+
validLimit(recordBytes, "recordBytes");
|
|
275
|
+
validLimit(totalBytes, "totalBytes");
|
|
276
|
+
if (totalBytes !== void 0 && !backend.getAllKeys) throw new Error("statewire: a totalBytes limit requires getAllKeys");
|
|
172
277
|
const configured = useMemo(() => ({
|
|
173
278
|
backend,
|
|
174
279
|
key,
|
|
175
280
|
namespace,
|
|
281
|
+
onEvict: reportEvict,
|
|
282
|
+
...codec === void 0 ? {} : { codec },
|
|
176
283
|
...retention === void 0 ? {} : { retention }
|
|
177
284
|
}), [
|
|
178
285
|
backend,
|
|
179
286
|
key,
|
|
180
287
|
namespace,
|
|
181
|
-
retention
|
|
288
|
+
retention,
|
|
289
|
+
codec,
|
|
290
|
+
reportEvict
|
|
182
291
|
]);
|
|
183
292
|
const storageKey = namespace + key;
|
|
184
293
|
if (retention !== void 0) {
|
|
@@ -198,17 +307,24 @@ const useKeyValueSessionStorage = (options) => {
|
|
|
198
307
|
}
|
|
199
308
|
const state = {
|
|
200
309
|
record: null,
|
|
310
|
+
bytes: 0,
|
|
311
|
+
others: void 0,
|
|
201
312
|
ready: void 0,
|
|
202
313
|
queue: Promise.resolve(),
|
|
203
314
|
pending: 0,
|
|
204
315
|
failure: void 0,
|
|
205
|
-
asynchronous: typeof raw !== "string" && raw !== null,
|
|
206
|
-
hydrated:
|
|
316
|
+
asynchronous: codec !== void 0 || typeof raw !== "string" && raw !== null,
|
|
317
|
+
hydrated: false
|
|
318
|
+
};
|
|
319
|
+
const hydrate = (record) => {
|
|
320
|
+
state.record = record;
|
|
321
|
+
state.hydrated = true;
|
|
207
322
|
};
|
|
208
323
|
if (state.asynchronous) {
|
|
209
|
-
state.ready = Promise.resolve(raw).then((value) => {
|
|
210
|
-
state.
|
|
211
|
-
|
|
324
|
+
state.ready = Promise.resolve(raw).then(async (value) => {
|
|
325
|
+
state.bytes = value?.length ?? 0;
|
|
326
|
+
const parsed = await parse(value, codec);
|
|
327
|
+
hydrate(parsed === null ? null : normalize(parsed, protocol));
|
|
212
328
|
}).catch((cause_0) => {
|
|
213
329
|
const error_1 = wrapStorageError(cause_0);
|
|
214
330
|
if (state.failure !== error_1) reportError(error_1);
|
|
@@ -217,7 +333,8 @@ const useKeyValueSessionStorage = (options) => {
|
|
|
217
333
|
});
|
|
218
334
|
state.ready.catch(() => {});
|
|
219
335
|
} else try {
|
|
220
|
-
state.
|
|
336
|
+
state.bytes = raw?.length ?? 0;
|
|
337
|
+
hydrate(decode(raw, protocol));
|
|
221
338
|
} catch (cause_1) {
|
|
222
339
|
const error_2 = wrapStorageError(cause_1);
|
|
223
340
|
reportError(error_2);
|
|
@@ -228,6 +345,7 @@ const useKeyValueSessionStorage = (options) => {
|
|
|
228
345
|
backend,
|
|
229
346
|
storageKey,
|
|
230
347
|
protocol,
|
|
348
|
+
codec,
|
|
231
349
|
reportError
|
|
232
350
|
]);
|
|
233
351
|
useEffect(() => {
|
|
@@ -253,94 +371,151 @@ const useKeyValueSessionStorage = (options) => {
|
|
|
253
371
|
retention,
|
|
254
372
|
reportError
|
|
255
373
|
]);
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
if (
|
|
374
|
+
useEffect(() => {
|
|
375
|
+
if (totalBytes === void 0) return;
|
|
376
|
+
otherBytes(backend, namespace, storageKey).then((bytes) => {
|
|
377
|
+
state_0.others = bytes;
|
|
378
|
+
}, () => {});
|
|
379
|
+
}, [
|
|
380
|
+
state_0,
|
|
381
|
+
backend,
|
|
382
|
+
namespace,
|
|
383
|
+
storageKey,
|
|
384
|
+
totalBytes
|
|
385
|
+
]);
|
|
386
|
+
return useMemo(() => {
|
|
387
|
+
const checkLimits = (record_0, bytes_0) => {
|
|
388
|
+
if (entryCount(record_0) <= entryCount(state_0.record)) return;
|
|
389
|
+
if (outboxEntries !== void 0 && entryCount(record_0) > outboxEntries) throw quotaError("outboxEntries");
|
|
390
|
+
if (recordBytes !== void 0 && bytes_0 > recordBytes) throw quotaError("recordBytes");
|
|
391
|
+
if (totalBytes !== void 0 && state_0.others !== void 0 && state_0.others + bytes_0 > totalBytes) throw quotaError("totalBytes");
|
|
392
|
+
};
|
|
393
|
+
const persist = async (encoded) => {
|
|
394
|
+
const stored = codec === void 0 ? encoded : await codec.encode(encoded);
|
|
271
395
|
try {
|
|
272
|
-
|
|
273
|
-
} catch (
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
396
|
+
await backend.setItem(storageKey, stored);
|
|
397
|
+
} catch (error_4) {
|
|
398
|
+
if (!isQuotaError(error_4)) throw error_4;
|
|
399
|
+
if (retention !== void 0) await sweep(configured, storageKey, true);
|
|
400
|
+
await backend.setItem(storageKey, stored);
|
|
277
401
|
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
});
|
|
288
|
-
state_0.record = record;
|
|
289
|
-
const recoverQuota = async (error_6) => {
|
|
290
|
-
if (!isQuotaError(error_6) || retention === void 0) throw error_6;
|
|
291
|
-
await sweep(configured, storageKey, true);
|
|
292
|
-
await backend.setItem(storageKey, encoded);
|
|
293
|
-
};
|
|
294
|
-
const write = async () => {
|
|
402
|
+
announce(backend, storageKey);
|
|
403
|
+
};
|
|
404
|
+
return {
|
|
405
|
+
durable: true,
|
|
406
|
+
...state_0.ready === void 0 ? {} : { ready: state_0.ready },
|
|
407
|
+
read: async () => {
|
|
408
|
+
if (state_0.failure !== void 0) throw state_0.failure;
|
|
409
|
+
await state_0.ready;
|
|
410
|
+
await state_0.queue.catch(() => {});
|
|
295
411
|
try {
|
|
296
|
-
await backend.
|
|
297
|
-
|
|
298
|
-
|
|
412
|
+
const raw_0 = await backend.getItem(storageKey);
|
|
413
|
+
const parsed_0 = await parse(raw_0, codec);
|
|
414
|
+
state_0.bytes = raw_0?.length ?? 0;
|
|
415
|
+
state_0.record = parsed_0 === null ? null : normalize(parsed_0, protocol);
|
|
416
|
+
return state_0.record;
|
|
417
|
+
} catch (cause_3) {
|
|
418
|
+
const error_5 = wrapStorageError(cause_3);
|
|
419
|
+
reportError(error_5);
|
|
420
|
+
throw error_5;
|
|
299
421
|
}
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
|
|
422
|
+
},
|
|
423
|
+
load: () => {
|
|
424
|
+
if (state_0.failure !== void 0) throw state_0.failure;
|
|
425
|
+
if (!state_0.hydrated) throw new Error("statewire: await session storage readiness before loading");
|
|
426
|
+
if (state_0.asynchronous || state_0.pending > 0) return state_0.record;
|
|
427
|
+
let raw_1;
|
|
303
428
|
try {
|
|
304
|
-
|
|
305
|
-
} catch (
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
429
|
+
raw_1 = backend.getItem(storageKey);
|
|
430
|
+
} catch (cause_4) {
|
|
431
|
+
const error_6 = wrapStorageError(cause_4);
|
|
432
|
+
reportError(error_6);
|
|
433
|
+
throw error_6;
|
|
434
|
+
}
|
|
435
|
+
if (typeof raw_1 !== "string" && raw_1 !== null) throw new Error("statewire: storage backend changed from synchronous to asynchronous");
|
|
436
|
+
try {
|
|
437
|
+
state_0.record = decode(raw_1, protocol);
|
|
438
|
+
return state_0.record;
|
|
439
|
+
} catch (cause_5) {
|
|
440
|
+
const error_7 = wrapStorageError(cause_5);
|
|
441
|
+
reportError(error_7);
|
|
442
|
+
throw error_7;
|
|
443
|
+
}
|
|
444
|
+
},
|
|
445
|
+
save: (record_1) => {
|
|
446
|
+
if (state_0.failure !== void 0) throw state_0.failure;
|
|
447
|
+
if (!state_0.hydrated) throw new Error("statewire: await session storage readiness before saving");
|
|
448
|
+
if (!isSessionRecord(record_1)) throw new Error("statewire: invalid session record");
|
|
449
|
+
const encoded_0 = JSON.stringify({
|
|
450
|
+
...record_1,
|
|
451
|
+
statewireStorageVersion: 5,
|
|
452
|
+
savedAt: Date.now()
|
|
453
|
+
});
|
|
454
|
+
checkLimits(record_1, encoded_0.length);
|
|
455
|
+
if (!state_0.asynchronous && state_0.pending === 0) {
|
|
456
|
+
try {
|
|
457
|
+
backend.setItem(storageKey, encoded_0);
|
|
458
|
+
} catch (error_8) {
|
|
459
|
+
if (!isQuotaError(error_8)) {
|
|
460
|
+
state_0.failure = wrapStorageError(error_8);
|
|
461
|
+
reportError(state_0.failure);
|
|
462
|
+
throw state_0.failure;
|
|
463
|
+
}
|
|
464
|
+
if (entryCount(record_1) > entryCount(state_0.record)) {
|
|
465
|
+
if (retention !== void 0) sweep(configured, storageKey, true).catch(() => {});
|
|
466
|
+
throw quotaError("backend");
|
|
467
|
+
}
|
|
468
|
+
state_0.record = record_1;
|
|
469
|
+
state_0.bytes = encoded_0.length;
|
|
470
|
+
state_0.pending++;
|
|
471
|
+
state_0.queue = persist(encoded_0).finally(() => {
|
|
472
|
+
state_0.pending--;
|
|
473
|
+
});
|
|
474
|
+
state_0.queue.catch((cause_6) => {
|
|
475
|
+
const failure = wrapStorageError(cause_6);
|
|
476
|
+
if (state_0.failure !== failure) reportError(failure);
|
|
477
|
+
state_0.failure = failure;
|
|
478
|
+
});
|
|
479
|
+
return;
|
|
310
480
|
}
|
|
311
|
-
|
|
481
|
+
state_0.record = record_1;
|
|
482
|
+
state_0.bytes = encoded_0.length;
|
|
483
|
+
announce(backend, storageKey);
|
|
484
|
+
return;
|
|
312
485
|
}
|
|
313
|
-
|
|
486
|
+
state_0.record = record_1;
|
|
487
|
+
state_0.bytes = encoded_0.length;
|
|
314
488
|
state_0.pending++;
|
|
315
|
-
state_0.queue =
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
state_0.
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
state_0.
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
489
|
+
state_0.queue = state_0.queue.then(() => state_0.ready).then(() => persist(encoded_0)).catch((cause_7) => {
|
|
490
|
+
const error_9 = wrapStorageError(cause_7);
|
|
491
|
+
if (state_0.failure !== error_9) reportError(error_9);
|
|
492
|
+
state_0.failure = error_9;
|
|
493
|
+
throw error_9;
|
|
494
|
+
}).finally(() => {
|
|
495
|
+
state_0.pending--;
|
|
496
|
+
});
|
|
497
|
+
state_0.queue.catch(() => {});
|
|
498
|
+
},
|
|
499
|
+
flush: () => {
|
|
500
|
+
if (state_0.failure !== void 0) throw state_0.failure;
|
|
501
|
+
if (!state_0.asynchronous && state_0.pending === 0) return;
|
|
502
|
+
return (async () => {
|
|
503
|
+
await state_0.ready;
|
|
504
|
+
for (;;) {
|
|
505
|
+
const queue = state_0.queue;
|
|
506
|
+
await queue;
|
|
507
|
+
if (queue === state_0.queue) return;
|
|
508
|
+
}
|
|
509
|
+
})();
|
|
510
|
+
},
|
|
511
|
+
onEvict: ({ reason, dropped }) => reportEvict({
|
|
512
|
+
key: storageKey,
|
|
513
|
+
reason,
|
|
514
|
+
dropped
|
|
515
|
+
}),
|
|
516
|
+
...migrate === void 0 ? {} : { migrate }
|
|
517
|
+
};
|
|
518
|
+
}, [
|
|
344
519
|
state_0,
|
|
345
520
|
backend,
|
|
346
521
|
storageKey,
|
|
@@ -348,11 +523,98 @@ const useKeyValueSessionStorage = (options) => {
|
|
|
348
523
|
migrate,
|
|
349
524
|
retention,
|
|
350
525
|
configured,
|
|
351
|
-
|
|
526
|
+
codec,
|
|
527
|
+
outboxEntries,
|
|
528
|
+
recordBytes,
|
|
529
|
+
totalBytes,
|
|
530
|
+
reportError,
|
|
531
|
+
reportEvict
|
|
352
532
|
]);
|
|
353
533
|
};
|
|
354
534
|
/** Persist sessions through a synchronous or asynchronous string storage backend. */
|
|
355
535
|
const KeyValueSessionStorage = resource(useKeyValueSessionStorage);
|
|
356
|
-
|
|
536
|
+
const pages = /* @__PURE__ */ new Map();
|
|
537
|
+
const useMemorySessionStorage = (t0) => {
|
|
538
|
+
const $ = c(2);
|
|
539
|
+
const { scope } = t0;
|
|
540
|
+
let t1;
|
|
541
|
+
if ($[0] !== scope) {
|
|
542
|
+
t1 = {
|
|
543
|
+
durable: false,
|
|
544
|
+
load: () => pages.get(scope) ?? null,
|
|
545
|
+
save: (record) => {
|
|
546
|
+
pages.set(scope, record);
|
|
547
|
+
}
|
|
548
|
+
};
|
|
549
|
+
$[0] = scope;
|
|
550
|
+
$[1] = t1;
|
|
551
|
+
} else t1 = $[1];
|
|
552
|
+
return t1;
|
|
553
|
+
};
|
|
554
|
+
/** Session storage in page memory, keyed by scope: the record and client identity survive remounts, not reloads. */
|
|
555
|
+
const MemorySessionStorage = resource(useMemorySessionStorage);
|
|
556
|
+
const entriesOf = (record) => Object.entries(record.lanes).flatMap(([protocol, lane]) => {
|
|
557
|
+
const view = (c, state) => ({
|
|
558
|
+
id: c.id,
|
|
559
|
+
protocol,
|
|
560
|
+
command: {
|
|
561
|
+
method: c.method,
|
|
562
|
+
params: c.meta === void 0 ? c.params : reviveDocumentValue(structuredClone(c.params), c.meta)
|
|
563
|
+
},
|
|
564
|
+
ts: c.ts,
|
|
565
|
+
state,
|
|
566
|
+
localMeta: c.localMeta,
|
|
567
|
+
durable: true
|
|
568
|
+
});
|
|
569
|
+
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
|
+
});
|
|
571
|
+
/** Reads the outboxes of every session under `namespace`, mounted or not. */
|
|
572
|
+
const readOutboxes = async (backend, options) => {
|
|
573
|
+
if (!backend.getAllKeys) throw new Error("statewire: readOutboxes requires getAllKeys");
|
|
574
|
+
const { namespace, codec, protocol = DEFAULT_PROTOCOL } = options;
|
|
575
|
+
if (!namespace) throw new Error("statewire: readOutboxes requires a namespace");
|
|
576
|
+
const result = [];
|
|
577
|
+
for (const key of await backend.getAllKeys()) {
|
|
578
|
+
if (!key.startsWith(namespace)) continue;
|
|
579
|
+
const parsed = await parse(await backend.getItem(key), codec);
|
|
580
|
+
if (parsed === null) continue;
|
|
581
|
+
const entries = entriesOf(normalize(parsed, protocol));
|
|
582
|
+
if (entries.length > 0) result.push({
|
|
583
|
+
key,
|
|
584
|
+
entries
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
return result;
|
|
588
|
+
};
|
|
589
|
+
/** Calls `listener` with `readOutboxes` now and after every save under `namespace`; returns unsubscribe. */
|
|
590
|
+
const subscribeOutboxes = (backend, options, listener, onError) => {
|
|
591
|
+
let closed = false;
|
|
592
|
+
let reading = Promise.resolve();
|
|
593
|
+
const refresh = () => {
|
|
594
|
+
reading = reading.then(async () => {
|
|
595
|
+
if (closed) return;
|
|
596
|
+
try {
|
|
597
|
+
const outboxes = await readOutboxes(backend, options);
|
|
598
|
+
if (!closed) listener(outboxes);
|
|
599
|
+
} catch (error) {
|
|
600
|
+
onError(error);
|
|
601
|
+
}
|
|
602
|
+
});
|
|
603
|
+
};
|
|
604
|
+
const onKey = (key) => {
|
|
605
|
+
if (key.startsWith(options.namespace)) refresh();
|
|
606
|
+
};
|
|
607
|
+
const local = savers.get(backend) ?? /* @__PURE__ */ new Set();
|
|
608
|
+
savers.set(backend, local);
|
|
609
|
+
local.add(onKey);
|
|
610
|
+
const remote = backend.subscribe?.(onKey);
|
|
611
|
+
refresh();
|
|
612
|
+
return () => {
|
|
613
|
+
closed = true;
|
|
614
|
+
local.delete(onKey);
|
|
615
|
+
remote?.();
|
|
616
|
+
};
|
|
617
|
+
};
|
|
618
|
+
export { IndexedDbSession, KeyValueSessionStorage, MemorySessionStorage, STORAGE_FORMAT, indexedDbBackend, readOutboxes, subscribeOutboxes };
|
|
357
619
|
|
|
358
620
|
//# sourceMappingURL=storage.js.map
|