@kyneta/changefeed 1.8.0 → 2.0.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/dist/index.d.ts CHANGED
@@ -72,31 +72,31 @@ interface BatchMetadata {
72
72
  readonly replay?: boolean;
73
73
  /**
74
74
  * Kyneta-internal structural directive — true iff the outermost
75
- * `change(doc, fn)` block threw and was wholly compensated via inverse
75
+ * `batch(doc, fn)` block threw and was wholly compensated via inverse
76
76
  * replay. The op list contains forward + inverse pairs that net to
77
- * identity at every path. Inner `change()`s that threw and were caught
78
- * by an outer `change()`'s try/catch produce a NON-aborted outermost
77
+ * identity at every path. Inner `batch()`s that threw and were caught
78
+ * by an outer `batch()`'s try/catch produce a NON-aborted outermost
79
79
  * Changeset; the absorbed forward + inverse pair sits in the op list
80
80
  * alongside surviving outer ops. Default `undefined` (== falsy) for
81
81
  * successful batches and replay batches.
82
82
  */
83
83
  readonly aborted?: boolean;
84
84
  /**
85
- * Identity-typed token supplied by the originating `change()` call.
85
+ * Identity-typed token supplied by the originating `batch()` call.
86
86
  * Compared with `===` only — never inspected or serialized.
87
87
  *
88
88
  * Subscribers that issued the change set this token on their own
89
- * `change()` call and compare against `cs.source` to suppress echoes
89
+ * `batch()` call and compare against `cs.source` to suppress echoes
90
90
  * (the changefeed will deliver the changeset back to the subscriber
91
91
  * that produced it; without the token, the subscriber cannot tell
92
92
  * its own writes from someone else's).
93
93
  *
94
94
  * Substrate replay paths explicitly drop `source` — any value reaching
95
- * a subscriber is therefore from a local `change()` on this peer.
95
+ * a subscriber is therefore from a local `batch()` on this peer.
96
96
  *
97
97
  * @example
98
98
  * const mySource = Symbol("my-binding")
99
- * change(ref, fn, { source: mySource })
99
+ * batch(ref, fn, { source: mySource })
100
100
  * cf.subscribe(cs => { if (cs.source === mySource) return; / apply / })
101
101
  */
102
102
  readonly source?: unknown;
@@ -106,7 +106,7 @@ interface BatchMetadata {
106
106
  * It wraps one or more changes with optional batch-level metadata.
107
107
  *
108
108
  * - Auto-commit produces a degenerate changeset of one change.
109
- * - `change(doc, fn)` and `applyChanges` produce multi-change batches.
109
+ * - `batch(doc, fn)` and `applyChanges` produce multi-change batches.
110
110
  * - The four metadata fields (`origin`, `replay`, `aborted`, `source`)
111
111
  * come from {@link BatchMetadata}; see that type for the orthogonal
112
112
  * two-axis classification.
@@ -316,5 +316,25 @@ interface ReactiveMapHandle<K, V, C extends ChangeBase> {
316
316
  */
317
317
  declare function createReactiveMap<K, V, C extends ChangeBase = ChangeBase>(): [ReactiveMap<K, V, C>, ReactiveMapHandle<K, V, C>];
318
318
  //#endregion
319
- export { type BatchMetadata, CHANGEFEED, type CallableChangefeed, type ChangeBase, type Changefeed, type ChangefeedProtocol, type Changeset, type HasChangefeed, type ReactiveMap, type ReactiveMapHandle, changefeed, createCallable, createChangefeed, createReactiveMap, hasChangefeed, staticChangefeed };
319
+ //#region src/watcher-table.d.ts
320
+ interface WatcherEntry<V> {
321
+ readonly value: V;
322
+ readonly unwatch: () => void;
323
+ }
324
+ interface WatcherTable<V> {
325
+ /** Re-adding an existing key tears down the prior watcher before installing. */
326
+ add(key: string, value: V): void;
327
+ /** Returns true iff a watcher was present (and was torn down). */
328
+ remove(key: string): boolean;
329
+ has(key: string): boolean;
330
+ get(key: string): V | undefined;
331
+ keys(): Iterable<string>;
332
+ values(): Iterable<V>;
333
+ entries(): Iterable<[string, V]>;
334
+ clear(): void;
335
+ readonly size: number;
336
+ }
337
+ declare function createWatcherTable<V>(install: (key: string, value: V) => () => void): WatcherTable<V>;
338
+ //#endregion
339
+ export { type BatchMetadata, CHANGEFEED, type CallableChangefeed, type ChangeBase, type Changefeed, type ChangefeedProtocol, type Changeset, type HasChangefeed, type ReactiveMap, type ReactiveMapHandle, type WatcherEntry, type WatcherTable, changefeed, createCallable, createChangefeed, createReactiveMap, createWatcherTable, hasChangefeed, staticChangefeed };
320
340
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../src/change.ts","../src/changefeed.ts","../src/callable.ts","../src/reactive-map.ts"],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/change.ts","../src/changefeed.ts","../src/callable.ts","../src/reactive-map.ts","../src/watcher-table.ts"],"mappings":";;AAyBA;;;;AACe;;;;UADE,UAAA;EAAA,SACN,IAAI;AAAA;;;AADf;;;;AACe;;;;AADf,cCMa,UAAA;;;;AAAkE;AAoC/E;;;;;;;;;AA+CiB;AAoBjB;;;;;;;;;;;;;;AAE8B;AA2B9B;UAhGiB,aAAA;EAgGkB;;;;;;;EAAA,SAxFxB,MAAA;EAwFyB;;;;;;;;EAAA,SA/EzB,MAAA;EAmFY;;;AAAgC;AAqBvD;;;;;;EArBuB,SAxEZ,OAAA;EA+Fc;;;;;;;;;;;;;;;;;;EAAA,SA5Ed,MAAA;AAAA;;;;AAgF4C;AAUvD;;;;;;;;;UAtEiB,SAAA,KAAc,UAAA,UAAoB,aAAA;EAuE7B;EAAA,SArEX,OAAA,WAAkB,CAAA;AAAA;;;;;;;;AAqEmB;AAWhD;;;;;;;;;;;;UArDiB,kBAAA,cAAgC,UAAA,GAAa,UAAA;EAqDI;EAAA,SAnDvD,OAAA,EAAS,CAAA;EAqDjB;EAnDD,SAAA,CAAU,QAAA,GAAW,SAAA,EAAW,SAAA,CAAU,CAAA;AAAA;;;AAmDd;AAkB9B;;;;;;;;;;;UAhDiB,UAAA,cAAwB,UAAA,GAAa,UAAA;EAgDW;EAAA,UA9CrD,UAAA,GAAa,kBAAA,CAAmB,CAAA,EAAG,CAAA;EA8CmB;EAAA,SA5CvD,OAAA,EAAS,CAAA;EAuEM;EArExB,SAAA,CAAU,QAAA,GAAW,SAAA,EAAW,SAAA,CAAU,CAAA;AAAA;;;;;;;;UAU3B,aAAA,wBAAqC,UAAA,GAAa,UAAA;EAAA,UACvD,UAAA,GAAa,kBAAA,CAAmB,CAAA,EAAG,CAAA;AAAA;;;;;iBAW/B,aAAA,wBAAqC,UAAA,GAAa,UAAA,CAAA,CAChE,KAAA,YACC,KAAA,IAAS,aAAA,CAAc,CAAA,EAAG,CAAA;;;;;AA+CX;iBA7BF,gBAAA,GAAA,CAAoB,IAAA,EAAM,CAAA,GAAI,kBAAA,CAAmB,CAAA;;;;;;;;;;;;;iBA2BjD,UAAA,cAAwB,UAAA,CAAA,CACtC,MAAA,EAAQ,aAAA,CAAc,CAAA,EAAG,CAAA,IACxB,UAAA,CAAW,CAAA,EAAG,CAAA;;;;;;;;;;;;;;;AAkCyC;iBAF1C,gBAAA,cAA8B,UAAA,GAAa,UAAA,CAAA,CACzD,UAAA,QAAkB,CAAA,IAChB,IAAA,EAAM,UAAA,CAAW,CAAA,EAAG,CAAA,GAAI,IAAA,GAAO,SAAA,EAAW,SAAA,CAAU,CAAA;;;;;;ADjRzC;;;KEHH,kBAAA,cAEA,UAAA,GAAa,UAAA,IACrB,UAAA,CAAW,CAAA,EAAG,CAAA,WAAY,CAAA;ADK9B;;;;AAA+E;AAoC/E;;;;;;;;;AA+CiB;AAoBjB;;;AAvGA,iBCmBgB,cAAA,cAA4B,UAAA,CAAA,CAC1C,IAAA,EAAM,UAAA,CAAW,CAAA,EAAG,CAAA,IACnB,kBAAA,CAAmB,CAAA,EAAG,CAAA;;;;;AF1BV;;;;ACKf;;;;AAA+E;AAoC/E;;;;;UE/BiB,WAAA,iBAA4B,UAAA,GAAa,UAAA,UAChD,kBAAA,CAAmB,WAAA,CAAY,CAAA,EAAG,CAAA,GAAI,CAAA;EF0DrC;EExDT,GAAA,CAAI,GAAA,EAAK,CAAA,GAAI,CAAA;EF2EE;EEzEf,GAAA,CAAI,GAAA,EAAK,CAAA;EF6FM;EE3Ff,IAAA,IAAQ,gBAAA,CAAiB,CAAA;EF2FD;EAAA,SEzFf,IAAA;EF2FkB;EAAA,CEzF1B,MAAA,CAAO,QAAP,KAAoB,gBAAA,EAAkB,CAAA,EAAG,CAAA;AAAA;;;;;;;;AFyFd;AA2B9B;;UEvGiB,iBAAA,iBAAkC,UAAA;EFuGF;EErG/C,GAAA,CAAI,GAAA,EAAK,CAAA,EAAG,KAAA,EAAO,CAAA;EFuGD;EErGlB,MAAA,CAAO,GAAA,EAAK,CAAA;EFuGoB;EErGhC,KAAA;EFqGyC;EEnGzC,IAAA,CAAK,SAAA,EAAW,SAAA,CAAU,CAAA;AAAA;;;;;;;;;;;AFmG2B;AAqBvD;;;;;;;;;iBE7FgB,iBAAA,iBAAkC,UAAA,GAAa,UAAA,CAAA,CAAA,IAC7D,WAAA,CAAY,CAAA,EAAG,CAAA,EAAG,CAAA,GAClB,iBAAA,CAAkB,CAAA,EAAG,CAAA,EAAG,CAAA;;;UCrFT,YAAA;EAAA,SACN,KAAA,EAAO,CAAC;EAAA,SACR,OAAA;AAAA;AAAA,UAGM,YAAA;EJSF;EIPb,GAAA,CAAI,GAAA,UAAa,KAAA,EAAO,CAAA;;EAExB,MAAA,CAAO,GAAA;EACP,GAAA,CAAI,GAAA;EACJ,GAAA,CAAI,GAAA,WAAc,CAAA;EAClB,IAAA,IAAQ,QAAA;EACR,MAAA,IAAU,QAAA,CAAS,CAAA;EACnB,OAAA,IAAW,QAAA,UAAkB,CAAA;EAC7B,KAAA;EAAA,SACS,IAAA;AAAA;AAAA,iBAGK,kBAAA,GAAA,CACd,OAAA,GAAU,GAAA,UAAa,KAAA,EAAO,CAAA,kBAC7B,YAAA,CAAa,CAAA"}
package/dist/index.js CHANGED
@@ -205,6 +205,51 @@ function createReactiveMap() {
205
205
  }];
206
206
  }
207
207
  //#endregion
208
- export { CHANGEFEED, changefeed, createCallable, createChangefeed, createReactiveMap, hasChangefeed, staticChangefeed };
208
+ //#region src/watcher-table.ts
209
+ function createWatcherTable(install) {
210
+ const table = /* @__PURE__ */ new Map();
211
+ return {
212
+ add(key, value) {
213
+ const prev = table.get(key);
214
+ if (prev) prev.unwatch();
215
+ const unwatch = install(key, value);
216
+ table.set(key, {
217
+ value,
218
+ unwatch
219
+ });
220
+ },
221
+ remove(key) {
222
+ const entry = table.get(key);
223
+ if (!entry) return false;
224
+ entry.unwatch();
225
+ table.delete(key);
226
+ return true;
227
+ },
228
+ has(key) {
229
+ return table.has(key);
230
+ },
231
+ get(key) {
232
+ return table.get(key)?.value;
233
+ },
234
+ keys() {
235
+ return table.keys();
236
+ },
237
+ *values() {
238
+ for (const entry of table.values()) yield entry.value;
239
+ },
240
+ *entries() {
241
+ for (const [k, entry] of table) yield [k, entry.value];
242
+ },
243
+ clear() {
244
+ for (const entry of table.values()) entry.unwatch();
245
+ table.clear();
246
+ },
247
+ get size() {
248
+ return table.size;
249
+ }
250
+ };
251
+ }
252
+ //#endregion
253
+ export { CHANGEFEED, changefeed, createCallable, createChangefeed, createReactiveMap, createWatcherTable, hasChangefeed, staticChangefeed };
209
254
 
210
255
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/changefeed.ts","../src/callable.ts","../src/reactive-map.ts"],"sourcesContent":["// Changefeed — the universal reactive contract.\n//\n// A changefeed is a reactive value with a current state and a stream\n// of future changes. You read `current` to see what's there now;\n// you subscribe to learn what changes next.\n//\n// The changefeed protocol is expressed through a single symbol: CHANGEFEED.\n//\n// Changes are delivered as `Changeset<C>` — a batch of one or more\n// changes with optional provenance metadata. Auto-commit wraps a\n// single change in a degenerate changeset of one; transactions and\n// `applyChanges` deliver multi-change batches. The subscriber API\n// is uniform regardless of batch size.\n//\n// This module is the canonical home of the reactive contract. It has\n// zero dependencies — no schema, no paths, no interpreters.\n\nimport type { ChangeBase } from \"./change.js\"\n\n// ---------------------------------------------------------------------------\n// Symbol\n// ---------------------------------------------------------------------------\n\n/**\n * The single symbol that marks a value as a changefeed. Accessing\n * `obj[CHANGEFEED]` yields a `ChangefeedProtocol<S, C>` — the current\n * value and a stream of future changes.\n *\n * Uses `Symbol.for` so that multiple copies of this module (e.g. in\n * different bundle chunks) share the same symbol identity.\n */\nexport const CHANGEFEED: unique symbol = Symbol.for(\"kyneta:changefeed\") as any\n\n// ---------------------------------------------------------------------------\n// BatchMetadata — the four orthogonal channels riding on every batch\n// ---------------------------------------------------------------------------\n\n/**\n * Batch-level metadata that rides on each Changeset (and its upstream\n * `BatchOptions` in `@kyneta/schema`). The four fields sit on a two-axis\n * classification:\n *\n * | provenance | outcome\n * -----------|------------------|----------\n * app-set | origin | (none)\n * subscr-set | source | (none)\n * kyneta-set | replay | aborted\n *\n * - **Provenance** answers \"where did this come from?\" — `origin` is the\n * app-level label, `source` is the originating caller's identity token,\n * `replay` is the kyneta-internal \"from elsewhere\" flag.\n * - **Outcome** answers \"what happened?\" — only kyneta sets it, and only\n * to signal the abort-compensation case.\n *\n * The fields are *orthogonal* — none subsumes another. (For example,\n * a tagged local write that throws produces `{ source: T, aborted: true }`\n * simultaneously.) Each field has its own consumer with its own typed\n * read; no string-convention discrimination is required.\n *\n * `Changeset<C>` extends this with `changes`. `BatchOptions` (in\n * `@kyneta/schema`) extends this with `compensating`, the only field\n * that lives upstream-only — substrate inverse-replay never reaches\n * subscribers, so it has no Changeset counterpart.\n *\n * Context: jj:qpultxsw (origin/replay), jj:ryquprut (aborted),\n * jj:wpvtoxmw (source).\n */\nexport interface BatchMetadata {\n /**\n * App-level provenance label. Subscribers MAY read this for routing\n * or display, but kyneta itself never branches on its value. For\n * kyneta-internal echo suppression use `source` instead.\n *\n * Example values: `\"sync\"`, `\"undo\"`, `\"migration\"`.\n */\n readonly origin?: string\n /**\n * Kyneta-internal structural directive — true iff this batch represents\n * state authored elsewhere (substrate event bridge, `merge` payload,\n * version travel). User-facing entry points (`change`, `applyChanges`)\n * never set this; only substrate event bridges and `merge` paths do.\n * Layered consumers (e.g. the exchange's auto-subscribe filter) read\n * this to discriminate \"echo from sync\" from \"local write.\"\n */\n readonly replay?: boolean\n /**\n * Kyneta-internal structural directive — true iff the outermost\n * `change(doc, fn)` block threw and was wholly compensated via inverse\n * replay. The op list contains forward + inverse pairs that net to\n * identity at every path. Inner `change()`s that threw and were caught\n * by an outer `change()`'s try/catch produce a NON-aborted outermost\n * Changeset; the absorbed forward + inverse pair sits in the op list\n * alongside surviving outer ops. Default `undefined` (== falsy) for\n * successful batches and replay batches.\n */\n readonly aborted?: boolean\n /**\n * Identity-typed token supplied by the originating `change()` call.\n * Compared with `===` only — never inspected or serialized.\n *\n * Subscribers that issued the change set this token on their own\n * `change()` call and compare against `cs.source` to suppress echoes\n * (the changefeed will deliver the changeset back to the subscriber\n * that produced it; without the token, the subscriber cannot tell\n * its own writes from someone else's).\n *\n * Substrate replay paths explicitly drop `source` — any value reaching\n * a subscriber is therefore from a local `change()` on this peer.\n *\n * @example\n * const mySource = Symbol(\"my-binding\")\n * change(ref, fn, { source: mySource })\n * cf.subscribe(cs => { if (cs.source === mySource) return; / apply / })\n */\n readonly source?: unknown\n}\n\n// ---------------------------------------------------------------------------\n// Changeset — the unit of batch delivery\n// ---------------------------------------------------------------------------\n\n/**\n * A changeset is the unit of delivery through the changefeed protocol.\n * It wraps one or more changes with optional batch-level metadata.\n *\n * - Auto-commit produces a degenerate changeset of one change.\n * - `change(doc, fn)` and `applyChanges` produce multi-change batches.\n * - The four metadata fields (`origin`, `replay`, `aborted`, `source`)\n * come from {@link BatchMetadata}; see that type for the orthogonal\n * two-axis classification.\n *\n * The subscriber API always receives a `Changeset`, making it uniform\n * regardless of how the changes were produced.\n */\nexport interface Changeset<C = ChangeBase> extends BatchMetadata {\n /** The individual changes in this batch. */\n readonly changes: readonly C[]\n}\n\n// ---------------------------------------------------------------------------\n// Core interfaces — protocol layer\n// ---------------------------------------------------------------------------\n\n/**\n * The protocol object that sits behind the `[CHANGEFEED]` symbol.\n *\n * A coalgebra: `current` gives the live state, `subscribe` gives the\n * stream of future changes. In automata-theory terms this is a Moore\n * machine with a push-based transition stream.\n *\n * Properties:\n * - `current` is a getter — always returns the live current value\n * - `subscribe` returns an unsubscribe function\n * - Subscribers receive a `Changeset<C>` — a batch of changes with\n * optional provenance. For auto-commit (single mutation), the\n * changeset contains exactly one change.\n * - Static (non-reactive) sources return a protocol whose tail never emits:\n * `{ current: value, subscribe: () => () => {} }`\n *\n * This is internal plumbing — developers interact with `Changefeed<S, C>`\n * (the developer-facing type that includes `[CHANGEFEED]`, `.current`,\n * and `.subscribe()` in one interface).\n */\nexport interface ChangefeedProtocol<S, C extends ChangeBase = ChangeBase> {\n /** The current value, always live (a getter). */\n readonly current: S\n /** Subscribe to future changes. Returns an unsubscribe function. */\n subscribe(callback: (changeset: Changeset<C>) => void): () => void\n}\n\n// ---------------------------------------------------------------------------\n// Core interfaces — developer-facing type\n// ---------------------------------------------------------------------------\n\n/**\n * The developer-facing changefeed type: a reactive value with direct\n * access to `.current`, `.subscribe()`, and the `[CHANGEFEED]` marker.\n *\n * Developers write `readonly peers: Changefeed<PeerMap, PeerChange>` —\n * no `Has` prefix, no separate protocol object, no triple declaration.\n *\n * A `Changefeed<S, C>` is the intersection of:\n * - The `[CHANGEFEED]` marker (for compiler detection and runtime protocol)\n * - Direct `.current` and `.subscribe()` access (for developer ergonomics)\n *\n * Use `changefeed(source)` to project any `HasChangefeed` into a\n * `Changefeed`, or `createChangefeed()` to build one from scratch.\n */\nexport interface Changefeed<S, C extends ChangeBase = ChangeBase> {\n /** The protocol object behind the symbol. */\n readonly [CHANGEFEED]: ChangefeedProtocol<S, C>\n /** The current value, always live (a getter). */\n readonly current: S\n /** Subscribe to future changes. Returns an unsubscribe function. */\n subscribe(callback: (changeset: Changeset<C>) => void): () => void\n}\n\n/**\n * An object that carries a changefeed protocol under the `[CHANGEFEED]`\n * symbol.\n *\n * Any ref, interpreted node, or enriched value can implement this\n * interface to participate in the reactive protocol.\n */\nexport interface HasChangefeed<S = unknown, A extends ChangeBase = ChangeBase> {\n readonly [CHANGEFEED]: ChangefeedProtocol<S, A>\n}\n\n// ---------------------------------------------------------------------------\n// Type guard\n// ---------------------------------------------------------------------------\n\n/**\n * Returns `true` if `value` has a `[CHANGEFEED]` property, i.e. it\n * implements the `HasChangefeed` interface.\n */\nexport function hasChangefeed<S = unknown, A extends ChangeBase = ChangeBase>(\n value: unknown,\n): value is HasChangefeed<S, A> {\n return (\n value !== null &&\n value !== undefined &&\n (typeof value === \"object\" || typeof value === \"function\") &&\n CHANGEFEED in (value as object)\n )\n}\n\n// ---------------------------------------------------------------------------\n// Static feed helper\n// ---------------------------------------------------------------------------\n\n/**\n * Creates a changefeed protocol that never emits changes — useful for\n * static/non-reactive data sources that still need to participate in\n * the changefeed protocol.\n */\nexport function staticChangefeed<S>(head: S): ChangefeedProtocol<S, never> {\n return {\n get current() {\n return head\n },\n subscribe() {\n return () => {}\n },\n }\n}\n\n// ---------------------------------------------------------------------------\n// Projector — lift HasChangefeed to Changefeed\n// ---------------------------------------------------------------------------\n\n/**\n * Project any object with `[CHANGEFEED]` into a developer-facing\n * `Changefeed<S, C>` — lifting the hidden protocol surface to direct\n * `.current` and `.subscribe()` accessibility.\n *\n * ```ts\n * const feed = changefeed(doc.title)\n * feed.current // live value\n * feed.subscribe(cb) // subscribe to changes\n * feed[CHANGEFEED] // the protocol object (same as doc.title[CHANGEFEED])\n * ```\n */\nexport function changefeed<S, C extends ChangeBase>(\n source: HasChangefeed<S, C>,\n): Changefeed<S, C> {\n const protocol = source[CHANGEFEED]\n return {\n [CHANGEFEED]: protocol,\n get current(): S {\n return protocol.current\n },\n subscribe(callback: (changeset: Changeset<C>) => void): () => void {\n return protocol.subscribe(callback)\n },\n }\n}\n\n// ---------------------------------------------------------------------------\n// Factory — create standalone Changefeed values\n// ---------------------------------------------------------------------------\n\n/**\n * Create a standalone `Changefeed<S, C>` with push semantics.\n *\n * Returns a tuple of the feed and an emit function. The feed's\n * `[CHANGEFEED]` returns the protocol view of itself. Manages its\n * own subscriber set internally.\n *\n * ```ts\n * const [feed, emit] = createChangefeed(() => count)\n * feed.current // read live value\n * feed.subscribe(cs => { ... }) // subscribe\n * hasChangefeed(feed) // true\n * emit({ changes: [{ type: \"replace\", value: 42 }] }) // push\n * ```\n */\nexport function createChangefeed<S, C extends ChangeBase = ChangeBase>(\n getCurrent: () => S,\n): [feed: Changefeed<S, C>, emit: (changeset: Changeset<C>) => void] {\n const subscribers = new Set<(changeset: Changeset<C>) => void>()\n\n const protocol: ChangefeedProtocol<S, C> = {\n get current(): S {\n return getCurrent()\n },\n subscribe(callback: (changeset: Changeset<C>) => void): () => void {\n subscribers.add(callback)\n return () => {\n subscribers.delete(callback)\n }\n },\n }\n\n const feed: Changefeed<S, C> = {\n [CHANGEFEED]: protocol,\n get current(): S {\n return getCurrent()\n },\n subscribe(callback: (changeset: Changeset<C>) => void): () => void {\n return protocol.subscribe(callback)\n },\n }\n\n const emit = (changeset: Changeset<C>): void => {\n for (const cb of subscribers) {\n cb(changeset)\n }\n }\n\n return [feed, emit]\n}\n","// callable — the createCallable combinator.\n//\n// Wraps a Changefeed<S, C> in a callable function-object so that\n// `feed()` returns `feed.current`. The callable preserves the full\n// changefeed contract: [CHANGEFEED], .current, .subscribe().\n//\n// This is the same function-object pattern used by LocalRef in\n// @kyneta/cast — a function with properties attached.\n\nimport type { ChangeBase } from \"./change.js\"\nimport type { Changefeed, ChangefeedProtocol, Changeset } from \"./changefeed.js\"\nimport { CHANGEFEED } from \"./changefeed.js\"\n\n// ---------------------------------------------------------------------------\n// Type\n// ---------------------------------------------------------------------------\n\n/**\n * A changefeed that is also callable — `feed()` returns `feed.current`.\n *\n * This is the intersection of `Changefeed<S, C>` and `() => S`.\n * The call signature provides ergonomic read access without `.current`.\n */\nexport type CallableChangefeed<\n S,\n C extends ChangeBase = ChangeBase,\n> = Changefeed<S, C> & (() => S)\n\n// ---------------------------------------------------------------------------\n// Factory\n// ---------------------------------------------------------------------------\n\n/**\n * Wrap a `Changefeed<S, C>` in a callable function-object.\n *\n * The returned object:\n * - `feed()` → `feed.current` (callable)\n * - `feed.current` → delegated getter\n * - `feed.subscribe(cb)` → delegated\n * - `feed[CHANGEFEED]` → delegated protocol\n * - `hasChangefeed(feed)` → `true`\n *\n * ```ts\n * const [source, emit] = createChangefeed(() => count)\n * const feed = createCallable(source)\n * feed() // read current value\n * feed.current // same as feed()\n * feed.subscribe(cb) // subscribe to changes\n * ```\n */\nexport function createCallable<S, C extends ChangeBase>(\n feed: Changefeed<S, C>,\n): CallableChangefeed<S, C> {\n const callable: any = () => feed.current\n\n // [CHANGEFEED] — non-enumerable getter delegating to source\n Object.defineProperty(callable, CHANGEFEED, {\n get(): ChangefeedProtocol<S, C> {\n return feed[CHANGEFEED]\n },\n enumerable: false,\n configurable: false,\n })\n\n // .current — getter delegating to source\n Object.defineProperty(callable, \"current\", {\n get(): S {\n return feed.current\n },\n enumerable: true,\n configurable: false,\n })\n\n // .subscribe — delegating to source\n callable.subscribe = (\n callback: (changeset: Changeset<C>) => void,\n ): (() => void) => {\n return feed.subscribe(callback)\n }\n\n return callable as CallableChangefeed<S, C>\n}\n","// reactive-map — a callable changefeed over a mutable Map.\n//\n// ReactiveMap<K, V, C> is a CallableChangefeed<ReadonlyMap<K, V>, C>\n// with lifted collection accessors (.get, .has, .keys, .size, iteration).\n// The handle provides raw map mutations (set, delete, clear) without\n// automatic emission — the consumer decides when and what to emit.\n//\n// This extracts the recurring pattern of \"callable changefeed over a\n// ReadonlyMap with convenience accessors\" (used by exchange.peers,\n// Catalog, and future reactive collections) into a single combinator.\n\nimport type { CallableChangefeed } from \"./callable.js\"\nimport type { ChangeBase } from \"./change.js\"\nimport type { ChangefeedProtocol, Changeset } from \"./changefeed.js\"\nimport { CHANGEFEED, createChangefeed } from \"./changefeed.js\"\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/**\n * A callable changefeed over a `ReadonlyMap<K, V>` with lifted\n * collection accessors.\n *\n * `reactiveMap()` returns a **snapshot** — a shallow copy of the\n * current `ReadonlyMap<K, V>`. Each call produces a new `Map` instance,\n * so external-store consumers (e.g. `useSyncExternalStore`) can detect\n * changes via reference identity.\n *\n * `.current` returns the **live** map — the same instance every time.\n * `.get()`, `.has()`, `.keys()`, `.size`, and `[Symbol.iterator]()`\n * delegate to the live internal map — no need to unwrap `.current` first.\n *\n * Extends `CallableChangefeed` — assignable anywhere a\n * `CallableChangefeed<ReadonlyMap<K, V>, C>` or `Changefeed` is expected.\n */\nexport interface ReactiveMap<K, V, C extends ChangeBase = ChangeBase>\n extends CallableChangefeed<ReadonlyMap<K, V>, C> {\n /** Get the value for a key, or `undefined` if absent. */\n get(key: K): V | undefined\n /** Whether the map contains a key. */\n has(key: K): boolean\n /** An iterator over all keys. */\n keys(): IterableIterator<K>\n /** The number of entries. */\n readonly size: number\n /** Iterate over `[key, value]` pairs. */\n [Symbol.iterator](): IterableIterator<[K, V]>\n}\n\n/**\n * The producer-side handle for a `ReactiveMap`.\n *\n * Provides raw map mutations (`set`, `delete`, `clear`) that modify\n * the internal map **without** emitting changes. Call `emit()` with\n * the appropriate changeset after mutations are complete.\n *\n * This separation lets the consumer batch mutations and emit a single\n * changeset — e.g. `clear()` → N × `set()` → one `emit()`.\n */\nexport interface ReactiveMapHandle<K, V, C extends ChangeBase> {\n /** Insert or overwrite an entry. Does NOT emit. */\n set(key: K, value: V): void\n /** Remove an entry. Returns `true` if the key was present. Does NOT emit. */\n delete(key: K): boolean\n /** Remove all entries. Does NOT emit. */\n clear(): void\n /** Push a changeset to all subscribers. */\n emit(changeset: Changeset<C>): void\n}\n\n// ---------------------------------------------------------------------------\n// Factory\n// ---------------------------------------------------------------------------\n\n/**\n * Create a `ReactiveMap<K, V, C>` and its producer-side handle.\n *\n * The reactive map owns its internal `Map<K, V>`. Consumers read via\n * the `ReactiveMap` surface (call signature, `.get()`, `.has()`, etc.).\n * Producers mutate via the `ReactiveMapHandle` (`set`, `delete`,\n * `clear`) and push notifications via `emit`.\n *\n * ```ts\n * const [peers, handle] = createReactiveMap<PeerId, PeerInfo, PeerChange>()\n *\n * handle.set(\"alice\", aliceInfo)\n * handle.emit({ changes: [{ type: \"peer-joined\", peer: aliceInfo }] })\n *\n * peers() // ReadonlyMap snapshot (shallow copy)\n * peers.current // live map (same instance always)\n * peers.get(\"alice\") // aliceInfo (reads live map)\n * peers.size // 1\n * ```\n */\nexport function createReactiveMap<K, V, C extends ChangeBase = ChangeBase>(): [\n ReactiveMap<K, V, C>,\n ReactiveMapHandle<K, V, C>,\n] {\n const map = new Map<K, V>()\n\n // Create the base changefeed + emit pair.\n // The thunk returns a shallow copy — each call produces a new Map.\n // This gives the callable snapshot semantics: external-store consumers\n // (e.g. useSyncExternalStore) detect identity changes when contents\n // change. The live map is still available via .current and the\n // lifted accessors.\n const [feed, emit] = createChangefeed<ReadonlyMap<K, V>, C>(\n () => new Map(map),\n )\n\n // Build the callable function-object.\n // We construct it manually (rather than using createCallable) so we\n // can attach the collection accessors in one pass.\n const callable: any = () => new Map(map) as ReadonlyMap<K, V>\n\n // ── Changefeed protocol ──\n\n Object.defineProperty(callable, CHANGEFEED, {\n get(): ChangefeedProtocol<ReadonlyMap<K, V>, C> {\n return feed[CHANGEFEED]\n },\n enumerable: false,\n configurable: false,\n })\n\n Object.defineProperty(callable, \"current\", {\n get(): ReadonlyMap<K, V> {\n return map\n },\n enumerable: true,\n configurable: false,\n })\n\n callable.subscribe = (\n callback: (changeset: Changeset<C>) => void,\n ): (() => void) => {\n return feed.subscribe(callback)\n }\n\n // ── Lifted collection accessors ──\n\n callable.get = (key: K): V | undefined => map.get(key)\n callable.has = (key: K): boolean => map.has(key)\n callable.keys = (): IterableIterator<K> => map.keys()\n\n Object.defineProperty(callable, \"size\", {\n get(): number {\n return map.size\n },\n enumerable: true,\n configurable: false,\n })\n\n callable[Symbol.iterator] = (): IterableIterator<[K, V]> =>\n map[Symbol.iterator]()\n\n // ── Handle (producer side) ──\n\n const handle: ReactiveMapHandle<K, V, C> = {\n set(key: K, value: V): void {\n map.set(key, value)\n },\n delete(key: K): boolean {\n return map.delete(key)\n },\n clear(): void {\n map.clear()\n },\n emit,\n }\n\n return [callable as ReactiveMap<K, V, C>, handle]\n}\n"],"mappings":";;;;;;;;;AA+BA,MAAa,aAA4B,OAAO,IAAI,mBAAmB;;;;;AAyLvE,SAAgB,cACd,OAC8B;CAC9B,OACE,UAAU,QACV,UAAU,KAAA,MACT,OAAO,UAAU,YAAY,OAAO,UAAU,eAC/C,cAAe;AAEnB;;;;;;AAWA,SAAgB,iBAAoB,MAAuC;CACzE,OAAO;EACL,IAAI,UAAU;GACZ,OAAO;EACT;EACA,YAAY;GACV,aAAa,CAAC;EAChB;CACF;AACF;;;;;;;;;;;;;AAkBA,SAAgB,WACd,QACkB;CAClB,MAAM,WAAW,OAAO;CACxB,OAAO;GACJ,aAAa;EACd,IAAI,UAAa;GACf,OAAO,SAAS;EAClB;EACA,UAAU,UAAyD;GACjE,OAAO,SAAS,UAAU,QAAQ;EACpC;CACF;AACF;;;;;;;;;;;;;;;;AAqBA,SAAgB,iBACd,YACmE;CACnE,MAAM,8BAAc,IAAI,IAAuC;CAE/D,MAAM,WAAqC;EACzC,IAAI,UAAa;GACf,OAAO,WAAW;EACpB;EACA,UAAU,UAAyD;GACjE,YAAY,IAAI,QAAQ;GACxB,aAAa;IACX,YAAY,OAAO,QAAQ;GAC7B;EACF;CACF;CAEA,MAAM,OAAyB;GAC5B,aAAa;EACd,IAAI,UAAa;GACf,OAAO,WAAW;EACpB;EACA,UAAU,UAAyD;GACjE,OAAO,SAAS,UAAU,QAAQ;EACpC;CACF;CAEA,MAAM,QAAQ,cAAkC;EAC9C,KAAK,MAAM,MAAM,aACf,GAAG,SAAS;CAEhB;CAEA,OAAO,CAAC,MAAM,IAAI;AACpB;;;;;;;;;;;;;;;;;;;;;ACzRA,SAAgB,eACd,MAC0B;CAC1B,MAAM,iBAAsB,KAAK;CAGjC,OAAO,eAAe,UAAU,YAAY;EAC1C,MAAgC;GAC9B,OAAO,KAAK;EACd;EACA,YAAY;EACZ,cAAc;CAChB,CAAC;CAGD,OAAO,eAAe,UAAU,WAAW;EACzC,MAAS;GACP,OAAO,KAAK;EACd;EACA,YAAY;EACZ,cAAc;CAChB,CAAC;CAGD,SAAS,aACP,aACiB;EACjB,OAAO,KAAK,UAAU,QAAQ;CAChC;CAEA,OAAO;AACT;;;;;;;;;;;;;;;;;;;;;;;ACcA,SAAgB,oBAGd;CACA,MAAM,sBAAM,IAAI,IAAU;CAQ1B,MAAM,CAAC,MAAM,QAAQ,uBACb,IAAI,IAAI,GAAG,CACnB;CAKA,MAAM,iBAAsB,IAAI,IAAI,GAAG;CAIvC,OAAO,eAAe,UAAU,YAAY;EAC1C,MAAgD;GAC9C,OAAO,KAAK;EACd;EACA,YAAY;EACZ,cAAc;CAChB,CAAC;CAED,OAAO,eAAe,UAAU,WAAW;EACzC,MAAyB;GACvB,OAAO;EACT;EACA,YAAY;EACZ,cAAc;CAChB,CAAC;CAED,SAAS,aACP,aACiB;EACjB,OAAO,KAAK,UAAU,QAAQ;CAChC;CAIA,SAAS,OAAO,QAA0B,IAAI,IAAI,GAAG;CACrD,SAAS,OAAO,QAAoB,IAAI,IAAI,GAAG;CAC/C,SAAS,aAAkC,IAAI,KAAK;CAEpD,OAAO,eAAe,UAAU,QAAQ;EACtC,MAAc;GACZ,OAAO,IAAI;EACb;EACA,YAAY;EACZ,cAAc;CAChB,CAAC;CAED,SAAS,OAAO,kBACd,IAAI,OAAO,UAAU;CAiBvB,OAAO,CAAC,UAAkC;EAZxC,IAAI,KAAQ,OAAgB;GAC1B,IAAI,IAAI,KAAK,KAAK;EACpB;EACA,OAAO,KAAiB;GACtB,OAAO,IAAI,OAAO,GAAG;EACvB;EACA,QAAc;GACZ,IAAI,MAAM;EACZ;EACA;CAG6C,CAAC;AAClD"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/changefeed.ts","../src/callable.ts","../src/reactive-map.ts","../src/watcher-table.ts"],"sourcesContent":["// Changefeed — the universal reactive contract.\n//\n// A changefeed is a reactive value with a current state and a stream\n// of future changes. You read `current` to see what's there now;\n// you subscribe to learn what changes next.\n//\n// The changefeed protocol is expressed through a single symbol: CHANGEFEED.\n//\n// Changes are delivered as `Changeset<C>` — a batch of one or more\n// changes with optional provenance metadata. Auto-commit wraps a\n// single change in a degenerate changeset of one; transactions and\n// `applyChanges` deliver multi-change batches. The subscriber API\n// is uniform regardless of batch size.\n//\n// This module is the canonical home of the reactive contract. It has\n// zero dependencies — no schema, no paths, no interpreters.\n\nimport type { ChangeBase } from \"./change.js\"\n\n// ---------------------------------------------------------------------------\n// Symbol\n// ---------------------------------------------------------------------------\n\n/**\n * The single symbol that marks a value as a changefeed. Accessing\n * `obj[CHANGEFEED]` yields a `ChangefeedProtocol<S, C>` — the current\n * value and a stream of future changes.\n *\n * Uses `Symbol.for` so that multiple copies of this module (e.g. in\n * different bundle chunks) share the same symbol identity.\n */\nexport const CHANGEFEED: unique symbol = Symbol.for(\"kyneta:changefeed\") as any\n\n// ---------------------------------------------------------------------------\n// BatchMetadata — the four orthogonal channels riding on every batch\n// ---------------------------------------------------------------------------\n\n/**\n * Batch-level metadata that rides on each Changeset (and its upstream\n * `BatchOptions` in `@kyneta/schema`). The four fields sit on a two-axis\n * classification:\n *\n * | provenance | outcome\n * -----------|------------------|----------\n * app-set | origin | (none)\n * subscr-set | source | (none)\n * kyneta-set | replay | aborted\n *\n * - **Provenance** answers \"where did this come from?\" — `origin` is the\n * app-level label, `source` is the originating caller's identity token,\n * `replay` is the kyneta-internal \"from elsewhere\" flag.\n * - **Outcome** answers \"what happened?\" — only kyneta sets it, and only\n * to signal the abort-compensation case.\n *\n * The fields are *orthogonal* — none subsumes another. (For example,\n * a tagged local write that throws produces `{ source: T, aborted: true }`\n * simultaneously.) Each field has its own consumer with its own typed\n * read; no string-convention discrimination is required.\n *\n * `Changeset<C>` extends this with `changes`. `BatchOptions` (in\n * `@kyneta/schema`) extends this with `compensating`, the only field\n * that lives upstream-only — substrate inverse-replay never reaches\n * subscribers, so it has no Changeset counterpart.\n *\n * Context: jj:qpultxsw (origin/replay), jj:ryquprut (aborted),\n * jj:wpvtoxmw (source).\n */\nexport interface BatchMetadata {\n /**\n * App-level provenance label. Subscribers MAY read this for routing\n * or display, but kyneta itself never branches on its value. For\n * kyneta-internal echo suppression use `source` instead.\n *\n * Example values: `\"sync\"`, `\"undo\"`, `\"migration\"`.\n */\n readonly origin?: string\n /**\n * Kyneta-internal structural directive — true iff this batch represents\n * state authored elsewhere (substrate event bridge, `merge` payload,\n * version travel). User-facing entry points (`change`, `applyChanges`)\n * never set this; only substrate event bridges and `merge` paths do.\n * Layered consumers (e.g. the exchange's auto-subscribe filter) read\n * this to discriminate \"echo from sync\" from \"local write.\"\n */\n readonly replay?: boolean\n /**\n * Kyneta-internal structural directive — true iff the outermost\n * `batch(doc, fn)` block threw and was wholly compensated via inverse\n * replay. The op list contains forward + inverse pairs that net to\n * identity at every path. Inner `batch()`s that threw and were caught\n * by an outer `batch()`'s try/catch produce a NON-aborted outermost\n * Changeset; the absorbed forward + inverse pair sits in the op list\n * alongside surviving outer ops. Default `undefined` (== falsy) for\n * successful batches and replay batches.\n */\n readonly aborted?: boolean\n /**\n * Identity-typed token supplied by the originating `batch()` call.\n * Compared with `===` only — never inspected or serialized.\n *\n * Subscribers that issued the change set this token on their own\n * `batch()` call and compare against `cs.source` to suppress echoes\n * (the changefeed will deliver the changeset back to the subscriber\n * that produced it; without the token, the subscriber cannot tell\n * its own writes from someone else's).\n *\n * Substrate replay paths explicitly drop `source` — any value reaching\n * a subscriber is therefore from a local `batch()` on this peer.\n *\n * @example\n * const mySource = Symbol(\"my-binding\")\n * batch(ref, fn, { source: mySource })\n * cf.subscribe(cs => { if (cs.source === mySource) return; / apply / })\n */\n readonly source?: unknown\n}\n\n// ---------------------------------------------------------------------------\n// Changeset — the unit of batch delivery\n// ---------------------------------------------------------------------------\n\n/**\n * A changeset is the unit of delivery through the changefeed protocol.\n * It wraps one or more changes with optional batch-level metadata.\n *\n * - Auto-commit produces a degenerate changeset of one change.\n * - `batch(doc, fn)` and `applyChanges` produce multi-change batches.\n * - The four metadata fields (`origin`, `replay`, `aborted`, `source`)\n * come from {@link BatchMetadata}; see that type for the orthogonal\n * two-axis classification.\n *\n * The subscriber API always receives a `Changeset`, making it uniform\n * regardless of how the changes were produced.\n */\nexport interface Changeset<C = ChangeBase> extends BatchMetadata {\n /** The individual changes in this batch. */\n readonly changes: readonly C[]\n}\n\n// ---------------------------------------------------------------------------\n// Core interfaces — protocol layer\n// ---------------------------------------------------------------------------\n\n/**\n * The protocol object that sits behind the `[CHANGEFEED]` symbol.\n *\n * A coalgebra: `current` gives the live state, `subscribe` gives the\n * stream of future changes. In automata-theory terms this is a Moore\n * machine with a push-based transition stream.\n *\n * Properties:\n * - `current` is a getter — always returns the live current value\n * - `subscribe` returns an unsubscribe function\n * - Subscribers receive a `Changeset<C>` — a batch of changes with\n * optional provenance. For auto-commit (single mutation), the\n * changeset contains exactly one change.\n * - Static (non-reactive) sources return a protocol whose tail never emits:\n * `{ current: value, subscribe: () => () => {} }`\n *\n * This is internal plumbing — developers interact with `Changefeed<S, C>`\n * (the developer-facing type that includes `[CHANGEFEED]`, `.current`,\n * and `.subscribe()` in one interface).\n */\nexport interface ChangefeedProtocol<S, C extends ChangeBase = ChangeBase> {\n /** The current value, always live (a getter). */\n readonly current: S\n /** Subscribe to future changes. Returns an unsubscribe function. */\n subscribe(callback: (changeset: Changeset<C>) => void): () => void\n}\n\n// ---------------------------------------------------------------------------\n// Core interfaces — developer-facing type\n// ---------------------------------------------------------------------------\n\n/**\n * The developer-facing changefeed type: a reactive value with direct\n * access to `.current`, `.subscribe()`, and the `[CHANGEFEED]` marker.\n *\n * Developers write `readonly peers: Changefeed<PeerMap, PeerChange>` —\n * no `Has` prefix, no separate protocol object, no triple declaration.\n *\n * A `Changefeed<S, C>` is the intersection of:\n * - The `[CHANGEFEED]` marker (for compiler detection and runtime protocol)\n * - Direct `.current` and `.subscribe()` access (for developer ergonomics)\n *\n * Use `changefeed(source)` to project any `HasChangefeed` into a\n * `Changefeed`, or `createChangefeed()` to build one from scratch.\n */\nexport interface Changefeed<S, C extends ChangeBase = ChangeBase> {\n /** The protocol object behind the symbol. */\n readonly [CHANGEFEED]: ChangefeedProtocol<S, C>\n /** The current value, always live (a getter). */\n readonly current: S\n /** Subscribe to future changes. Returns an unsubscribe function. */\n subscribe(callback: (changeset: Changeset<C>) => void): () => void\n}\n\n/**\n * An object that carries a changefeed protocol under the `[CHANGEFEED]`\n * symbol.\n *\n * Any ref, interpreted node, or enriched value can implement this\n * interface to participate in the reactive protocol.\n */\nexport interface HasChangefeed<S = unknown, A extends ChangeBase = ChangeBase> {\n readonly [CHANGEFEED]: ChangefeedProtocol<S, A>\n}\n\n// ---------------------------------------------------------------------------\n// Type guard\n// ---------------------------------------------------------------------------\n\n/**\n * Returns `true` if `value` has a `[CHANGEFEED]` property, i.e. it\n * implements the `HasChangefeed` interface.\n */\nexport function hasChangefeed<S = unknown, A extends ChangeBase = ChangeBase>(\n value: unknown,\n): value is HasChangefeed<S, A> {\n return (\n value !== null &&\n value !== undefined &&\n (typeof value === \"object\" || typeof value === \"function\") &&\n CHANGEFEED in (value as object)\n )\n}\n\n// ---------------------------------------------------------------------------\n// Static feed helper\n// ---------------------------------------------------------------------------\n\n/**\n * Creates a changefeed protocol that never emits changes — useful for\n * static/non-reactive data sources that still need to participate in\n * the changefeed protocol.\n */\nexport function staticChangefeed<S>(head: S): ChangefeedProtocol<S, never> {\n return {\n get current() {\n return head\n },\n subscribe() {\n return () => {}\n },\n }\n}\n\n// ---------------------------------------------------------------------------\n// Projector — lift HasChangefeed to Changefeed\n// ---------------------------------------------------------------------------\n\n/**\n * Project any object with `[CHANGEFEED]` into a developer-facing\n * `Changefeed<S, C>` — lifting the hidden protocol surface to direct\n * `.current` and `.subscribe()` accessibility.\n *\n * ```ts\n * const feed = changefeed(doc.title)\n * feed.current // live value\n * feed.subscribe(cb) // subscribe to changes\n * feed[CHANGEFEED] // the protocol object (same as doc.title[CHANGEFEED])\n * ```\n */\nexport function changefeed<S, C extends ChangeBase>(\n source: HasChangefeed<S, C>,\n): Changefeed<S, C> {\n const protocol = source[CHANGEFEED]\n return {\n [CHANGEFEED]: protocol,\n get current(): S {\n return protocol.current\n },\n subscribe(callback: (changeset: Changeset<C>) => void): () => void {\n return protocol.subscribe(callback)\n },\n }\n}\n\n// ---------------------------------------------------------------------------\n// Factory — create standalone Changefeed values\n// ---------------------------------------------------------------------------\n\n/**\n * Create a standalone `Changefeed<S, C>` with push semantics.\n *\n * Returns a tuple of the feed and an emit function. The feed's\n * `[CHANGEFEED]` returns the protocol view of itself. Manages its\n * own subscriber set internally.\n *\n * ```ts\n * const [feed, emit] = createChangefeed(() => count)\n * feed.current // read live value\n * feed.subscribe(cs => { ... }) // subscribe\n * hasChangefeed(feed) // true\n * emit({ changes: [{ type: \"replace\", value: 42 }] }) // push\n * ```\n */\nexport function createChangefeed<S, C extends ChangeBase = ChangeBase>(\n getCurrent: () => S,\n): [feed: Changefeed<S, C>, emit: (changeset: Changeset<C>) => void] {\n const subscribers = new Set<(changeset: Changeset<C>) => void>()\n\n const protocol: ChangefeedProtocol<S, C> = {\n get current(): S {\n return getCurrent()\n },\n subscribe(callback: (changeset: Changeset<C>) => void): () => void {\n subscribers.add(callback)\n return () => {\n subscribers.delete(callback)\n }\n },\n }\n\n const feed: Changefeed<S, C> = {\n [CHANGEFEED]: protocol,\n get current(): S {\n return getCurrent()\n },\n subscribe(callback: (changeset: Changeset<C>) => void): () => void {\n return protocol.subscribe(callback)\n },\n }\n\n const emit = (changeset: Changeset<C>): void => {\n for (const cb of subscribers) {\n cb(changeset)\n }\n }\n\n return [feed, emit]\n}\n","// callable — the createCallable combinator.\n//\n// Wraps a Changefeed<S, C> in a callable function-object so that\n// `feed()` returns `feed.current`. The callable preserves the full\n// changefeed contract: [CHANGEFEED], .current, .subscribe().\n//\n// This is the same function-object pattern used by LocalRef in\n// @kyneta/cast — a function with properties attached.\n\nimport type { ChangeBase } from \"./change.js\"\nimport type { Changefeed, ChangefeedProtocol, Changeset } from \"./changefeed.js\"\nimport { CHANGEFEED } from \"./changefeed.js\"\n\n// ---------------------------------------------------------------------------\n// Type\n// ---------------------------------------------------------------------------\n\n/**\n * A changefeed that is also callable — `feed()` returns `feed.current`.\n *\n * This is the intersection of `Changefeed<S, C>` and `() => S`.\n * The call signature provides ergonomic read access without `.current`.\n */\nexport type CallableChangefeed<\n S,\n C extends ChangeBase = ChangeBase,\n> = Changefeed<S, C> & (() => S)\n\n// ---------------------------------------------------------------------------\n// Factory\n// ---------------------------------------------------------------------------\n\n/**\n * Wrap a `Changefeed<S, C>` in a callable function-object.\n *\n * The returned object:\n * - `feed()` → `feed.current` (callable)\n * - `feed.current` → delegated getter\n * - `feed.subscribe(cb)` → delegated\n * - `feed[CHANGEFEED]` → delegated protocol\n * - `hasChangefeed(feed)` → `true`\n *\n * ```ts\n * const [source, emit] = createChangefeed(() => count)\n * const feed = createCallable(source)\n * feed() // read current value\n * feed.current // same as feed()\n * feed.subscribe(cb) // subscribe to changes\n * ```\n */\nexport function createCallable<S, C extends ChangeBase>(\n feed: Changefeed<S, C>,\n): CallableChangefeed<S, C> {\n const callable: any = () => feed.current\n\n // [CHANGEFEED] — non-enumerable getter delegating to source\n Object.defineProperty(callable, CHANGEFEED, {\n get(): ChangefeedProtocol<S, C> {\n return feed[CHANGEFEED]\n },\n enumerable: false,\n configurable: false,\n })\n\n // .current — getter delegating to source\n Object.defineProperty(callable, \"current\", {\n get(): S {\n return feed.current\n },\n enumerable: true,\n configurable: false,\n })\n\n // .subscribe — delegating to source\n callable.subscribe = (\n callback: (changeset: Changeset<C>) => void,\n ): (() => void) => {\n return feed.subscribe(callback)\n }\n\n return callable as CallableChangefeed<S, C>\n}\n","// reactive-map — a callable changefeed over a mutable Map.\n//\n// ReactiveMap<K, V, C> is a CallableChangefeed<ReadonlyMap<K, V>, C>\n// with lifted collection accessors (.get, .has, .keys, .size, iteration).\n// The handle provides raw map mutations (set, delete, clear) without\n// automatic emission — the consumer decides when and what to emit.\n//\n// This extracts the recurring pattern of \"callable changefeed over a\n// ReadonlyMap with convenience accessors\" (used by exchange.peers,\n// Catalog, and future reactive collections) into a single combinator.\n\nimport type { CallableChangefeed } from \"./callable.js\"\nimport type { ChangeBase } from \"./change.js\"\nimport type { ChangefeedProtocol, Changeset } from \"./changefeed.js\"\nimport { CHANGEFEED, createChangefeed } from \"./changefeed.js\"\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/**\n * A callable changefeed over a `ReadonlyMap<K, V>` with lifted\n * collection accessors.\n *\n * `reactiveMap()` returns a **snapshot** — a shallow copy of the\n * current `ReadonlyMap<K, V>`. Each call produces a new `Map` instance,\n * so external-store consumers (e.g. `useSyncExternalStore`) can detect\n * changes via reference identity.\n *\n * `.current` returns the **live** map — the same instance every time.\n * `.get()`, `.has()`, `.keys()`, `.size`, and `[Symbol.iterator]()`\n * delegate to the live internal map — no need to unwrap `.current` first.\n *\n * Extends `CallableChangefeed` — assignable anywhere a\n * `CallableChangefeed<ReadonlyMap<K, V>, C>` or `Changefeed` is expected.\n */\nexport interface ReactiveMap<K, V, C extends ChangeBase = ChangeBase>\n extends CallableChangefeed<ReadonlyMap<K, V>, C> {\n /** Get the value for a key, or `undefined` if absent. */\n get(key: K): V | undefined\n /** Whether the map contains a key. */\n has(key: K): boolean\n /** An iterator over all keys. */\n keys(): IterableIterator<K>\n /** The number of entries. */\n readonly size: number\n /** Iterate over `[key, value]` pairs. */\n [Symbol.iterator](): IterableIterator<[K, V]>\n}\n\n/**\n * The producer-side handle for a `ReactiveMap`.\n *\n * Provides raw map mutations (`set`, `delete`, `clear`) that modify\n * the internal map **without** emitting changes. Call `emit()` with\n * the appropriate changeset after mutations are complete.\n *\n * This separation lets the consumer batch mutations and emit a single\n * changeset — e.g. `clear()` → N × `set()` → one `emit()`.\n */\nexport interface ReactiveMapHandle<K, V, C extends ChangeBase> {\n /** Insert or overwrite an entry. Does NOT emit. */\n set(key: K, value: V): void\n /** Remove an entry. Returns `true` if the key was present. Does NOT emit. */\n delete(key: K): boolean\n /** Remove all entries. Does NOT emit. */\n clear(): void\n /** Push a changeset to all subscribers. */\n emit(changeset: Changeset<C>): void\n}\n\n// ---------------------------------------------------------------------------\n// Factory\n// ---------------------------------------------------------------------------\n\n/**\n * Create a `ReactiveMap<K, V, C>` and its producer-side handle.\n *\n * The reactive map owns its internal `Map<K, V>`. Consumers read via\n * the `ReactiveMap` surface (call signature, `.get()`, `.has()`, etc.).\n * Producers mutate via the `ReactiveMapHandle` (`set`, `delete`,\n * `clear`) and push notifications via `emit`.\n *\n * ```ts\n * const [peers, handle] = createReactiveMap<PeerId, PeerInfo, PeerChange>()\n *\n * handle.set(\"alice\", aliceInfo)\n * handle.emit({ changes: [{ type: \"peer-joined\", peer: aliceInfo }] })\n *\n * peers() // ReadonlyMap snapshot (shallow copy)\n * peers.current // live map (same instance always)\n * peers.get(\"alice\") // aliceInfo (reads live map)\n * peers.size // 1\n * ```\n */\nexport function createReactiveMap<K, V, C extends ChangeBase = ChangeBase>(): [\n ReactiveMap<K, V, C>,\n ReactiveMapHandle<K, V, C>,\n] {\n const map = new Map<K, V>()\n\n // Create the base changefeed + emit pair.\n // The thunk returns a shallow copy — each call produces a new Map.\n // This gives the callable snapshot semantics: external-store consumers\n // (e.g. useSyncExternalStore) detect identity changes when contents\n // change. The live map is still available via .current and the\n // lifted accessors.\n const [feed, emit] = createChangefeed<ReadonlyMap<K, V>, C>(\n () => new Map(map),\n )\n\n // Build the callable function-object.\n // We construct it manually (rather than using createCallable) so we\n // can attach the collection accessors in one pass.\n const callable: any = () => new Map(map) as ReadonlyMap<K, V>\n\n // ── Changefeed protocol ──\n\n Object.defineProperty(callable, CHANGEFEED, {\n get(): ChangefeedProtocol<ReadonlyMap<K, V>, C> {\n return feed[CHANGEFEED]\n },\n enumerable: false,\n configurable: false,\n })\n\n Object.defineProperty(callable, \"current\", {\n get(): ReadonlyMap<K, V> {\n return map\n },\n enumerable: true,\n configurable: false,\n })\n\n callable.subscribe = (\n callback: (changeset: Changeset<C>) => void,\n ): (() => void) => {\n return feed.subscribe(callback)\n }\n\n // ── Lifted collection accessors ──\n\n callable.get = (key: K): V | undefined => map.get(key)\n callable.has = (key: K): boolean => map.has(key)\n callable.keys = (): IterableIterator<K> => map.keys()\n\n Object.defineProperty(callable, \"size\", {\n get(): number {\n return map.size\n },\n enumerable: true,\n configurable: false,\n })\n\n callable[Symbol.iterator] = (): IterableIterator<[K, V]> =>\n map[Symbol.iterator]()\n\n // ── Handle (producer side) ──\n\n const handle: ReactiveMapHandle<K, V, C> = {\n set(key: K, value: V): void {\n map.set(key, value)\n },\n delete(key: K): boolean {\n return map.delete(key)\n },\n clear(): void {\n map.clear()\n },\n emit,\n }\n\n return [callable as ReactiveMap<K, V, C>, handle]\n}\n","// watcher-table — generic per-key subscription-lifecycle helper.\n//\n// A small table that owns the teardown of one watcher per string key:\n// re-adding a key tears down the prior watcher before installing the new\n// one; `remove`/`clear` run teardowns. The `install` callback returns a\n// teardown for its OWN watcher only — secondary cleanup of caller-owned\n// resources is the caller's responsibility (running it inside the install\n// closure would fire on every re-add, wrong for owned-resource teardown).\n//\n// Tier-0, zero-dependency. Used by `@kyneta/index` (`fromList`, `flatMap`,\n// `filter`, `Index.by`) and `@kyneta/reactive` (per-dependency subscriptions).\n\nexport interface WatcherEntry<V> {\n readonly value: V\n readonly unwatch: () => void\n}\n\nexport interface WatcherTable<V> {\n /** Re-adding an existing key tears down the prior watcher before installing. */\n add(key: string, value: V): void\n /** Returns true iff a watcher was present (and was torn down). */\n remove(key: string): boolean\n has(key: string): boolean\n get(key: string): V | undefined\n keys(): Iterable<string>\n values(): Iterable<V>\n entries(): Iterable<[string, V]>\n clear(): void\n readonly size: number\n}\n\nexport function createWatcherTable<V>(\n install: (key: string, value: V) => () => void,\n): WatcherTable<V> {\n const table = new Map<string, WatcherEntry<V>>()\n\n return {\n add(key: string, value: V): void {\n const prev = table.get(key)\n if (prev) prev.unwatch()\n const unwatch = install(key, value)\n table.set(key, { value, unwatch })\n },\n remove(key: string): boolean {\n const entry = table.get(key)\n if (!entry) return false\n entry.unwatch()\n table.delete(key)\n return true\n },\n has(key: string): boolean {\n return table.has(key)\n },\n get(key: string): V | undefined {\n return table.get(key)?.value\n },\n keys(): Iterable<string> {\n return table.keys()\n },\n *values(): Iterable<V> {\n for (const entry of table.values()) yield entry.value\n },\n *entries(): Iterable<[string, V]> {\n for (const [k, entry] of table) yield [k, entry.value]\n },\n clear(): void {\n for (const entry of table.values()) entry.unwatch()\n table.clear()\n },\n get size(): number {\n return table.size\n },\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kyneta/changefeed",
3
- "version": "1.8.0",
3
+ "version": "2.0.0",
4
4
  "description": "Universal reactive contract — a Moore machine identified by [CHANGEFEED]",
5
5
  "author": "Duane Johnson",
6
6
  "license": "MIT",
package/src/changefeed.ts CHANGED
@@ -85,31 +85,31 @@ export interface BatchMetadata {
85
85
  readonly replay?: boolean
86
86
  /**
87
87
  * Kyneta-internal structural directive — true iff the outermost
88
- * `change(doc, fn)` block threw and was wholly compensated via inverse
88
+ * `batch(doc, fn)` block threw and was wholly compensated via inverse
89
89
  * replay. The op list contains forward + inverse pairs that net to
90
- * identity at every path. Inner `change()`s that threw and were caught
91
- * by an outer `change()`'s try/catch produce a NON-aborted outermost
90
+ * identity at every path. Inner `batch()`s that threw and were caught
91
+ * by an outer `batch()`'s try/catch produce a NON-aborted outermost
92
92
  * Changeset; the absorbed forward + inverse pair sits in the op list
93
93
  * alongside surviving outer ops. Default `undefined` (== falsy) for
94
94
  * successful batches and replay batches.
95
95
  */
96
96
  readonly aborted?: boolean
97
97
  /**
98
- * Identity-typed token supplied by the originating `change()` call.
98
+ * Identity-typed token supplied by the originating `batch()` call.
99
99
  * Compared with `===` only — never inspected or serialized.
100
100
  *
101
101
  * Subscribers that issued the change set this token on their own
102
- * `change()` call and compare against `cs.source` to suppress echoes
102
+ * `batch()` call and compare against `cs.source` to suppress echoes
103
103
  * (the changefeed will deliver the changeset back to the subscriber
104
104
  * that produced it; without the token, the subscriber cannot tell
105
105
  * its own writes from someone else's).
106
106
  *
107
107
  * Substrate replay paths explicitly drop `source` — any value reaching
108
- * a subscriber is therefore from a local `change()` on this peer.
108
+ * a subscriber is therefore from a local `batch()` on this peer.
109
109
  *
110
110
  * @example
111
111
  * const mySource = Symbol("my-binding")
112
- * change(ref, fn, { source: mySource })
112
+ * batch(ref, fn, { source: mySource })
113
113
  * cf.subscribe(cs => { if (cs.source === mySource) return; / apply / })
114
114
  */
115
115
  readonly source?: unknown
@@ -124,7 +124,7 @@ export interface BatchMetadata {
124
124
  * It wraps one or more changes with optional batch-level metadata.
125
125
  *
126
126
  * - Auto-commit produces a degenerate changeset of one change.
127
- * - `change(doc, fn)` and `applyChanges` produce multi-change batches.
127
+ * - `batch(doc, fn)` and `applyChanges` produce multi-change batches.
128
128
  * - The four metadata fields (`origin`, `replay`, `aborted`, `source`)
129
129
  * come from {@link BatchMetadata}; see that type for the orthogonal
130
130
  * two-axis classification.
package/src/index.ts CHANGED
@@ -26,3 +26,7 @@ export {
26
26
  // ReactiveMap — callable changefeed over a mutable Map
27
27
  export type { ReactiveMap, ReactiveMapHandle } from "./reactive-map.js"
28
28
  export { createReactiveMap } from "./reactive-map.js"
29
+ // WatcherTable — generic per-key subscription-lifecycle helper (tier-0).
30
+ // Used by @kyneta/index and @kyneta/reactive.
31
+ export type { WatcherEntry, WatcherTable } from "./watcher-table.js"
32
+ export { createWatcherTable } from "./watcher-table.js"
@@ -0,0 +1,74 @@
1
+ // watcher-table — generic per-key subscription-lifecycle helper.
2
+ //
3
+ // A small table that owns the teardown of one watcher per string key:
4
+ // re-adding a key tears down the prior watcher before installing the new
5
+ // one; `remove`/`clear` run teardowns. The `install` callback returns a
6
+ // teardown for its OWN watcher only — secondary cleanup of caller-owned
7
+ // resources is the caller's responsibility (running it inside the install
8
+ // closure would fire on every re-add, wrong for owned-resource teardown).
9
+ //
10
+ // Tier-0, zero-dependency. Used by `@kyneta/index` (`fromList`, `flatMap`,
11
+ // `filter`, `Index.by`) and `@kyneta/reactive` (per-dependency subscriptions).
12
+
13
+ export interface WatcherEntry<V> {
14
+ readonly value: V
15
+ readonly unwatch: () => void
16
+ }
17
+
18
+ export interface WatcherTable<V> {
19
+ /** Re-adding an existing key tears down the prior watcher before installing. */
20
+ add(key: string, value: V): void
21
+ /** Returns true iff a watcher was present (and was torn down). */
22
+ remove(key: string): boolean
23
+ has(key: string): boolean
24
+ get(key: string): V | undefined
25
+ keys(): Iterable<string>
26
+ values(): Iterable<V>
27
+ entries(): Iterable<[string, V]>
28
+ clear(): void
29
+ readonly size: number
30
+ }
31
+
32
+ export function createWatcherTable<V>(
33
+ install: (key: string, value: V) => () => void,
34
+ ): WatcherTable<V> {
35
+ const table = new Map<string, WatcherEntry<V>>()
36
+
37
+ return {
38
+ add(key: string, value: V): void {
39
+ const prev = table.get(key)
40
+ if (prev) prev.unwatch()
41
+ const unwatch = install(key, value)
42
+ table.set(key, { value, unwatch })
43
+ },
44
+ remove(key: string): boolean {
45
+ const entry = table.get(key)
46
+ if (!entry) return false
47
+ entry.unwatch()
48
+ table.delete(key)
49
+ return true
50
+ },
51
+ has(key: string): boolean {
52
+ return table.has(key)
53
+ },
54
+ get(key: string): V | undefined {
55
+ return table.get(key)?.value
56
+ },
57
+ keys(): Iterable<string> {
58
+ return table.keys()
59
+ },
60
+ *values(): Iterable<V> {
61
+ for (const entry of table.values()) yield entry.value
62
+ },
63
+ *entries(): Iterable<[string, V]> {
64
+ for (const [k, entry] of table) yield [k, entry.value]
65
+ },
66
+ clear(): void {
67
+ for (const entry of table.values()) entry.unwatch()
68
+ table.clear()
69
+ },
70
+ get size(): number {
71
+ return table.size
72
+ },
73
+ }
74
+ }