@earendil-works/chord 1.1.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +78 -0
- package/README.md +79 -195
- package/dist/cell.d.ts +122 -0
- package/dist/cell.d.ts.map +1 -0
- package/dist/cell.js +356 -0
- package/dist/cell.js.map +1 -0
- package/dist/cluster.d.ts +27 -0
- package/dist/cluster.d.ts.map +1 -0
- package/dist/cluster.js +230 -0
- package/dist/cluster.js.map +1 -0
- package/dist/connection.d.ts +270 -0
- package/dist/connection.d.ts.map +1 -0
- package/dist/connection.js +1216 -0
- package/dist/connection.js.map +1 -0
- package/dist/context/index.d.ts +12 -1
- package/dist/context/index.d.ts.map +1 -1
- package/dist/context/index.js.map +1 -1
- package/dist/contract.d.ts +208 -0
- package/dist/contract.d.ts.map +1 -0
- package/dist/contract.js +246 -0
- package/dist/contract.js.map +1 -0
- package/dist/declare.d.ts +33 -0
- package/dist/declare.d.ts.map +1 -0
- package/dist/declare.js +2 -0
- package/dist/declare.js.map +1 -0
- package/dist/delta/apply-immutable-trusted.js.map +1 -1
- package/dist/delta/diff.d.ts +1 -1
- package/dist/delta/diff.d.ts.map +1 -1
- package/dist/delta/diff.js.map +1 -1
- package/dist/delta/index.d.ts +4 -3
- package/dist/delta/index.d.ts.map +1 -1
- package/dist/delta/index.js +45 -0
- package/dist/delta/index.js.map +1 -1
- package/dist/delta/revision-validator.d.ts +1 -1
- package/dist/delta/revision-validator.d.ts.map +1 -1
- package/dist/delta/revision-validator.js.map +1 -1
- package/dist/delta/tracker.d.ts +1 -1
- package/dist/delta/tracker.d.ts.map +1 -1
- package/dist/delta/tracker.js +40 -4
- package/dist/delta/tracker.js.map +1 -1
- package/dist/errors.d.ts +28 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +59 -0
- package/dist/errors.js.map +1 -0
- package/dist/host.d.ts +348 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +1000 -0
- package/dist/host.js.map +1 -0
- package/dist/index.d.ts +17 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -8
- package/dist/index.js.map +1 -1
- package/dist/json.d.ts +9 -1
- package/dist/json.d.ts.map +1 -1
- package/dist/json.js.map +1 -1
- package/dist/link.d.ts +107 -0
- package/dist/link.d.ts.map +1 -0
- package/dist/link.js +355 -0
- package/dist/link.js.map +1 -0
- package/dist/loader.d.ts +4 -0
- package/dist/loader.d.ts.map +1 -0
- package/dist/loader.js +73 -0
- package/dist/loader.js.map +1 -0
- package/dist/manager.d.ts +115 -0
- package/dist/manager.d.ts.map +1 -0
- package/dist/manager.js +345 -0
- package/dist/manager.js.map +1 -0
- package/dist/node.d.ts +3 -4
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +3 -2
- package/dist/node.js.map +1 -1
- package/dist/object.d.ts +438 -0
- package/dist/object.d.ts.map +1 -0
- package/dist/object.js +1571 -0
- package/dist/object.js.map +1 -0
- package/dist/output.d.ts +85 -0
- package/dist/output.d.ts.map +1 -0
- package/dist/output.js +350 -0
- package/dist/output.js.map +1 -0
- package/dist/publish.d.ts +5 -0
- package/dist/publish.d.ts.map +1 -0
- package/dist/publish.js +40 -0
- package/dist/publish.js.map +1 -0
- package/dist/registry.d.ts +25 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +6 -0
- package/dist/registry.js.map +1 -0
- package/dist/reload.d.ts +126 -0
- package/dist/reload.d.ts.map +1 -0
- package/dist/reload.js +653 -0
- package/dist/reload.js.map +1 -0
- package/dist/remote.d.ts +165 -0
- package/dist/remote.d.ts.map +1 -0
- package/dist/remote.js +612 -0
- package/dist/remote.js.map +1 -0
- package/dist/schema.d.ts +21 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +228 -0
- package/dist/schema.js.map +1 -0
- package/dist/state.d.ts +92 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/{services/state.js → state.js} +28 -138
- package/dist/state.js.map +1 -0
- package/dist/types.d.ts +69 -258
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/websocket.d.ts +15 -0
- package/dist/websocket.d.ts.map +1 -0
- package/dist/websocket.js +16 -0
- package/dist/websocket.js.map +1 -0
- package/dist/wire.d.ts +216 -0
- package/dist/wire.d.ts.map +1 -0
- package/dist/wire.js +165 -0
- package/dist/wire.js.map +1 -0
- package/docs/guide.md +814 -0
- package/docs/spec.md +288 -0
- package/docs/tutorial.md +931 -0
- package/package.json +14 -10
- package/src/delta/README.md +5 -3
- package/dist/api.d.ts +0 -20
- package/dist/api.d.ts.map +0 -1
- package/dist/api.js +0 -77
- package/dist/api.js.map +0 -1
- package/dist/bundler.d.ts +0 -6
- package/dist/bundler.d.ts.map +0 -1
- package/dist/bundler.js +0 -3
- package/dist/bundler.js.map +0 -1
- package/dist/facets/host.d.ts +0 -12
- package/dist/facets/host.d.ts.map +0 -1
- package/dist/facets/host.js +0 -763
- package/dist/facets/host.js.map +0 -1
- package/dist/facets/loader.d.ts +0 -3
- package/dist/facets/loader.d.ts.map +0 -1
- package/dist/facets/loader.js +0 -5
- package/dist/facets/loader.js.map +0 -1
- package/dist/node/bundle-loader.d.ts +0 -30
- package/dist/node/bundle-loader.d.ts.map +0 -1
- package/dist/node/bundle-loader.js +0 -377
- package/dist/node/bundle-loader.js.map +0 -1
- package/dist/node/bundle.d.ts +0 -26
- package/dist/node/bundle.d.ts.map +0 -1
- package/dist/node/bundle.js +0 -183
- package/dist/node/bundle.js.map +0 -1
- package/dist/node/manifest.d.ts +0 -36
- package/dist/node/manifest.d.ts.map +0 -1
- package/dist/node/manifest.js +0 -6
- package/dist/node/manifest.js.map +0 -1
- package/dist/node/package.d.ts +0 -15
- package/dist/node/package.d.ts.map +0 -1
- package/dist/node/package.js +0 -199
- package/dist/node/package.js.map +0 -1
- package/dist/services/consumer.d.ts +0 -11
- package/dist/services/consumer.d.ts.map +0 -1
- package/dist/services/consumer.js +0 -585
- package/dist/services/consumer.js.map +0 -1
- package/dist/services/errors.d.ts +0 -8
- package/dist/services/errors.d.ts.map +0 -1
- package/dist/services/errors.js +0 -22
- package/dist/services/errors.js.map +0 -1
- package/dist/services/handle.d.ts +0 -15
- package/dist/services/handle.d.ts.map +0 -1
- package/dist/services/handle.js +0 -101
- package/dist/services/handle.js.map +0 -1
- package/dist/services/instances.d.ts +0 -26
- package/dist/services/instances.d.ts.map +0 -1
- package/dist/services/instances.js +0 -140
- package/dist/services/instances.js.map +0 -1
- package/dist/services/loopback.d.ts +0 -5
- package/dist/services/loopback.d.ts.map +0 -1
- package/dist/services/loopback.js +0 -15
- package/dist/services/loopback.js.map +0 -1
- package/dist/services/provider.d.ts +0 -37
- package/dist/services/provider.d.ts.map +0 -1
- package/dist/services/provider.js +0 -521
- package/dist/services/provider.js.map +0 -1
- package/dist/services/state-codec.d.ts +0 -15
- package/dist/services/state-codec.d.ts.map +0 -1
- package/dist/services/state-codec.js +0 -133
- package/dist/services/state-codec.js.map +0 -1
- package/dist/services/state-internals.d.ts +0 -13
- package/dist/services/state-internals.d.ts.map +0 -1
- package/dist/services/state-internals.js +0 -10
- package/dist/services/state-internals.js.map +0 -1
- package/dist/services/state.d.ts +0 -28
- package/dist/services/state.d.ts.map +0 -1
- package/dist/services/state.js.map +0 -1
- package/dist/services/wire.d.ts +0 -63
- package/dist/services/wire.d.ts.map +0 -1
- package/dist/services/wire.js +0 -186
- package/dist/services/wire.js.map +0 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [1.3.0] - 2026-10-15
|
|
4
|
+
|
|
5
|
+
### Breaking Changes
|
|
6
|
+
|
|
7
|
+
- Terminology follows the concepts: a reference (what `get` returns) is `Ref<C>` (was `Handle<C>`), its status `RefStatus` (was `HandleStatus`); what a factory returns is `ImplementationOf<C>` (was `BehaviorOf<C>`); a resource's options are a `ResourceDeclaration` (was `ResourceSpec`); epochs are code versions: `Ctx.codeVersion`, `RootOptions.codeVersion`, `Host.codeVersion`/`previousCodeVersion`, and the `codeVersion` fields of frames and reload reports (all were `epoch`). Docs and messages say object (was record) and reference (was handle and hold); the `hold` and `release` frame types keep their names
|
|
8
|
+
- A remote object's variant is named `variant` (was `kind`): factories return `{ variant: "editor", … }`, references expose `ref.variant` and `shape.variant`, and the `shape` frame carries `variant`. `variant` is reserved as a member name; `kind` no longer is
|
|
9
|
+
- Links have a protocol version (`PROTOCOL_VERSION`, now 2), announced in `hello`. Peers of different versions do not talk: the side that notices closes the link with reason `protocol-version <n>`, and both sides report it through `LinkOptions.onProtocolMismatch`. Peers without a version (older Chord) are refused
|
|
10
|
+
- `serve(…, { lifetime })` replaces `eager`: `"referenced"` (the default) creates an object at the first `get` of its address and disposes it after the last reference is released (plus `linger`); `"host"` creates the type's one object when its facet loads and keeps it until the host closes or a reload removes its facet. `lifetime: "host"` is refused at `serve` for remote types, for inputs that do not accept `{}` and together with `linger`; every `get` of such a type reaches its one object, created for the host principal, and another input is `invalid/input`. `eager: { input }` and the extra objects host-principal `get`s with other inputs created are gone. Only `"host"` objects may register (reason `invalid/not_host_lifetime`, was `not_eager`). `LoadOptions.awaitHostObjects` replaces `awaitEager`
|
|
11
|
+
- Declarations are calls by name: `self.state(name, schema, initial, { version?, migrate? })` returns the cell, `self.resource(name, { version, deps?, exclusive?, open, close? })` the resource (`rev` is now `version`), `self.on(name, source, [select,] handler)` declares a listener; `self.derive(sources, fn)` and `self.task(fn)` take no name. `cell()`, `derivation()`, `resource()`, `listener()`, `self.resources`, `self.listen`, `self.tasks` and the declaration maps are gone, and so are the types `CellDecl`, `ResourceDecl`, `DerivationDecl`, `ListenerDecl`. A repeated name of one kind in a run is `invalid/duplicate`
|
|
12
|
+
- State versions belong to cells: `ServeOptions.state` is gone; a cell's `version` (default 1) and `migrate(old, from)` replace it. A raised version migrates only that cell; a lowered one refuses the reload
|
|
13
|
+
- A derivation is new at every reload (computed at the commit) instead of kept with its function replaced. A listener whose state source a reload replaced subscribes again and gets the current value again: listeners on state must reconcile. Listeners on state subscribe when they activate, so they get the value current then
|
|
14
|
+
- `method(args?, result?)`: without arguments a method is called `ref.name(ctx?)` and implemented `(ctx) => …`; without a result it is void (callers get `undefined`). Descriptions record both by leaving `args`/`result` out (fingerprints of such methods change), `call` frames omit `args` and `result` frames `value` for them. Coverage requires both sides to agree on arguments; a void consumer accepts a result
|
|
15
|
+
- Remote contracts declare where their objects live, and callers no longer name a host: `remote(name, { …, host: hostNamed("server") })` or `host: hostFamily("session:", "session")` (the host `session:` + the input's required string field `session`, checked by TypeScript). `host` is required on remote contracts; `GetOptions.at` is removed. A `get` normalizes the input, resolves the host, and acquires locally or over the link to it. `serve` refuses a type whose placement does not include the host (also at a reload), a host refuses a hold whose input its own contract places elsewhere (`invalid/placement`), and a relay refuses a hold whose target disagrees with its contract token. The placement is part of the description and fingerprint; coverage requires equal placements, and `accepts` adapters do not bridge placements. Remote references are addressed by type, normalized input and principal, without a host name. `PROTOCOL_VERSION` is 3 (remote tokens carry `host`; a hold's input is normalized). New exports: `hostNamed`, `hostFamily`, `Placement`, `PlacementFor`, `HostNamed`, `HostFamily`
|
|
16
|
+
- `FacetApi` has a `signal` (below): code that builds a `FacetApi` by hand, for example to wrap `serve`, must pass it on
|
|
17
|
+
- Principals are typed and supplied by the app: `createHost<P>`, `Host<P>`, `defineFacet<P>`, `FacetApi<P>`, `Serve<P>`, `Factory<C, P>`, `Self<P>`, `Ctx<P>`, `GetOptions<P>`, `RootOptions<P>`, `Route<P>`, `AcceptOptions<P>` and `PrincipalMatch<P>` take the app's principal type (default `unknown`), so factories, methods, listeners and tasks narrow on it without casts. Chord invents no principals any more: `createHost({ self })` (required) is the principal of the host's own code, replacing the built-in `hostPrincipal` (`{ host: true }`, removed); `connect(link, { principal })` (required) is what the parent's own requests act as, replacing `{ host: <parent name> }`; `Route.principal` is required, replacing the default `{ host: <host name> }`. `HostOptions.upstream` is `{ link, principal, ...LinkOptions }` (was a link, with `upstreamOptions`). The host's own principal is recognized by `principalKey`, not object identity. The `Principal` type is gone; `ConnectOptions` and `PrincipalPattern` are new
|
|
18
|
+
- `serve(type, factory, { lifetime: "host" })` compiles only for a local type whose input has no required fields, and without `linger`; `serve` still refuses the rest at runtime. `ServeOptions` is a union of `ReferencedServeOptions` and `HostServeOptions`, and `serve` takes `ServeOptionsFor<C>`
|
|
19
|
+
- An awaited reference has its state, local or remote. `ctx.get` and `host.get` are async: they resolve once the reference is live and every state member of its variant has its first snapshot, and reject with the activation error, `aborted` (the ctx aborted or its call settled; `host.get`'s `signal`) or `unavailable/ready_timeout` after `HostOptions.readyTimeout` (default 60000 ms), releasing only that reference. `host.hold` is the synchronous form (possibly `loading`) for UI runtimes; `ref.ready` stays. `self.get` stays synchronous: an object goes live only once its references are ready and hydrated, and the state it exposes or derives from (also forwarded from a remote reference held another way) has its values; activation and reload prepare wait at most `readyTimeout` (a reference cycle across hosts fails with a hint instead of hanging)
|
|
20
|
+
- State members are typed `T` (was `T | undefined`) and `StateSource<T>` is `Readable<T>`. Reading state before the first hydration throws `not_ready`, outside the current variant `not_in_variant`, after the reference ended its end error. A remote reference is a union discriminated by `variant`: another variant's members are reachable only after narrowing. `MembersOf` is gone. A listener added before a member is readable gets its snapshot then (no `undefined` snapshot first)
|
|
21
|
+
- A remote reference stays subscribed to its variant's state while held, with or without listeners (it no longer forgets its value when the last listener leaves). `GetOptions.state: false` makes a method-only reference: no state unless another reference to the object wants it, ready once live
|
|
22
|
+
- `Shape.codeVersion` and the `shape` frame's `codeVersion` are gone
|
|
23
|
+
- `PROTOCOL_VERSION` is 4: a `hold` implies the state of its variant (`state: false` opts out until `sub`; `unsub` stops it); the provider answers with units, a `shape` listing the members the consumer lacks (`state`) followed by their snapshots; `state` frames are addressed by hold and member (`hold`, `m`) with `as: "snap" | "reset" | "ops" | "error"` and `value` (absent: `undefined`); `sub`/`unsub` take a hold; `resync` asks for a member again; `again` marks a re-hold after a link loss; `origin` (replacing `client`) names the client connection a hold comes from
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- `@earendil-works/chord/output`: `OutputBuffer`, which keeps a head or tail window of a stream at a cost proportional to each chunk, `Progress`, which paces commits of a changing value by their size, plus `boundOutput`, `sanitizeOutput`, `characterEnd`, `utf8ByteLength`, and the `OutputLimits`, `BoundedOutput`, `OutputSlice`, and `OutputSkip` types. Moved from `@earendil-works/pi-durable`, which now uses them from here.
|
|
28
|
+
- Remote references to one object share one binding per host and key (type, normalized input identity, principal key, contract fingerprint): one hold, one stream per state member, calls over the same hold; every reference keeps its own status, observers and `release`. `HostOptions.linger` (`{ types: { [typeName]: ms }, cap }`, cap default 4) keeps a binding held and hydrated after its last reference is released, so a reference taken again meanwhile costs nothing on the link
|
|
29
|
+
- Holds from clients carry an opaque `origin`: the id the accepting host gave the client connection, forwarded by relays and honored only from the parent link, so a host can tell client connections apart; a disconnect releases that client's holds at once
|
|
30
|
+
- Flow control by units: the first unit of a new reference passes the window, so a `get` never waits behind other references' state; units for reloads and re-holds wait, built lazily from the current values; waiting ops are capped per link (`LinkOptions.pendingCap`, default four windows), beyond which the oldest streams become resets (a relay asks downstream with `resync`)
|
|
31
|
+
- `StateOptions`, `EventOf`, `ValueOf` and `Lifetime` types
|
|
32
|
+
- `self.state(name, schema, { init }, options?)`: a cell whose first value comes from its source. `init(ctx)` (may be async) runs once the object's resources are open and its references live, before the object goes live, so no method, listener, task or subscriber sees another value; a throw or an invalid value fails the activation. A reload keeps the cell without running `init`; a cell a reload adds runs it while the reload prepares (a failure refuses the reload). Until then the cell's `value` throws `not_ready`. `StateInit` type
|
|
33
|
+
- Links are flow controlled end to end: peers acknowledge what they processed (`ack` frames, announced with `acks` in `hello`), and while a window of frames is unacknowledged (`window` in `LinkOptions`, default 256 KiB), state frames wait at the sender, coalesced per subscription, instead of queuing in front of call results. A stalled peer costs a bounded slot per subscription; a slot that outgrows it becomes a reset. Calls, results and control frames are never held back, so under load a result can arrive before state changes made before it
|
|
34
|
+
- `maxCalls` in `AcceptOptions` limits the calls a peer may have in progress on a link; further ones are answered at once with `unavailable` (reason `too_many_calls`) and the link stays open
|
|
35
|
+
- `FacetApi.signal` aborts once that run of the facet is no longer installed: a committed reload replaced or removed it, the reload that loaded it aborted, or the host closed. While a reload prepares, the installed and the candidate run are both live
|
|
36
|
+
- `HostOptions.taskStopDeadline` (default 5000 ms) bounds how long a reload waits for aborted tasks before it starts their replacements
|
|
37
|
+
- Links deflate call arguments, results and state snapshots whose JSON exceeds `PACK_THRESHOLD` (64K characters): over IPC as a base64 field, over WebSockets as a binary message. Relaying hosts forward the deflated bytes unread; the host that uses the payload inflates it. `webSocketLink(socket, { compress, acceptPacked })` (`WebSocketLinkOptions`) controls sending and accepting packed frames (servers should refuse them from untrusted clients), and `inflateMessage` turns a packed WebSocket message back into a text frame
|
|
38
|
+
|
|
39
|
+
### Changed
|
|
40
|
+
|
|
41
|
+
- `Link.send` returns the encoded frame's size as sent (deflated payloads count as sent) when the transport knows it (memory and WebSocket links); flow control counts these sizes, so links that return nothing (IPC, custom links) are not windowed
|
|
42
|
+
- A reload starts an object's new tasks only after its aborted ones finished (at most `taskStopDeadline`, then anyway, reported); before, the old and the new run of a task worked side by side
|
|
43
|
+
- When a reload forces the retirement of the previous code version, it first aborts the roots still running on it (their `ctx.signal`, reason `superseded`) and gives them up to 1 s to stop before it closes what only that code used; a call with such a ctx is `superseded`. Before, resources were closed under running roots, which were never told
|
|
44
|
+
|
|
45
|
+
### Fixed
|
|
46
|
+
|
|
47
|
+
- `nodeLoader` registers every facet's directory before importing any, so a facet that imports a file of a facet listed after it gets the same module copy as that facet; before, the load order could produce duplicates.
|
|
48
|
+
- A link that is already closed when a host accepts or routes to it is no longer kept as a connection: before, a route that returned a dead link (a child process that exited before its link was wrapped) made every later call to that host name fail with `unavailable` until restart, and leaked the connection's ping timer.
|
|
49
|
+
- Fixed `processLock()` failing with `listen EINVAL` on macOS and the BSDs when the temporary directory is too deep for a socket path (sun_path holds 104 bytes); the lock socket then lives in a private per-user directory, `/tmp/chord2-<uid>`
|
|
50
|
+
- A child host whose hello arrived while a cluster reload ran was marked in step without taking part, so it stayed on the old code version and every call to it failed `superseded` until it reconnected. It is now synced or reconciled when the reload ends; a child already behind is fenced at once, so its roots' calls up are admitted meanwhile. A reconciliation that is running when a reload starts likewise syncs its child only after that reload, against the committed code version
|
|
51
|
+
- A child host retired its previous code version as soon as a cluster reload's `prepare` arrived, before its parent did, so the parent's old roots got `superseded` on it. The coordinator now retires before it sends `prepare`, a child waits for its parent's `retire` (at most `retireDeadline`), and a child coming in step (or about to be reconciled) is told which code versions the parent retired before
|
|
52
|
+
- Disposed objects stayed reachable from the host until an object with the same address was created again (every distinct guest, session or input leaked one object, its references and the code they bind)
|
|
53
|
+
- References kept every code version's services, and with them the factory code of every reload, reachable for as long as they lived (held references, objects of lifetime `"host"`); old code generations could never be collected
|
|
54
|
+
- Call, task and root contexts were linked to long-lived signals with `AbortSignal.any`; Node keeps such dependants reachable while they have abort listeners, so a listener on `ctx.signal` kept its closure (and its code generation) alive for the object's lifetime. Contexts now detach when they close
|
|
55
|
+
- IPC links and the host manager's control channel wrote to a peer process that had exited without a callback, so the `EPIPE` Node reports for a write in the gap between the exit and `disconnect` (an `ack` sent just after a worker shut down) became an unhandled `error` event that crashed the process. The link now ends on a failed write (reason `ipc send failed: EPIPE`), and control messages to a gone peer are dropped
|
|
56
|
+
- A relayed call ran under the origin's cancel signal only, so a forced retirement of the relaying host could not cancel it before closing the old code's resources; it now follows its root's signal
|
|
57
|
+
- A resource kept across a reload closed with the `close` of the factory run that first opened it, and kept that run's code reachable; it now adopts the newest declaration at commit
|
|
58
|
+
- `forkDriver` passed its own `CHORD2_LOCK` to forked children, so a child host that forked its own children made those without a lock of their own fail with `locked`
|
|
59
|
+
- A call through a remote reference accepted a ctx from another host, or an object not made by Chord, and carried its root over the link; it now rejects it with `invalid/foreign_ctx` like a local reference ([#10959](https://github.com/earendil-works/pi/issues/10959))
|
|
60
|
+
|
|
61
|
+
## [1.2.0] - 2026-10-12
|
|
62
|
+
|
|
63
|
+
### Breaking Changes
|
|
64
|
+
|
|
65
|
+
- 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`
|
|
66
|
+
- 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
|
|
67
|
+
- 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`
|
|
68
|
+
- 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
|
|
69
|
+
|
|
70
|
+
### Changed
|
|
71
|
+
|
|
72
|
+
- `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
|
|
73
|
+
- Depends on `typebox` instead of `esbuild`
|
|
74
|
+
|
|
75
|
+
### Fixed
|
|
76
|
+
|
|
77
|
+
- 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
|
|
78
|
+
|
package/README.md
CHANGED
|
@@ -1,108 +1,70 @@
|
|
|
1
1
|
# @earendil-works/chord
|
|
2
2
|
|
|
3
|
-
Chord
|
|
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
|
-
|
|
5
|
+
- An **object** is state plus code at an address. It is created on demand and lives while someone references it.
|
|
6
|
+
- Everyone gets a **reference**, never the object. References 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
|
-
|
|
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
|
-
|
|
13
|
+
## Entry points
|
|
17
14
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
15
|
+
| Import | Contents | Runs in |
|
|
16
|
+
|--------|----------|---------|
|
|
17
|
+
| `@earendil-works/chord` | Contracts (`local`, `remote`, `method`, `opaque`), declarations (through the factory's `self`: `state`, `derive`, `resource`, `on`, `task`), 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 |
|
|
21
|
+
| `@earendil-works/chord/output` | Bounded output streams and paced progress: `OutputBuffer` (head or tail window of a stream, `OutputLimits`, `OutputSkip`), `boundOutput`, `sanitizeOutput`, `utf8ByteLength`, and `Progress` (adaptive commits of a changing value). | Node and browsers |
|
|
23
22
|
|
|
24
|
-
|
|
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.)
|
|
23
|
+
## Quick start
|
|
28
24
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
25
|
+
```ts
|
|
26
|
+
import { Type } from "typebox";
|
|
27
|
+
import { createHost, hostNamed, method, remote } from "@earendil-works/chord";
|
|
28
|
+
|
|
29
|
+
// contract.ts: shared by provider and consumers
|
|
30
|
+
export const Counter = remote("demo.counter", {
|
|
31
|
+
version: 1,
|
|
32
|
+
input: Type.Object({ name: Type.String() }),
|
|
33
|
+
host: hostNamed("server"), // where its objects live; callers never name a host
|
|
34
|
+
variants: {
|
|
35
|
+
default: {
|
|
36
|
+
state: { count: Type.Number() },
|
|
37
|
+
methods: { increment: method(Type.Object({ by: Type.Number() })) },
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
});
|
|
71
41
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
42
|
+
const host = createHost({ name: "server", self: { kind: "server" } });
|
|
43
|
+
|
|
44
|
+
host.serve(Counter, ({ name }, _principal, self) => {
|
|
45
|
+
const count = self.state("count", Type.Number(), 0);
|
|
46
|
+
return {
|
|
47
|
+
variant: "default",
|
|
48
|
+
count,
|
|
49
|
+
increment: async ({ by }, ctx) => {
|
|
50
|
+
count.change(ctx, (n) => n + by);
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
});
|
|
81
54
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
outer protocol.
|
|
55
|
+
await host.root(async (ctx) => {
|
|
56
|
+
const clicks = await ctx.get(Counter, { name: "clicks" }); // ready, with its state; released when this root ends
|
|
57
|
+
clicks.count.subscribe((n) => console.log("count", n));
|
|
58
|
+
await clicks.increment({ by: 2 }, ctx); // logs 0, then 2
|
|
59
|
+
});
|
|
60
|
+
```
|
|
89
61
|
|
|
90
|
-
|
|
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.
|
|
62
|
+
## Documentation
|
|
99
63
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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.
|
|
64
|
+
- [Tutorial](docs/tutorial.md): a shared todo list, built up section by section. Every example runs as `test/tutorial.test.ts`.
|
|
65
|
+
- [Usage guide](docs/guide.md): API reference for contracts, objects, state, roots, lifetime, plugins, hot reload, hosts, and links.
|
|
66
|
+
- [Specification](docs/spec.md): compatibility, lifetime, and reload semantics, plus known limitations and implementation decisions.
|
|
67
|
+
- [Delta guide](src/delta/README.md): ownership, lifecycle, operation, and replica contracts of `@earendil-works/chord/delta`.
|
|
106
68
|
|
|
107
69
|
## Tracking JSON deltas
|
|
108
70
|
|
|
@@ -131,16 +93,19 @@ freezing. `track(initial)`, `prepareReplace(value)`, `replicatedState(initial)`,
|
|
|
131
93
|
`replace(context, value)` take ownership of alias-free strict-JSON roots without
|
|
132
94
|
walking them. Callers must not mutate transferred or published data. Values placed
|
|
133
95
|
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
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
boundary.
|
|
96
|
+
the draft changes. Published values are not frozen. In-process consumers may share
|
|
97
|
+
their containers with the provider; mutating a consumed value violates the contract
|
|
98
|
+
and can corrupt authority. Clone or serialize at any mutable trust boundary.
|
|
138
99
|
|
|
139
|
-
Replicated state provides the same model through a callback
|
|
100
|
+
Replicated state provides the same model through a callback. `replicatedState` is a
|
|
101
|
+
standalone observable value, independent of hosts; Durable uses it for document,
|
|
102
|
+
view, and task-graph states:
|
|
140
103
|
|
|
141
104
|
```ts
|
|
105
|
+
import { replicatedState } from "@earendil-works/chord";
|
|
106
|
+
|
|
142
107
|
const initial = { output: "", count: 0 };
|
|
143
|
-
const status =
|
|
108
|
+
const status = replicatedState(initial); // transfers ownership of initial
|
|
144
109
|
status.change(context, (draft) => {
|
|
145
110
|
draft.output += "done\n";
|
|
146
111
|
draft.count += 1;
|
|
@@ -148,11 +113,14 @@ status.change(context, (draft) => {
|
|
|
148
113
|
```
|
|
149
114
|
|
|
150
115
|
A successful `change()` publishes exactly one atomic revision. If its callback
|
|
151
|
-
throws, the original value and sequence remain unchanged. Draft
|
|
152
|
-
unusable when the callback returns.
|
|
153
|
-
operations, array splices and permutations, sets, and deletes; large
|
|
154
|
-
fold into a complete replacement.
|
|
155
|
-
|
|
116
|
+
throws, the original value and sequence remain unchanged. Draft references become
|
|
117
|
+
unusable when the callback returns. The tracker emits string append and
|
|
118
|
+
front-truncate operations, array splices and permutations, sets, and deletes; large
|
|
119
|
+
edit sets may fold into a complete replacement.
|
|
120
|
+
|
|
121
|
+
`replicatedState(source, { onError })` instead attaches a publication-only state to
|
|
122
|
+
an authoritative revision source (`ReplicatedStateSource`): it captures a snapshot,
|
|
123
|
+
then publishes every committed frame in cursor order.
|
|
156
124
|
|
|
157
125
|
### Public state subscriptions
|
|
158
126
|
|
|
@@ -166,99 +134,15 @@ Each subscription retains at most 100 pending complete-value deliveries, excludi
|
|
|
166
134
|
the running callback. On overflow, only the newest pending value, context, and
|
|
167
135
|
delivery metadata are retained; a not-yet-started initial hydration is preserved.
|
|
168
136
|
Thus public delivery sequences may skip. This is a frame-count policy, not a byte
|
|
169
|
-
limit.
|
|
170
|
-
revision, independently of slow public callbacks.
|
|
137
|
+
limit.
|
|
171
138
|
|
|
172
139
|
The returned unsubscribe function immediately discards pending work and prevents
|
|
173
140
|
new callbacks. It neither aborts nor waits for the running callback. Synchronous
|
|
174
141
|
throws and promise rejections are observed, reported, and do not stop other
|
|
175
|
-
subscriptions or subsequent callbacks. Attached sources
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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.
|
|
142
|
+
subscriptions or subsequent callbacks. Attached sources use their `onError`
|
|
143
|
+
handler; mutable states (and attached sources without a handler) report failures
|
|
144
|
+
by throwing in a microtask. Unsubscribing does not hide a later rejection from the
|
|
145
|
+
running callback.
|
|
262
146
|
|
|
263
|
-
|
|
264
|
-
|
|
147
|
+
Cells of Chord objects and their remote readables have their own subscription
|
|
148
|
+
model; see [State](docs/guide.md#state) in the usage guide.
|
package/dist/cell.d.ts
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
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 an object'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
|
+
/**
|
|
24
|
+
* Whether a state source has its value (entry 8): a remote reference's member before its first snapshot has none, and
|
|
25
|
+
* neither has anything derived from or forwarding it. Sources without this symbol always have their value.
|
|
26
|
+
*/
|
|
27
|
+
export declare const HYDRATION: unique symbol;
|
|
28
|
+
export interface Hydration {
|
|
29
|
+
readonly hydrated: boolean;
|
|
30
|
+
/** Resolves once hydrated; rejects if the source ends first. */
|
|
31
|
+
whenHydrated(): Promise<void>;
|
|
32
|
+
}
|
|
33
|
+
/** The hydration of a readable, or undefined for a source that always has its value. */
|
|
34
|
+
export declare function hydrationOf(value: unknown): Hydration | undefined;
|
|
35
|
+
/** The hydrations of `sources` that are not hydrated yet. */
|
|
36
|
+
export declare const unhydrated: (sources: Iterable<unknown>) => Hydration[];
|
|
37
|
+
/** An object-owned state source (cell or derivation). Writes recompute dependents atomically. */
|
|
38
|
+
export declare abstract class Source<T> implements Readable<T> {
|
|
39
|
+
readonly key: string;
|
|
40
|
+
readonly seq: number;
|
|
41
|
+
readonly dependents: Set<Derived<unknown>>;
|
|
42
|
+
/** Schemas of remote state members currently exposing this source (checked per write in dev mode). */
|
|
43
|
+
schemas: readonly TSchema[];
|
|
44
|
+
protected readonly options: SourceOptions;
|
|
45
|
+
protected readonly fanout: Fanout<T>;
|
|
46
|
+
constructor(key: string, options: SourceOptions);
|
|
47
|
+
abstract get value(): T;
|
|
48
|
+
subscribe(listener: (value: T) => void): () => void;
|
|
49
|
+
events(listener: (event: StateEvent<T>) => void): () => void;
|
|
50
|
+
/** Per-write validation, dev mode only. */
|
|
51
|
+
validate(value: unknown): void;
|
|
52
|
+
protected ownSchemas(): readonly TSchema[];
|
|
53
|
+
}
|
|
54
|
+
/** A state cell holding any JSON value, validated against its schema at creation and (dev mode) on writes. */
|
|
55
|
+
export declare class Cell<T> extends Source<T> {
|
|
56
|
+
#private;
|
|
57
|
+
/** Replaced at a reload commit that changes the schema (state-version bump). */
|
|
58
|
+
schema: TSchema;
|
|
59
|
+
/**
|
|
60
|
+
* `initial` is always validated against `schema`; ownership moves to the cell. Without `initial`, the cell has no
|
|
61
|
+
* value until `initialize` (a cell declared with `init`, filled before its object goes live).
|
|
62
|
+
*/
|
|
63
|
+
constructor(key: string, schema: TSchema, initial: {
|
|
64
|
+
readonly value: T;
|
|
65
|
+
} | undefined, guard: WriteGuard, options: SourceOptions);
|
|
66
|
+
/** Sets the first value of a cell declared with `init` (validated; ownership moves to the cell). Publishes nothing. */
|
|
67
|
+
initialize(value: T): void;
|
|
68
|
+
protected ownSchemas(): readonly TSchema[];
|
|
69
|
+
get value(): T;
|
|
70
|
+
/**
|
|
71
|
+
* Mutates a draft (only mutated paths are published), or returns a new value (`n => n + 1`). A returned value is
|
|
72
|
+
* always validated against the schema. The draft is unusable after `fn` returns.
|
|
73
|
+
*/
|
|
74
|
+
change(ctx: Ctx, fn: (draft: Draft<T>) => T): void;
|
|
75
|
+
change(ctx: Ctx, fn: (draft: Draft<T>) => void): void;
|
|
76
|
+
/**
|
|
77
|
+
* Replaces the whole value. Ownership of `value` moves to the cell: never mutate it afterwards.
|
|
78
|
+
*
|
|
79
|
+
* Subscribers get a structural diff (`diffRevisions`) when `diff` is true, or by default when the new value shares
|
|
80
|
+
* containers with the old one (a new immutable revision of the same state, e.g. mirrored from another store);
|
|
81
|
+
* otherwise a single `r` op. The resulting value is the same either way; only the ops differ.
|
|
82
|
+
*/
|
|
83
|
+
replace(ctx: Ctx, value: T, options?: {
|
|
84
|
+
readonly diff?: boolean;
|
|
85
|
+
}): void;
|
|
86
|
+
/**
|
|
87
|
+
* Installs a migrated value and its schema (reload commit). The caller validated the value and the derivations
|
|
88
|
+
* over it against every schema that will apply, so nothing is checked here. Subscribers receive a reset.
|
|
89
|
+
*/
|
|
90
|
+
reset(value: T, schema: TSchema): void;
|
|
91
|
+
}
|
|
92
|
+
export type Sources = readonly Readable<unknown>[];
|
|
93
|
+
export type SourceValues<S extends Sources> = {
|
|
94
|
+
[K in keyof S]: S[K] extends Readable<infer T> ? T : never;
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* A synchronous derivation. Sources are object cells/derivations (recomputed atomically with the write) or other
|
|
98
|
+
* readables such as reference state members (recomputed when they publish). Subscribers receive diffs.
|
|
99
|
+
*/
|
|
100
|
+
export declare class Derived<T> extends Source<T> {
|
|
101
|
+
#private;
|
|
102
|
+
readonly sources: readonly Readable<unknown>[];
|
|
103
|
+
constructor(key: string, sources: Sources, fn: (...values: never[]) => unknown, options: SourceOptions);
|
|
104
|
+
get value(): T;
|
|
105
|
+
/** Hydrated once every source is (a derivation over a remote member waits for its snapshot). */
|
|
106
|
+
get [HYDRATION](): Hydration | undefined;
|
|
107
|
+
/**
|
|
108
|
+
* Computes the first value (or takes it from `precomputed`, a reload's validated dry run) and starts following
|
|
109
|
+
* the sources (at install, never inside a factory).
|
|
110
|
+
*/
|
|
111
|
+
attach(precomputed?: ReadonlyMap<unknown, unknown>): void;
|
|
112
|
+
detach(): void;
|
|
113
|
+
compute(read: (source: Readable<unknown>) => unknown): T;
|
|
114
|
+
/** Recomputes and republishes, including dependents. Errors are reported (the source write already happened). */
|
|
115
|
+
refresh(): void;
|
|
116
|
+
install(next: T): void;
|
|
117
|
+
}
|
|
118
|
+
export declare const isSource: (value: unknown) => value is Cell<unknown> | Derived<unknown>;
|
|
119
|
+
/** Thrown by placeholders used before activation finished. */
|
|
120
|
+
export declare const notReady: (what: string) => ChordError;
|
|
121
|
+
export {};
|
|
122
|
+
//# 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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"}
|