stitchkit 0.75.1 → 0.76.1

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.
@@ -29,6 +29,18 @@
29
29
  * and the owner's to configure. Saying "durable" without saying whose durability
30
30
  * would be the more comfortable lie.
31
31
  *
32
+ * ## Two ways in, because there are two lifecycles
33
+ *
34
+ * {@link keyspaceResource} declares it to the application kernel, which is right
35
+ * whenever there is a kernel: it closes after everything that writes to it,
36
+ * because a writer that depends on it is stopped first.
37
+ *
38
+ * {@link openKeyspace} opens one directly, for an application that owns its own
39
+ * lifecycle — a server that binds its signals and closes what it holds in an
40
+ * order it wrote itself. The resource is a thin wrapper over exactly this, not a
41
+ * second implementation: without it the only way to open a keyspace would be to
42
+ * have a kernel, which is a requirement nothing about a keyspace justifies.
43
+ *
32
44
  * ## It is a resource, not a global
33
45
  *
34
46
  * A keyspace is opened as a {@link ManagedResource} and its handle is read with
@@ -127,6 +139,31 @@ export declare function defineKeyspace<TValue>(name: string, declaration: {
127
139
  schema: z.ZodType<TValue>;
128
140
  key: (value: TValue) => string;
129
141
  }): KeyspaceDeclaration<TValue>;
142
+ /**
143
+ * An opened keyspace, plus the lifecycle its owner drives.
144
+ *
145
+ * The same four phases a {@link ManagedResource} has, because they are the same
146
+ * four phases — an application without a kernel calls them in the order it
147
+ * chose, and one with a kernel gets them called for it.
148
+ */
149
+ export interface OpenedKeyspace<TValue> {
150
+ readonly keyspace: OpenKeyspace<TValue>;
151
+ /** Refuse new writes. The queued ones still finish. */
152
+ stopAdmission(): void;
153
+ /** Wait for every accepted write to reach the backend. */
154
+ drain(): Promise<void>;
155
+ /** Report anything still unwritten, then close the backend. */
156
+ close(): Promise<void>;
157
+ }
158
+ /**
159
+ * Open a keyspace directly, for an application that owns its own lifecycle.
160
+ *
161
+ * Loads every stored record before it returns, so `get` and `list` are
162
+ * answerable from the first call. Where there is a kernel, prefer
163
+ * {@link keyspaceResource} — it gets the ordering right without anyone
164
+ * remembering to.
165
+ */
166
+ export declare function openKeyspace<TValue>(declaration: KeyspaceDeclaration<TValue>, config: KeyspaceResourceConfig<TValue>): Promise<OpenedKeyspace<TValue>>;
130
167
  /**
131
168
  * A keyspace as a managed resource. Its `start` publishes the {@link OpenKeyspace}.
132
169
  */
@@ -1 +1 @@
1
- {"version":3,"file":"keyspace.d.ts","sourceRoot":"","sources":["../../src/application/keyspace.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"keyspace.d.ts","sourceRoot":"","sources":["../../src/application/keyspace.ts"],"names":[],"mappings":"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"}
@@ -80,8 +80,17 @@ export interface WatchHubConfig {
80
80
  read(operation: WatchOperation, args: unknown): Promise<unknown>;
81
81
  /** Whether an operation may be watched at all. Refusal is answered in words. */
82
82
  watchable(operation: WatchOperation): boolean;
83
- /** The topics whose announcement means this operation's answer may have changed. */
84
- invalidatedBy(operation: WatchOperation): readonly string[];
83
+ /**
84
+ * The topics whose announcement means this answer may have changed.
85
+ *
86
+ * Given the arguments as well as the operation, so a topic can name what the
87
+ * answer actually depends on: `chat.transcript:<address>` rather than
88
+ * `chat.transcript`. Without that narrowing, one address changing wakes every
89
+ * watcher of the operation — twenty conversations open means twenty reads for
90
+ * one change, and nineteen of them publish nothing because nothing changed.
91
+ * The read is still paid.
92
+ */
93
+ invalidatedBy(operation: WatchOperation, args: unknown): readonly string[];
85
94
  /** Subscribe to a topic; returns the unsubscribe. Normally an event bus's `on`. */
86
95
  subscribe(topic: string, listener: () => void): () => void;
87
96
  /** The most keys one subscriber may watch at once. Default 64. */
@@ -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,oFAAoF;IACpF,aAAa,CAAC,SAAS,EAAE,cAAc,GAAG,SAAS,MAAM,EAAE,CAAC;IAC5D,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,wBAAsB,QAAQ,CAAC,SAAS,EAAE,cAAc,EAAE,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAU1F;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,cAAc,GAAG,QAAQ,CA0M/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,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,wBAAsB,QAAQ,CAAC,SAAS,EAAE,cAAc,EAAE,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAU1F;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,cAAc,GAAG,QAAQ,CA4M/D"}
@@ -6,7 +6,7 @@ export { type DiagnosticJournal, type DiagnosticJournalCloseResult, DiagnosticJo
6
6
  export { type ApplicationEventSink, type ApplicationEventSinkConfig, type ApplicationLifecycleEvent, ApplicationLifecycleEventSchema, applicationLifecycleEvent, createApplicationEventSink, } from './application/events.js';
7
7
  export { type ApplicationHealthHandlerOptions, ApplicationHealthHandlerOptionsSchema, type ApplicationOperationalHandlers, type ApplicationOperationalHandlersOptions, ApplicationOperationalHandlersOptionsSchema, createApplicationHealthHandler, createApplicationOperationalHandlers, } from './application/health.js';
8
8
  export { type ApplicationAdmission, ApplicationAdmissionError, type ApplicationConfig, type ApplicationHandle, type ApplicationOperationLease, type ApplicationResourceFailure, type ApplicationResourcePhase, type ApplicationShutdownBudget, ApplicationShutdownBudgetSchema, type ApplicationShutdownOptions, ApplicationShutdownOptionsSchema, createApplication, } from './application/kernel.js';
9
- export { defineKeyspace, type KeyspaceBackend, type KeyspaceChange, type KeyspaceDeclaration, type KeyspaceResourceConfig, keyspaceResource, memoryKeyspaceBackend, type OpenKeyspace, } from './application/keyspace.js';
9
+ export { defineKeyspace, type KeyspaceBackend, type KeyspaceChange, type KeyspaceDeclaration, type KeyspaceResourceConfig, keyspaceResource, memoryKeyspaceBackend, type OpenedKeyspace, type OpenKeyspace, openKeyspace, } from './application/keyspace.js';
10
10
  export { type SqliteKeyspaceBackendConfig, sqliteKeyspaceBackend, } from './application/keyspace-sqlite.js';
11
11
  export { type ApplicationSnapshotSink, type ApplicationSnapshotSinkConfig, type ApplicationSnapshotSinkError, type ApplicationSnapshotSinkStatus, ApplicationSnapshotSinkStatusSchema, createApplicationSnapshotSink, type RevisionedApplicationSnapshot, } from './application/latest-sink.js';
12
12
  export { defineManagedResource, type ManagedResource, type ManagedResourceContext, type ManagedResourceDependency, type ManagedResourcePublished, type ManagedResourcePublishesNoValue, type ManagedResourceStartResult, managedResourceDependencyId, } from './application/resource.js';
@@ -1 +1 @@
1
- {"version":3,"file":"application.d.ts","sourceRoot":"","sources":["../src/application.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"application.d.ts","sourceRoot":"","sources":["../src/application.ts"],"names":[],"mappings":"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"}
@@ -1223,7 +1223,32 @@ function defineKeyspace(name, declaration) {
1223
1223
  }
1224
1224
  return { name, schema: declaration.schema, key: declaration.key };
1225
1225
  }
1226
+ async function openKeyspace(declaration, config) {
1227
+ const machine = keyspaceMachine(declaration, config);
1228
+ await machine.load();
1229
+ return {
1230
+ keyspace: machine.keyspace,
1231
+ stopAdmission: machine.stopAdmission,
1232
+ drain: machine.drain,
1233
+ close: machine.close
1234
+ };
1235
+ }
1226
1236
  function keyspaceResource(declaration, config) {
1237
+ const machine = keyspaceMachine(declaration, config);
1238
+ return {
1239
+ id: config.id ?? `keyspace:${declaration.name}`,
1240
+ ...config.dependsOn && { dependsOn: config.dependsOn },
1241
+ async start(_context) {
1242
+ await machine.load();
1243
+ return { value: machine.keyspace };
1244
+ },
1245
+ stopAdmission: machine.stopAdmission,
1246
+ drain: machine.drain,
1247
+ close: machine.close,
1248
+ force: machine.force
1249
+ };
1250
+ }
1251
+ function keyspaceMachine(declaration, config) {
1227
1252
  const maxPending = config.maxPendingWrites ?? 1024;
1228
1253
  const records = new Map;
1229
1254
  let admitting = true;
@@ -1281,14 +1306,12 @@ function keyspaceResource(declaration, config) {
1281
1306
  config.onUnwritten?.(pending);
1282
1307
  }
1283
1308
  return {
1284
- id: config.id ?? `keyspace:${declaration.name}`,
1285
- ...config.dependsOn && { dependsOn: config.dependsOn },
1286
- async start(_context) {
1309
+ keyspace,
1310
+ async load() {
1287
1311
  for (const stored of await config.backend.load()) {
1288
1312
  const value = declaration.schema.parse(stored);
1289
1313
  records.set(declaration.key(value), value);
1290
1314
  }
1291
- return { value: keyspace };
1292
1315
  },
1293
1316
  stopAdmission() {
1294
1317
  admitting = false;
@@ -1610,7 +1633,7 @@ function createWatchHub(config) {
1610
1633
  state: { key, phase: "opening" },
1611
1634
  backoff: createBackoff(config.backoff ?? DEFAULT_BACKOFF)
1612
1635
  };
1613
- for (const topic of config.invalidatedBy(operation)) {
1636
+ for (const topic of config.invalidatedBy(operation, args)) {
1614
1637
  source.unsubscribes.push(config.subscribe(topic, () => {
1615
1638
  source.dirty = true;
1616
1639
  pump(source);
@@ -1724,6 +1747,7 @@ export {
1724
1747
  sqliteKeyspaceBackend,
1725
1748
  readDiagnosticJournalLockDiagnosis,
1726
1749
  projectApplicationStatus,
1750
+ openKeyspace,
1727
1751
  memoryKeyspaceBackend,
1728
1752
  managedServerResource,
1729
1753
  managedResourceDependencyId,
@@ -17,6 +17,23 @@
17
17
  * `createUrlBuilder`, and the identity is the contract's own
18
18
  * `(prefix, endpoint key)` — the same pair the server labels every request with.
19
19
  *
20
+ * ## A connection is not forever, and recovering is this client's job
21
+ *
22
+ * A socket drops and comes back, and the hub forgets everything the old
23
+ * connection held — it releases a subscriber's keys the moment that subscriber
24
+ * detaches. So a client that opened each question once and never again is a
25
+ * client whose face freezes the first time a server restarts, *silently*,
26
+ * because nothing tells the subscriber that what it is looking at stopped being
27
+ * live.
28
+ *
29
+ * This client holds every key and every listener, so nobody else can do it: on a
30
+ * drop it tells subscribers the source is gone and forgets that anything was
31
+ * opened; on a fresh connection it re-opens every key that still has a listener.
32
+ * That is why {@link WatchTransport} **requires** `onConnectionChange` rather
33
+ * than using it when offered — a transport that cannot say when it reconnected
34
+ * cannot host a recovering client, and an optional hook would turn recovery into
35
+ * something that silently did not happen.
36
+ *
20
37
  * ## Retention
21
38
  *
22
39
  * The last value of a key is kept while anyone holds it, and for `holdMs` after
@@ -27,7 +44,7 @@
27
44
  * sense a rendering component can use.
28
45
  */
29
46
  import type { ContractDef, EndpointDef } from '../contract/define.js';
30
- import { type WatchKey, type WatchStateFrame } from './watch-contract.js';
47
+ import { WATCH_CLOSE, WATCH_OPEN, WATCH_STATE, WATCH_VALUE, type WatchKey, type WatchStateFrame, type WatchValueFrame } from './watch-contract.js';
31
48
  export interface WatchListeners<TValue> {
32
49
  value(value: TValue): void;
33
50
  /** Phase and, when unhealthy, the read's own code and message. */
@@ -39,16 +56,72 @@ export interface WatchHandle<TValue> {
39
56
  /** Drops every listener this handle registered and releases the key. */
40
57
  close(): void;
41
58
  }
42
- /** The transport half a watch client needs — a bound realtime client for `watchContract`. */
59
+ /** What the server sends a watcher, by event name. */
60
+ export interface WatchInboundEvents {
61
+ [WATCH_VALUE]: WatchValueFrame;
62
+ [WATCH_STATE]: WatchStateFrame;
63
+ }
64
+ /**
65
+ * A realtime client, as much of one as this adapter needs to see.
66
+ *
67
+ * Four members and no types on them, because the point of this shape is that a
68
+ * *typed* client satisfies it: a bound realtime client's `on` is generic over
69
+ * the events of the contract it was bound to, and TypeScript will not relate
70
+ * that signature to any concrete one written here — measured, both with a payload
71
+ * parameter and with the tuple form the realtime handler actually has.
72
+ */
73
+ export interface RealtimeClientLike {
74
+ on: (...args: never[]) => unknown;
75
+ emit: (...args: never[]) => unknown;
76
+ request: (...args: never[]) => unknown;
77
+ onConnectionChange: (...args: never[]) => unknown;
78
+ }
79
+ /**
80
+ * Hand a bound realtime client to {@link createWatchClient}.
81
+ *
82
+ * The guide used to say "pass a bound realtime client" and the types refused it,
83
+ * which is the worst combination: an instruction that reads as supported and
84
+ * fails at the call site. This is the conversion, once, in the framework —
85
+ * rather than the same cast copied into every application that follows the
86
+ * guide.
87
+ */
88
+ export declare function watchTransport(client: RealtimeClientLike): WatchTransport;
89
+ /**
90
+ * The transport half a watch client needs — a realtime client bound to a
91
+ * contract that carries `watchContract`.
92
+ *
93
+ * Written in the protocol's own four event names rather than in `string`, so an
94
+ * application bringing its own transport knows exactly what to implement.
95
+ *
96
+ * A **bound realtime client does not satisfy this shape**, and no phrasing of it
97
+ * would fix that: its `on` is generic over the events of the contract it was
98
+ * bound to, and TypeScript will not relate two generic signatures like these.
99
+ * Pass it through {@link watchTransport}, which is that conversion done once
100
+ * here instead of once per application.
101
+ */
43
102
  export interface WatchTransport {
44
- on(event: string, handler: (payload: never) => void): () => void;
45
- emit(event: string, payload: unknown): void;
46
- request(event: string, payload: unknown, options: {
103
+ on<TEvent extends keyof WatchInboundEvents>(event: TEvent, handler: (payload: WatchInboundEvents[TEvent]) => void): () => void;
104
+ emit(event: typeof WATCH_CLOSE, payload: {
105
+ key: WatchKey;
106
+ }): unknown;
107
+ request(event: typeof WATCH_OPEN, payload: {
108
+ key: WatchKey;
109
+ args: unknown;
110
+ }, options: {
47
111
  timeoutMs: number;
48
112
  }): Promise<{
49
113
  accepted: boolean;
50
114
  reason?: string;
51
115
  }>;
116
+ /**
117
+ * Observe connection changes. Required, because recovery depends on it.
118
+ *
119
+ * The hub releases a subscriber's keys when its connection detaches, so every
120
+ * question opened over the old socket is gone the moment it drops. Without
121
+ * this the client would go on believing it was subscribed and show a face that
122
+ * had quietly stopped updating.
123
+ */
124
+ onConnectionChange(listener: (connected: boolean, reason?: string) => void): () => void;
52
125
  }
53
126
  export interface WatchClientConfig {
54
127
  readonly transport: WatchTransport;
@@ -1 +1 @@
1
- {"version":3,"file":"watch-client.d.ts","sourceRoot":"","sources":["../../src/live/watch-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEnE,OAAO,EAKL,KAAK,QAAQ,EACb,KAAK,eAAe,EAGrB,MAAM,kBAAkB,CAAC;AAE1B,MAAM,WAAW,cAAc,CAAC,MAAM;IACpC,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,kEAAkE;IAClE,KAAK,CAAC,CAAC,KAAK,EAAE,eAAe,GAAG,IAAI,CAAC;CACtC;AAED,MAAM,WAAW,WAAW,CAAC,MAAM;IACjC,sFAAsF;IACtF,SAAS,CAAC,SAAS,EAAE,cAAc,CAAC,MAAM,CAAC,GAAG,MAAM,IAAI,CAAC;IACzD,wEAAwE;IACxE,KAAK,IAAI,IAAI,CAAC;CACf;AAED,6FAA6F;AAC7F,MAAM,WAAW,cAAc;IAC7B,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE,KAAK,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IACjE,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IAC5C,OAAO,CACL,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,OAAO,EAChB,OAAO,EAAE;QAAE,SAAS,EAAE,MAAM,CAAA;KAAE,GAC7B,OAAO,CAAC;QAAE,QAAQ,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACpD;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,SAAS,EAAE,cAAc,CAAC;IACnC,+EAA+E;IAC/E,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,8DAA8D;IAC9D,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,wEAAwE;IACxE,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;CAC9D;AAcD,MAAM,MAAM,gBAAgB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,IAAI;KACnE,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,WAAW,CAAC,OAAO,CAAC;CACzE,CAAC;AAEF,wBAAgB,iBAAiB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,EACrE,QAAQ,EAAE,WAAW,CAAC,CAAC,EAAE,MAAM,CAAC,EAChC,MAAM,EAAE,iBAAiB,GACxB,gBAAgB,CAAC,CAAC,CAAC,CAqKrB"}
1
+ {"version":3,"file":"watch-client.d.ts","sourceRoot":"","sources":["../../src/live/watch-client.ts"],"names":[],"mappings":"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"}
package/dist/live.d.ts CHANGED
@@ -13,6 +13,6 @@
13
13
  * a watched read live in `stitchkit/application`. → ADR 0150.
14
14
  */
15
15
  export { defineEvents, type EventDecision, type EventDeliveryMode, type EventPayloads, type EventsConfig, type EventsDeclaration, type EventTopicDeclaration, type EventTopicRegistry, type EventTopicsOfMode, type EventUndecided, toRealtimeContract, type WireTopic, } from './live/events.js';
16
- export { createWatchClient, type TypedWatchClient, type WatchClientConfig, type WatchHandle, type WatchListeners, type WatchTransport, } from './live/watch-client.js';
16
+ export { createWatchClient, type RealtimeClientLike, type TypedWatchClient, type WatchClientConfig, type WatchHandle, type WatchInboundEvents, type WatchListeners, type WatchTransport, watchTransport, } from './live/watch-client.js';
17
17
  export { WATCH_CLOSE, WATCH_OPEN, WATCH_STATE, WATCH_VALUE, type WatchKey, WatchKeySchema, type WatchStateFrame, WatchStateSchema, type WatchValueFrame, WatchValueSchema, watchContract, watchKeyString, } from './live/watch-contract.js';
18
18
  //# sourceMappingURL=live.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"live.d.ts","sourceRoot":"","sources":["../src/live.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,EACL,YAAY,EACZ,KAAK,aAAa,EAClB,KAAK,iBAAiB,EACtB,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,KAAK,iBAAiB,EACtB,KAAK,qBAAqB,EAC1B,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,KAAK,cAAc,EACnB,kBAAkB,EAClB,KAAK,SAAS,GACf,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,iBAAiB,EACjB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACtB,KAAK,WAAW,EAChB,KAAK,cAAc,EACnB,KAAK,cAAc,GACpB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,WAAW,EACX,UAAU,EACV,WAAW,EACX,WAAW,EACX,KAAK,QAAQ,EACb,cAAc,EACd,KAAK,eAAe,EACpB,gBAAgB,EAChB,KAAK,eAAe,EACpB,gBAAgB,EAChB,aAAa,EACb,cAAc,GACf,MAAM,uBAAuB,CAAC"}
1
+ {"version":3,"file":"live.d.ts","sourceRoot":"","sources":["../src/live.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,EACL,YAAY,EACZ,KAAK,aAAa,EAClB,KAAK,iBAAiB,EACtB,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,KAAK,iBAAiB,EACtB,KAAK,qBAAqB,EAC1B,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,KAAK,cAAc,EACnB,kBAAkB,EAClB,KAAK,SAAS,GACf,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,iBAAiB,EACjB,KAAK,kBAAkB,EACvB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACtB,KAAK,WAAW,EAChB,KAAK,kBAAkB,EACvB,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,cAAc,GACf,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,WAAW,EACX,UAAU,EACV,WAAW,EACX,WAAW,EACX,KAAK,QAAQ,EACb,cAAc,EACd,KAAK,eAAe,EACpB,gBAAgB,EAChB,KAAK,eAAe,EACpB,gBAAgB,EAChB,aAAa,EACb,cAAc,GACf,MAAM,uBAAuB,CAAC"}
package/dist/live.js CHANGED
@@ -116,12 +116,20 @@ function watchKeyString(key) {
116
116
  }
117
117
 
118
118
  // src/live/watch-client.ts
119
+ function watchTransport(client) {
120
+ return client;
121
+ }
119
122
  function createWatchClient(contract, config) {
120
123
  const entries = new Map;
121
124
  const digests = new Map;
122
125
  const holdMs = config.holdMs ?? 0;
123
126
  const openTimeoutMs = config.openTimeoutMs ?? 1e4;
124
127
  const service = contract.meta.prefix;
128
+ function publishState(entry, state) {
129
+ entry.state = state;
130
+ for (const listener of [...entry.listeners])
131
+ listener.state?.(state);
132
+ }
125
133
  config.transport.on(WATCH_VALUE, (frame) => {
126
134
  const entry = entries.get(watchKeyString(frame.key));
127
135
  if (!entry)
@@ -138,9 +146,27 @@ function createWatchClient(contract, config) {
138
146
  const entry = entries.get(watchKeyString(frame.key));
139
147
  if (!entry)
140
148
  return;
141
- entry.state = frame;
142
- for (const listener of [...entry.listeners])
143
- listener.state?.(frame);
149
+ publishState(entry, frame);
150
+ });
151
+ config.transport.onConnectionChange((connected, reason) => {
152
+ if (!connected) {
153
+ for (const entry of entries.values()) {
154
+ entry.opened = false;
155
+ publishState(entry, {
156
+ key: entry.key,
157
+ phase: "unavailable",
158
+ reason: "source-unavailable",
159
+ ...reason !== undefined && { message: reason }
160
+ });
161
+ }
162
+ return;
163
+ }
164
+ for (const entry of entries.values()) {
165
+ if (entry.listeners.size === 0)
166
+ continue;
167
+ publishState(entry, { key: entry.key, phase: "opening" });
168
+ open(entry);
169
+ }
144
170
  });
145
171
  function cachedDigest(action, args) {
146
172
  return digests.get(`${service}/${action}/${JSON.stringify(stableValue(args))}`);
@@ -154,7 +180,7 @@ function createWatchClient(contract, config) {
154
180
  digests.set(cacheKey, digest);
155
181
  return digest;
156
182
  }
157
- function entryFor(key) {
183
+ function entryFor(key, args) {
158
184
  const id = watchKeyString(key);
159
185
  const existing = entries.get(id);
160
186
  if (existing) {
@@ -164,14 +190,44 @@ function createWatchClient(contract, config) {
164
190
  }
165
191
  const entry = {
166
192
  key,
193
+ args,
167
194
  listeners: new Set,
168
195
  revision: 0,
169
196
  hasValue: false,
197
+ opened: false,
170
198
  state: { key, phase: "opening" }
171
199
  };
172
200
  entries.set(id, entry);
173
201
  return entry;
174
202
  }
203
+ async function open(entry) {
204
+ if (entry.opened)
205
+ return;
206
+ entry.opened = true;
207
+ try {
208
+ const acknowledgement = await config.transport.request(WATCH_OPEN, { key: entry.key, args: entry.args }, { timeoutMs: openTimeoutMs });
209
+ if (!acknowledgement.accepted) {
210
+ const reason = acknowledgement.reason ?? "the server refused this watch";
211
+ config.onRefused?.(entry.key, reason);
212
+ publishState(entry, {
213
+ key: entry.key,
214
+ phase: "unavailable",
215
+ reason: "source-unavailable",
216
+ message: reason
217
+ });
218
+ }
219
+ } catch (error) {
220
+ entry.opened = false;
221
+ const code = typeof error === "object" && error !== null && "code" in error ? String(Reflect.get(error, "code")) : undefined;
222
+ publishState(entry, {
223
+ key: entry.key,
224
+ phase: "unavailable",
225
+ reason: "source-unavailable",
226
+ ...code !== undefined && { code },
227
+ message: error instanceof Error ? error.message : String(error)
228
+ });
229
+ }
230
+ }
175
231
  function releaseEntry(entry) {
176
232
  if (entry.listeners.size > 0)
177
233
  return;
@@ -191,36 +247,16 @@ function createWatchClient(contract, config) {
191
247
  function handleFor(action, args) {
192
248
  const mine = new Set;
193
249
  let entry;
194
- let opened = false;
195
250
  const known = cachedDigest(action, args);
196
251
  if (known !== undefined)
197
- entry = entryFor({ service, action, digest: known });
252
+ entry = entryFor({ service, action, digest: known }, args);
198
253
  const ready = (async () => {
199
254
  if (entry)
200
255
  return entry;
201
256
  const digest = await resolveDigest(action, args);
202
- entry = entryFor({ service, action, digest });
257
+ entry = entryFor({ service, action, digest }, args);
203
258
  return entry;
204
259
  })();
205
- async function open(target) {
206
- if (opened)
207
- return;
208
- opened = true;
209
- const acknowledgement = await config.transport.request(WATCH_OPEN, { key: target.key, args }, { timeoutMs: openTimeoutMs });
210
- if (!acknowledgement.accepted) {
211
- const reason = acknowledgement.reason ?? "the server refused this watch";
212
- config.onRefused?.(target.key, reason);
213
- const refusal = {
214
- key: target.key,
215
- phase: "unavailable",
216
- reason: "source-unavailable",
217
- message: reason
218
- };
219
- target.state = refusal;
220
- for (const listener of [...target.listeners])
221
- listener.state?.(refusal);
222
- }
223
- }
224
260
  return {
225
261
  subscribe(listeners) {
226
262
  const registered = listeners;
@@ -269,6 +305,7 @@ function createWatchClient(contract, config) {
269
305
  return client;
270
306
  }
271
307
  export {
308
+ watchTransport,
272
309
  watchKeyString,
273
310
  watchContract,
274
311
  toRealtimeContract,
@@ -875,6 +875,14 @@ function createTrustFence(config) {
875
875
  }
876
876
  trusted.push(authority);
877
877
  }
878
+ const trustedOrigins = [];
879
+ for (const entry of config.trustedOrigins ?? []) {
880
+ const authority = readAuthority(entry);
881
+ if (!authority) {
882
+ throw new Error(`[stitchkit] trust fence: trustedOrigins entry "${entry}" is not an authority. An entry is \`host\` or \`host:port\` — no scheme, no path, no wildcard, the same form as trustedHosts.`);
883
+ }
884
+ trustedOrigins.push(authority);
885
+ }
878
886
  function refuse(refusal) {
879
887
  config.onRefused?.(refusal);
880
888
  config.logger?.warn?.("[stitchkit] trust fence refused a request", {
@@ -911,7 +919,9 @@ function createTrustFence(config) {
911
919
  } catch {
912
920
  originAuthority = undefined;
913
921
  }
914
- const agrees = originAuthority !== undefined && originAuthority.hostname === requested.hostname && comparablePort(originAuthority.port) === comparablePort(requested.port);
922
+ const sameAuthority = originAuthority !== undefined && originAuthority.hostname === requested.hostname && comparablePort(originAuthority.port) === comparablePort(requested.port);
923
+ const declared = originAuthority !== undefined && trustedOrigins.some((entry) => entry.hostname === originAuthority.hostname && (entry.port === null || entry.port === originAuthority.port));
924
+ const agrees = sameAuthority || declared;
915
925
  if (!agrees) {
916
926
  return refuse({ reason: "origin-mismatch", lane, host, origin, site });
917
927
  }
@@ -84,6 +84,27 @@ export interface TrustFenceConfig {
84
84
  * list it when you want it.
85
85
  */
86
86
  readonly trustedHosts: readonly string[];
87
+ /**
88
+ * Origins allowed to address this server from a different authority.
89
+ *
90
+ * Same entry format as {@link trustedHosts} — `host` or `host:port`, compared
91
+ * against the `Origin`'s authority. Absent, only an `Origin` that matches the
92
+ * `Host` is accepted, which is the safe default and also refuses the most
93
+ * ordinary arrangement there is: a UI dev server on one port talking to an API
94
+ * on the next. That is the case this exists for.
95
+ *
96
+ * Worth knowing what the `Origin` check is and is not for, because the
97
+ * question comes up the moment it refuses something. It is **not** what stops
98
+ * DNS rebinding: that attack is same-origin by construction — the page believes
99
+ * it *is* the target authority — so it sends a matching `Origin` or none at
100
+ * all, and {@link trustedHosts} is what refuses it. What the `Origin` check
101
+ * stops is a plain cross-origin request from a page that never needs to read
102
+ * the reply: CORS governs whether a response can be *read*, never whether the
103
+ * request is *sent*, so a state-changing endpoint is reachable without it.
104
+ * That is why the answer here is a declared list rather than dropping the
105
+ * check.
106
+ */
107
+ readonly trustedOrigins?: readonly string[];
87
108
  /** Called on every refusal, before the 403 is written. */
88
109
  readonly onRefused?: (refusal: TrustRefusal) => void;
89
110
  /** Refusals are logged at `warn` when a logger is supplied. */
@@ -1 +1 @@
1
- {"version":3,"file":"trust-fence.d.ts","sourceRoot":"","sources":["../../../src/server/middleware/trust-fence.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AACjE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAE/C,qCAAqC;AACrC,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,QAAQ,CAAC;AAE1C,MAAM,MAAM,kBAAkB;AAC5B,yEAAyE;AACvE,cAAc;AAChB,wEAAwE;GACtE,gBAAgB;AAClB,iEAAiE;GAC/D,iBAAiB;AACnB,4DAA4D;GAC1D,YAAY,CAAC;AAEjB,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,MAAM,EAAE,kBAAkB,CAAC;IACpC,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,6EAA6E;IAC7E,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAED,MAAM,WAAW,gBAAgB;IAC/B;;;;;;;;;;;;;OAaG;IACH,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,0DAA0D;IAC1D,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,YAAY,KAAK,IAAI,CAAC;IACrD,+DAA+D;IAC/D,QAAQ,CAAC,MAAM,CAAC,EAAE,YAAY,CAAC;CAChC;AAED,MAAM,WAAW,UAAU;IACzB;;;;;;OAMG;IACH,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC,cAAc,EAAE,WAAW,CAAC,CAAC;IAClD,iFAAiF;IACjF,QAAQ,CAAC,YAAY,EAAE,qBAAqB,CAAC;IAC7C,oFAAoF;IACpF,KAAK,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,GAAG,YAAY,GAAG,SAAS,CAAC;CACpE;AAyDD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAM1D;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,GAAG,UAAU,CA2FrE"}
1
+ {"version":3,"file":"trust-fence.d.ts","sourceRoot":"","sources":["../../../src/server/middleware/trust-fence.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AACjE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAE/C,qCAAqC;AACrC,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,QAAQ,CAAC;AAE1C,MAAM,MAAM,kBAAkB;AAC5B,yEAAyE;AACvE,cAAc;AAChB,wEAAwE;GACtE,gBAAgB;AAClB,iEAAiE;GAC/D,iBAAiB;AACnB,4DAA4D;GAC1D,YAAY,CAAC;AAEjB,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,MAAM,EAAE,kBAAkB,CAAC;IACpC,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,6EAA6E;IAC7E,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAED,MAAM,WAAW,gBAAgB;IAC/B;;;;;;;;;;;;;OAaG;IACH,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC;;;;;;;;;;;;;;;;;;;OAmBG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5C,0DAA0D;IAC1D,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,YAAY,KAAK,IAAI,CAAC;IACrD,+DAA+D;IAC/D,QAAQ,CAAC,MAAM,CAAC,EAAE,YAAY,CAAC;CAChC;AAED,MAAM,WAAW,UAAU;IACzB;;;;;;OAMG;IACH,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC,cAAc,EAAE,WAAW,CAAC,CAAC;IAClD,iFAAiF;IACjF,QAAQ,CAAC,YAAY,EAAE,qBAAqB,CAAC;IAC7C,oFAAoF;IACpF,KAAK,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,GAAG,YAAY,GAAG,SAAS,CAAC;CACpE;AAyDD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAM1D;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,GAAG,UAAU,CA6GrE"}
package/llms-full.txt CHANGED
@@ -62,7 +62,7 @@ own, recorded as an ADR.
62
62
  | `stitchkit/testing` | tests on Bun or Node | stable | in-process generated clients over a real Fetch handler, plus the store and managed-resource conformance kits |
63
63
  | `stitchkit/declaration` | build and deployment tooling (Bun or Node) | evolving | `ProjectDeclarationSchema` — the one machine-readable statement a repository makes about itself |
64
64
  | `stitchkit/react` | browser | stable | `createCursorQuery`, `createCacheBridge` |
65
- | `stitchkit/agent-runtime` | server | evolving<br>_redefined in 11 of the 20 minors since 0.56.2, most recently 0.75.0_ | optional durable conversation/run loop, history, models, prompts, fencing and events |
65
+ | `stitchkit/agent-runtime` | server | evolving<br>_redefined in 11 of the 21 minors since 0.56.2, most recently 0.75.0_ | optional durable conversation/run loop, history, models, prompts, fencing and events |
66
66
  | `stitchkit/agent-runtime/harness` | server | evolving | resource-aware process-local facade over the canonical Agent runtime; supervision stays outside |
67
67
  | `stitchkit/agent-runtime/coding-tools` | server (Bun or Node) | evolving | bounded host-authorized direct file and shell tools; a root boundary, not an OS sandbox |
68
68
  | `stitchkit/agent-runtime/openrouter` | server | evolving | isolated OpenRouter language-model adapter |
@@ -70,7 +70,7 @@ own, recorded as an ADR.
70
70
  | `stitchkit/agent-runtime/sqlite/bun` | server (Bun) | evolving | durable built-in SQLite store for the agent runtime |
71
71
  | `stitchkit/agent-runtime/sqlite/node` | server (Node ≥ 22.5) | evolving | durable built-in SQLite store for the agent runtime |
72
72
  | `stitchkit-tui` | terminal (Bun) | evolving | optional official OpenTUI host over a caller-composed headless runtime |
73
- | `stitchkit/application` | server | evolving<br>_redefined in 4 of the 20 minors since 0.56.2, most recently 0.72.0_ | managed resource graph, readiness, admission, schedules and bounded shutdown |
73
+ | `stitchkit/application` | server | evolving<br>_redefined in 4 of the 21 minors since 0.56.2, most recently 0.72.0_ | managed resource graph, readiness, admission, schedules and bounded shutdown |
74
74
  | `stitchkit/application/grammy` | server | evolving | isolated grammY polling and webhook lifecycle adapters |
75
75
  | `stitchkit/application/opentelemetry` | server | evolving | maps application snapshots onto an injected OpenTelemetry `Meter` |
76
76
 
@@ -7617,9 +7617,17 @@ const hub = createWatchHub({
7617
7617
 
7618
7618
  ```ts
7619
7619
  // browser
7620
- import { createWatchClient } from 'stitchkit/live';
7621
-
7622
- const watch = createWatchClient(notesContract, { transport: live, holdMs: 30_000 });
7620
+ import { createWatchClient, watchTransport } from 'stitchkit/live';
7621
+
7622
+ // `live` is the bound realtime client. It goes through `watchTransport`, and
7623
+ // that is not ceremony: its `on` is generic over the contract it was bound to,
7624
+ // and TypeScript will not relate that signature to the one a watch client
7625
+ // declares. The conversion lives in the framework so it is written once rather
7626
+ // than in every application.
7627
+ const watch = createWatchClient(notesContract, {
7628
+ transport: watchTransport(live),
7629
+ holdMs: 30_000,
7630
+ });
7623
7631
 
7624
7632
  const handle = watch.list({ folder: 'inbox' });
7625
7633
  const stop = handle.subscribe({
@@ -7636,6 +7644,19 @@ order-independent — `{a, b}` and `{b, a}` are the same question, which a plain
7636
7644
  `JSON.stringify` key would get wrong exactly when two components happen to build
7637
7645
  their argument object in a different order.
7638
7646
 
7647
+ ### Narrow the topic to the arguments
7648
+
7649
+ `invalidatedBy` is given the arguments as well as the operation, so a topic can
7650
+ name what the answer actually depends on:
7651
+
7652
+ ```ts
7653
+ invalidatedBy: (operation, args) => [`chat.transcript:${args.address}`],
7654
+ ```
7655
+
7656
+ Without that, one address changing wakes every watcher of the operation. Twenty
7657
+ conversations open means twenty reads for one change — nineteen of them publish
7658
+ nothing, because nothing changed, and the read is paid anyway.
7659
+
7639
7660
  ### Who shares with whom
7640
7661
 
7641
7662
  A key is `(service, action, digest of arguments)`, and everyone on that key gets
@@ -7661,6 +7682,20 @@ A failed read arrives as `unavailable` with the read's own `code` and `message`.
7661
7682
  The hub retries on its own backoff, and a success clears the state without anyone
7662
7683
  asking.
7663
7684
 
7685
+ ### Across a reconnect
7686
+
7687
+ The hub releases a connection's keys when it detaches, so everything opened over
7688
+ the old socket is gone the moment it drops. The client recovers on its own: on a
7689
+ drop your `state` listener gets `unavailable` / `source-unavailable`, and on the
7690
+ next connection every key that still has a listener is re-opened and the values
7691
+ resume. That is what `onConnectionChange` on the transport is for, and why it is
7692
+ required rather than optional — a client that cannot be told has no way to
7693
+ notice, and the face stops updating without a word.
7694
+
7695
+ Nothing an open can fail with escapes as a rejected promise: a disconnected
7696
+ socket, a timeout, a refusal all arrive as `unavailable` carrying the error's own
7697
+ code and message, and the next connection retries.
7698
+
7664
7699
  ### `watch` or `createLiveStateController`?
7665
7700
 
7666
7701
  > **If you would have written `applyEvent` as `(_, next) => next`, you want
@@ -7710,8 +7745,23 @@ point:
7710
7745
  immediately. An event emitted before the memory update is a wake-up to the old
7711
7746
  value.
7712
7747
 
7713
- It is a **resource**, read with `context.use(...)`, not a function you call
7714
- wherever you need it. The kernel resolves its resource graph in the constructor
7748
+ Where there is no kernel — a server that binds its own signals and closes what
7749
+ it holds in an order it wrote open it directly instead:
7750
+
7751
+ ```ts
7752
+ const opened = await openKeyspace(sessions, { backend });
7753
+ opened.keyspace.get(id); // synchronous, from memory
7754
+ // on shutdown, in the order you chose:
7755
+ opened.stopAdmission();
7756
+ await opened.drain();
7757
+ await opened.close();
7758
+ ```
7759
+
7760
+ The resource is a thin wrapper over exactly that, so there is one implementation
7761
+ and two lifecycles, not two keyspaces.
7762
+
7763
+ Where there **is** a kernel it is a **resource**, read with `context.use(...)`,
7764
+ rather than a function you call wherever you need it. The kernel resolves its resource graph in the constructor
7715
7765
  and cannot register one afterwards, so a keyspace opened by a bare call inside
7716
7766
  another resource's `start` is never drained, never closed, and never ordered
7717
7767
  against the things that write to it.
@@ -7745,6 +7795,32 @@ createServer({
7745
7795
  });
7746
7796
  ```
7747
7797
 
7798
+ ### A UI on one port, an API on the next
7799
+
7800
+ The most ordinary arrangement in development is also the one the fence refuses by
7801
+ default: a UI dev server on `:5180` calling an API on `:5181`. The browser sends
7802
+ `Origin: http://localhost:5180` to `Host: localhost:5181`, they differ, and both
7803
+ lanes answer 403. Declare the second origin:
7804
+
7805
+ ```ts
7806
+ createTrustFence({
7807
+ trustedHosts: ['localhost:5181'],
7808
+ trustedOrigins: ['localhost:5180'],
7809
+ });
7810
+ ```
7811
+
7812
+ Same entry format as `trustedHosts`, compared against the `Origin`'s authority.
7813
+ Declaring one does not widen anything else: an undeclared origin is still
7814
+ refused, and `trustedHosts` still decides which authority the server answers on.
7815
+
7816
+ It is worth knowing what the `Origin` check is for, because it is not what most
7817
+ people assume. It is **not** the DNS-rebinding defence — that attack is
7818
+ same-origin by construction, so it sends a matching `Origin` or none at all, and
7819
+ `trustedHosts` is what refuses it. What the `Origin` check stops is a plain
7820
+ cross-origin request from a page that never needs to read the reply: CORS governs
7821
+ whether a response can be *read*, never whether the request is *sent*, so a
7822
+ state-changing endpoint is reachable without it.
7823
+
7748
7824
  ### Install **both** halves
7749
7825
 
7750
7826
  `fence.hooks` fences HTTP. `fence.allowRequest` fences the Socket.IO lane — and
@@ -9918,6 +9994,28 @@ makes one thing your job rather than the resolver's:
9918
9994
  The mechanical part is identical either way. Only the *noticing* differs, and an
9919
9995
  exact pin moves it onto you.
9920
9996
 
9997
+ ## Released migration: 0.76.0
9998
+
9999
+ One change, and only if you hand `createWatchClient` a transport you wrote yourself.
10000
+
10001
+ ```bash
10002
+ rg -n "createWatchClient" --glob '*.ts'
10003
+ ```
10004
+
10005
+ If the `transport:` you pass is a bound realtime client — `bindRealtimeClient(...)` or
10006
+ `createRealtimeClient(...)` — there is nothing to do: it already carries
10007
+ `onConnectionChange`. If it is an object you assembled, add the fourth member:
10008
+
10009
+ ```ts
10010
+ onConnectionChange(listener: (connected: boolean, reason?: string) => void): () => void
10011
+ ```
10012
+
10013
+ It is required because the client recovers through it: on a drop it publishes
10014
+ `unavailable` to subscribers and forgets what was opened, and on a fresh
10015
+ connection it re-opens every key that still has a listener. Without it a watch
10016
+ stays "open" on the client after a server restart while the hub remembers
10017
+ nothing, and the face stops updating without saying so.
10018
+
9921
10019
  ## Released migration: 0.75.0
9922
10020
 
9923
10021
  Two mechanical renames and one option that has to move. Nothing about a passing
@@ -13050,6 +13148,7 @@ realtime contract from `stitchkit`, and the server halves live in `stitchkit/app
13050
13148
  | `EventPayloads` / `EventTopicsOfMode` / `WireTopic` | _types_ | payload map keyed by wire topic, the topics of one mode, and the `prefix.name` a topic is addressed by |
13051
13149
  | `createWatchClient` | function | contract-shaped watch client: `watch.action(args)` returns a ref-counted handle sharing one subscription |
13052
13150
  | `WatchHandle` / `WatchListeners` / `WatchClientConfig` / `WatchTransport` / `TypedWatchClient` | _types_ | subscribe/close, the value and state listeners, hold window and open deadline, and the bound realtime client it rides |
13151
+ | `watchTransport` / `RealtimeClientLike` / `WatchInboundEvents` | function / _types_ | hands a bound realtime client to a watch client — two generic signatures TypeScript will not relate, converted once here |
13053
13152
  | `watchContract` | const | the four-event realtime contract a watched read travels on |
13054
13153
  | `WATCH_OPEN` / `WATCH_CLOSE` / `WATCH_VALUE` / `WATCH_STATE` | const | the event names of that contract |
13055
13154
  | `WatchKey` / `WatchKeySchema` / `watchKeyString` | type / schema / function | `(service, action, arguments digest)` — the identity both ends compute the same way |
@@ -13480,6 +13579,7 @@ the function that derives it from a snapshot,
13480
13579
  | Export | Kind | Summary |
13481
13580
  |--------|------|---------|
13482
13581
  | `defineKeyspace` / `keyspaceResource` | function | a named record set read synchronously from memory and written through one serialised chain; backend first, then memory, then the change event |
13582
+ | `openKeyspace` / `OpenedKeyspace` | function / type | the same keyspace opened directly, for an application that owns its own lifecycle rather than declaring resources to a kernel |
13483
13583
  | `KeyspaceBackend` / `KeyspaceDeclaration` / `KeyspaceChange` / `KeyspaceResourceConfig` / `OpenKeyspace` | _types_ | the four-method durability port, the declaration, the announced change, the resource's options and the published handle |
13484
13584
  | `memoryKeyspaceBackend` / `sqliteKeyspaceBackend` / `SqliteKeyspaceBackendConfig` | function / type | a disposable in-process backend, and one table with a key and a JSON payload over a caller-owned database |
13485
13585
  | `SqliteDatabase` / `SqliteStatement` / `SqliteValue` | _types_ | the minimal synchronous SQLite boundary the framework types against |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stitchkit",
3
- "version": "0.75.1",
3
+ "version": "0.76.1",
4
4
  "description": "Contract-first backend framework — one defineContract() into an HTTP API, MCP tools, AI-agent tools and a typed client. Bun and Node.",
5
5
  "keywords": [
6
6
  "bun",