@happyvertical/smrt-web 0.38.6 → 0.38.7
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/AGENTS.md +85 -0
- package/dist/index.d.ts +143 -0
- package/dist/index.js +425 -20
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -48,20 +48,20 @@ async function wipeDurableStore(namespace) {
|
|
|
48
48
|
//#region src/offline/durable-queue.ts
|
|
49
49
|
var OUTBOX_STORE = "outbox";
|
|
50
50
|
var OUTBOX_STATE_INDEX = "state";
|
|
51
|
-
function promisifyRequest(request) {
|
|
51
|
+
function promisifyRequest$2(request) {
|
|
52
52
|
return new Promise((resolve, reject) => {
|
|
53
53
|
request.onsuccess = () => resolve(request.result);
|
|
54
54
|
request.onerror = () => reject(request.error ?? /* @__PURE__ */ new Error("[smrt-web] IndexedDB request failed"));
|
|
55
55
|
});
|
|
56
56
|
}
|
|
57
|
-
function awaitTransaction(tx) {
|
|
57
|
+
function awaitTransaction$2(tx) {
|
|
58
58
|
return new Promise((resolve, reject) => {
|
|
59
59
|
tx.oncomplete = () => resolve();
|
|
60
60
|
tx.onerror = () => reject(tx.error ?? /* @__PURE__ */ new Error("[smrt-web] IndexedDB transaction failed"));
|
|
61
61
|
tx.onabort = () => reject(tx.error ?? /* @__PURE__ */ new Error("[smrt-web] IndexedDB transaction aborted"));
|
|
62
62
|
});
|
|
63
63
|
}
|
|
64
|
-
async function probeIndexedDb() {
|
|
64
|
+
async function probeIndexedDb$1() {
|
|
65
65
|
const idb = globalThis.indexedDB;
|
|
66
66
|
if (!idb) return false;
|
|
67
67
|
const probeName = "__smrt_web_outbox_probe__";
|
|
@@ -108,8 +108,8 @@ var DurableOutboxQueue = class {
|
|
|
108
108
|
enqueuedAt: now
|
|
109
109
|
};
|
|
110
110
|
const tx = this.db.transaction(OUTBOX_STORE, "readwrite");
|
|
111
|
-
const seq = await promisifyRequest(tx.objectStore(OUTBOX_STORE).add(row));
|
|
112
|
-
await awaitTransaction(tx);
|
|
111
|
+
const seq = await promisifyRequest$2(tx.objectStore(OUTBOX_STORE).add(row));
|
|
112
|
+
await awaitTransaction$2(tx);
|
|
113
113
|
return seq;
|
|
114
114
|
}
|
|
115
115
|
/**
|
|
@@ -121,9 +121,9 @@ var DurableOutboxQueue = class {
|
|
|
121
121
|
async markState(seq, patch) {
|
|
122
122
|
const tx = this.db.transaction(OUTBOX_STORE, "readwrite");
|
|
123
123
|
const store = tx.objectStore(OUTBOX_STORE);
|
|
124
|
-
const existing = await promisifyRequest(store.get(seq));
|
|
124
|
+
const existing = await promisifyRequest$2(store.get(seq));
|
|
125
125
|
if (!existing) {
|
|
126
|
-
await awaitTransaction(tx);
|
|
126
|
+
await awaitTransaction$2(tx);
|
|
127
127
|
return;
|
|
128
128
|
}
|
|
129
129
|
const next = {
|
|
@@ -131,8 +131,8 @@ var DurableOutboxQueue = class {
|
|
|
131
131
|
...patch,
|
|
132
132
|
seq
|
|
133
133
|
};
|
|
134
|
-
await promisifyRequest(store.put(next));
|
|
135
|
-
await awaitTransaction(tx);
|
|
134
|
+
await promisifyRequest$2(store.put(next));
|
|
135
|
+
await awaitTransaction$2(tx);
|
|
136
136
|
}
|
|
137
137
|
/**
|
|
138
138
|
* All rows that are due to (re)send at `now`: state `pending` AND
|
|
@@ -142,21 +142,21 @@ var DurableOutboxQueue = class {
|
|
|
142
142
|
*/
|
|
143
143
|
async listPending(now) {
|
|
144
144
|
const tx = this.db.transaction(OUTBOX_STORE, "readonly");
|
|
145
|
-
const rows = await promisifyRequest(tx.objectStore(OUTBOX_STORE).index(OUTBOX_STATE_INDEX).getAll(IDBKeyRange.only("pending")));
|
|
146
|
-
await awaitTransaction(tx);
|
|
145
|
+
const rows = await promisifyRequest$2(tx.objectStore(OUTBOX_STORE).index(OUTBOX_STATE_INDEX).getAll(IDBKeyRange.only("pending")));
|
|
146
|
+
await awaitTransaction$2(tx);
|
|
147
147
|
return rows.filter((row) => row.nextAttemptAt <= now).sort((a, b) => (a.seq ?? 0) - (b.seq ?? 0));
|
|
148
148
|
}
|
|
149
149
|
/** Remove the row at `seq` (a terminal transition drops it). */
|
|
150
150
|
async remove(seq) {
|
|
151
151
|
const tx = this.db.transaction(OUTBOX_STORE, "readwrite");
|
|
152
|
-
await promisifyRequest(tx.objectStore(OUTBOX_STORE).delete(seq));
|
|
153
|
-
await awaitTransaction(tx);
|
|
152
|
+
await promisifyRequest$2(tx.objectStore(OUTBOX_STORE).delete(seq));
|
|
153
|
+
await awaitTransaction$2(tx);
|
|
154
154
|
}
|
|
155
155
|
/** Every row currently in the queue (any state), ascending `seq`. */
|
|
156
156
|
async all() {
|
|
157
157
|
const tx = this.db.transaction(OUTBOX_STORE, "readonly");
|
|
158
|
-
const rows = await promisifyRequest(tx.objectStore(OUTBOX_STORE).getAll());
|
|
159
|
-
await awaitTransaction(tx);
|
|
158
|
+
const rows = await promisifyRequest$2(tx.objectStore(OUTBOX_STORE).getAll());
|
|
159
|
+
await awaitTransaction$2(tx);
|
|
160
160
|
return rows.sort((a, b) => (a.seq ?? 0) - (b.seq ?? 0));
|
|
161
161
|
}
|
|
162
162
|
/**
|
|
@@ -166,8 +166,8 @@ var DurableOutboxQueue = class {
|
|
|
166
166
|
*/
|
|
167
167
|
async clear() {
|
|
168
168
|
const tx = this.db.transaction(OUTBOX_STORE, "readwrite");
|
|
169
|
-
await promisifyRequest(tx.objectStore(OUTBOX_STORE).clear());
|
|
170
|
-
await awaitTransaction(tx);
|
|
169
|
+
await promisifyRequest$2(tx.objectStore(OUTBOX_STORE).clear());
|
|
170
|
+
await awaitTransaction$2(tx);
|
|
171
171
|
}
|
|
172
172
|
/** Close the underlying database handle (called on engine dispose). */
|
|
173
173
|
close() {
|
|
@@ -328,7 +328,7 @@ var OutboxEngine = class {
|
|
|
328
328
|
}
|
|
329
329
|
/** Open the durable queue (or mark degraded if IndexedDB is unusable). */
|
|
330
330
|
async open() {
|
|
331
|
-
if (!await probeIndexedDb()) {
|
|
331
|
+
if (!await probeIndexedDb$1()) {
|
|
332
332
|
this.degraded = true;
|
|
333
333
|
console.warn("[smrt-web] IndexedDB unavailable — the offline outbox is disabled; offline writes will not be durable.");
|
|
334
334
|
return;
|
|
@@ -860,6 +860,246 @@ function getOutboxHandle(namespace) {
|
|
|
860
860
|
};
|
|
861
861
|
}
|
|
862
862
|
//#endregion
|
|
863
|
+
//#region src/persistence/snapshot-store.ts
|
|
864
|
+
var SNAPSHOT_STORE = "snapshots";
|
|
865
|
+
var SNAPSHOT_DB_SUFFIX = "::snapshots";
|
|
866
|
+
function promisifyRequest$1(request) {
|
|
867
|
+
return new Promise((resolve, reject) => {
|
|
868
|
+
request.onsuccess = () => resolve(request.result);
|
|
869
|
+
request.onerror = () => reject(request.error ?? /* @__PURE__ */ new Error("[smrt-web] IndexedDB request failed"));
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
function awaitTransaction$1(tx) {
|
|
873
|
+
return new Promise((resolve, reject) => {
|
|
874
|
+
tx.oncomplete = () => resolve();
|
|
875
|
+
tx.onerror = () => reject(tx.error ?? /* @__PURE__ */ new Error("[smrt-web] IndexedDB transaction failed"));
|
|
876
|
+
tx.onabort = () => reject(tx.error ?? /* @__PURE__ */ new Error("[smrt-web] IndexedDB transaction aborted"));
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
async function probeIndexedDb() {
|
|
880
|
+
const idb = globalThis.indexedDB;
|
|
881
|
+
if (!idb) return false;
|
|
882
|
+
const probeName = "__smrt_web_snapshot_probe__";
|
|
883
|
+
try {
|
|
884
|
+
(await new Promise((resolve, reject) => {
|
|
885
|
+
const request = idb.open(probeName, 1);
|
|
886
|
+
request.onsuccess = () => resolve(request.result);
|
|
887
|
+
request.onerror = () => reject(request.error ?? /* @__PURE__ */ new Error("probe failed"));
|
|
888
|
+
request.onblocked = () => reject(/* @__PURE__ */ new Error("probe blocked"));
|
|
889
|
+
})).close();
|
|
890
|
+
try {
|
|
891
|
+
idb.deleteDatabase(probeName);
|
|
892
|
+
} catch {}
|
|
893
|
+
return true;
|
|
894
|
+
} catch {
|
|
895
|
+
return false;
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
var SnapshotStore = class {
|
|
899
|
+
db;
|
|
900
|
+
/** The IndexedDB database name (== the durable-store namespace). */
|
|
901
|
+
dbName;
|
|
902
|
+
constructor(db, dbName) {
|
|
903
|
+
this.db = db;
|
|
904
|
+
this.dbName = dbName;
|
|
905
|
+
}
|
|
906
|
+
/**
|
|
907
|
+
* Read the persisted rows for `collection`, or `undefined` if none were ever
|
|
908
|
+
* saved (or the record is malformed). `undefined` is the warm-start "nothing
|
|
909
|
+
* on disk" signal — the engine then fetches fresh.
|
|
910
|
+
*/
|
|
911
|
+
async load(collection) {
|
|
912
|
+
const tx = this.db.transaction(SNAPSHOT_STORE, "readonly");
|
|
913
|
+
const record = await promisifyRequest$1(tx.objectStore(SNAPSHOT_STORE).get(collection));
|
|
914
|
+
await awaitTransaction$1(tx);
|
|
915
|
+
if (!record || !Array.isArray(record.rows)) return void 0;
|
|
916
|
+
return record.rows;
|
|
917
|
+
}
|
|
918
|
+
/**
|
|
919
|
+
* Write (replacing) the snapshot for `collection`. Resolves once the write is
|
|
920
|
+
* durably committed. A single blob per collection — the whole current row set,
|
|
921
|
+
* not a delta — so a restore is one read with no reconciliation.
|
|
922
|
+
*/
|
|
923
|
+
async save(collection, rows) {
|
|
924
|
+
const tx = this.db.transaction(SNAPSHOT_STORE, "readwrite");
|
|
925
|
+
const store = tx.objectStore(SNAPSHOT_STORE);
|
|
926
|
+
const record = {
|
|
927
|
+
collection,
|
|
928
|
+
rows
|
|
929
|
+
};
|
|
930
|
+
await promisifyRequest$1(store.put(record));
|
|
931
|
+
await awaitTransaction$1(tx);
|
|
932
|
+
}
|
|
933
|
+
/**
|
|
934
|
+
* Drop the snapshot for a single `collection` (its capability's own teardown
|
|
935
|
+
* does NOT clear — the persisted rows must survive for the next load; this is
|
|
936
|
+
* only for an explicit targeted purge). Kept for completeness / tests.
|
|
937
|
+
*/
|
|
938
|
+
async remove(collection) {
|
|
939
|
+
const tx = this.db.transaction(SNAPSHOT_STORE, "readwrite");
|
|
940
|
+
await promisifyRequest$1(tx.objectStore(SNAPSHOT_STORE).delete(collection));
|
|
941
|
+
await awaitTransaction$1(tx);
|
|
942
|
+
}
|
|
943
|
+
/**
|
|
944
|
+
* Drop EVERY snapshot — the durable-store `clear()` for `wipeDurableStore`.
|
|
945
|
+
* Empties the store but keeps the database so a subsequent save still works.
|
|
946
|
+
*/
|
|
947
|
+
async clear() {
|
|
948
|
+
const tx = this.db.transaction(SNAPSHOT_STORE, "readwrite");
|
|
949
|
+
await promisifyRequest$1(tx.objectStore(SNAPSHOT_STORE).clear());
|
|
950
|
+
await awaitTransaction$1(tx);
|
|
951
|
+
}
|
|
952
|
+
/** Close the underlying database handle (called on the last detach). */
|
|
953
|
+
close() {
|
|
954
|
+
this.db.close();
|
|
955
|
+
}
|
|
956
|
+
};
|
|
957
|
+
function openSnapshotStore(namespace) {
|
|
958
|
+
const idb = globalThis.indexedDB;
|
|
959
|
+
if (!idb) return Promise.reject(/* @__PURE__ */ new Error("[smrt-web] IndexedDB is unavailable in this environment"));
|
|
960
|
+
const dbName = `${namespace}${SNAPSHOT_DB_SUFFIX}`;
|
|
961
|
+
return new Promise((resolve, reject) => {
|
|
962
|
+
const request = idb.open(dbName, 1);
|
|
963
|
+
request.onupgradeneeded = () => {
|
|
964
|
+
const db = request.result;
|
|
965
|
+
if (!db.objectStoreNames.contains("snapshots")) db.createObjectStore(SNAPSHOT_STORE, { keyPath: "collection" });
|
|
966
|
+
};
|
|
967
|
+
request.onsuccess = () => resolve(new SnapshotStore(request.result, dbName));
|
|
968
|
+
request.onerror = () => reject(request.error ?? /* @__PURE__ */ new Error(`[smrt-web] failed to open snapshot database "${dbName}"`));
|
|
969
|
+
request.onblocked = () => reject(/* @__PURE__ */ new Error(`[smrt-web] opening snapshot database "${dbName}" was blocked`));
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
//#endregion
|
|
973
|
+
//#region src/persistence.ts
|
|
974
|
+
var DEFAULT_PERSIST_DEBOUNCE_MS = 250;
|
|
975
|
+
var enginesByNamespace = /* @__PURE__ */ new Map();
|
|
976
|
+
var warnedNoIndexedDb = false;
|
|
977
|
+
function warnNoIndexedDbOnce() {
|
|
978
|
+
if (warnedNoIndexedDb) return;
|
|
979
|
+
warnedNoIndexedDb = true;
|
|
980
|
+
console.warn("[smrt-web] IndexedDB is unavailable; persistence is disabled (collections behave as non-persistent).");
|
|
981
|
+
}
|
|
982
|
+
function acquireSnapshotEngine(namespace) {
|
|
983
|
+
const existing = enginesByNamespace.get(namespace);
|
|
984
|
+
if (existing) {
|
|
985
|
+
existing.refCount += 1;
|
|
986
|
+
return existing;
|
|
987
|
+
}
|
|
988
|
+
const engine = {
|
|
989
|
+
store: void 0,
|
|
990
|
+
refCount: 1,
|
|
991
|
+
unregister: void 0,
|
|
992
|
+
ready: Promise.resolve(void 0)
|
|
993
|
+
};
|
|
994
|
+
engine.ready = (async () => {
|
|
995
|
+
if (!await probeIndexedDb()) {
|
|
996
|
+
warnNoIndexedDbOnce();
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
try {
|
|
1000
|
+
const store = await openSnapshotStore(namespace);
|
|
1001
|
+
engine.store = store;
|
|
1002
|
+
engine.unregister = registerDurableResource(namespace, {
|
|
1003
|
+
kind: "persisted-collection",
|
|
1004
|
+
clear: () => store.clear()
|
|
1005
|
+
});
|
|
1006
|
+
return store;
|
|
1007
|
+
} catch {
|
|
1008
|
+
warnNoIndexedDbOnce();
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
})();
|
|
1012
|
+
enginesByNamespace.set(namespace, engine);
|
|
1013
|
+
return engine;
|
|
1014
|
+
}
|
|
1015
|
+
async function releaseSnapshotEngine(namespace) {
|
|
1016
|
+
const engine = enginesByNamespace.get(namespace);
|
|
1017
|
+
if (!engine) return;
|
|
1018
|
+
engine.refCount -= 1;
|
|
1019
|
+
if (engine.refCount > 0) return;
|
|
1020
|
+
enginesByNamespace.delete(namespace);
|
|
1021
|
+
await engine.ready;
|
|
1022
|
+
engine.unregister?.();
|
|
1023
|
+
engine.unregister = void 0;
|
|
1024
|
+
engine.store?.close();
|
|
1025
|
+
engine.store = void 0;
|
|
1026
|
+
}
|
|
1027
|
+
function persistCollection(config) {
|
|
1028
|
+
const namespace = durableStoreNamespace(config.namespace);
|
|
1029
|
+
const collectionName = config.collection;
|
|
1030
|
+
const debounceMs = config.debounceMs ?? 250;
|
|
1031
|
+
let engine;
|
|
1032
|
+
let subscription;
|
|
1033
|
+
let debounceTimer;
|
|
1034
|
+
let readSnapshot;
|
|
1035
|
+
let detached = false;
|
|
1036
|
+
let flushing;
|
|
1037
|
+
let dirty = false;
|
|
1038
|
+
const doFlush = async () => {
|
|
1039
|
+
while (dirty && !detached) {
|
|
1040
|
+
dirty = false;
|
|
1041
|
+
if (!engine || !readSnapshot) return;
|
|
1042
|
+
const store = await engine.ready;
|
|
1043
|
+
if (detached || !store) return;
|
|
1044
|
+
const rows = readSnapshot().map((row) => ({ ...row }));
|
|
1045
|
+
try {
|
|
1046
|
+
await store.save(collectionName, rows);
|
|
1047
|
+
} catch {}
|
|
1048
|
+
}
|
|
1049
|
+
};
|
|
1050
|
+
const runFlush = () => {
|
|
1051
|
+
dirty = true;
|
|
1052
|
+
if (flushing) return;
|
|
1053
|
+
flushing = doFlush().finally(() => {
|
|
1054
|
+
flushing = void 0;
|
|
1055
|
+
if (dirty && !detached) runFlush();
|
|
1056
|
+
});
|
|
1057
|
+
};
|
|
1058
|
+
const scheduleFlush = () => {
|
|
1059
|
+
if (detached) return;
|
|
1060
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
1061
|
+
debounceTimer = setTimeout(() => {
|
|
1062
|
+
debounceTimer = void 0;
|
|
1063
|
+
runFlush();
|
|
1064
|
+
}, Math.max(0, debounceMs));
|
|
1065
|
+
debounceTimer.unref?.();
|
|
1066
|
+
};
|
|
1067
|
+
return {
|
|
1068
|
+
name: "persistence",
|
|
1069
|
+
async warmStart(ctx) {
|
|
1070
|
+
const acquired = acquireSnapshotEngine(namespace);
|
|
1071
|
+
engine = acquired;
|
|
1072
|
+
readSnapshot = ctx.snapshot ? () => ctx.snapshot?.() ?? [] : void 0;
|
|
1073
|
+
const store = await acquired.ready;
|
|
1074
|
+
if (!store) return void 0;
|
|
1075
|
+
const rows = await store.load(collectionName);
|
|
1076
|
+
if (!rows || rows.length === 0) return void 0;
|
|
1077
|
+
return rows;
|
|
1078
|
+
},
|
|
1079
|
+
onAttach(ctx) {
|
|
1080
|
+
if (!engine) engine = acquireSnapshotEngine(namespace);
|
|
1081
|
+
if (!readSnapshot && ctx.snapshot) readSnapshot = () => ctx.snapshot?.() ?? [];
|
|
1082
|
+
if (!ctx.snapshot || !ctx.subscribe || !readSnapshot) return;
|
|
1083
|
+
subscription = ctx.subscribe(() => scheduleFlush());
|
|
1084
|
+
scheduleFlush();
|
|
1085
|
+
},
|
|
1086
|
+
async teardown() {
|
|
1087
|
+
detached = true;
|
|
1088
|
+
if (debounceTimer) {
|
|
1089
|
+
clearTimeout(debounceTimer);
|
|
1090
|
+
debounceTimer = void 0;
|
|
1091
|
+
}
|
|
1092
|
+
subscription?.unsubscribe();
|
|
1093
|
+
subscription = void 0;
|
|
1094
|
+
readSnapshot = void 0;
|
|
1095
|
+
if (flushing) await flushing;
|
|
1096
|
+
const current = engine;
|
|
1097
|
+
engine = void 0;
|
|
1098
|
+
if (current) await releaseSnapshotEngine(namespace);
|
|
1099
|
+
}
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
//#endregion
|
|
863
1103
|
//#region src/sse-client.ts
|
|
864
1104
|
var EVENT_SOURCE_CLOSED = 2;
|
|
865
1105
|
function defaultEventSourceFactory(url, init) {
|
|
@@ -1033,6 +1273,166 @@ function liveInvalidation(config) {
|
|
|
1033
1273
|
};
|
|
1034
1274
|
}
|
|
1035
1275
|
//#endregion
|
|
1276
|
+
//#region src/update-state/meta-store.ts
|
|
1277
|
+
var META_STORE = "meta";
|
|
1278
|
+
var META_DB_SUFFIX = "::meta";
|
|
1279
|
+
var LAST_SEEN_MANIFEST_HASH_KEY = "lastSeenManifestHash";
|
|
1280
|
+
function promisifyRequest(request) {
|
|
1281
|
+
return new Promise((resolve, reject) => {
|
|
1282
|
+
request.onsuccess = () => resolve(request.result);
|
|
1283
|
+
request.onerror = () => reject(request.error ?? /* @__PURE__ */ new Error("[smrt-web] IndexedDB request failed"));
|
|
1284
|
+
});
|
|
1285
|
+
}
|
|
1286
|
+
function awaitTransaction(tx) {
|
|
1287
|
+
return new Promise((resolve, reject) => {
|
|
1288
|
+
tx.oncomplete = () => resolve();
|
|
1289
|
+
tx.onerror = () => reject(tx.error ?? /* @__PURE__ */ new Error("[smrt-web] IndexedDB transaction failed"));
|
|
1290
|
+
tx.onabort = () => reject(tx.error ?? /* @__PURE__ */ new Error("[smrt-web] IndexedDB transaction aborted"));
|
|
1291
|
+
});
|
|
1292
|
+
}
|
|
1293
|
+
var VersionMetaStore = class {
|
|
1294
|
+
db;
|
|
1295
|
+
/** The IndexedDB database name (== the durable-store namespace). */
|
|
1296
|
+
dbName;
|
|
1297
|
+
constructor(db, dbName) {
|
|
1298
|
+
this.db = db;
|
|
1299
|
+
this.dbName = dbName;
|
|
1300
|
+
}
|
|
1301
|
+
/** Read the value for `key`, or `undefined` if unset / malformed. */
|
|
1302
|
+
async get(key) {
|
|
1303
|
+
const tx = this.db.transaction(META_STORE, "readonly");
|
|
1304
|
+
const record = await promisifyRequest(tx.objectStore(META_STORE).get(key));
|
|
1305
|
+
await awaitTransaction(tx);
|
|
1306
|
+
return record && typeof record.value === "string" ? record.value : void 0;
|
|
1307
|
+
}
|
|
1308
|
+
/** Write (replacing) the value for `key`; resolves once durably committed. */
|
|
1309
|
+
async set(key, value) {
|
|
1310
|
+
const tx = this.db.transaction(META_STORE, "readwrite");
|
|
1311
|
+
const record = {
|
|
1312
|
+
key,
|
|
1313
|
+
value
|
|
1314
|
+
};
|
|
1315
|
+
await promisifyRequest(tx.objectStore(META_STORE).put(record));
|
|
1316
|
+
await awaitTransaction(tx);
|
|
1317
|
+
}
|
|
1318
|
+
/**
|
|
1319
|
+
* Drop EVERY meta record — the durable-store `clear()` for
|
|
1320
|
+
* {@link wipeDurableStore}, so a logout also clears the last-seen manifest
|
|
1321
|
+
* hash (the AC "wipe clears the last-seen-hash record").
|
|
1322
|
+
*/
|
|
1323
|
+
async clear() {
|
|
1324
|
+
const tx = this.db.transaction(META_STORE, "readwrite");
|
|
1325
|
+
await promisifyRequest(tx.objectStore(META_STORE).clear());
|
|
1326
|
+
await awaitTransaction(tx);
|
|
1327
|
+
}
|
|
1328
|
+
/** Close the underlying database handle. */
|
|
1329
|
+
close() {
|
|
1330
|
+
this.db.close();
|
|
1331
|
+
}
|
|
1332
|
+
};
|
|
1333
|
+
function openVersionMetaStore(namespace) {
|
|
1334
|
+
const idb = globalThis.indexedDB;
|
|
1335
|
+
if (!idb) return Promise.reject(/* @__PURE__ */ new Error("[smrt-web] IndexedDB is unavailable in this environment"));
|
|
1336
|
+
const dbName = `${namespace}${META_DB_SUFFIX}`;
|
|
1337
|
+
return new Promise((resolve, reject) => {
|
|
1338
|
+
const request = idb.open(dbName, 1);
|
|
1339
|
+
request.onupgradeneeded = () => {
|
|
1340
|
+
const db = request.result;
|
|
1341
|
+
if (!db.objectStoreNames.contains("meta")) db.createObjectStore(META_STORE, { keyPath: "key" });
|
|
1342
|
+
};
|
|
1343
|
+
request.onsuccess = () => resolve(new VersionMetaStore(request.result, dbName));
|
|
1344
|
+
request.onerror = () => reject(request.error ?? /* @__PURE__ */ new Error(`[smrt-web] failed to open meta database "${dbName}"`));
|
|
1345
|
+
request.onblocked = () => reject(/* @__PURE__ */ new Error(`[smrt-web] opening meta database "${dbName}" was blocked`));
|
|
1346
|
+
});
|
|
1347
|
+
}
|
|
1348
|
+
//#endregion
|
|
1349
|
+
//#region src/update-state.ts
|
|
1350
|
+
function createUpdateState(config) {
|
|
1351
|
+
const namespace = durableStoreNamespace(config.namespace);
|
|
1352
|
+
let bundle = false;
|
|
1353
|
+
let contract = false;
|
|
1354
|
+
const subscribers = /* @__PURE__ */ new Set();
|
|
1355
|
+
let metaStore;
|
|
1356
|
+
let unregister;
|
|
1357
|
+
let disposed = false;
|
|
1358
|
+
const snapshot = () => ({
|
|
1359
|
+
bundle,
|
|
1360
|
+
contract,
|
|
1361
|
+
updateAvailable: bundle || contract
|
|
1362
|
+
});
|
|
1363
|
+
const notify = () => {
|
|
1364
|
+
const state = snapshot();
|
|
1365
|
+
for (const callback of [...subscribers]) try {
|
|
1366
|
+
callback(state);
|
|
1367
|
+
} catch (error) {
|
|
1368
|
+
console.warn("[smrt-web] updateAvailable subscriber threw", error);
|
|
1369
|
+
}
|
|
1370
|
+
};
|
|
1371
|
+
const setBundle = () => {
|
|
1372
|
+
if (bundle) return;
|
|
1373
|
+
bundle = true;
|
|
1374
|
+
notify();
|
|
1375
|
+
};
|
|
1376
|
+
const setContract = () => {
|
|
1377
|
+
if (contract) return;
|
|
1378
|
+
contract = true;
|
|
1379
|
+
notify();
|
|
1380
|
+
};
|
|
1381
|
+
return {
|
|
1382
|
+
get: snapshot,
|
|
1383
|
+
subscribe(callback) {
|
|
1384
|
+
subscribers.add(callback);
|
|
1385
|
+
try {
|
|
1386
|
+
callback(snapshot());
|
|
1387
|
+
} catch (error) {
|
|
1388
|
+
console.warn("[smrt-web] updateAvailable subscriber threw", error);
|
|
1389
|
+
}
|
|
1390
|
+
return () => {
|
|
1391
|
+
subscribers.delete(callback);
|
|
1392
|
+
};
|
|
1393
|
+
},
|
|
1394
|
+
notifyBundleUpdated: setBundle,
|
|
1395
|
+
ready: (async () => {
|
|
1396
|
+
const runningHash = config.manifestHash;
|
|
1397
|
+
if (runningHash === void 0) return;
|
|
1398
|
+
let store;
|
|
1399
|
+
try {
|
|
1400
|
+
store = await openVersionMetaStore(namespace);
|
|
1401
|
+
} catch {
|
|
1402
|
+
return;
|
|
1403
|
+
}
|
|
1404
|
+
if (disposed) {
|
|
1405
|
+
store.close();
|
|
1406
|
+
return;
|
|
1407
|
+
}
|
|
1408
|
+
metaStore = store;
|
|
1409
|
+
unregister = registerDurableResource(namespace, {
|
|
1410
|
+
kind: "persisted-collection",
|
|
1411
|
+
clear: () => store.clear()
|
|
1412
|
+
});
|
|
1413
|
+
let lastSeen;
|
|
1414
|
+
try {
|
|
1415
|
+
lastSeen = await store.get(LAST_SEEN_MANIFEST_HASH_KEY);
|
|
1416
|
+
} catch {
|
|
1417
|
+
lastSeen = void 0;
|
|
1418
|
+
}
|
|
1419
|
+
if (disposed) return;
|
|
1420
|
+
if (lastSeen !== void 0 && lastSeen !== runningHash) setContract();
|
|
1421
|
+
if (lastSeen !== runningHash) try {
|
|
1422
|
+
await store.set(LAST_SEEN_MANIFEST_HASH_KEY, runningHash);
|
|
1423
|
+
} catch {}
|
|
1424
|
+
})(),
|
|
1425
|
+
dispose() {
|
|
1426
|
+
disposed = true;
|
|
1427
|
+
unregister?.();
|
|
1428
|
+
unregister = void 0;
|
|
1429
|
+
metaStore?.close();
|
|
1430
|
+
metaStore = void 0;
|
|
1431
|
+
subscribers.clear();
|
|
1432
|
+
}
|
|
1433
|
+
};
|
|
1434
|
+
}
|
|
1435
|
+
//#endregion
|
|
1036
1436
|
//#region src/index.ts
|
|
1037
1437
|
var SmrtWebRequestError = class extends Error {
|
|
1038
1438
|
payload;
|
|
@@ -1174,7 +1574,12 @@ function createSmrtCollection(definition, options) {
|
|
|
1174
1574
|
get cacheId() {
|
|
1175
1575
|
return cacheId;
|
|
1176
1576
|
},
|
|
1177
|
-
invalidate: () => invalidateRelated()
|
|
1577
|
+
invalidate: () => invalidateRelated(),
|
|
1578
|
+
snapshot: () => collection.toArray.map((row) => toPlainRow(row)),
|
|
1579
|
+
subscribe: (callback) => {
|
|
1580
|
+
const subscription = collection.subscribeChanges((changes) => callback(projectChanges(changes)));
|
|
1581
|
+
return { unsubscribe: () => subscription.unsubscribe() };
|
|
1582
|
+
}
|
|
1178
1583
|
};
|
|
1179
1584
|
for (const capability of capabilities) {
|
|
1180
1585
|
let extra;
|
|
@@ -1374,6 +1779,6 @@ function createSmrtCollection(definition, options) {
|
|
|
1374
1779
|
return handle;
|
|
1375
1780
|
}
|
|
1376
1781
|
//#endregion
|
|
1377
|
-
export { SmrtWebRequestError, createDefinitionFetchers, createSmrtCollection, createSmrtWebClient, createSmrtWebEventSubscriber, durableStoreNamespace, getEngineCollection, getOutboxHandle, liveInvalidation, newLocalId, offlineOutbox, registerDurableResource, runWrapMutation, unwrapItemResult, unwrapListResult, wipeDurableStore };
|
|
1782
|
+
export { DEFAULT_PERSIST_DEBOUNCE_MS, SmrtWebRequestError, createDefinitionFetchers, createSmrtCollection, createSmrtWebClient, createSmrtWebEventSubscriber, createUpdateState, durableStoreNamespace, getEngineCollection, getOutboxHandle, liveInvalidation, newLocalId, offlineOutbox, persistCollection, registerDurableResource, runWrapMutation, unwrapItemResult, unwrapListResult, wipeDurableStore };
|
|
1378
1783
|
|
|
1379
1784
|
//# sourceMappingURL=index.js.map
|