@happyvertical/smrt-web 0.43.1 → 0.43.3

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
@@ -41,6 +41,7 @@ module you are editing. This file keeps what holds in every module.
41
41
  | `webmcp.ts` | framework-agnostic WebMCP registrar; registers generated collection tools and routes mutations through shared smrt-web cache state | — |
42
42
  | `persistence/` + `update-state.ts` | the read-cache rehydrate capability and the framework-free `updateAvailable` primitive (bundle + contract signals) | [agents/version-persistence.md](agents/version-persistence.md) |
43
43
  | `data-query.ts` | dependency-free browser mirror and defensive response normalizer for the canonical bounded data-query envelope (#2444) | — |
44
+ | `remote-query.ts` | query-shaped remote pages over a `SmrtWebCollection`, with keyed stale cache, execution modes, cancellation/latest-query-wins, and optional query-scoped live subscriptions (#2445) | — |
44
45
 
45
46
  ## The engine-absorption boundary (ratified conditions, #1761)
46
47
 
package/README.md CHANGED
@@ -9,6 +9,12 @@ Use it for browser-side collection state, shared request deduplication, offline
9
9
  writes, persisted read caches, and live invalidation. Svelte bindings live in
10
10
  [`@happyvertical/smrt-svelte/web`](../smrt-svelte/README.md).
11
11
 
12
+ Query-backed surfaces can use `createSmrtWebQuery(collection, transport)` to
13
+ fetch one canonical bounded page. Visible runs update state; `background`,
14
+ `prefetch`, and `silent` runs stay out of visible state. The controller keeps
15
+ stale rows available while refreshing, cancels superseded visible runs, and can
16
+ attach a query-scoped live subscription.
17
+
12
18
  ## Installation
13
19
 
14
20
  ```bash
@@ -119,6 +125,7 @@ core.
119
125
  | Group | Main exports |
120
126
  | --- | --- |
121
127
  | Collections | `createSmrtCollection`, `createSmrtWebClient`, `newLocalId` |
128
+ | Remote queries | `createSmrtWebQuery`, `SmrtWebQueryTransport` |
122
129
  | HTTP | `createDefinitionFetchers`, `unwrapListResult`, `unwrapItemResult` |
123
130
  | Offline | `offlineOutbox`, `getOutboxHandle` |
124
131
  | Persistence | `persistCollection`, `wipeDurableStore` |
@@ -1343,6 +1343,425 @@ function persistCollection(config) {
1343
1343
  };
1344
1344
  }
1345
1345
  //#endregion
1346
+ //#region src/remote-query.ts
1347
+ function canonicalize(value) {
1348
+ if (Array.isArray(value)) return value.map(canonicalize);
1349
+ if (value && typeof value === "object") {
1350
+ const record = value;
1351
+ return Object.fromEntries(Object.keys(record).sort().map((key) => [key, canonicalize(record[key])]));
1352
+ }
1353
+ return value;
1354
+ }
1355
+ function keyFor(request) {
1356
+ const { requestId: _requestId, ...semantic } = request;
1357
+ return JSON.stringify(canonicalize(semantic));
1358
+ }
1359
+ function rebindRequestId(result, request) {
1360
+ return result.requestId === request.requestId ? result : {
1361
+ ...result,
1362
+ requestId: request.requestId
1363
+ };
1364
+ }
1365
+ function abortError() {
1366
+ if (typeof DOMException !== "undefined") return new DOMException("The operation was aborted", "AbortError");
1367
+ const error = /* @__PURE__ */ new Error("The operation was aborted");
1368
+ error.name = "AbortError";
1369
+ return error;
1370
+ }
1371
+ function isAbort(error) {
1372
+ return !!error && typeof error === "object" && error.name === "AbortError";
1373
+ }
1374
+ function pageOf(result) {
1375
+ if (!result.page) return void 0;
1376
+ return result.page.kind === "offset" ? { ...result.page } : { ...result.page };
1377
+ }
1378
+ function createSmrtWebQuery(_collection, transport, options = {}) {
1379
+ const staleTimeMs = options.staleTimeMs ?? 3e4;
1380
+ const cache = /* @__PURE__ */ new Map();
1381
+ const inFlight = /* @__PURE__ */ new Map();
1382
+ const latestSuccessfulFlight = /* @__PURE__ */ new Map();
1383
+ let flightSequence = 0;
1384
+ const listeners = /* @__PURE__ */ new Set();
1385
+ let request;
1386
+ let visibleController;
1387
+ const runControllers = /* @__PURE__ */ new Set();
1388
+ let generation = 0;
1389
+ let live;
1390
+ let liveIntent;
1391
+ let liveGeneration = 0;
1392
+ let disposed = false;
1393
+ let state = {
1394
+ rows: [],
1395
+ page: void 0,
1396
+ total: void 0,
1397
+ loading: false,
1398
+ refreshing: false,
1399
+ stale: false,
1400
+ error: null,
1401
+ lastUpdated: void 0,
1402
+ result: void 0
1403
+ };
1404
+ const publish = (next) => {
1405
+ state = next;
1406
+ for (const listener of listeners) listener(state);
1407
+ };
1408
+ const cached = (key) => cache.get(key);
1409
+ const cacheSuccess = (key, result, sequence, updatedAt = Date.now()) => {
1410
+ if (disposed) return false;
1411
+ const latest = latestSuccessfulFlight.get(key);
1412
+ if (latest === void 0 || sequence > latest) {
1413
+ latestSuccessfulFlight.set(key, sequence);
1414
+ cache.set(key, {
1415
+ result,
1416
+ updatedAt
1417
+ });
1418
+ return true;
1419
+ }
1420
+ return false;
1421
+ };
1422
+ const apply = (result, candidate, updatedAt = Date.now(), successSequence) => {
1423
+ if (successSequence !== void 0 && !cacheSuccess(keyFor(candidate), result, successSequence, updatedAt)) return;
1424
+ publish({
1425
+ rows: result.rows,
1426
+ page: pageOf(result),
1427
+ total: result.total,
1428
+ loading: false,
1429
+ refreshing: false,
1430
+ stale: result.freshness.state === "stale",
1431
+ error: null,
1432
+ lastUpdated: updatedAt,
1433
+ result
1434
+ });
1435
+ };
1436
+ const runTransport = async (candidate, controller) => {
1437
+ runControllers.add(controller);
1438
+ try {
1439
+ const result = await executeSmrtWebDataQuery(transport, candidate, { signal: controller.signal });
1440
+ if (controller.signal.aborted) throw abortError();
1441
+ return result;
1442
+ } catch (error) {
1443
+ if (controller.signal.aborted) throw abortError();
1444
+ throw error;
1445
+ } finally {
1446
+ runControllers.delete(controller);
1447
+ }
1448
+ };
1449
+ const execute = async (candidate, runOptions = {}) => {
1450
+ if (disposed) throw abortError();
1451
+ const mode = runOptions.mode ?? "visible";
1452
+ const key = keyFor(candidate);
1453
+ if (runOptions.signal?.aborted || (runOptions.deadlineMs ?? 1) <= 0) throw abortError();
1454
+ const rebindIntent = mode === "visible" && liveIntent?.active && (!live || request !== void 0 && keyFor(request) !== key) ? liveIntent : void 0;
1455
+ const entry = cached(key);
1456
+ const fresh = entry !== void 0 && Date.now() - entry.updatedAt < staleTimeMs;
1457
+ if (mode !== "visible" && !runOptions.force && fresh) return rebindRequestId(entry.result, candidate);
1458
+ if (mode === "visible" && !runOptions.force && fresh) {
1459
+ generation += 1;
1460
+ visibleController?.abort(abortError());
1461
+ if (disposed) throw abortError();
1462
+ visibleController = void 0;
1463
+ request = candidate;
1464
+ if (rebindIntent) live?.disconnect();
1465
+ const result = rebindRequestId(entry.result, candidate);
1466
+ apply(result, candidate, entry.updatedAt);
1467
+ if (disposed) throw abortError();
1468
+ if (rebindIntent?.active && !live) resumeLive(rebindIntent);
1469
+ return result;
1470
+ }
1471
+ if (mode === "visible") {
1472
+ generation += 1;
1473
+ visibleController?.abort(abortError());
1474
+ if (disposed) throw abortError();
1475
+ const invocationController = new AbortController();
1476
+ visibleController = invocationController;
1477
+ request = candidate;
1478
+ if (rebindIntent) live?.disconnect();
1479
+ const current = generation;
1480
+ const signal = runOptions.signal;
1481
+ let removeCallerAbort;
1482
+ if (signal?.aborted) invocationController.abort(signal.reason);
1483
+ else if (signal) {
1484
+ const abortCaller = () => invocationController.abort(signal.reason);
1485
+ signal.addEventListener("abort", abortCaller, { once: true });
1486
+ removeCallerAbort = () => signal.removeEventListener("abort", abortCaller);
1487
+ }
1488
+ if (entry) {
1489
+ const cachedResult = rebindRequestId(entry.result, candidate);
1490
+ publish({
1491
+ rows: cachedResult.rows,
1492
+ page: pageOf(cachedResult),
1493
+ total: cachedResult.total,
1494
+ loading: false,
1495
+ refreshing: true,
1496
+ stale: true,
1497
+ error: null,
1498
+ lastUpdated: entry.updatedAt,
1499
+ result: cachedResult
1500
+ });
1501
+ } else publish({
1502
+ ...state,
1503
+ loading: true,
1504
+ refreshing: false,
1505
+ stale: false,
1506
+ error: null
1507
+ });
1508
+ runOptions = {
1509
+ ...runOptions,
1510
+ signal: invocationController.signal,
1511
+ force: true
1512
+ };
1513
+ try {
1514
+ if (disposed) throw abortError();
1515
+ const result = await runShared(candidate, key, runOptions);
1516
+ if (current === generation) {
1517
+ if (cached(key)?.result === result) apply(result, candidate);
1518
+ else publish({
1519
+ ...state,
1520
+ loading: false,
1521
+ refreshing: false
1522
+ });
1523
+ if (rebindIntent?.active && !live) resumeLive(rebindIntent);
1524
+ }
1525
+ return result;
1526
+ } catch (error) {
1527
+ if (current === generation) if (isAbort(error)) publish({
1528
+ ...state,
1529
+ loading: false,
1530
+ refreshing: false
1531
+ });
1532
+ else publish({
1533
+ ...state,
1534
+ loading: false,
1535
+ refreshing: false,
1536
+ stale: entry !== void 0,
1537
+ error
1538
+ });
1539
+ throw error;
1540
+ } finally {
1541
+ removeCallerAbort?.();
1542
+ if (visibleController === invocationController) visibleController = void 0;
1543
+ }
1544
+ }
1545
+ return runShared(candidate, key, runOptions);
1546
+ };
1547
+ const waitForFlight = (flight, key, candidate, runOptions) => {
1548
+ if (runOptions.signal?.aborted || (runOptions.deadlineMs ?? 1) <= 0) return Promise.reject(abortError());
1549
+ return new Promise((resolve, reject) => {
1550
+ let settled = false;
1551
+ const settle = () => {
1552
+ if (settled) return;
1553
+ settled = true;
1554
+ flight.waiters -= 1;
1555
+ if (timer) clearTimeout(timer);
1556
+ runOptions.signal?.removeEventListener("abort", cancel);
1557
+ };
1558
+ const cancel = () => {
1559
+ if (settled) return;
1560
+ settle();
1561
+ if (flight.waiters === 0 && !flight.settled) {
1562
+ flight.controller.abort(abortError());
1563
+ if (inFlight.get(key) === flight) inFlight.delete(key);
1564
+ }
1565
+ reject(abortError());
1566
+ };
1567
+ const timer = runOptions.deadlineMs === void 0 ? void 0 : setTimeout(cancel, runOptions.deadlineMs);
1568
+ flight.waiters += 1;
1569
+ runOptions.signal?.addEventListener("abort", cancel, { once: true });
1570
+ flight.promise.then((result) => {
1571
+ if (settled) return;
1572
+ settle();
1573
+ resolve(rebindRequestId(result, candidate));
1574
+ }, (error) => {
1575
+ if (settled) return;
1576
+ settle();
1577
+ reject(error);
1578
+ });
1579
+ });
1580
+ };
1581
+ const runShared = async (candidate, key, runOptions) => {
1582
+ if (runOptions.signal?.aborted || (runOptions.deadlineMs ?? 1) <= 0) throw abortError();
1583
+ if (!runOptions.force) {
1584
+ const existing = inFlight.get(key);
1585
+ if (existing) return waitForFlight(existing, key, candidate, runOptions);
1586
+ }
1587
+ const flight = ++flightSequence;
1588
+ const controller = new AbortController();
1589
+ const running = runTransport(candidate, controller).then((result) => {
1590
+ if (controller.signal.aborted) throw abortError();
1591
+ cacheSuccess(key, result, flight);
1592
+ return result;
1593
+ });
1594
+ const shared = {
1595
+ controller,
1596
+ promise: running,
1597
+ waiters: 0,
1598
+ settled: false
1599
+ };
1600
+ inFlight.set(key, shared);
1601
+ running.then(() => {
1602
+ shared.settled = true;
1603
+ if (inFlight.get(key) === shared) inFlight.delete(key);
1604
+ }, () => {
1605
+ shared.settled = true;
1606
+ if (inFlight.get(key) === shared) inFlight.delete(key);
1607
+ });
1608
+ return waitForFlight(shared, key, candidate, runOptions);
1609
+ };
1610
+ const refresh = (refreshOptions = {}) => {
1611
+ if (!request) return Promise.resolve(void 0);
1612
+ return execute(request, {
1613
+ ...refreshOptions,
1614
+ mode: "visible",
1615
+ force: true
1616
+ });
1617
+ };
1618
+ const startLive = (intent) => {
1619
+ if (disposed || !intent.active || !request || !transport.subscribe) return void 0;
1620
+ const candidate = request;
1621
+ const currentGeneration = ++liveGeneration;
1622
+ const controller = new AbortController();
1623
+ let connected = true;
1624
+ let subscription;
1625
+ let handle;
1626
+ const disconnect = () => {
1627
+ if (!connected) return;
1628
+ connected = false;
1629
+ controller.abort();
1630
+ subscription.unsubscribe();
1631
+ if (live === handle) live = void 0;
1632
+ };
1633
+ const unsubscribe = () => {
1634
+ if (!intent.active) return;
1635
+ intent.active = false;
1636
+ disconnect();
1637
+ if (liveIntent === intent) liveIntent = void 0;
1638
+ if (live?.intent === intent) live.disconnect();
1639
+ };
1640
+ const reconnect = () => {
1641
+ if (!intent.active || disposed) return;
1642
+ if (!connected) {
1643
+ if (live?.intent === intent) live.reconnect();
1644
+ return;
1645
+ }
1646
+ if (currentGeneration !== liveGeneration) return;
1647
+ disconnect();
1648
+ const reconnectRequest = request;
1649
+ if (!reconnectRequest || keyFor(reconnectRequest) !== keyFor(candidate)) return;
1650
+ execute(reconnectRequest, {
1651
+ mode: "visible",
1652
+ force: true
1653
+ }).catch(() => void 0).finally(() => {
1654
+ if (intent.active && !disposed && liveIntent === intent && !live && request && keyFor(request) === keyFor(candidate)) resumeLive(intent);
1655
+ });
1656
+ };
1657
+ handle = {
1658
+ intent,
1659
+ disconnect,
1660
+ unsubscribe,
1661
+ reconnect
1662
+ };
1663
+ subscription = transport.subscribe(candidate, (raw) => {
1664
+ if (!connected || !intent.active || disposed || currentGeneration !== liveGeneration || controller.signal.aborted) return;
1665
+ const liveFlight = ++flightSequence;
1666
+ (async () => {
1667
+ const result = await executeSmrtWebDataQuery({ query: async () => raw }, candidate);
1668
+ if (connected && intent.active && !disposed && currentGeneration === liveGeneration && request && keyFor(request) === keyFor(candidate)) apply(rebindRequestId(result, request), request, Date.now(), liveFlight);
1669
+ })().catch((error) => {
1670
+ const latestSuccessful = latestSuccessfulFlight.get(keyFor(candidate));
1671
+ if (!isAbort(error) && connected && intent.active && !disposed && currentGeneration === liveGeneration && request && keyFor(request) === keyFor(candidate) && (latestSuccessful === void 0 || liveFlight > latestSuccessful)) publish({
1672
+ ...state,
1673
+ error
1674
+ });
1675
+ });
1676
+ }, { signal: controller.signal });
1677
+ if (disposed || !intent.active) {
1678
+ disconnect();
1679
+ return;
1680
+ }
1681
+ live = handle;
1682
+ return handle;
1683
+ };
1684
+ const clearLiveIntent = (intent) => {
1685
+ intent.active = false;
1686
+ if (liveIntent === intent) liveIntent = void 0;
1687
+ };
1688
+ const resumeLive = (intent) => {
1689
+ try {
1690
+ startLive(intent);
1691
+ } catch (error) {
1692
+ clearLiveIntent(intent);
1693
+ if (!disposed) publish({
1694
+ ...state,
1695
+ error
1696
+ });
1697
+ }
1698
+ };
1699
+ const subscribeLive = () => {
1700
+ if (disposed || !request || !transport.subscribe) return void 0;
1701
+ if (liveIntent) {
1702
+ liveIntent.active = false;
1703
+ live?.disconnect();
1704
+ }
1705
+ const intent = { active: true };
1706
+ liveIntent = intent;
1707
+ try {
1708
+ return startLive(intent);
1709
+ } catch (error) {
1710
+ clearLiveIntent(intent);
1711
+ throw error;
1712
+ }
1713
+ };
1714
+ return {
1715
+ get state() {
1716
+ return state;
1717
+ },
1718
+ get request() {
1719
+ return request;
1720
+ },
1721
+ execute,
1722
+ refresh,
1723
+ retry: () => refresh(),
1724
+ subscribe(listener) {
1725
+ listeners.add(listener);
1726
+ listener(state);
1727
+ return () => listeners.delete(listener);
1728
+ },
1729
+ subscribeLive,
1730
+ invalidate() {
1731
+ if (!request) return;
1732
+ const key = keyFor(request);
1733
+ const entry = cache.get(key);
1734
+ latestSuccessfulFlight.set(key, ++flightSequence);
1735
+ if (entry) cache.set(key, {
1736
+ ...entry,
1737
+ updatedAt: 0
1738
+ });
1739
+ publish({
1740
+ ...state,
1741
+ loading: false,
1742
+ refreshing: false,
1743
+ stale: true
1744
+ });
1745
+ },
1746
+ dispose() {
1747
+ disposed = true;
1748
+ generation += 1;
1749
+ liveGeneration += 1;
1750
+ visibleController?.abort(abortError());
1751
+ for (const controller of runControllers) controller.abort(abortError());
1752
+ if (liveIntent) liveIntent.active = false;
1753
+ live?.disconnect();
1754
+ live = void 0;
1755
+ liveIntent = void 0;
1756
+ request = void 0;
1757
+ listeners.clear();
1758
+ cache.clear();
1759
+ latestSuccessfulFlight.clear();
1760
+ inFlight.clear();
1761
+ }
1762
+ };
1763
+ }
1764
+ //#endregion
1346
1765
  //#region src/sse-client.ts
1347
1766
  var EVENT_SOURCE_CLOSED = 2;
1348
1767
  function defaultEventSourceFactory(url, init) {
@@ -2287,6 +2706,6 @@ function createSmrtCollection(definition, options) {
2287
2706
  return handle;
2288
2707
  }
2289
2708
  //#endregion
2290
- export { executeSmrtWebDataQuery as A, MAX_SMRT_WEB_DATA_QUERY_FACET_VALUES as C, MAX_SMRT_WEB_DATA_QUERY_ROWS as D, MAX_SMRT_WEB_DATA_QUERY_RESULT_BYTES as E, runWrapMutation as M, MAX_SMRT_WEB_DATA_QUERY_STRING_LENGTH as O, MAX_SMRT_WEB_DATA_QUERY_FACETS as S, MAX_SMRT_WEB_DATA_QUERY_PAGE_LIMIT as T, offlineOutbox as _, createSmrtWebClient as a, wipeDurableStore as b, unwrapItemResult as c, createUpdateState as d, createSmrtWebEventSubscriber as f, getOutboxHandle as g, persistCollection as h, createSmrtCollection as i, normalizeSmrtWebDataQueryResult as j, MAX_SMRT_WEB_DATA_QUERY_WARNINGS as k, unwrapListResult as l, DEFAULT_PERSIST_DEBOUNCE_MS as m, buildListQuery as n, getEngineCollection as o, liveInvalidation as p, createDefinitionFetchers as r, newLocalId as s, SmrtWebRequestError as t, registerWebMcpTools as u, durableStoreNamespace as v, MAX_SMRT_WEB_DATA_QUERY_OFFSET as w, MAX_SMRT_WEB_DATA_QUERY_CONTAINER_ITEMS as x, registerDurableResource as y };
2709
+ export { MAX_SMRT_WEB_DATA_QUERY_WARNINGS as A, MAX_SMRT_WEB_DATA_QUERY_FACETS as C, MAX_SMRT_WEB_DATA_QUERY_RESULT_BYTES as D, MAX_SMRT_WEB_DATA_QUERY_PAGE_LIMIT as E, normalizeSmrtWebDataQueryResult as M, runWrapMutation as N, MAX_SMRT_WEB_DATA_QUERY_ROWS as O, MAX_SMRT_WEB_DATA_QUERY_CONTAINER_ITEMS as S, MAX_SMRT_WEB_DATA_QUERY_OFFSET as T, getOutboxHandle as _, createSmrtWebClient as a, registerDurableResource as b, unwrapItemResult as c, createUpdateState as d, createSmrtWebEventSubscriber as f, persistCollection as g, DEFAULT_PERSIST_DEBOUNCE_MS as h, createSmrtCollection as i, executeSmrtWebDataQuery as j, MAX_SMRT_WEB_DATA_QUERY_STRING_LENGTH as k, unwrapListResult as l, createSmrtWebQuery as m, buildListQuery as n, getEngineCollection as o, liveInvalidation as p, createDefinitionFetchers as r, newLocalId as s, SmrtWebRequestError as t, registerWebMcpTools as u, offlineOutbox as v, MAX_SMRT_WEB_DATA_QUERY_FACET_VALUES as w, wipeDurableStore as x, durableStoreNamespace as y };
2291
2710
 
2292
- //# sourceMappingURL=src-CDdW9uYx.js.map
2711
+ //# sourceMappingURL=src-D9RvoJSQ.js.map