stitchkit 0.89.0 → 0.90.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/CHANGELOG.md CHANGED
@@ -15,6 +15,62 @@ additive**; the first breaking change landed in 0.10.0. Grep the file for
15
15
 
16
16
  ## [Unreleased]
17
17
 
18
+ ## [0.90.0] — 2026-09-15
19
+
20
+ ### ⚠️ Breaking changes
21
+
22
+ **Who must act:** applications that bind the watch protocol themselves or read
23
+ `WatchValueFrame.value` directly — a hand-written subscriber, a test double, a custom
24
+ server binding. Applications using `createWatchClient` and the starter's binding need
25
+ no code change. **Both ends must come from the same major:** a client older than 0.90.0
26
+ reads a `delta` frame as a value of `undefined`, silently.
27
+
28
+ - **`stitchkit.watch.value` is a discriminated union on `kind`.** `full` carries the value,
29
+ `delta` carries a structural difference against a `base` revision, `unchanged` says the
30
+ value held is still current. Every frame carries `fingerprint`, the order-independent
31
+ digest of the value the receiver holds after applying it.
32
+
33
+ ```ts
34
+ // before: frame.value
35
+ // after:
36
+ if (frame.kind === 'full') hold(frame.value);
37
+ else if (frame.kind === 'delta') hold(applyWatchDelta(held, frame.delta));
38
+ // 'unchanged' leaves what you hold standing
39
+ ```
40
+
41
+ - **`AttachedWatcher.open` takes an optional third argument, `have`.** A custom server
42
+ binding forwarding `stitchkit.watch.open` should pass the payload's `have` through;
43
+ omitting it costs a whole value on every reconnection and is otherwise harmless.
44
+
45
+ ### Added
46
+
47
+ - **Watched reads cross as differences.** The hub chooses per subscriber, from the revision
48
+ it last delivered to that subscriber, and sends a difference only when it is strictly
49
+ smaller than the value. Measured: a ~75 KB list answer with one field changed crosses as a
50
+ frame under 1 KB. `deltaMemoryBytes` (default 262144, `0` disables) bounds the superseded
51
+ values kept per key. → ADR 0183.
52
+ - **A reconnection offers what it holds.** `stitchkit.watch.open` may carry
53
+ `have: { revision, fingerprint }`; a subscriber returning to an answer that has not moved
54
+ is told `unchanged` instead of being sent the value again. Requires the key to still exist
55
+ on the hub — set `holdMs` past your reconnect delay.
56
+ - **Reassembly is checked.** The client verifies the rebuilt value against the server's
57
+ fingerprint on every frame and resynchronises **that one key** on any mismatch, publishing
58
+ `resync-required`. Other keys on the same socket are untouched.
59
+ - **`watchDiff` / `applyWatchDelta` / `watchDeltaWins`** and the `WatchDelta` shapes are
60
+ exported from `stitchkit/live` for applications building their own binding.
61
+ - **`auditChanges`** — the sink `filter` most projects were writing themselves: drops `GET`,
62
+ `HEAD` and `OPTIONS`, keeps `401` and `403` whatever the verb was, keeps an unrecognised
63
+ verb. One predicate across HTTP, MCP and agent calls. → ADR 0184.
64
+ - **`createSpooledSink`** — writes the audit row to a local append-only file before offering
65
+ it to the store and replays what a previous process left undelivered. At least once; the
66
+ store must be idempotent on the record key (`spanId` by default).
67
+
68
+ ### Unchanged on purpose
69
+
70
+ - **Auditing stays opt-in.** `createHandler` and the tool mounts are untouched. Making a
71
+ decision about audit mandatory at startup was considered and rejected — the measurement it
72
+ rested on counted projects without an HTTP surface as projects that had forgotten. → ADR 0184.
73
+
18
74
  ## [0.89.0] — 2026-09-12
19
75
 
20
76
  ### ⚠️ Breaking changes
@@ -38,7 +38,7 @@
38
38
  * → ADR 0153.
39
39
  */
40
40
  import { type BackoffPolicy } from '../browser/resumable.js';
41
- import { type WatchKey, type WatchStateFrame, type WatchValueFrame } from '../live/watch-contract.js';
41
+ import { type WatchHave, type WatchKey, type WatchStateFrame, type WatchValueFrame } from '../live/watch-contract.js';
42
42
  import type { StitchLogger } from '../logger.js';
43
43
  /** The operation a watched read runs — `OperationIdentity`'s two stable halves. */
44
44
  export interface WatchOperation {
@@ -52,7 +52,15 @@ export interface WatchSubscriber {
52
52
  state(frame: WatchStateFrame): void;
53
53
  }
54
54
  export interface AttachedWatcher {
55
- open(key: WatchKey, args: unknown): {
55
+ /**
56
+ * Start watching a key.
57
+ *
58
+ * `have` is what the caller already holds from an earlier connection. Offered,
59
+ * it turns a reconnection into a difference — or into nothing at all when the
60
+ * answer has not moved — instead of the whole value again. Omitted, the caller
61
+ * is treated as holding nothing, which is what a first subscription is.
62
+ */
63
+ open(key: WatchKey, args: unknown, have?: WatchHave): {
56
64
  accepted: boolean;
57
65
  reason?: string;
58
66
  };
@@ -105,6 +113,20 @@ export interface WatchHubConfig {
105
113
  holdMs?: number;
106
114
  /** Retry pacing after a failed read. */
107
115
  backoff?: BackoffPolicy;
116
+ /**
117
+ * How many bytes of superseded values one key may keep, for differences.
118
+ *
119
+ * A difference needs the value the receiver actually holds, which is no longer
120
+ * the current one. This is the ceiling on that memory, per key, and it is a
121
+ * byte budget rather than a count of revisions because the thing being
122
+ * protected is the process's heap and revisions have no size.
123
+ *
124
+ * Zero disables differences entirely: every frame carries the whole value,
125
+ * which is what this hub did before they existed. Default 262144 (256 KiB),
126
+ * which holds several revisions of the large answers differences are for and
127
+ * a great many small ones.
128
+ */
129
+ deltaMemoryBytes?: number;
108
130
  /** Whether two answers are the same. Defaults to key-order-independent JSON equality. */
109
131
  same?(previous: unknown, next: unknown): boolean;
110
132
  logger?: StitchLogger;
@@ -1 +1 @@
1
- {"version":3,"file":"watch-hub.d.ts","sourceRoot":"","sources":["../../src/application/watch-hub.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,OAAO,EAAE,KAAK,aAAa,EAAiB,MAAM,sBAAsB,CAAC;AAEzE,OAAO,EACL,KAAK,QAAQ,EACb,KAAK,eAAe,EACpB,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAChC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAE9C,mFAAmF;AACnF,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,eAAe;IAC9B,yDAAyD;IACzD,KAAK,CAAC,KAAK,EAAE,eAAe,GAAG,IAAI,CAAC;IACpC,oDAAoD;IACpD,KAAK,CAAC,KAAK,EAAE,eAAe,GAAG,IAAI,CAAC;CACrC;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,CAAC,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,GAAG;QAAE,QAAQ,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC3E,KAAK,CAAC,GAAG,EAAE,QAAQ,GAAG,IAAI,CAAC;IAC3B,yEAAyE;IACzE,MAAM,IAAI,IAAI,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B;;;;;;;;;;;;;;;OAeG;IACH,IAAI,CAAC,SAAS,EAAE,cAAc,EAAE,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACjE,gFAAgF;IAChF,SAAS,CAAC,SAAS,EAAE,cAAc,GAAG,OAAO,CAAC;IAC9C;;;;;;;;;OASG;IACH,aAAa,CAAC,SAAS,EAAE,cAAc,EAAE,IAAI,EAAE,OAAO,GAAG,SAAS,MAAM,EAAE,CAAC;IAC3E,mFAAmF;IACnF,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC;IAC3D,kEAAkE;IAClE,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,wCAAwC;IACxC,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB,yFAAyF;IACzF,IAAI,CAAC,CAAC,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC;IACjD,MAAM,CAAC,EAAE,YAAY,CAAC;CACvB;AAED,MAAM,WAAW,QAAQ;IACvB,MAAM,CAAC,UAAU,EAAE,eAAe,GAAG,eAAe,CAAC;IACrD;;;;;;OAMG;IACH,SAAS,IAAI,MAAM,CAAC;IACpB,qEAAqE;IACrE,IAAI,IAAI,MAAM,CAAC;IACf,KAAK,IAAI,IAAI,CAAC;CACf;AAqBD;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,SAAS,EAAE,cAAc,EAAE,IAAI,EAAE,OAAO,GAAG,QAAQ,CAU3E;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,cAAc,GAAG,QAAQ,CAoR/D"}
1
+ {"version":3,"file":"watch-hub.d.ts","sourceRoot":"","sources":["../../src/application/watch-hub.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,OAAO,EAAE,KAAK,aAAa,EAAiB,MAAM,sBAAsB,CAAC;AAEzE,OAAO,EACL,KAAK,SAAS,EACd,KAAK,QAAQ,EACb,KAAK,eAAe,EACpB,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAEhC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAE9C,mFAAmF;AACnF,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,eAAe;IAC9B,yDAAyD;IACzD,KAAK,CAAC,KAAK,EAAE,eAAe,GAAG,IAAI,CAAC;IACpC,oDAAoD;IACpD,KAAK,CAAC,KAAK,EAAE,eAAe,GAAG,IAAI,CAAC;CACrC;AAED,MAAM,WAAW,eAAe;IAC9B;;;;;;;OAOG;IACH,IAAI,CAAC,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,SAAS,GAAG;QAAE,QAAQ,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC7F,KAAK,CAAC,GAAG,EAAE,QAAQ,GAAG,IAAI,CAAC;IAC3B,yEAAyE;IACzE,MAAM,IAAI,IAAI,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B;;;;;;;;;;;;;;;OAeG;IACH,IAAI,CAAC,SAAS,EAAE,cAAc,EAAE,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACjE,gFAAgF;IAChF,SAAS,CAAC,SAAS,EAAE,cAAc,GAAG,OAAO,CAAC;IAC9C;;;;;;;;;OASG;IACH,aAAa,CAAC,SAAS,EAAE,cAAc,EAAE,IAAI,EAAE,OAAO,GAAG,SAAS,MAAM,EAAE,CAAC;IAC3E,mFAAmF;IACnF,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC;IAC3D,kEAAkE;IAClE,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,wCAAwC;IACxC,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB;;;;;;;;;;;;OAYG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,yFAAyF;IACzF,IAAI,CAAC,CAAC,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC;IACjD,MAAM,CAAC,EAAE,YAAY,CAAC;CACvB;AAED,MAAM,WAAW,QAAQ;IACvB,MAAM,CAAC,UAAU,EAAE,eAAe,GAAG,eAAe,CAAC;IACrD;;;;;;OAMG;IACH,SAAS,IAAI,MAAM,CAAC;IACpB,qEAAqE;IACrE,IAAI,IAAI,MAAM,CAAC;IACf,KAAK,IAAI,IAAI,CAAC;CACf;AA2CD;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,SAAS,EAAE,cAAc,EAAE,IAAI,EAAE,OAAO,GAAG,QAAQ,CAU3E;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,cAAc,GAAG,QAAQ,CA4Y/D"}
@@ -66,8 +66,10 @@ import {
66
66
  createBackoff
67
67
  } from "./index-s8cnx02s.js";
68
68
  import {
69
+ deltaWins,
70
+ diff,
69
71
  watchKeyString
70
- } from "./index-dre2ywck.js";
72
+ } from "./index-2na0qrfn.js";
71
73
  import"./index-ywd0y6m1.js";
72
74
  import {
73
75
  BoundedChannelPolicySchema,
@@ -1563,17 +1565,20 @@ function createWatchHub(config) {
1563
1565
  const held = new Map;
1564
1566
  const maxWatches = config.maxWatchesPerSubscriber ?? 64;
1565
1567
  const holdMs = config.holdMs ?? 0;
1568
+ const deltaMemoryBytes = config.deltaMemoryBytes ?? 262144;
1566
1569
  const same = config.same ?? ((previous, next) => JSON.stringify(stableValue(previous)) === JSON.stringify(stableValue(next)));
1567
1570
  let reads = 0;
1568
1571
  let closed = false;
1569
- function tell(key, deliver) {
1572
+ function tell(key, deliver2) {
1570
1573
  try {
1571
- deliver();
1574
+ deliver2();
1575
+ return true;
1572
1576
  } catch (error) {
1573
1577
  config.logger?.warn?.("[stitchkit] watch subscriber threw", {
1574
1578
  key: watchKeyString(key),
1575
1579
  error: error instanceof Error ? error.message : String(error)
1576
1580
  });
1581
+ return false;
1577
1582
  }
1578
1583
  }
1579
1584
  function announceState(source, state) {
@@ -1582,13 +1587,85 @@ function createWatchHub(config) {
1582
1587
  tell(source.key, () => subscriber.state(state));
1583
1588
  }
1584
1589
  }
1585
- function publish(source, value) {
1590
+ function remember(source, revision, value, fingerprint) {
1591
+ if (deltaMemoryBytes === 0)
1592
+ return;
1593
+ const bytes = (JSON.stringify(value) ?? "null").length;
1594
+ if (bytes > deltaMemoryBytes)
1595
+ return;
1596
+ source.history.set(revision, { value, fingerprint, bytes });
1597
+ source.historyBytes += bytes;
1598
+ for (const [oldest, entry] of source.history) {
1599
+ if (source.historyBytes <= deltaMemoryBytes)
1600
+ break;
1601
+ source.history.delete(oldest);
1602
+ source.historyBytes -= entry.bytes;
1603
+ }
1604
+ }
1605
+ function frameFor(source, subscriber) {
1606
+ const value = source.value;
1607
+ const fingerprint = source.fingerprint ?? valueFingerprint(value);
1608
+ const full = {
1609
+ kind: "full",
1610
+ key: source.key,
1611
+ revision: source.revision,
1612
+ fingerprint,
1613
+ value
1614
+ };
1615
+ const base = source.baselines.get(subscriber);
1616
+ if (base === undefined)
1617
+ return full;
1618
+ if (base === source.revision) {
1619
+ return { kind: "unchanged", key: source.key, revision: source.revision, fingerprint };
1620
+ }
1621
+ const held2 = source.history.get(base);
1622
+ if (!held2)
1623
+ return full;
1624
+ const delta = diff(held2.value, value);
1625
+ if (delta === undefined || !deltaWins(delta, value))
1626
+ return full;
1627
+ return {
1628
+ kind: "delta",
1629
+ key: source.key,
1630
+ revision: source.revision,
1631
+ fingerprint,
1632
+ base,
1633
+ delta
1634
+ };
1635
+ }
1636
+ function adoptBaseline(source, subscriber, have) {
1637
+ if (source.fingerprint === have.fingerprint) {
1638
+ source.baselines.set(subscriber, source.revision);
1639
+ return;
1640
+ }
1641
+ const named = source.history.get(have.revision);
1642
+ if (named?.fingerprint === have.fingerprint) {
1643
+ source.baselines.set(subscriber, have.revision);
1644
+ return;
1645
+ }
1646
+ for (const [revision, entry] of source.history) {
1647
+ if (entry.fingerprint !== have.fingerprint)
1648
+ continue;
1649
+ source.baselines.set(subscriber, revision);
1650
+ return;
1651
+ }
1652
+ }
1653
+ function deliver(source, subscriber) {
1654
+ const frame = frameFor(source, subscriber);
1655
+ const delivered = tell(source.key, () => subscriber.value(frame));
1656
+ if (delivered)
1657
+ source.baselines.set(subscriber, source.revision);
1658
+ return delivered;
1659
+ }
1660
+ function publish(source, value, fingerprint) {
1661
+ if (source.fingerprint !== undefined) {
1662
+ remember(source, source.revision, source.value, source.fingerprint);
1663
+ }
1586
1664
  source.revision += 1;
1587
1665
  source.value = value;
1588
- const frame = { key: source.key, revision: source.revision, value };
1589
- for (const subscriber of source.subscribers) {
1590
- tell(source.key, () => subscriber.value(frame));
1591
- }
1666
+ source.fingerprint = fingerprint;
1667
+ for (const subscriber of source.subscribers)
1668
+ deliver(source, subscriber);
1592
1669
  }
1593
1670
  async function pump(source) {
1594
1671
  if (source.reading || closed)
@@ -1605,7 +1682,7 @@ function createWatchHub(config) {
1605
1682
  const unchanged = source.signature !== undefined && same(source.value, value);
1606
1683
  source.signature = signature;
1607
1684
  if (!unchanged)
1608
- publish(source, value);
1685
+ publish(source, value, valueFingerprint(value));
1609
1686
  if (source.state.phase !== "live") {
1610
1687
  announceState(source, { key: source.key, phase: "live" });
1611
1688
  }
@@ -1655,6 +1732,9 @@ function createWatchHub(config) {
1655
1732
  subscribers: new Set,
1656
1733
  unsubscribes: [],
1657
1734
  revision: 0,
1735
+ history: new Map,
1736
+ historyBytes: 0,
1737
+ baselines: new Map,
1658
1738
  reading: false,
1659
1739
  dirty: true,
1660
1740
  state: { key, phase: "opening" },
@@ -1696,7 +1776,7 @@ function createWatchHub(config) {
1696
1776
  const keys = new Set;
1697
1777
  held.set(subscriber, keys);
1698
1778
  return {
1699
- open(key, args) {
1779
+ open(key, args, have) {
1700
1780
  if (closed)
1701
1781
  return { accepted: false, reason: "the watch hub is closed" };
1702
1782
  const operation = { service: key.service, action: key.action };
@@ -1718,13 +1798,18 @@ function createWatchHub(config) {
1718
1798
  const source = acquire(operation, key, args);
1719
1799
  source.subscribers.add(subscriber);
1720
1800
  keys.add(id);
1801
+ if (have)
1802
+ adoptBaseline(source, subscriber, have);
1721
1803
  try {
1722
1804
  if (source.signature !== undefined) {
1723
- subscriber.value({ key, revision: source.revision, value: source.value });
1805
+ const frame = frameFor(source, subscriber);
1806
+ subscriber.value(frame);
1807
+ source.baselines.set(subscriber, source.revision);
1724
1808
  }
1725
1809
  subscriber.state(source.state);
1726
1810
  } catch (error) {
1727
1811
  source.subscribers.delete(subscriber);
1812
+ source.baselines.delete(subscriber);
1728
1813
  keys.delete(id);
1729
1814
  release(source);
1730
1815
  return {
@@ -1743,6 +1828,7 @@ function createWatchHub(config) {
1743
1828
  if (!source)
1744
1829
  return;
1745
1830
  source.subscribers.delete(subscriber);
1831
+ source.baselines.delete(subscriber);
1746
1832
  release(source);
1747
1833
  },
1748
1834
  detach() {
@@ -1751,6 +1837,7 @@ function createWatchHub(config) {
1751
1837
  if (!source)
1752
1838
  continue;
1753
1839
  source.subscribers.delete(subscriber);
1840
+ source.baselines.delete(subscriber);
1754
1841
  release(source);
1755
1842
  }
1756
1843
  keys.clear();
@@ -1779,6 +1866,9 @@ function createWatchHub(config) {
1779
1866
  }
1780
1867
  };
1781
1868
  }
1869
+ function valueFingerprint(value) {
1870
+ return argumentsDigest({ value });
1871
+ }
1782
1872
  function readErrorCode(error) {
1783
1873
  if (typeof error !== "object" || error === null)
1784
1874
  return;
@@ -0,0 +1,214 @@
1
+ import {
2
+ LiveStatePhaseSchema,
3
+ LiveStateStopReasonSchema
4
+ } from "./index-ywd0y6m1.js";
5
+ import {
6
+ stableValue
7
+ } from "./index-a916km3r.js";
8
+
9
+ // src/live/watch-delta.ts
10
+ import { z } from "zod";
11
+ var WatchDeltaSchema = z.lazy(() => z.union([
12
+ z.object({ t: z.literal("set"), v: z.unknown() }),
13
+ z.object({
14
+ t: z.literal("obj"),
15
+ set: z.record(z.string(), WatchDeltaSchema).optional(),
16
+ drop: z.array(z.string()).optional()
17
+ }),
18
+ z.object({ t: z.literal("arr"), ops: z.array(WatchArrayOpSchema) })
19
+ ]));
20
+ var WatchArrayOpSchema = z.lazy(() => z.union([
21
+ z.object({
22
+ o: z.literal("copy"),
23
+ from: z.number().int().nonnegative(),
24
+ count: z.number().int().positive()
25
+ }),
26
+ z.object({
27
+ o: z.literal("patch"),
28
+ from: z.number().int().nonnegative(),
29
+ d: WatchDeltaSchema
30
+ }),
31
+ z.object({ o: z.literal("put"), v: z.unknown() })
32
+ ]));
33
+ function isRecord(value) {
34
+ return typeof value === "object" && value !== null && !Array.isArray(value);
35
+ }
36
+ function signature(value) {
37
+ return JSON.stringify(stableValue(value)) ?? "undefined";
38
+ }
39
+ function diff(previous, next) {
40
+ if (signature(previous) === signature(next))
41
+ return;
42
+ if (isRecord(previous) && isRecord(next))
43
+ return objectDiff(previous, next);
44
+ if (Array.isArray(previous) && Array.isArray(next))
45
+ return arrayDiff(previous, next);
46
+ return { t: "set", v: next };
47
+ }
48
+ function objectDiff(previous, next) {
49
+ const set = {};
50
+ const drop = [];
51
+ for (const key of Object.keys(next)) {
52
+ if (!Object.hasOwn(previous, key)) {
53
+ set[key] = { t: "set", v: next[key] };
54
+ continue;
55
+ }
56
+ const nested = diff(previous[key], next[key]);
57
+ if (nested !== undefined)
58
+ set[key] = nested;
59
+ }
60
+ for (const key of Object.keys(previous)) {
61
+ if (!Object.hasOwn(next, key))
62
+ drop.push(key);
63
+ }
64
+ return {
65
+ t: "obj",
66
+ ...Object.keys(set).length > 0 && { set },
67
+ ...drop.length > 0 && { drop }
68
+ };
69
+ }
70
+ function arrayDiff(previous, next) {
71
+ const previousSignatures = previous.map(signature);
72
+ const positions = new Map;
73
+ previousSignatures.forEach((value, index) => {
74
+ const bucket = positions.get(value);
75
+ if (bucket)
76
+ bucket.push(index);
77
+ else
78
+ positions.set(value, [index]);
79
+ });
80
+ const taken = new Set;
81
+ const ops = [];
82
+ let cursor = 0;
83
+ const extend = (from) => {
84
+ const last = ops.at(-1);
85
+ if (last?.o !== "copy" || last.from + last.count !== from)
86
+ return false;
87
+ ops[ops.length - 1] = { o: "copy", from: last.from, count: last.count + 1 };
88
+ return true;
89
+ };
90
+ for (const element of next) {
91
+ const wanted = signature(element);
92
+ const last = ops.at(-1);
93
+ const continuation = last?.o === "copy" ? last.from + last.count : undefined;
94
+ if (continuation !== undefined && continuation < previous.length && previousSignatures[continuation] === wanted && !taken.has(continuation)) {
95
+ taken.add(continuation);
96
+ extend(continuation);
97
+ cursor = continuation + 1;
98
+ continue;
99
+ }
100
+ const bucket = positions.get(wanted);
101
+ const found = bucket?.find((index) => !taken.has(index));
102
+ if (found !== undefined) {
103
+ taken.add(found);
104
+ if (!extend(found))
105
+ ops.push({ o: "copy", from: found, count: 1 });
106
+ cursor = found + 1;
107
+ continue;
108
+ }
109
+ const counterpart = cursor < previous.length && !taken.has(cursor) ? cursor : undefined;
110
+ const nested = counterpart === undefined ? undefined : diff(previous[counterpart], element);
111
+ if (counterpart !== undefined && nested !== undefined) {
112
+ taken.add(counterpart);
113
+ ops.push({ o: "patch", from: counterpart, d: nested });
114
+ cursor = counterpart + 1;
115
+ continue;
116
+ }
117
+ ops.push({ o: "put", v: element });
118
+ }
119
+ return { t: "arr", ops };
120
+ }
121
+ function apply(previous, delta) {
122
+ if (delta.t === "set")
123
+ return delta.v;
124
+ if (delta.t === "obj") {
125
+ if (!isRecord(previous))
126
+ throw new Error("an object difference needs an object to apply to");
127
+ const result2 = { ...previous };
128
+ for (const key of delta.drop ?? [])
129
+ delete result2[key];
130
+ for (const [key, nested] of Object.entries(delta.set ?? {})) {
131
+ result2[key] = apply(Object.hasOwn(result2, key) ? result2[key] : undefined, nested);
132
+ }
133
+ return result2;
134
+ }
135
+ if (!Array.isArray(previous))
136
+ throw new Error("an array difference needs an array to apply to");
137
+ const result = [];
138
+ for (const op of delta.ops) {
139
+ if (op.o === "put") {
140
+ result.push(op.v);
141
+ continue;
142
+ }
143
+ if (op.from >= previous.length) {
144
+ throw new Error(`the difference reads element ${op.from} of an array of ${previous.length}`);
145
+ }
146
+ if (op.o === "patch") {
147
+ result.push(apply(previous[op.from], op.d));
148
+ continue;
149
+ }
150
+ if (op.from + op.count > previous.length) {
151
+ throw new Error(`the difference copies ${op.count} from ${op.from} of an array of ${previous.length}`);
152
+ }
153
+ result.push(...previous.slice(op.from, op.from + op.count));
154
+ }
155
+ return result;
156
+ }
157
+ function deltaWins(delta, value) {
158
+ return JSON.stringify(delta).length < (JSON.stringify(value) ?? "null").length;
159
+ }
160
+
161
+ // src/live/watch-contract.ts
162
+ import { z as z2 } from "zod";
163
+ var WatchKeySchema = z2.object({
164
+ service: z2.string().min(1),
165
+ action: z2.string().min(1),
166
+ digest: z2.string().min(1)
167
+ }).strict().readonly();
168
+ var WatchHaveSchema = z2.object({ revision: z2.number().int().nonnegative(), fingerprint: z2.string().min(1) }).strict().readonly();
169
+ var WatchOpenSchema = z2.object({ key: WatchKeySchema, args: z2.unknown(), have: WatchHaveSchema.optional() }).readonly();
170
+ var WatchAcceptedSchema = z2.object({
171
+ accepted: z2.boolean(),
172
+ reason: z2.string().optional()
173
+ }).strict().readonly();
174
+ var watchValueParts = {
175
+ key: WatchKeySchema,
176
+ revision: z2.number().int().nonnegative(),
177
+ fingerprint: z2.string().min(1)
178
+ };
179
+ var WatchValueSchema = z2.discriminatedUnion("kind", [
180
+ z2.object({ kind: z2.literal("full"), ...watchValueParts, value: z2.unknown() }).readonly(),
181
+ z2.object({
182
+ kind: z2.literal("delta"),
183
+ ...watchValueParts,
184
+ base: z2.number().int().nonnegative(),
185
+ delta: WatchDeltaSchema
186
+ }).readonly(),
187
+ z2.object({ kind: z2.literal("unchanged"), ...watchValueParts }).readonly()
188
+ ]);
189
+ var WatchStateSchema = z2.object({
190
+ key: WatchKeySchema,
191
+ phase: LiveStatePhaseSchema,
192
+ reason: LiveStateStopReasonSchema.optional(),
193
+ code: z2.string().optional(),
194
+ message: z2.string().optional()
195
+ }).readonly();
196
+ var WATCH_OPEN = "stitchkit.watch.open";
197
+ var WATCH_CLOSE = "stitchkit.watch.close";
198
+ var WATCH_VALUE = "stitchkit.watch.value";
199
+ var WATCH_STATE = "stitchkit.watch.state";
200
+ var watchContract = {
201
+ serverToClient: {
202
+ [WATCH_VALUE]: { args: z2.tuple([WatchValueSchema]) },
203
+ [WATCH_STATE]: { args: z2.tuple([WatchStateSchema]) }
204
+ },
205
+ clientToServer: {
206
+ [WATCH_OPEN]: { args: z2.tuple([WatchOpenSchema]), ack: WatchAcceptedSchema },
207
+ [WATCH_CLOSE]: { args: z2.tuple([z2.object({ key: WatchKeySchema }).readonly()]) }
208
+ }
209
+ };
210
+ function watchKeyString(key) {
211
+ return `${key.service}/${key.action}/${key.digest}`;
212
+ }
213
+
214
+ export { WatchDeltaSchema, WatchArrayOpSchema, diff, apply, deltaWins, WatchKeySchema, WatchHaveSchema, WatchValueSchema, WatchStateSchema, WATCH_OPEN, WATCH_CLOSE, WATCH_VALUE, WATCH_STATE, watchContract, watchKeyString };
@@ -44,7 +44,7 @@
44
44
  * sense a rendering component can use.
45
45
  */
46
46
  import type { ContractDef, EndpointDef } from '../contract/define.js';
47
- import { WATCH_CLOSE, WATCH_OPEN, WATCH_STATE, WATCH_VALUE, type WatchKey, type WatchStateFrame, type WatchValueFrame } from './watch-contract.js';
47
+ import { WATCH_CLOSE, WATCH_OPEN, WATCH_STATE, WATCH_VALUE, type WatchHave, type WatchKey, type WatchStateFrame, type WatchValueFrame } from './watch-contract.js';
48
48
  export interface WatchListeners<TValue> {
49
49
  value(value: TValue): void;
50
50
  /** Phase and, when unhealthy, the read's own code and message. */
@@ -107,6 +107,7 @@ export interface WatchTransport {
107
107
  request(event: typeof WATCH_OPEN, payload: {
108
108
  key: WatchKey;
109
109
  args: unknown;
110
+ have?: WatchHave;
110
111
  }, options: {
111
112
  timeoutMs: number;
112
113
  }): Promise<{
@@ -1 +1 @@
1
- {"version":3,"file":"watch-client.d.ts","sourceRoot":"","sources":["../../src/live/watch-client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"watch-client.d.ts","sourceRoot":"","sources":["../../src/live/watch-client.ts"],"names":[],"mappings":"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"}
@@ -26,6 +26,20 @@ export declare const WatchKeySchema: z.ZodReadonly<z.ZodObject<{
26
26
  digest: z.ZodString;
27
27
  }, z.core.$strict>>;
28
28
  export type WatchKey = z.infer<typeof WatchKeySchema>;
29
+ /**
30
+ * What a subscriber already holds, offered when it opens a key.
31
+ *
32
+ * Both halves are needed and neither is redundant. The revision says *which*
33
+ * answer it has; the fingerprint proves it is that answer and not a different
34
+ * one wearing the same number — revisions restart at zero when a hub restarts or
35
+ * a key is released and re-acquired, so a bare revision would let a client keep
36
+ * a value from a previous life of the source and be told it was current.
37
+ */
38
+ export declare const WatchHaveSchema: z.ZodReadonly<z.ZodObject<{
39
+ revision: z.ZodNumber;
40
+ fingerprint: z.ZodString;
41
+ }, z.core.$strict>>;
42
+ export type WatchHave = z.infer<typeof WatchHaveSchema>;
29
43
  export declare const WatchOpenSchema: z.ZodReadonly<z.ZodObject<{
30
44
  key: z.ZodReadonly<z.ZodObject<{
31
45
  service: z.ZodString;
@@ -33,20 +47,46 @@ export declare const WatchOpenSchema: z.ZodReadonly<z.ZodObject<{
33
47
  digest: z.ZodString;
34
48
  }, z.core.$strict>>;
35
49
  args: z.ZodUnknown;
50
+ have: z.ZodOptional<z.ZodReadonly<z.ZodObject<{
51
+ revision: z.ZodNumber;
52
+ fingerprint: z.ZodString;
53
+ }, z.core.$strict>>>;
36
54
  }, z.core.$strip>>;
37
55
  export declare const WatchAcceptedSchema: z.ZodReadonly<z.ZodObject<{
38
56
  accepted: z.ZodBoolean;
39
57
  reason: z.ZodOptional<z.ZodString>;
40
58
  }, z.core.$strict>>;
41
- export declare const WatchValueSchema: z.ZodReadonly<z.ZodObject<{
59
+ export declare const WatchValueSchema: z.ZodDiscriminatedUnion<[z.ZodReadonly<z.ZodObject<{
42
60
  key: z.ZodReadonly<z.ZodObject<{
43
61
  service: z.ZodString;
44
62
  action: z.ZodString;
45
63
  digest: z.ZodString;
46
64
  }, z.core.$strict>>;
47
65
  revision: z.ZodNumber;
66
+ fingerprint: z.ZodString;
67
+ kind: z.ZodLiteral<"full">;
48
68
  value: z.ZodUnknown;
49
- }, z.core.$strip>>;
69
+ }, z.core.$strip>>, z.ZodReadonly<z.ZodObject<{
70
+ key: z.ZodReadonly<z.ZodObject<{
71
+ service: z.ZodString;
72
+ action: z.ZodString;
73
+ digest: z.ZodString;
74
+ }, z.core.$strict>>;
75
+ revision: z.ZodNumber;
76
+ fingerprint: z.ZodString;
77
+ kind: z.ZodLiteral<"delta">;
78
+ base: z.ZodNumber;
79
+ delta: z.ZodType<import("./watch-delta.js").WatchDelta, unknown, z.core.$ZodTypeInternals<import("./watch-delta.js").WatchDelta, unknown>>;
80
+ }, z.core.$strip>>, z.ZodReadonly<z.ZodObject<{
81
+ key: z.ZodReadonly<z.ZodObject<{
82
+ service: z.ZodString;
83
+ action: z.ZodString;
84
+ digest: z.ZodString;
85
+ }, z.core.$strict>>;
86
+ revision: z.ZodNumber;
87
+ fingerprint: z.ZodString;
88
+ kind: z.ZodLiteral<"unchanged">;
89
+ }, z.core.$strip>>], "kind">;
50
90
  export declare const WatchStateSchema: z.ZodReadonly<z.ZodObject<{
51
91
  key: z.ZodReadonly<z.ZodObject<{
52
92
  service: z.ZodString;
@@ -88,15 +128,37 @@ export declare const WATCH_STATE = "stitchkit.watch.state";
88
128
  export declare const watchContract: {
89
129
  readonly serverToClient: {
90
130
  readonly "stitchkit.watch.value": {
91
- readonly args: z.ZodTuple<[z.ZodReadonly<z.ZodObject<{
131
+ readonly args: z.ZodTuple<[z.ZodDiscriminatedUnion<[z.ZodReadonly<z.ZodObject<{
92
132
  key: z.ZodReadonly<z.ZodObject<{
93
133
  service: z.ZodString;
94
134
  action: z.ZodString;
95
135
  digest: z.ZodString;
96
136
  }, z.core.$strict>>;
97
137
  revision: z.ZodNumber;
138
+ fingerprint: z.ZodString;
139
+ kind: z.ZodLiteral<"full">;
98
140
  value: z.ZodUnknown;
99
- }, z.core.$strip>>], null>;
141
+ }, z.core.$strip>>, z.ZodReadonly<z.ZodObject<{
142
+ key: z.ZodReadonly<z.ZodObject<{
143
+ service: z.ZodString;
144
+ action: z.ZodString;
145
+ digest: z.ZodString;
146
+ }, z.core.$strict>>;
147
+ revision: z.ZodNumber;
148
+ fingerprint: z.ZodString;
149
+ kind: z.ZodLiteral<"delta">;
150
+ base: z.ZodNumber;
151
+ delta: z.ZodType<import("./watch-delta.js").WatchDelta, unknown, z.core.$ZodTypeInternals<import("./watch-delta.js").WatchDelta, unknown>>;
152
+ }, z.core.$strip>>, z.ZodReadonly<z.ZodObject<{
153
+ key: z.ZodReadonly<z.ZodObject<{
154
+ service: z.ZodString;
155
+ action: z.ZodString;
156
+ digest: z.ZodString;
157
+ }, z.core.$strict>>;
158
+ revision: z.ZodNumber;
159
+ fingerprint: z.ZodString;
160
+ kind: z.ZodLiteral<"unchanged">;
161
+ }, z.core.$strip>>], "kind">], null>;
100
162
  };
101
163
  readonly "stitchkit.watch.state": {
102
164
  readonly args: z.ZodTuple<[z.ZodReadonly<z.ZodObject<{
@@ -135,6 +197,10 @@ export declare const watchContract: {
135
197
  digest: z.ZodString;
136
198
  }, z.core.$strict>>;
137
199
  args: z.ZodUnknown;
200
+ have: z.ZodOptional<z.ZodReadonly<z.ZodObject<{
201
+ revision: z.ZodNumber;
202
+ fingerprint: z.ZodString;
203
+ }, z.core.$strict>>>;
138
204
  }, z.core.$strip>>], null>;
139
205
  readonly ack: z.ZodReadonly<z.ZodObject<{
140
206
  accepted: z.ZodBoolean;