@earendil-works/chord 1.0.4 → 1.2.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.
Files changed (186) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +77 -194
  3. package/dist/cell.d.ts +101 -0
  4. package/dist/cell.d.ts.map +1 -0
  5. package/dist/cell.js +316 -0
  6. package/dist/cell.js.map +1 -0
  7. package/dist/cluster.d.ts +26 -0
  8. package/dist/cluster.d.ts.map +1 -0
  9. package/dist/cluster.js +214 -0
  10. package/dist/cluster.js.map +1 -0
  11. package/dist/connection.d.ts +157 -0
  12. package/dist/connection.d.ts.map +1 -0
  13. package/dist/connection.js +757 -0
  14. package/dist/connection.js.map +1 -0
  15. package/dist/context/index.d.ts +12 -1
  16. package/dist/context/index.d.ts.map +1 -1
  17. package/dist/context/index.js.map +1 -1
  18. package/dist/contract.d.ts +133 -0
  19. package/dist/contract.d.ts.map +1 -0
  20. package/dist/contract.js +157 -0
  21. package/dist/contract.js.map +1 -0
  22. package/dist/declare.d.ts +70 -0
  23. package/dist/declare.d.ts.map +1 -0
  24. package/dist/declare.js +22 -0
  25. package/dist/declare.js.map +1 -0
  26. package/dist/delta/apply-immutable-trusted.js.map +1 -1
  27. package/dist/delta/diff.d.ts +1 -1
  28. package/dist/delta/diff.d.ts.map +1 -1
  29. package/dist/delta/diff.js.map +1 -1
  30. package/dist/delta/index.d.ts +2 -2
  31. package/dist/delta/index.d.ts.map +1 -1
  32. package/dist/delta/index.js +41 -0
  33. package/dist/delta/index.js.map +1 -1
  34. package/dist/delta/revision-validator.d.ts +1 -1
  35. package/dist/delta/revision-validator.d.ts.map +1 -1
  36. package/dist/delta/revision-validator.js.map +1 -1
  37. package/dist/delta/tracker.d.ts +1 -1
  38. package/dist/delta/tracker.d.ts.map +1 -1
  39. package/dist/delta/tracker.js +40 -4
  40. package/dist/delta/tracker.js.map +1 -1
  41. package/dist/errors.d.ts +27 -0
  42. package/dist/errors.d.ts.map +1 -0
  43. package/dist/errors.js +44 -0
  44. package/dist/errors.js.map +1 -0
  45. package/dist/host.d.ts +254 -0
  46. package/dist/host.d.ts.map +1 -0
  47. package/dist/host.js +795 -0
  48. package/dist/host.js.map +1 -0
  49. package/dist/index.d.ts +17 -10
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +8 -8
  52. package/dist/index.js.map +1 -1
  53. package/dist/json.d.ts +9 -1
  54. package/dist/json.d.ts.map +1 -1
  55. package/dist/json.js.map +1 -1
  56. package/dist/link.d.ts +72 -0
  57. package/dist/link.d.ts.map +1 -0
  58. package/dist/link.js +169 -0
  59. package/dist/link.js.map +1 -0
  60. package/dist/loader.d.ts +4 -0
  61. package/dist/loader.d.ts.map +1 -0
  62. package/dist/loader.js +68 -0
  63. package/dist/loader.js.map +1 -0
  64. package/dist/manager.d.ts +115 -0
  65. package/dist/manager.d.ts.map +1 -0
  66. package/dist/manager.js +319 -0
  67. package/dist/manager.js.map +1 -0
  68. package/dist/node.d.ts +3 -4
  69. package/dist/node.d.ts.map +1 -1
  70. package/dist/node.js +3 -2
  71. package/dist/node.js.map +1 -1
  72. package/dist/publish.d.ts +5 -0
  73. package/dist/publish.d.ts.map +1 -0
  74. package/dist/publish.js +40 -0
  75. package/dist/publish.js.map +1 -0
  76. package/dist/record.d.ts +320 -0
  77. package/dist/record.d.ts.map +1 -0
  78. package/dist/record.js +1292 -0
  79. package/dist/record.js.map +1 -0
  80. package/dist/registry.d.ts +25 -0
  81. package/dist/registry.d.ts.map +1 -0
  82. package/dist/registry.js +6 -0
  83. package/dist/registry.js.map +1 -0
  84. package/dist/reload.d.ts +118 -0
  85. package/dist/reload.d.ts.map +1 -0
  86. package/dist/reload.js +589 -0
  87. package/dist/reload.js.map +1 -0
  88. package/dist/remote.d.ts +70 -0
  89. package/dist/remote.d.ts.map +1 -0
  90. package/dist/remote.js +308 -0
  91. package/dist/remote.js.map +1 -0
  92. package/dist/schema.d.ts +21 -0
  93. package/dist/schema.d.ts.map +1 -0
  94. package/dist/schema.js +228 -0
  95. package/dist/schema.js.map +1 -0
  96. package/dist/state.d.ts +92 -0
  97. package/dist/state.d.ts.map +1 -0
  98. package/dist/{services/state.js → state.js} +28 -138
  99. package/dist/state.js.map +1 -0
  100. package/dist/types.d.ts +62 -262
  101. package/dist/types.d.ts.map +1 -1
  102. package/dist/types.js.map +1 -1
  103. package/dist/websocket.d.ts +16 -0
  104. package/dist/websocket.d.ts.map +1 -0
  105. package/dist/websocket.js +15 -0
  106. package/dist/websocket.js.map +1 -0
  107. package/dist/wire.d.ts +135 -0
  108. package/dist/wire.d.ts.map +1 -0
  109. package/dist/wire.js +112 -0
  110. package/dist/wire.js.map +1 -0
  111. package/docs/guide.md +750 -0
  112. package/docs/spec.md +261 -0
  113. package/docs/tutorial.md +909 -0
  114. package/package.json +10 -11
  115. package/src/delta/README.md +5 -3
  116. package/dist/api.d.ts +0 -20
  117. package/dist/api.d.ts.map +0 -1
  118. package/dist/api.js +0 -77
  119. package/dist/api.js.map +0 -1
  120. package/dist/bundler.d.ts +0 -6
  121. package/dist/bundler.d.ts.map +0 -1
  122. package/dist/bundler.js +0 -3
  123. package/dist/bundler.js.map +0 -1
  124. package/dist/facets/host.d.ts +0 -12
  125. package/dist/facets/host.d.ts.map +0 -1
  126. package/dist/facets/host.js +0 -763
  127. package/dist/facets/host.js.map +0 -1
  128. package/dist/facets/loader.d.ts +0 -3
  129. package/dist/facets/loader.d.ts.map +0 -1
  130. package/dist/facets/loader.js +0 -5
  131. package/dist/facets/loader.js.map +0 -1
  132. package/dist/node/bundle-loader.d.ts +0 -30
  133. package/dist/node/bundle-loader.d.ts.map +0 -1
  134. package/dist/node/bundle-loader.js +0 -377
  135. package/dist/node/bundle-loader.js.map +0 -1
  136. package/dist/node/bundle.d.ts +0 -26
  137. package/dist/node/bundle.d.ts.map +0 -1
  138. package/dist/node/bundle.js +0 -183
  139. package/dist/node/bundle.js.map +0 -1
  140. package/dist/node/manifest.d.ts +0 -36
  141. package/dist/node/manifest.d.ts.map +0 -1
  142. package/dist/node/manifest.js +0 -6
  143. package/dist/node/manifest.js.map +0 -1
  144. package/dist/node/package.d.ts +0 -15
  145. package/dist/node/package.d.ts.map +0 -1
  146. package/dist/node/package.js +0 -199
  147. package/dist/node/package.js.map +0 -1
  148. package/dist/services/consumer.d.ts +0 -11
  149. package/dist/services/consumer.d.ts.map +0 -1
  150. package/dist/services/consumer.js +0 -585
  151. package/dist/services/consumer.js.map +0 -1
  152. package/dist/services/errors.d.ts +0 -8
  153. package/dist/services/errors.d.ts.map +0 -1
  154. package/dist/services/errors.js +0 -22
  155. package/dist/services/errors.js.map +0 -1
  156. package/dist/services/handle.d.ts +0 -15
  157. package/dist/services/handle.d.ts.map +0 -1
  158. package/dist/services/handle.js +0 -101
  159. package/dist/services/handle.js.map +0 -1
  160. package/dist/services/instances.d.ts +0 -26
  161. package/dist/services/instances.d.ts.map +0 -1
  162. package/dist/services/instances.js +0 -140
  163. package/dist/services/instances.js.map +0 -1
  164. package/dist/services/loopback.d.ts +0 -5
  165. package/dist/services/loopback.d.ts.map +0 -1
  166. package/dist/services/loopback.js +0 -15
  167. package/dist/services/loopback.js.map +0 -1
  168. package/dist/services/provider.d.ts +0 -37
  169. package/dist/services/provider.d.ts.map +0 -1
  170. package/dist/services/provider.js +0 -521
  171. package/dist/services/provider.js.map +0 -1
  172. package/dist/services/state-codec.d.ts +0 -15
  173. package/dist/services/state-codec.d.ts.map +0 -1
  174. package/dist/services/state-codec.js +0 -133
  175. package/dist/services/state-codec.js.map +0 -1
  176. package/dist/services/state-internals.d.ts +0 -13
  177. package/dist/services/state-internals.d.ts.map +0 -1
  178. package/dist/services/state-internals.js +0 -10
  179. package/dist/services/state-internals.js.map +0 -1
  180. package/dist/services/state.d.ts +0 -28
  181. package/dist/services/state.d.ts.map +0 -1
  182. package/dist/services/state.js.map +0 -1
  183. package/dist/services/wire.d.ts +0 -63
  184. package/dist/services/wire.d.ts.map +0 -1
  185. package/dist/services/wire.js +0 -186
  186. package/dist/services/wire.js.map +0 -1
package/CHANGELOG.md ADDED
@@ -0,0 +1,20 @@
1
+ # Changelog
2
+
3
+ ## [1.2.0] - 2026-10-12
4
+
5
+ ### Breaking Changes
6
+
7
+ - Replaced the facet and service runtime with a typed host runtime: contracts (`local`, `remote`, `method`, `opaque`) are runtime values described with TypeBox schemas, objects are served by hosts (`createHost`, `defineFacet`, `defineRegistry`) and reached through handles, and hosts exchange JSON frames over memory, Node IPC, or WebSocket links (`memoryLinkPair`, `ipcLink`, `webSocketLink`, `acceptWebSockets`), with hot reload of plugins. See `docs/tutorial.md` and `docs/guide.md`
8
+ - Removed `defineService`, `createFacetHost`, `createRemoteServiceBinding`, `createStaticFacetLoader`, `combineFacetLoaders`, the remote-service endpoint, provider, error, and wire helpers, and their types. The old `defineFacet`, `Facet`, and `FacetLoader` are replaced by the new runtime's names of the same spelling
9
+ - Removed the `./bundler` export and the facet bundle loaders of `./node`; `./node` now exports the child host manager (`createHostManager`, `forkDriver`, `childControl`, `processLock`) and the facet module loader `nodeLoader`
10
+ - Removed `@earendil-works/pi-client`, `@earendil-works/pi-protocol`, and `@earendil-works/pi-server`, which served only the old runtime; they are no longer released
11
+
12
+ ### Changed
13
+
14
+ - `replicatedState`, `copyJson`, `isJsonValue`, `Context`, `JsonValue`, `JsonRepresentation`, `Draft`, and the replicated state types keep their names on the root export; `./context` and `./delta` are unchanged, and `./context` also exports the `Context` and `ContextKey` types
15
+ - Depends on `typebox` instead of `esbuild`
16
+
17
+ ### Fixed
18
+
19
+ - Fixed appending to an array costing a walk over the whole array: the delta tracker now plans inserts and removals that keep the remaining elements in order from the changed ranges alone, and `applyImmutable` builds an appended array in one copy instead of two. 32,000 one-item appends, one commit each, take 1.4 s instead of 11.3 s
20
+
package/README.md CHANGED
@@ -1,108 +1,69 @@
1
1
  # @earendil-works/chord
2
2
 
3
- Chord is an application-composition runtime for systems assembled from
4
- plugins/extensions. It provides facets, services, replicated state, and a
5
- pluggable remote-service boundary. It is developed as a standalone package in
6
- the Pi monorepo, but it is not a Pi package: it does not depend on any other Pi
7
- workspace package and can be used by unrelated applications.
3
+ Chord builds applications out of **objects** that live in several processes (a server, workers, browser tabs) and can be **hot reloaded** while they run.
8
4
 
9
- ## What Chord is for
5
+ - An **object** is state plus code at an address. It is created on demand and lives while someone holds it.
6
+ - Everyone gets a **handle**, never the object. Handles work the same locally and across processes, and survive reloads.
7
+ - Every object knows its **principal**: an owner and a guest get different objects, with different methods.
8
+ - **State** is observable JSON. Remote viewers get a snapshot followed by deltas.
9
+ - **Hot reload** swaps code while it runs. Work in flight finishes on the code it started with, state is kept, and incompatible changes migrate at a safe point.
10
10
 
11
- A single application feature may need to run in several environments: for
12
- example, an agent worker, a terminal UI, and a remote WebUI. Chord provides the
13
- generic machinery to write such extensions in a way that is both delightful for
14
- humans as well as agents.
11
+ Contracts are runtime values described with [TypeBox](https://github.com/sinclairzx81/typebox) schemas. Hosts exchange JSON frames over links: in memory, a Node IPC channel, or a WebSocket.
15
12
 
16
- The design has a few connected pieces:
13
+ ## Entry points
17
14
 
18
- - **Plugins** are synchronous setup units that declare the services they provide
19
- and require. After every plugin has declared its shape, a host validates the
20
- complete dependency graph, binds services, activates providers before consumers,
21
- and disposes resources in reverse dependency order. These units are called
22
- *facets*.
15
+ | Import | Contents | Runs in |
16
+ |--------|----------|---------|
17
+ | `@earendil-works/chord` | Contracts (`local`, `remote`, `method`, `opaque`), declarations (`cell`, `derivation`, `listener`, `resource`), hosts (`createHost`, `defineFacet`, `defineRegistry`), links (`memoryLinkPair`, `ipcLink`, `webSocketLink`, `acceptWebSockets`), errors (`ChordError`, `isChordError`, `forbidden`, `invalid`). Also the shared base: `Context`, `JsonValue`, `JsonRepresentation`, `Draft`, `copyJson`, `isJsonValue`, and `replicatedState` with its types. | Node and browsers |
18
+ | `@earendil-works/chord/node` | Child host processes: `createHostManager`, `forkDriver`, `childControl`, `processLock`; the facet module loader `nodeLoader`. | Node |
19
+ | `@earendil-works/chord/context` | Explicit cancellation and invocation values: `BACKGROUND_CONTEXT`, `withCancel`, `withAbortSignal`, `withoutAbortSignal`, `withContextValue`, `awaitWithContext`. | Node and browsers |
20
+ | `@earendil-works/chord/delta` | The transactional JSON tracker, structural diff, immutable apply, and the wire encoder/decoder for operation batches. | Node and browsers |
23
21
 
24
- - **Facets** are parts of a plugin. Each facet is bundled up separately and runs
25
- in the process or environment where it's supposed to run. You can use facets
26
- to split a plugin into separate pieces that need to be loaded into different
27
- processes and environments (think backend, browser, TUI etc.)
22
+ ## Quick start
28
23
 
29
- - **Services** are typed, stable tokens with either one provider (**singleton**)
30
- or dynamic keyed instances (**keyed**). A service can be process-local, with
31
- an unrestricted JavaScript contract, or remotely exposable. Consumers retain a
32
- stable facade while a provider disconnects or is replaced.
33
-
34
- - **Replicated state** exposes authoritative state to local and remote
35
- connected consumers. Producers publish atomic overlay transactions with
36
- `change(context, callback)`; consumers receive complete immutable values. Draft
37
- proxies exist only during the callback and become unusable afterward. Preparation
38
- materializes a structurally shared immutable candidate and one exact decoded
39
- operation batch, while each remote client/state stream owns independent path-codec
40
- state. Replicas become unready on disconnect or replacement until rehydrated.
41
-
42
- - **Delta tracking** records and coalesces operations over tracked plain JSON.
43
- It preserves common string and array operations, supports durable base
44
- batches, and validates untrusted operations as they are applied. Batches
45
- guarantee convergence but are not canonical or necessarily minimal.
46
-
47
- - **Remote service sources** advertise services available outside a facet host
48
- and open bindings for the services its facets require. Bindings carry logical
49
- calls and subscriptions through an application-supplied adapter. Chord
50
- requires strict-JSON arguments, results, snapshots, updates, and catalogues,
51
- but does not prescribe framing, routing, transport, or an application wire
52
- envelope. `JsonRepresentation<T>` derives a wire-safe type for application data
53
- with unknown payloads, while `isJsonValue()` validates received values at an
54
- adapter boundary. Symmetric RPC peers are planned as one optional
55
- implementation of this boundary.
56
-
57
- - **Context** Chord provides a Go-like context system for cancellation and
58
- invocation-scoped application values. Applications can carry permissions or
59
- telemetry through those values without Chord depending on either.
60
-
61
- The current runtime exports service tokens, singleton and keyed providers,
62
- remote bindings, replicated state, facet hosts, and facet loaders from
63
- `@earendil-works/chord`. Import public types and general runtime APIs from the
64
- package root. Context constants and functions live in
65
- `@earendil-works/chord/context` because their generic names should not pollute
66
- the root API.
67
- Chord-owned identifiers use the `chord.*` namespace and its reserved service
68
- prefix is `$chord.*`.
69
-
70
- ## Remote service adapters
24
+ ```ts
25
+ import { Type } from "typebox";
26
+ import { cell, createHost, method, remote } from "@earendil-works/chord";
27
+
28
+ // contract.ts: shared by provider and consumers
29
+ export const Counter = remote("demo.counter", {
30
+ version: 1,
31
+ input: Type.Object({ name: Type.String() }),
32
+ variants: {
33
+ default: {
34
+ state: { count: Type.Number() },
35
+ methods: { increment: method(Type.Object({ by: Type.Number() }), Type.Null()) },
36
+ },
37
+ },
38
+ });
71
39
 
72
- Chord owns its transport-independent service wire grammar. Consumer adapters
73
- use `createServiceCatalogueCall()`, `createServiceSubscribeCall()`, and
74
- `createServiceUnsubscribeCall()` for `$chord.service` control calls.
75
- `createRemoteServiceEndpoint()` handles those calls for one provider consumer,
76
- including subscription activation and cleanup. `parseServiceCall()`,
77
- `parseServiceCatalogue()`, and the decoded/wire snapshot and update parsers
78
- validate Chord semantics after an adapter has established a strict-JSON
79
- boundary. `RemoteServiceErrorCode` and `REMOTE_SERVICE_ERROR_CODES` define the
80
- service errors that may cross that boundary.
40
+ const host = createHost({ name: "server", principalKey: (p) => p.kind });
41
+
42
+ host.serve(Counter, ({ name }, _principal, self) => {
43
+ const state = self.state({ count: cell(Type.Number(), 0) });
44
+ return {
45
+ kind: "default",
46
+ count: state.count,
47
+ increment: async ({ by }, ctx) => {
48
+ state.count.change(ctx, (n) => n + by);
49
+ return null;
50
+ },
51
+ };
52
+ });
81
53
 
82
- Replicated state operations use one `createServiceStateEncoder()` at the
83
- provider side and one `createServiceStateDecoder()` at the consumer side for
84
- each subscription. Those registries create an independent Delta path dictionary
85
- for every instance/member state and reset it on replacement, unavailability,
86
- close, or fresh hydration. Applications may place these values inside any
87
- routing, request, response, or event envelope; Chord does not prescribe that
88
- outer protocol.
54
+ await host.root(async (ctx) => {
55
+ const clicks = ctx.get(Counter, { name: "clicks" }); // a handle; held until this root ends
56
+ clicks.count.subscribe((n) => console.log("count", n));
57
+ await clicks.increment({ by: 2 }, ctx); // logs 0, then 2
58
+ });
59
+ ```
89
60
 
90
- Provider subscriptions atomically capture a snapshot and buffer subsequent
91
- updates until activation. Activation and reentrant publication use the same FIFO.
92
- The provider retains at most 100 pending updates per subscription, excluding the
93
- running delivery. Adding update 101 replaces the entire pending queue with
94
- `{ type: "reset", snapshot }`: a current full subscription snapshot whose state
95
- members contain `[["r", value]]` and their new sequence baselines. It also captures
96
- current instance membership, so discarded spawn/close/replacement events cannot
97
- leave stale instances behind. Live keyed generations retain their existing handles.
98
- The reset carries the context of the publication that triggered overflow.
61
+ ## Documentation
99
62
 
100
- Consumers and codecs must handle this explicit reset before accepting subsequent
101
- ordinary deltas. A root operation in an ordinary `state` update does **not** permit
102
- a sequence gap. Resets restart the subscription's path dictionaries, and later
103
- deltas must be contiguous from each reset baseline. Transport adapters must
104
- preserve snapshot/reset/update order; they must not drop encoded delta batches or
105
- assume their own asynchronous queues are bounded by the provider's queue.
63
+ - [Tutorial](docs/tutorial.md): a shared todo list, built up section by section. Every example runs as `test/tutorial.test.ts`.
64
+ - [Usage guide](docs/guide.md): API reference for contracts, objects, state, roots, lifetime, plugins, hot reload, hosts, and links.
65
+ - [Specification](docs/spec.md): compatibility, lifetime, and reload semantics, plus known limitations and implementation decisions.
66
+ - [Delta guide](src/delta/README.md): ownership, lifecycle, operation, and replica contracts of `@earendil-works/chord/delta`.
106
67
 
107
68
  ## Tracking JSON deltas
108
69
 
@@ -131,16 +92,19 @@ freezing. `track(initial)`, `prepareReplace(value)`, `replicatedState(initial)`,
131
92
  `replace(context, value)` take ownership of alias-free strict-JSON roots without
132
93
  walking them. Callers must not mutate transferred or published data. Values placed
133
94
  through drafts are already copied, so that walk also rejects non-strict JSON before
134
- the draft changes. Published values are not frozen. In-process loopback consumers
135
- may share their containers with the provider; mutating a consumed value violates
136
- the contract and can corrupt authority. Clone or serialize at any mutable trust
137
- boundary.
95
+ the draft changes. Published values are not frozen. In-process consumers may share
96
+ their containers with the provider; mutating a consumed value violates the contract
97
+ and can corrupt authority. Clone or serialize at any mutable trust boundary.
138
98
 
139
- Replicated state provides the same model through a callback:
99
+ Replicated state provides the same model through a callback. `replicatedState` is a
100
+ standalone observable value, independent of hosts; Durable uses it for document,
101
+ view, and task-graph states:
140
102
 
141
103
  ```ts
104
+ import { replicatedState } from "@earendil-works/chord";
105
+
142
106
  const initial = { output: "", count: 0 };
143
- const status = env.replicatedState(initial); // transfers ownership of initial
107
+ const status = replicatedState(initial); // transfers ownership of initial
144
108
  status.change(context, (draft) => {
145
109
  draft.output += "done\n";
146
110
  draft.count += 1;
@@ -149,10 +113,13 @@ status.change(context, (draft) => {
149
113
 
150
114
  A successful `change()` publishes exactly one atomic revision. If its callback
151
115
  throws, the original value and sequence remain unchanged. Draft handles become
152
- unusable when the callback returns. Chord emits string append and front-truncate
153
- operations, array splices and permutations, sets, and deletes; large edit sets may
154
- fold into a complete replacement. Remote connection plumbing encodes each batch
155
- independently for every client/state pairing.
116
+ unusable when the callback returns. The tracker emits string append and
117
+ front-truncate operations, array splices and permutations, sets, and deletes; large
118
+ edit sets may fold into a complete replacement.
119
+
120
+ `replicatedState(source, { onError })` instead attaches a publication-only state to
121
+ an authoritative revision source (`ReplicatedStateSource`): it captures a snapshot,
122
+ then publishes every committed frame in cursor order.
156
123
 
157
124
  ### Public state subscriptions
158
125
 
@@ -166,99 +133,15 @@ Each subscription retains at most 100 pending complete-value deliveries, excludi
166
133
  the running callback. On overflow, only the newest pending value, context, and
167
134
  delivery metadata are retained; a not-yet-started initial hydration is preserved.
168
135
  Thus public delivery sequences may skip. This is a frame-count policy, not a byte
169
- limit. Internal exact-operation subscriptions remain synchronous and receive every
170
- revision, independently of slow public callbacks.
136
+ limit.
171
137
 
172
138
  The returned unsubscribe function immediately discards pending work and prevents
173
139
  new callbacks. It neither aborts nor waits for the running callback. Synchronous
174
140
  throws and promise rejections are observed, reported, and do not stop other
175
- subscriptions or subsequent callbacks. Attached sources and remote bindings use
176
- their `onError` handler; local mutable states (and attached sources without a
177
- handler) report failures by throwing in a microtask. Unsubscribing does not hide a
178
- later rejection from the running callback.
179
-
180
- The standalone [Delta guide](src/delta/README.md) defines the complete ownership,
181
- lifecycle, operation, and replica contracts.
182
-
183
- ## Bundling and loading facets
184
-
185
- `@earendil-works/chord/bundler` uses esbuild to turn ESM or TypeScript application
186
- entries into independent, content-addressed CommonJS files. The package-level API
187
- reads plugin identity and build configuration from `package.json`, then applies
188
- facet path conventions supplied by the host application:
189
-
190
- ```json
191
- {
192
- "name": "@example/my-plugin",
193
- "version": "1.0.0",
194
- "type": "module",
195
- "peerDependencies": {
196
- "@earendil-works/chord": "^0.84.4"
197
- },
198
- "chord": {
199
- "facets": {
200
- "worker": "./src/custom-worker.ts",
201
- "presentation": false
202
- }
203
- }
204
- }
205
- ```
206
-
207
- ```ts
208
- import { bundleFacetPackage } from "@earendil-works/chord/bundler";
209
-
210
- await bundleFacetPackage({
211
- packagePath: "/path/to/my-plugin",
212
- outdir: "/application-owned/plugin-builds/my-plugin",
213
- defaultFacets: {
214
- worker: "src/worker.ts",
215
- presentation: "src/presentation.ts",
216
- },
217
- });
218
- ```
219
-
220
- Existing conventional files become entries unless `chord.facets` overrides or
221
- disables them. Peer dependencies are externalized and resolved against the host
222
- when loading. Chord never installs dependencies or runs package lifecycle
223
- scripts. `bundleFacets()` remains available as the lower-level API for callers
224
- that already have explicit plugin identity and entry mappings.
225
-
226
- The output directory contains one `.cjs` file per entry plus
227
- `chord-facets.json`. Load one application-selected entry through the Node-only
228
- loader:
229
-
230
- ```ts
231
- import { createFacetBundleLoader } from "@earendil-works/chord/node";
232
-
233
- const loader = createFacetBundleLoader({
234
- manifestPath: "/application-owned/plugin-builds/my-plugin/chord-facets.json",
235
- entry: "worker",
236
- resolveExternal: (specifier) => import.meta.resolve(specifier),
237
- });
238
- const loaded = await loader.load();
239
- ```
240
-
241
- Each `load()` verifies SHA-256 integrity and compiles the CommonJS body directly
242
- with `node:vm` instead of putting the plugin into Node's CommonJS or ESM module
243
- cache. Externals are resolved by the host and loaded through a restricted
244
- `require`; esbuild lowers dynamic imports so they use the same path. Disposing a
245
- retired generation releases the loader's facet references, making its compiled
246
- code eligible for garbage collection once plugin-owned resources are also gone.
247
-
248
- For transport to another Node host, `readFacetBundleArtifact()` packages one
249
- verified manifest entry with its source, and `createFacetBundleArtifactLoader()`
250
- materializes fresh temporary generations while resolving externals against the
251
- receiving host.
252
-
253
- To reload, load a candidate, pass its facets to `FacetHost.reload()`, dispose the
254
- candidate on failure, and dispose the retired `LoadedFacets` only after a
255
- successful cutover. The host activates and validates the candidate while the
256
- currently active providers remain routed, then replaces each singleton directly
257
- without an unavailable interval. Stable service handles therefore do not become
258
- disconnected during an ordinary reload. Keyed instances
259
- remain incarnation-specific and replacements receive fresh generations. The
260
- bundler writes a complete temporary directory before replacing the previous
261
- output, so loaders do not observe partially built generations.
141
+ subscriptions or subsequent callbacks. Attached sources use their `onError`
142
+ handler; mutable states (and attached sources without a handler) report failures
143
+ by throwing in a microtask. Unsubscribing does not hide a later rejection from the
144
+ running callback.
262
145
 
263
- See [PLANNING.md](PLANNING.md) for the broader RPC and generation-loading
264
- architecture.
146
+ Cells of Chord objects and their remote readables have their own subscription
147
+ model; see [State](docs/guide.md#state) in the usage guide.
package/dist/cell.d.ts ADDED
@@ -0,0 +1,101 @@
1
+ import type { TSchema } from "typebox";
2
+ import { type Draft } from "./delta/index.ts";
3
+ import { ChordError } from "./errors.ts";
4
+ import type { Ctx, Readable, StateEvent } from "./types.ts";
5
+ export interface SourceOptions {
6
+ readonly dev: boolean;
7
+ readonly report: (error: unknown) => void;
8
+ }
9
+ /** Checks that `ctx` may write a record's cells now; throws otherwise. */
10
+ export type WriteGuard = (ctx: Ctx) => void;
11
+ /** Fans out values and delta events; listener errors are reported, not thrown to the writer. */
12
+ declare class Fanout<T> {
13
+ #private;
14
+ constructor(report: (error: unknown) => void);
15
+ subscribe(listener: (value: T) => void, current: T): () => void;
16
+ events(listener: (event: StateEvent<T>) => void, current: T): () => void;
17
+ /**
18
+ * Queues delivery to the listeners subscribed now. A listener that subscribes later already got this revision in
19
+ * its snapshot; one that unsubscribes before delivery is skipped.
20
+ */
21
+ emit(event: StateEvent<T>): void;
22
+ }
23
+ /** A record-owned state source (cell or derivation). Writes recompute dependents atomically. */
24
+ export declare abstract class Source<T> implements Readable<T> {
25
+ readonly key: string;
26
+ readonly seq: number;
27
+ readonly dependents: Set<Derived<unknown>>;
28
+ /** Schemas of remote state members currently exposing this source (checked per write in dev mode). */
29
+ schemas: readonly TSchema[];
30
+ protected readonly options: SourceOptions;
31
+ protected readonly fanout: Fanout<T>;
32
+ constructor(key: string, options: SourceOptions);
33
+ abstract get value(): T;
34
+ subscribe(listener: (value: T) => void): () => void;
35
+ events(listener: (event: StateEvent<T>) => void): () => void;
36
+ /** Per-write validation, dev mode only. */
37
+ validate(value: unknown): void;
38
+ protected ownSchemas(): readonly TSchema[];
39
+ }
40
+ /** A state cell holding any JSON value, validated against its schema at creation and (dev mode) on writes. */
41
+ export declare class Cell<T> extends Source<T> {
42
+ #private;
43
+ /** Replaced at a reload commit that changes the schema (state-version bump). */
44
+ schema: TSchema;
45
+ /** `initial` is always validated against `schema`; ownership moves to the cell. */
46
+ constructor(key: string, schema: TSchema, initial: T, guard: WriteGuard, options: SourceOptions);
47
+ protected ownSchemas(): readonly TSchema[];
48
+ get value(): T;
49
+ /**
50
+ * Mutates a draft (only mutated paths are published), or returns a new value (`n => n + 1`). A returned value is
51
+ * always validated against the schema. The draft is unusable after `fn` returns.
52
+ */
53
+ change(ctx: Ctx, fn: (draft: Draft<T>) => T): void;
54
+ change(ctx: Ctx, fn: (draft: Draft<T>) => void): void;
55
+ /**
56
+ * Replaces the whole value. Ownership of `value` moves to the cell: never mutate it afterwards.
57
+ *
58
+ * Subscribers get a structural diff (`diffRevisions`) when `diff` is true, or by default when the new value shares
59
+ * containers with the old one (a new immutable revision of the same state, e.g. mirrored from another store);
60
+ * otherwise a single `r` op. The resulting value is the same either way; only the ops differ.
61
+ */
62
+ replace(ctx: Ctx, value: T, options?: {
63
+ readonly diff?: boolean;
64
+ }): void;
65
+ /**
66
+ * Installs a migrated value and its schema (reload commit). The caller validated the value and the derivations
67
+ * over it against every schema that will apply, so nothing is checked here. Subscribers receive a reset.
68
+ */
69
+ reset(value: T, schema: TSchema): void;
70
+ }
71
+ export type Sources = readonly Readable<unknown>[];
72
+ export type SourceValues<S extends Sources> = {
73
+ [K in keyof S]: S[K] extends Readable<infer T> ? T : never;
74
+ };
75
+ /**
76
+ * A synchronous derivation. Sources are record cells/derivations (recomputed atomically with the write) or other
77
+ * readables such as handle state members (recomputed when they publish). Subscribers receive diffs.
78
+ */
79
+ export declare class Derived<T> extends Source<T> {
80
+ #private;
81
+ readonly sources: readonly Readable<unknown>[];
82
+ constructor(key: string, sources: Sources, fn: (...values: never[]) => unknown, options: SourceOptions);
83
+ get value(): T;
84
+ /**
85
+ * Computes the first value (or takes it from `precomputed`, a reload's validated dry run) and starts following
86
+ * the sources (at install, never inside a factory).
87
+ */
88
+ attach(precomputed?: ReadonlyMap<unknown, unknown>): void;
89
+ detach(): void;
90
+ compute(read: (source: Readable<unknown>) => unknown, fn?: (...values: never[]) => unknown): T;
91
+ /** Replaces the function without recomputing (reload commit, before migrated cells are installed). */
92
+ setFn(fn: (...values: never[]) => unknown): void;
93
+ /** Recomputes and republishes, including dependents. Errors are reported (the source write already happened). */
94
+ refresh(): void;
95
+ install(next: T): void;
96
+ }
97
+ export declare const isSource: (value: unknown) => value is Cell<unknown> | Derived<unknown>;
98
+ /** Thrown by placeholders used before activation finished. */
99
+ export declare const notReady: (what: string) => ChordError;
100
+ export {};
101
+ //# sourceMappingURL=cell.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cell.d.ts","sourceRoot":"","sources":["../src/cell.ts"],"names":[],"mappings":"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"}