@happyvertical/smrt-web 0.55.23 → 0.56.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/AGENTS.md +1 -1
- package/README.md +19 -1
- package/agents/capability-seam.md +12 -0
- package/dist/chunks/{src-BFQJky1a.js → src-ClpJxx94.js} +340 -189
- package/dist/chunks/src-ClpJxx94.js.map +1 -0
- package/dist/durable-store-sweep.d.ts +68 -0
- package/dist/durable-store-sweep.d.ts.map +1 -0
- package/dist/durable-store.d.ts +11 -2
- package/dist/durable-store.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/offline/engine.d.ts +16 -0
- package/dist/offline/engine.d.ts.map +1 -1
- package/dist/webmcp.js +1 -1
- package/package.json +3 -3
- package/dist/chunks/src-BFQJky1a.js.map +0 -1
package/AGENTS.md
CHANGED
|
@@ -35,7 +35,7 @@ module you are editing. This file keeps what holds in every module.
|
|
|
35
35
|
|
|
36
36
|
| Module | Scope | Module doc |
|
|
37
37
|
|---|---|---|
|
|
38
|
-
| `index.ts` hooks + `durable-store.ts` | the six capability hook points, hook error isolation, the no-op guarantee,
|
|
38
|
+
| `index.ts` hooks + `durable-store.ts` + `durable-store-sweep.ts` | the six capability hook points, hook error isolation, the no-op guarantee, the shared durable-store namespacing/wipe registry, and the shared-device namespace list/prune sweep | [agents/capability-seam.md](agents/capability-seam.md) |
|
|
39
39
|
| `offline/` | durable offline writes — config, sync-apply or consumer-declared transport replay (#3021), idempotency, the shared namespace-keyed engine, and Web Locks leader election | [agents/offline-outbox.md](agents/offline-outbox.md) |
|
|
40
40
|
| `sse-client.ts` | the client half of live cache invalidation — the app-wide subscriber, the wire contract it consumes, and SSE-vs-polling behaviour | [agents/live-invalidation.md](agents/live-invalidation.md) |
|
|
41
41
|
| `webmcp.ts` | framework-agnostic WebMCP registrar; validates an optionally bounded/namespaced, effect-filtered prospective set atomically, keeps legacy list-backed tools on collection state, and executes canonical tool-only definitions directly through REST fetchers. `registerWebMcpBespokeTool` is the single-tool sibling a UI layer's hand-written component tool routes through (`useWebMcpTool` in smrt-svelte, #2586): same fail-closed effect classification and `effects` policy, no `namespace`/`maxTools` | — |
|
package/README.md
CHANGED
|
@@ -138,6 +138,24 @@ leader, with the same backoff and `pending → uploading → synced | failed` st
|
|
|
138
138
|
machine. A reloaded write waits until a queue with its name attaches; it is
|
|
139
139
|
never re-routed to `sync/apply`.
|
|
140
140
|
|
|
141
|
+
### Sweeping stale data on shared devices
|
|
142
|
+
|
|
143
|
+
`listDurableNamespaces()` returns the durable-store namespaces this browser
|
|
144
|
+
holds (names only, no contents). `pruneDurableNamespace(namespace, { olderThan })`
|
|
145
|
+
removes that namespace's queued offline writes enqueued before `olderThan` (a
|
|
146
|
+
`Date` or epoch ms) without mounting its queue: nothing replays, no transport
|
|
147
|
+
attaches, and only `{ removed, skipped }` counts come back. Items on a route a
|
|
148
|
+
live queue (in any tab) is replaying are skipped. Use it to age out another
|
|
149
|
+
person's queued work on shared hardware; persisted read snapshots carry no age
|
|
150
|
+
and are left to `wipeDurableStore`.
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
const cutoff = Date.now() - 7 * 24 * 60 * 60 * 1000;
|
|
154
|
+
for (const namespace of await listDurableNamespaces()) {
|
|
155
|
+
await pruneDurableNamespace(namespace, { olderThan: cutoff });
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
141
159
|
### Persisted read cache
|
|
142
160
|
|
|
143
161
|
`persistCollection()` warm-starts from an IndexedDB snapshot, then revalidates
|
|
@@ -179,7 +197,7 @@ core.
|
|
|
179
197
|
| Remote queries | `createSmrtWebQuery`, `SmrtWebQueryTransport` |
|
|
180
198
|
| Data-surface actions | `executeSmrtWebDataSurfaceAction`, `SmrtWebDataSurfaceActionTransport`, `dataSurfaceActionCommandTransport` |
|
|
181
199
|
| HTTP | `createDefinitionFetchers`, `unwrapListResult`, `unwrapItemResult` |
|
|
182
|
-
| Offline | `offlineOutbox`, `offlineCommandQueue`, `getOutboxHandle` |
|
|
200
|
+
| Offline | `offlineOutbox`, `offlineCommandQueue`, `getOutboxHandle`, `listDurableNamespaces`, `pruneDurableNamespace` |
|
|
183
201
|
| Persistence | `persistCollection`, `persistDataSurface`, `wipeDurableStore` |
|
|
184
202
|
| Live updates | `createSmrtWebEventSubscriber`, `liveInvalidation` |
|
|
185
203
|
| Version awareness | `createUpdateState` |
|
|
@@ -90,6 +90,18 @@ bookkeeping, ZERO `@tanstack/*` imports.
|
|
|
90
90
|
clears every registered `DurableResource` under a namespace (best-effort — a
|
|
91
91
|
rejected `clear()` doesn't abort the sweep) then drops it; a safe no-op on an
|
|
92
92
|
unknown/empty namespace.
|
|
93
|
+
- Shared-device sweep (#3234, `durable-store-sweep.ts`): the registry above
|
|
94
|
+
only reaches resources registered in THIS page load.
|
|
95
|
+
`listDurableNamespaces()` adds every namespace with an smrt-web IndexedDB
|
|
96
|
+
database (outbox = bare namespace; `::snapshots` / `::meta` suffixes
|
|
97
|
+
stripped) by NAME via `indexedDB.databases()`, never opening one.
|
|
98
|
+
`pruneDurableNamespace(ns, { olderThan })` deletes outbox rows with
|
|
99
|
+
`enqueuedAt < olderThan` straight from IndexedDB — no `OutboxEngine`, no
|
|
100
|
+
binding, no transport, no `fetch`; counts only. It must never create a
|
|
101
|
+
database (opens versionless and aborts `upgradeneeded`), and per route it
|
|
102
|
+
takes the replayer's own leader lock (`outboxLeaderLockName`) with
|
|
103
|
+
`ifAvailable`, skipping a held route; without Web Locks it skips a namespace
|
|
104
|
+
whose outbox this page has registered.
|
|
93
105
|
|
|
94
106
|
Rationale: the persistence slice's storage (SQLite-WASM/OPFS) and the outbox's
|
|
95
107
|
IndexedDB queue are **separate storage engines**, so the shared foundation lives
|
|
@@ -480,6 +480,15 @@ async function wipeDurableStore(namespace) {
|
|
|
480
480
|
registry.delete(namespace);
|
|
481
481
|
await Promise.allSettled(snapshot.map((resource) => resource.clear()));
|
|
482
482
|
}
|
|
483
|
+
function hasDurableResource(namespace, kind) {
|
|
484
|
+
const resources = registry.get(namespace);
|
|
485
|
+
if (!resources) return false;
|
|
486
|
+
for (const resource of resources) if (resource.kind === kind) return true;
|
|
487
|
+
return false;
|
|
488
|
+
}
|
|
489
|
+
function registeredDurableNamespaces() {
|
|
490
|
+
return [...registry.keys()];
|
|
491
|
+
}
|
|
483
492
|
//#endregion
|
|
484
493
|
//#region src/offline/durable-queue.ts
|
|
485
494
|
var OUTBOX_STORE = "outbox";
|
|
@@ -630,13 +639,13 @@ function openDurableOutboxQueue(dbName) {
|
|
|
630
639
|
}
|
|
631
640
|
//#endregion
|
|
632
641
|
//#region src/offline/leader.ts
|
|
633
|
-
function getLockManager() {
|
|
642
|
+
function getLockManager$1() {
|
|
634
643
|
const locks = globalThis.navigator?.locks;
|
|
635
644
|
if (locks && typeof locks.request === "function") return locks;
|
|
636
645
|
}
|
|
637
646
|
var warnedNoLocks = false;
|
|
638
647
|
function acquireLeadership(lockName, onAcquired, onReleased) {
|
|
639
|
-
const locks = getLockManager();
|
|
648
|
+
const locks = getLockManager$1();
|
|
640
649
|
if (!locks) {
|
|
641
650
|
if (!warnedNoLocks) {
|
|
642
651
|
warnedNoLocks = true;
|
|
@@ -708,6 +717,10 @@ var SYNC_APPLY_ROUTE = "sync-apply";
|
|
|
708
717
|
function routeOf(row) {
|
|
709
718
|
return row.transport === void 0 ? SYNC_APPLY_ROUTE : `transport:${row.transport}`;
|
|
710
719
|
}
|
|
720
|
+
function outboxLeaderLockName(namespace, route) {
|
|
721
|
+
const root = `smrt-web-outbox-leader:${namespace}`;
|
|
722
|
+
return route === SYNC_APPLY_ROUTE ? root : `${root}:${route}`;
|
|
723
|
+
}
|
|
711
724
|
function newItemId() {
|
|
712
725
|
const cryptoRef = globalThis.crypto;
|
|
713
726
|
if (cryptoRef?.randomUUID) return cryptoRef.randomUUID();
|
|
@@ -845,8 +858,7 @@ var OutboxEngine = class {
|
|
|
845
858
|
existing.refs += 1;
|
|
846
859
|
return;
|
|
847
860
|
}
|
|
848
|
-
const
|
|
849
|
-
const lockName = route === SYNC_APPLY_ROUTE ? root : `${root}:${route}`;
|
|
861
|
+
const lockName = outboxLeaderLockName(this.config.namespace, route);
|
|
850
862
|
const entry = {
|
|
851
863
|
release: () => {},
|
|
852
864
|
isLeader: false,
|
|
@@ -1405,6 +1417,328 @@ async function releaseOutboxEngine(namespace, engine, object, record) {
|
|
|
1405
1417
|
if (await engine.unregisterCollection(object, record) && engines.get(namespace) === engine) engines.delete(namespace);
|
|
1406
1418
|
}
|
|
1407
1419
|
//#endregion
|
|
1420
|
+
//#region src/persistence/snapshot-store.ts
|
|
1421
|
+
var SNAPSHOT_STORE = "snapshots";
|
|
1422
|
+
var SNAPSHOT_DB_SUFFIX = "::snapshots";
|
|
1423
|
+
function promisifyRequest$1(request) {
|
|
1424
|
+
return new Promise((resolve, reject) => {
|
|
1425
|
+
request.onsuccess = () => resolve(request.result);
|
|
1426
|
+
request.onerror = () => reject(request.error ?? /* @__PURE__ */ new Error("[smrt-web] IndexedDB request failed"));
|
|
1427
|
+
});
|
|
1428
|
+
}
|
|
1429
|
+
function awaitTransaction$1(tx) {
|
|
1430
|
+
return new Promise((resolve, reject) => {
|
|
1431
|
+
tx.oncomplete = () => resolve();
|
|
1432
|
+
tx.onerror = () => reject(tx.error ?? /* @__PURE__ */ new Error("[smrt-web] IndexedDB transaction failed"));
|
|
1433
|
+
tx.onabort = () => reject(tx.error ?? /* @__PURE__ */ new Error("[smrt-web] IndexedDB transaction aborted"));
|
|
1434
|
+
});
|
|
1435
|
+
}
|
|
1436
|
+
async function probeIndexedDb() {
|
|
1437
|
+
const idb = globalThis.indexedDB;
|
|
1438
|
+
if (!idb) return false;
|
|
1439
|
+
const probeName = "__smrt_web_snapshot_probe__";
|
|
1440
|
+
try {
|
|
1441
|
+
(await new Promise((resolve, reject) => {
|
|
1442
|
+
const request = idb.open(probeName, 1);
|
|
1443
|
+
request.onsuccess = () => resolve(request.result);
|
|
1444
|
+
request.onerror = () => reject(request.error ?? /* @__PURE__ */ new Error("probe failed"));
|
|
1445
|
+
request.onblocked = () => reject(/* @__PURE__ */ new Error("probe blocked"));
|
|
1446
|
+
})).close();
|
|
1447
|
+
try {
|
|
1448
|
+
idb.deleteDatabase(probeName);
|
|
1449
|
+
} catch {}
|
|
1450
|
+
return true;
|
|
1451
|
+
} catch {
|
|
1452
|
+
return false;
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
var SnapshotStore = class {
|
|
1456
|
+
db;
|
|
1457
|
+
/** The IndexedDB database name (== the durable-store namespace). */
|
|
1458
|
+
dbName;
|
|
1459
|
+
constructor(db, dbName) {
|
|
1460
|
+
this.db = db;
|
|
1461
|
+
this.dbName = dbName;
|
|
1462
|
+
}
|
|
1463
|
+
/**
|
|
1464
|
+
* Read the persisted rows for `collection`, or `undefined` if none were ever
|
|
1465
|
+
* saved (or the record is malformed). `undefined` is the warm-start "nothing
|
|
1466
|
+
* on disk" signal — the engine then fetches fresh.
|
|
1467
|
+
*/
|
|
1468
|
+
async load(collection) {
|
|
1469
|
+
const tx = this.db.transaction(SNAPSHOT_STORE, "readonly");
|
|
1470
|
+
const record = await promisifyRequest$1(tx.objectStore(SNAPSHOT_STORE).get(collection));
|
|
1471
|
+
await awaitTransaction$1(tx);
|
|
1472
|
+
if (!record || !Array.isArray(record.rows)) return void 0;
|
|
1473
|
+
return record.rows;
|
|
1474
|
+
}
|
|
1475
|
+
/**
|
|
1476
|
+
* Write (replacing) the snapshot for `collection`. Resolves once the write is
|
|
1477
|
+
* durably committed. A single blob per collection — the whole current row set,
|
|
1478
|
+
* not a delta — so a restore is one read with no reconciliation.
|
|
1479
|
+
*/
|
|
1480
|
+
async save(collection, rows) {
|
|
1481
|
+
const tx = this.db.transaction(SNAPSHOT_STORE, "readwrite");
|
|
1482
|
+
const store = tx.objectStore(SNAPSHOT_STORE);
|
|
1483
|
+
const record = {
|
|
1484
|
+
collection,
|
|
1485
|
+
rows
|
|
1486
|
+
};
|
|
1487
|
+
await promisifyRequest$1(store.put(record));
|
|
1488
|
+
await awaitTransaction$1(tx);
|
|
1489
|
+
}
|
|
1490
|
+
/**
|
|
1491
|
+
* Drop the snapshot for a single `collection` (its capability's own teardown
|
|
1492
|
+
* does NOT clear — the persisted rows must survive for the next load; this is
|
|
1493
|
+
* only for an explicit targeted purge). Kept for completeness / tests.
|
|
1494
|
+
*/
|
|
1495
|
+
async remove(collection) {
|
|
1496
|
+
const tx = this.db.transaction(SNAPSHOT_STORE, "readwrite");
|
|
1497
|
+
await promisifyRequest$1(tx.objectStore(SNAPSHOT_STORE).delete(collection));
|
|
1498
|
+
await awaitTransaction$1(tx);
|
|
1499
|
+
}
|
|
1500
|
+
/**
|
|
1501
|
+
* Drop EVERY snapshot — the durable-store `clear()` for `wipeDurableStore`.
|
|
1502
|
+
* Empties the store but keeps the database so a subsequent save still works.
|
|
1503
|
+
*/
|
|
1504
|
+
async clear() {
|
|
1505
|
+
const tx = this.db.transaction(SNAPSHOT_STORE, "readwrite");
|
|
1506
|
+
await promisifyRequest$1(tx.objectStore(SNAPSHOT_STORE).clear());
|
|
1507
|
+
await awaitTransaction$1(tx);
|
|
1508
|
+
}
|
|
1509
|
+
/** Close the underlying database handle (called on the last detach). */
|
|
1510
|
+
close() {
|
|
1511
|
+
this.db.close();
|
|
1512
|
+
}
|
|
1513
|
+
};
|
|
1514
|
+
function openSnapshotStore(namespace) {
|
|
1515
|
+
const idb = globalThis.indexedDB;
|
|
1516
|
+
if (!idb) return Promise.reject(/* @__PURE__ */ new Error("[smrt-web] IndexedDB is unavailable in this environment"));
|
|
1517
|
+
const dbName = `${namespace}${SNAPSHOT_DB_SUFFIX}`;
|
|
1518
|
+
return new Promise((resolve, reject) => {
|
|
1519
|
+
const request = idb.open(dbName, 1);
|
|
1520
|
+
request.onupgradeneeded = () => {
|
|
1521
|
+
const db = request.result;
|
|
1522
|
+
if (!db.objectStoreNames.contains("snapshots")) db.createObjectStore(SNAPSHOT_STORE, { keyPath: "collection" });
|
|
1523
|
+
};
|
|
1524
|
+
request.onsuccess = () => resolve(new SnapshotStore(request.result, dbName));
|
|
1525
|
+
request.onerror = () => reject(request.error ?? /* @__PURE__ */ new Error(`[smrt-web] failed to open snapshot database "${dbName}"`));
|
|
1526
|
+
request.onblocked = () => reject(/* @__PURE__ */ new Error(`[smrt-web] opening snapshot database "${dbName}" was blocked`));
|
|
1527
|
+
});
|
|
1528
|
+
}
|
|
1529
|
+
//#endregion
|
|
1530
|
+
//#region src/update-state/meta-store.ts
|
|
1531
|
+
var META_STORE = "meta";
|
|
1532
|
+
var META_DB_SUFFIX = "::meta";
|
|
1533
|
+
var LAST_SEEN_MANIFEST_HASH_KEY = "lastSeenManifestHash";
|
|
1534
|
+
function promisifyRequest(request) {
|
|
1535
|
+
return new Promise((resolve, reject) => {
|
|
1536
|
+
request.onsuccess = () => resolve(request.result);
|
|
1537
|
+
request.onerror = () => reject(request.error ?? /* @__PURE__ */ new Error("[smrt-web] IndexedDB request failed"));
|
|
1538
|
+
});
|
|
1539
|
+
}
|
|
1540
|
+
function awaitTransaction(tx) {
|
|
1541
|
+
return new Promise((resolve, reject) => {
|
|
1542
|
+
tx.oncomplete = () => resolve();
|
|
1543
|
+
tx.onerror = () => reject(tx.error ?? /* @__PURE__ */ new Error("[smrt-web] IndexedDB transaction failed"));
|
|
1544
|
+
tx.onabort = () => reject(tx.error ?? /* @__PURE__ */ new Error("[smrt-web] IndexedDB transaction aborted"));
|
|
1545
|
+
});
|
|
1546
|
+
}
|
|
1547
|
+
var VersionMetaStore = class {
|
|
1548
|
+
db;
|
|
1549
|
+
/** The IndexedDB database name (== the durable-store namespace). */
|
|
1550
|
+
dbName;
|
|
1551
|
+
constructor(db, dbName) {
|
|
1552
|
+
this.db = db;
|
|
1553
|
+
this.dbName = dbName;
|
|
1554
|
+
}
|
|
1555
|
+
/** Read the value for `key`, or `undefined` if unset / malformed. */
|
|
1556
|
+
async get(key) {
|
|
1557
|
+
const tx = this.db.transaction(META_STORE, "readonly");
|
|
1558
|
+
const record = await promisifyRequest(tx.objectStore(META_STORE).get(key));
|
|
1559
|
+
await awaitTransaction(tx);
|
|
1560
|
+
return record && typeof record.value === "string" ? record.value : void 0;
|
|
1561
|
+
}
|
|
1562
|
+
/** Write (replacing) the value for `key`; resolves once durably committed. */
|
|
1563
|
+
async set(key, value) {
|
|
1564
|
+
const tx = this.db.transaction(META_STORE, "readwrite");
|
|
1565
|
+
const record = {
|
|
1566
|
+
key,
|
|
1567
|
+
value
|
|
1568
|
+
};
|
|
1569
|
+
await promisifyRequest(tx.objectStore(META_STORE).put(record));
|
|
1570
|
+
await awaitTransaction(tx);
|
|
1571
|
+
}
|
|
1572
|
+
/**
|
|
1573
|
+
* Drop EVERY meta record — the durable-store `clear()` for
|
|
1574
|
+
* {@link wipeDurableStore}, so a logout also clears the last-seen manifest
|
|
1575
|
+
* hash (the AC "wipe clears the last-seen-hash record").
|
|
1576
|
+
*/
|
|
1577
|
+
async clear() {
|
|
1578
|
+
const tx = this.db.transaction(META_STORE, "readwrite");
|
|
1579
|
+
await promisifyRequest(tx.objectStore(META_STORE).clear());
|
|
1580
|
+
await awaitTransaction(tx);
|
|
1581
|
+
}
|
|
1582
|
+
/** Close the underlying database handle. */
|
|
1583
|
+
close() {
|
|
1584
|
+
this.db.close();
|
|
1585
|
+
}
|
|
1586
|
+
};
|
|
1587
|
+
function openVersionMetaStore(namespace) {
|
|
1588
|
+
const idb = globalThis.indexedDB;
|
|
1589
|
+
if (!idb) return Promise.reject(/* @__PURE__ */ new Error("[smrt-web] IndexedDB is unavailable in this environment"));
|
|
1590
|
+
const dbName = `${namespace}${META_DB_SUFFIX}`;
|
|
1591
|
+
return new Promise((resolve, reject) => {
|
|
1592
|
+
const request = idb.open(dbName, 1);
|
|
1593
|
+
request.onupgradeneeded = () => {
|
|
1594
|
+
const db = request.result;
|
|
1595
|
+
if (!db.objectStoreNames.contains("meta")) db.createObjectStore(META_STORE, { keyPath: "key" });
|
|
1596
|
+
};
|
|
1597
|
+
request.onsuccess = () => resolve(new VersionMetaStore(request.result, dbName));
|
|
1598
|
+
request.onerror = () => reject(request.error ?? /* @__PURE__ */ new Error(`[smrt-web] failed to open meta database "${dbName}"`));
|
|
1599
|
+
request.onblocked = () => reject(/* @__PURE__ */ new Error(`[smrt-web] opening meta database "${dbName}" was blocked`));
|
|
1600
|
+
});
|
|
1601
|
+
}
|
|
1602
|
+
//#endregion
|
|
1603
|
+
//#region src/durable-store-sweep.ts
|
|
1604
|
+
var NAMESPACE_PATTERN$1 = /^smrt-web:[^:]*:(?:_[^:]*)?:(?:_[^:]*)?:[^:]*$/;
|
|
1605
|
+
function isDurableStoreNamespace(value) {
|
|
1606
|
+
return typeof value === "string" && NAMESPACE_PATTERN$1.test(value);
|
|
1607
|
+
}
|
|
1608
|
+
function namespaceOfDatabase(name) {
|
|
1609
|
+
if (name === void 0) return void 0;
|
|
1610
|
+
for (const suffix of [SNAPSHOT_DB_SUFFIX, META_DB_SUFFIX]) if (name.endsWith(suffix)) {
|
|
1611
|
+
const base = name.slice(0, -suffix.length);
|
|
1612
|
+
return isDurableStoreNamespace(base) ? base : void 0;
|
|
1613
|
+
}
|
|
1614
|
+
return isDurableStoreNamespace(name) ? name : void 0;
|
|
1615
|
+
}
|
|
1616
|
+
function getIndexedDb() {
|
|
1617
|
+
return globalThis.indexedDB;
|
|
1618
|
+
}
|
|
1619
|
+
async function listDurableNamespaces() {
|
|
1620
|
+
const names = new Set(registeredDurableNamespaces());
|
|
1621
|
+
const idb = getIndexedDb();
|
|
1622
|
+
if (idb && typeof idb.databases === "function") for (const info of await idb.databases()) {
|
|
1623
|
+
const namespace = namespaceOfDatabase(info.name);
|
|
1624
|
+
if (namespace !== void 0) names.add(namespace);
|
|
1625
|
+
}
|
|
1626
|
+
return [...names].sort();
|
|
1627
|
+
}
|
|
1628
|
+
function resolveCutoff(olderThan) {
|
|
1629
|
+
const cutoff = olderThan instanceof Date ? olderThan.getTime() : typeof olderThan === "number" ? olderThan : NaN;
|
|
1630
|
+
if (!Number.isFinite(cutoff)) throw new TypeError("pruneDurableNamespace requires olderThan as a valid Date or epoch milliseconds");
|
|
1631
|
+
return cutoff;
|
|
1632
|
+
}
|
|
1633
|
+
function openExistingDatabase(idb, name) {
|
|
1634
|
+
return new Promise((resolve, reject) => {
|
|
1635
|
+
let created = false;
|
|
1636
|
+
const request = idb.open(name);
|
|
1637
|
+
request.onupgradeneeded = () => {
|
|
1638
|
+
created = true;
|
|
1639
|
+
request.transaction?.abort();
|
|
1640
|
+
};
|
|
1641
|
+
request.onsuccess = () => {
|
|
1642
|
+
const db = request.result;
|
|
1643
|
+
if (created) {
|
|
1644
|
+
db.close();
|
|
1645
|
+
resolve(void 0);
|
|
1646
|
+
return;
|
|
1647
|
+
}
|
|
1648
|
+
db.onversionchange = () => db.close();
|
|
1649
|
+
resolve(db);
|
|
1650
|
+
};
|
|
1651
|
+
request.onerror = (event) => {
|
|
1652
|
+
if (created) {
|
|
1653
|
+
event.preventDefault();
|
|
1654
|
+
resolve(void 0);
|
|
1655
|
+
return;
|
|
1656
|
+
}
|
|
1657
|
+
reject(request.error ?? /* @__PURE__ */ new Error(`[smrt-web] failed to open durable database "${name}"`));
|
|
1658
|
+
};
|
|
1659
|
+
request.onblocked = () => reject(/* @__PURE__ */ new Error(`[smrt-web] opening durable database "${name}" was blocked`));
|
|
1660
|
+
});
|
|
1661
|
+
}
|
|
1662
|
+
function isStale(row, cutoff) {
|
|
1663
|
+
return typeof row.enqueuedAt === "number" && row.enqueuedAt < cutoff;
|
|
1664
|
+
}
|
|
1665
|
+
function countStaleByRoute(db, cutoff) {
|
|
1666
|
+
return new Promise((resolve, reject) => {
|
|
1667
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1668
|
+
const tx = db.transaction(OUTBOX_STORE, "readonly");
|
|
1669
|
+
const request = tx.objectStore(OUTBOX_STORE).openCursor();
|
|
1670
|
+
request.onsuccess = () => {
|
|
1671
|
+
const cursor = request.result;
|
|
1672
|
+
if (!cursor) return;
|
|
1673
|
+
const row = cursor.value;
|
|
1674
|
+
if (isStale(row, cutoff)) {
|
|
1675
|
+
const route = routeOf(row);
|
|
1676
|
+
counts.set(route, (counts.get(route) ?? 0) + 1);
|
|
1677
|
+
}
|
|
1678
|
+
cursor.continue();
|
|
1679
|
+
};
|
|
1680
|
+
tx.oncomplete = () => resolve(counts);
|
|
1681
|
+
tx.onerror = () => reject(tx.error ?? /* @__PURE__ */ new Error("[smrt-web] IndexedDB transaction failed"));
|
|
1682
|
+
tx.onabort = () => reject(tx.error ?? /* @__PURE__ */ new Error("[smrt-web] IndexedDB transaction aborted"));
|
|
1683
|
+
});
|
|
1684
|
+
}
|
|
1685
|
+
function deleteStaleRows(db, cutoff, route) {
|
|
1686
|
+
return new Promise((resolve, reject) => {
|
|
1687
|
+
let removed = 0;
|
|
1688
|
+
const tx = db.transaction(OUTBOX_STORE, "readwrite");
|
|
1689
|
+
const request = tx.objectStore(OUTBOX_STORE).openCursor();
|
|
1690
|
+
request.onsuccess = () => {
|
|
1691
|
+
const cursor = request.result;
|
|
1692
|
+
if (!cursor) return;
|
|
1693
|
+
const row = cursor.value;
|
|
1694
|
+
if (routeOf(row) === route && isStale(row, cutoff)) {
|
|
1695
|
+
cursor.delete();
|
|
1696
|
+
removed += 1;
|
|
1697
|
+
}
|
|
1698
|
+
cursor.continue();
|
|
1699
|
+
};
|
|
1700
|
+
tx.oncomplete = () => resolve(removed);
|
|
1701
|
+
tx.onerror = () => reject(tx.error ?? /* @__PURE__ */ new Error("[smrt-web] IndexedDB transaction failed"));
|
|
1702
|
+
tx.onabort = () => reject(tx.error ?? /* @__PURE__ */ new Error("[smrt-web] IndexedDB transaction aborted"));
|
|
1703
|
+
});
|
|
1704
|
+
}
|
|
1705
|
+
function getLockManager() {
|
|
1706
|
+
const locks = globalThis.navigator?.locks;
|
|
1707
|
+
return locks && typeof locks.request === "function" ? locks : void 0;
|
|
1708
|
+
}
|
|
1709
|
+
async function whileRouteIdle(namespace, route, work) {
|
|
1710
|
+
const locks = getLockManager();
|
|
1711
|
+
if (!locks) return hasDurableResource(namespace, "outbox") ? void 0 : work();
|
|
1712
|
+
return locks.request(outboxLeaderLockName(namespace, route), {
|
|
1713
|
+
mode: "exclusive",
|
|
1714
|
+
ifAvailable: true
|
|
1715
|
+
}, async (lock) => lock === null ? void 0 : work());
|
|
1716
|
+
}
|
|
1717
|
+
async function pruneDurableNamespace(namespace, options) {
|
|
1718
|
+
if (!isDurableStoreNamespace(namespace)) throw new TypeError("pruneDurableNamespace requires a durableStoreNamespace() string");
|
|
1719
|
+
const cutoff = resolveCutoff(options?.olderThan);
|
|
1720
|
+
const result = {
|
|
1721
|
+
removed: 0,
|
|
1722
|
+
skipped: 0
|
|
1723
|
+
};
|
|
1724
|
+
const idb = getIndexedDb();
|
|
1725
|
+
if (!idb) return result;
|
|
1726
|
+
const db = await openExistingDatabase(idb, namespace);
|
|
1727
|
+
if (!db) return result;
|
|
1728
|
+
try {
|
|
1729
|
+
if (!db.objectStoreNames.contains("outbox")) return result;
|
|
1730
|
+
const staleByRoute = await countStaleByRoute(db, cutoff);
|
|
1731
|
+
for (const [route, count] of staleByRoute) {
|
|
1732
|
+
const removed = await whileRouteIdle(namespace, route, () => deleteStaleRows(db, cutoff, route));
|
|
1733
|
+
if (removed === void 0) result.skipped += count;
|
|
1734
|
+
else result.removed += removed;
|
|
1735
|
+
}
|
|
1736
|
+
return result;
|
|
1737
|
+
} finally {
|
|
1738
|
+
db.close();
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
//#endregion
|
|
1408
1742
|
//#region src/offline.ts
|
|
1409
1743
|
function resolveBackoff(backoff) {
|
|
1410
1744
|
return {
|
|
@@ -1537,116 +1871,6 @@ function getOutboxHandle(namespace) {
|
|
|
1537
1871
|
};
|
|
1538
1872
|
}
|
|
1539
1873
|
//#endregion
|
|
1540
|
-
//#region src/persistence/snapshot-store.ts
|
|
1541
|
-
var SNAPSHOT_STORE = "snapshots";
|
|
1542
|
-
var SNAPSHOT_DB_SUFFIX = "::snapshots";
|
|
1543
|
-
function promisifyRequest$1(request) {
|
|
1544
|
-
return new Promise((resolve, reject) => {
|
|
1545
|
-
request.onsuccess = () => resolve(request.result);
|
|
1546
|
-
request.onerror = () => reject(request.error ?? /* @__PURE__ */ new Error("[smrt-web] IndexedDB request failed"));
|
|
1547
|
-
});
|
|
1548
|
-
}
|
|
1549
|
-
function awaitTransaction$1(tx) {
|
|
1550
|
-
return new Promise((resolve, reject) => {
|
|
1551
|
-
tx.oncomplete = () => resolve();
|
|
1552
|
-
tx.onerror = () => reject(tx.error ?? /* @__PURE__ */ new Error("[smrt-web] IndexedDB transaction failed"));
|
|
1553
|
-
tx.onabort = () => reject(tx.error ?? /* @__PURE__ */ new Error("[smrt-web] IndexedDB transaction aborted"));
|
|
1554
|
-
});
|
|
1555
|
-
}
|
|
1556
|
-
async function probeIndexedDb() {
|
|
1557
|
-
const idb = globalThis.indexedDB;
|
|
1558
|
-
if (!idb) return false;
|
|
1559
|
-
const probeName = "__smrt_web_snapshot_probe__";
|
|
1560
|
-
try {
|
|
1561
|
-
(await new Promise((resolve, reject) => {
|
|
1562
|
-
const request = idb.open(probeName, 1);
|
|
1563
|
-
request.onsuccess = () => resolve(request.result);
|
|
1564
|
-
request.onerror = () => reject(request.error ?? /* @__PURE__ */ new Error("probe failed"));
|
|
1565
|
-
request.onblocked = () => reject(/* @__PURE__ */ new Error("probe blocked"));
|
|
1566
|
-
})).close();
|
|
1567
|
-
try {
|
|
1568
|
-
idb.deleteDatabase(probeName);
|
|
1569
|
-
} catch {}
|
|
1570
|
-
return true;
|
|
1571
|
-
} catch {
|
|
1572
|
-
return false;
|
|
1573
|
-
}
|
|
1574
|
-
}
|
|
1575
|
-
var SnapshotStore = class {
|
|
1576
|
-
db;
|
|
1577
|
-
/** The IndexedDB database name (== the durable-store namespace). */
|
|
1578
|
-
dbName;
|
|
1579
|
-
constructor(db, dbName) {
|
|
1580
|
-
this.db = db;
|
|
1581
|
-
this.dbName = dbName;
|
|
1582
|
-
}
|
|
1583
|
-
/**
|
|
1584
|
-
* Read the persisted rows for `collection`, or `undefined` if none were ever
|
|
1585
|
-
* saved (or the record is malformed). `undefined` is the warm-start "nothing
|
|
1586
|
-
* on disk" signal — the engine then fetches fresh.
|
|
1587
|
-
*/
|
|
1588
|
-
async load(collection) {
|
|
1589
|
-
const tx = this.db.transaction(SNAPSHOT_STORE, "readonly");
|
|
1590
|
-
const record = await promisifyRequest$1(tx.objectStore(SNAPSHOT_STORE).get(collection));
|
|
1591
|
-
await awaitTransaction$1(tx);
|
|
1592
|
-
if (!record || !Array.isArray(record.rows)) return void 0;
|
|
1593
|
-
return record.rows;
|
|
1594
|
-
}
|
|
1595
|
-
/**
|
|
1596
|
-
* Write (replacing) the snapshot for `collection`. Resolves once the write is
|
|
1597
|
-
* durably committed. A single blob per collection — the whole current row set,
|
|
1598
|
-
* not a delta — so a restore is one read with no reconciliation.
|
|
1599
|
-
*/
|
|
1600
|
-
async save(collection, rows) {
|
|
1601
|
-
const tx = this.db.transaction(SNAPSHOT_STORE, "readwrite");
|
|
1602
|
-
const store = tx.objectStore(SNAPSHOT_STORE);
|
|
1603
|
-
const record = {
|
|
1604
|
-
collection,
|
|
1605
|
-
rows
|
|
1606
|
-
};
|
|
1607
|
-
await promisifyRequest$1(store.put(record));
|
|
1608
|
-
await awaitTransaction$1(tx);
|
|
1609
|
-
}
|
|
1610
|
-
/**
|
|
1611
|
-
* Drop the snapshot for a single `collection` (its capability's own teardown
|
|
1612
|
-
* does NOT clear — the persisted rows must survive for the next load; this is
|
|
1613
|
-
* only for an explicit targeted purge). Kept for completeness / tests.
|
|
1614
|
-
*/
|
|
1615
|
-
async remove(collection) {
|
|
1616
|
-
const tx = this.db.transaction(SNAPSHOT_STORE, "readwrite");
|
|
1617
|
-
await promisifyRequest$1(tx.objectStore(SNAPSHOT_STORE).delete(collection));
|
|
1618
|
-
await awaitTransaction$1(tx);
|
|
1619
|
-
}
|
|
1620
|
-
/**
|
|
1621
|
-
* Drop EVERY snapshot — the durable-store `clear()` for `wipeDurableStore`.
|
|
1622
|
-
* Empties the store but keeps the database so a subsequent save still works.
|
|
1623
|
-
*/
|
|
1624
|
-
async clear() {
|
|
1625
|
-
const tx = this.db.transaction(SNAPSHOT_STORE, "readwrite");
|
|
1626
|
-
await promisifyRequest$1(tx.objectStore(SNAPSHOT_STORE).clear());
|
|
1627
|
-
await awaitTransaction$1(tx);
|
|
1628
|
-
}
|
|
1629
|
-
/** Close the underlying database handle (called on the last detach). */
|
|
1630
|
-
close() {
|
|
1631
|
-
this.db.close();
|
|
1632
|
-
}
|
|
1633
|
-
};
|
|
1634
|
-
function openSnapshotStore(namespace) {
|
|
1635
|
-
const idb = globalThis.indexedDB;
|
|
1636
|
-
if (!idb) return Promise.reject(/* @__PURE__ */ new Error("[smrt-web] IndexedDB is unavailable in this environment"));
|
|
1637
|
-
const dbName = `${namespace}${SNAPSHOT_DB_SUFFIX}`;
|
|
1638
|
-
return new Promise((resolve, reject) => {
|
|
1639
|
-
const request = idb.open(dbName, 1);
|
|
1640
|
-
request.onupgradeneeded = () => {
|
|
1641
|
-
const db = request.result;
|
|
1642
|
-
if (!db.objectStoreNames.contains("snapshots")) db.createObjectStore(SNAPSHOT_STORE, { keyPath: "collection" });
|
|
1643
|
-
};
|
|
1644
|
-
request.onsuccess = () => resolve(new SnapshotStore(request.result, dbName));
|
|
1645
|
-
request.onerror = () => reject(request.error ?? /* @__PURE__ */ new Error(`[smrt-web] failed to open snapshot database "${dbName}"`));
|
|
1646
|
-
request.onblocked = () => reject(/* @__PURE__ */ new Error(`[smrt-web] opening snapshot database "${dbName}" was blocked`));
|
|
1647
|
-
});
|
|
1648
|
-
}
|
|
1649
|
-
//#endregion
|
|
1650
1874
|
//#region src/persistence.ts
|
|
1651
1875
|
var DEFAULT_PERSIST_DEBOUNCE_MS = 250;
|
|
1652
1876
|
function registerForWipe(engine, namespace) {
|
|
@@ -2461,79 +2685,6 @@ function liveInvalidation(config) {
|
|
|
2461
2685
|
};
|
|
2462
2686
|
}
|
|
2463
2687
|
//#endregion
|
|
2464
|
-
//#region src/update-state/meta-store.ts
|
|
2465
|
-
var META_STORE = "meta";
|
|
2466
|
-
var META_DB_SUFFIX = "::meta";
|
|
2467
|
-
var LAST_SEEN_MANIFEST_HASH_KEY = "lastSeenManifestHash";
|
|
2468
|
-
function promisifyRequest(request) {
|
|
2469
|
-
return new Promise((resolve, reject) => {
|
|
2470
|
-
request.onsuccess = () => resolve(request.result);
|
|
2471
|
-
request.onerror = () => reject(request.error ?? /* @__PURE__ */ new Error("[smrt-web] IndexedDB request failed"));
|
|
2472
|
-
});
|
|
2473
|
-
}
|
|
2474
|
-
function awaitTransaction(tx) {
|
|
2475
|
-
return new Promise((resolve, reject) => {
|
|
2476
|
-
tx.oncomplete = () => resolve();
|
|
2477
|
-
tx.onerror = () => reject(tx.error ?? /* @__PURE__ */ new Error("[smrt-web] IndexedDB transaction failed"));
|
|
2478
|
-
tx.onabort = () => reject(tx.error ?? /* @__PURE__ */ new Error("[smrt-web] IndexedDB transaction aborted"));
|
|
2479
|
-
});
|
|
2480
|
-
}
|
|
2481
|
-
var VersionMetaStore = class {
|
|
2482
|
-
db;
|
|
2483
|
-
/** The IndexedDB database name (== the durable-store namespace). */
|
|
2484
|
-
dbName;
|
|
2485
|
-
constructor(db, dbName) {
|
|
2486
|
-
this.db = db;
|
|
2487
|
-
this.dbName = dbName;
|
|
2488
|
-
}
|
|
2489
|
-
/** Read the value for `key`, or `undefined` if unset / malformed. */
|
|
2490
|
-
async get(key) {
|
|
2491
|
-
const tx = this.db.transaction(META_STORE, "readonly");
|
|
2492
|
-
const record = await promisifyRequest(tx.objectStore(META_STORE).get(key));
|
|
2493
|
-
await awaitTransaction(tx);
|
|
2494
|
-
return record && typeof record.value === "string" ? record.value : void 0;
|
|
2495
|
-
}
|
|
2496
|
-
/** Write (replacing) the value for `key`; resolves once durably committed. */
|
|
2497
|
-
async set(key, value) {
|
|
2498
|
-
const tx = this.db.transaction(META_STORE, "readwrite");
|
|
2499
|
-
const record = {
|
|
2500
|
-
key,
|
|
2501
|
-
value
|
|
2502
|
-
};
|
|
2503
|
-
await promisifyRequest(tx.objectStore(META_STORE).put(record));
|
|
2504
|
-
await awaitTransaction(tx);
|
|
2505
|
-
}
|
|
2506
|
-
/**
|
|
2507
|
-
* Drop EVERY meta record — the durable-store `clear()` for
|
|
2508
|
-
* {@link wipeDurableStore}, so a logout also clears the last-seen manifest
|
|
2509
|
-
* hash (the AC "wipe clears the last-seen-hash record").
|
|
2510
|
-
*/
|
|
2511
|
-
async clear() {
|
|
2512
|
-
const tx = this.db.transaction(META_STORE, "readwrite");
|
|
2513
|
-
await promisifyRequest(tx.objectStore(META_STORE).clear());
|
|
2514
|
-
await awaitTransaction(tx);
|
|
2515
|
-
}
|
|
2516
|
-
/** Close the underlying database handle. */
|
|
2517
|
-
close() {
|
|
2518
|
-
this.db.close();
|
|
2519
|
-
}
|
|
2520
|
-
};
|
|
2521
|
-
function openVersionMetaStore(namespace) {
|
|
2522
|
-
const idb = globalThis.indexedDB;
|
|
2523
|
-
if (!idb) return Promise.reject(/* @__PURE__ */ new Error("[smrt-web] IndexedDB is unavailable in this environment"));
|
|
2524
|
-
const dbName = `${namespace}${META_DB_SUFFIX}`;
|
|
2525
|
-
return new Promise((resolve, reject) => {
|
|
2526
|
-
const request = idb.open(dbName, 1);
|
|
2527
|
-
request.onupgradeneeded = () => {
|
|
2528
|
-
const db = request.result;
|
|
2529
|
-
if (!db.objectStoreNames.contains("meta")) db.createObjectStore(META_STORE, { keyPath: "key" });
|
|
2530
|
-
};
|
|
2531
|
-
request.onsuccess = () => resolve(new VersionMetaStore(request.result, dbName));
|
|
2532
|
-
request.onerror = () => reject(request.error ?? /* @__PURE__ */ new Error(`[smrt-web] failed to open meta database "${dbName}"`));
|
|
2533
|
-
request.onblocked = () => reject(/* @__PURE__ */ new Error(`[smrt-web] opening meta database "${dbName}" was blocked`));
|
|
2534
|
-
});
|
|
2535
|
-
}
|
|
2536
|
-
//#endregion
|
|
2537
2688
|
//#region src/update-state.ts
|
|
2538
2689
|
function createUpdateState(config) {
|
|
2539
2690
|
const namespace = durableStoreNamespace(config.namespace);
|
|
@@ -3554,6 +3705,6 @@ function createSmrtCollection(definition, options) {
|
|
|
3554
3705
|
return handle;
|
|
3555
3706
|
}
|
|
3556
3707
|
//#endregion
|
|
3557
|
-
export {
|
|
3708
|
+
export { wipeDurableStore as A, MAX_SMRT_WEB_DATA_QUERY_FACETS as B, getOutboxHandle as C, pruneDurableNamespace as D, listDurableNamespaces as E, classifySmrtWebDataSurfaceActionResult as F, MAX_SMRT_WEB_DATA_QUERY_ROWS as G, MAX_SMRT_WEB_DATA_QUERY_OFFSET as H, dataSurfaceActionCommandTransport as I, executeSmrtWebDataQuery as J, MAX_SMRT_WEB_DATA_QUERY_STRING_LENGTH as K, executeSmrtWebDataSurfaceAction as L, MAX_SMRT_WEB_DATA_SURFACE_ACTION_IDENTIFIER_LENGTH as M, MAX_SMRT_WEB_DATA_SURFACE_ACTION_JSON_DEPTH as N, durableStoreNamespace as O, MAX_SMRT_WEB_DATA_SURFACE_ACTION_RESULT_BYTES as P, normalizeSmrtWebDataSurfaceActionResult as R, persistDataSurface as S, offlineOutbox as T, MAX_SMRT_WEB_DATA_QUERY_PAGE_LIMIT as U, MAX_SMRT_WEB_DATA_QUERY_FACET_VALUES as V, MAX_SMRT_WEB_DATA_QUERY_RESULT_BYTES as W, runWrapMutation as X, normalizeSmrtWebDataQueryResult as Y, createSmrtWebEventSubscriber as _, createSmrtWebClient as a, DEFAULT_PERSIST_DEBOUNCE_MS as b, newLocalId as c, unwrapListResult as d, validateSmrtWebClient as f, createUpdateState as g, registerWebMcpTools as h, createSmrtCollection as i, MAX_SMRT_WEB_DATA_SURFACE_ACTION_CONTAINER_ITEMS as j, registerDurableResource as k, throwIfSmrtWebError as l, registerWebMcpBespokeTool as m, buildListQuery as n, getEngineCollection as o, registerViewIntent as p, MAX_SMRT_WEB_DATA_QUERY_WARNINGS as q, createDefinitionFetchers as r, invalidateSmrtWebCollections as s, SmrtWebRequestError as t, unwrapItemResult as u, liveInvalidation as v, offlineCommandQueue as w, persistCollection as x, createSmrtWebQuery as y, MAX_SMRT_WEB_DATA_QUERY_CONTAINER_ITEMS as z };
|
|
3558
3709
|
|
|
3559
|
-
//# sourceMappingURL=src-
|
|
3710
|
+
//# sourceMappingURL=src-ClpJxx94.js.map
|