@happyvertical/smrt-web 0.55.23 → 0.55.24

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 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, and the shared durable-store namespacing/wipe registry | [agents/capability-seam.md](agents/capability-seam.md) |
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 root = `smrt-web-outbox-leader:${this.config.namespace}`;
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 { MAX_SMRT_WEB_DATA_SURFACE_ACTION_IDENTIFIER_LENGTH as A, MAX_SMRT_WEB_DATA_QUERY_OFFSET as B, getOutboxHandle as C, registerDurableResource as D, durableStoreNamespace as E, executeSmrtWebDataSurfaceAction as F, MAX_SMRT_WEB_DATA_QUERY_WARNINGS as G, MAX_SMRT_WEB_DATA_QUERY_RESULT_BYTES as H, normalizeSmrtWebDataSurfaceActionResult as I, runWrapMutation as J, executeSmrtWebDataQuery as K, MAX_SMRT_WEB_DATA_QUERY_CONTAINER_ITEMS as L, MAX_SMRT_WEB_DATA_SURFACE_ACTION_RESULT_BYTES as M, classifySmrtWebDataSurfaceActionResult as N, wipeDurableStore as O, dataSurfaceActionCommandTransport as P, MAX_SMRT_WEB_DATA_QUERY_FACETS as R, persistDataSurface as S, offlineOutbox as T, MAX_SMRT_WEB_DATA_QUERY_ROWS as U, MAX_SMRT_WEB_DATA_QUERY_PAGE_LIMIT as V, MAX_SMRT_WEB_DATA_QUERY_STRING_LENGTH as W, 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_JSON_DEPTH as j, MAX_SMRT_WEB_DATA_SURFACE_ACTION_CONTAINER_ITEMS as k, throwIfSmrtWebError as l, registerWebMcpBespokeTool as m, buildListQuery as n, getEngineCollection as o, registerViewIntent as p, normalizeSmrtWebDataQueryResult 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_FACET_VALUES as z };
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-BFQJky1a.js.map
3710
+ //# sourceMappingURL=src-ClpJxx94.js.map