@earendil-works/chord 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/README.md +205 -0
  2. package/dist/api.d.ts +15 -0
  3. package/dist/api.d.ts.map +1 -0
  4. package/dist/api.js +72 -0
  5. package/dist/api.js.map +1 -0
  6. package/dist/bundler.d.ts +6 -0
  7. package/dist/bundler.d.ts.map +1 -0
  8. package/dist/bundler.js +3 -0
  9. package/dist/bundler.js.map +1 -0
  10. package/dist/context/index.d.ts +24 -0
  11. package/dist/context/index.d.ts.map +1 -0
  12. package/dist/context/index.js +97 -0
  13. package/dist/context/index.js.map +1 -0
  14. package/dist/delta/index.d.ts +119 -0
  15. package/dist/delta/index.d.ts.map +1 -0
  16. package/dist/delta/index.js +1213 -0
  17. package/dist/delta/index.js.map +1 -0
  18. package/dist/facets/host.d.ts +12 -0
  19. package/dist/facets/host.d.ts.map +1 -0
  20. package/dist/facets/host.js +763 -0
  21. package/dist/facets/host.js.map +1 -0
  22. package/dist/facets/loader.d.ts +3 -0
  23. package/dist/facets/loader.d.ts.map +1 -0
  24. package/dist/facets/loader.js +5 -0
  25. package/dist/facets/loader.js.map +1 -0
  26. package/dist/index.d.ts +11 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +10 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/json.d.ts +4 -0
  31. package/dist/json.d.ts.map +1 -0
  32. package/dist/json.js +57 -0
  33. package/dist/json.js.map +1 -0
  34. package/dist/node/bundle-loader.d.ts +30 -0
  35. package/dist/node/bundle-loader.d.ts.map +1 -0
  36. package/dist/node/bundle-loader.js +377 -0
  37. package/dist/node/bundle-loader.js.map +1 -0
  38. package/dist/node/bundle.d.ts +26 -0
  39. package/dist/node/bundle.d.ts.map +1 -0
  40. package/dist/node/bundle.js +183 -0
  41. package/dist/node/bundle.js.map +1 -0
  42. package/dist/node/manifest.d.ts +36 -0
  43. package/dist/node/manifest.d.ts.map +1 -0
  44. package/dist/node/manifest.js +6 -0
  45. package/dist/node/manifest.js.map +1 -0
  46. package/dist/node/package.d.ts +15 -0
  47. package/dist/node/package.d.ts.map +1 -0
  48. package/dist/node/package.js +199 -0
  49. package/dist/node/package.js.map +1 -0
  50. package/dist/node.d.ts +5 -0
  51. package/dist/node.d.ts.map +1 -0
  52. package/dist/node.js +3 -0
  53. package/dist/node.js.map +1 -0
  54. package/dist/services/consumer.d.ts +11 -0
  55. package/dist/services/consumer.d.ts.map +1 -0
  56. package/dist/services/consumer.js +537 -0
  57. package/dist/services/consumer.js.map +1 -0
  58. package/dist/services/errors.d.ts +8 -0
  59. package/dist/services/errors.d.ts.map +1 -0
  60. package/dist/services/errors.js +22 -0
  61. package/dist/services/errors.js.map +1 -0
  62. package/dist/services/handle.d.ts +15 -0
  63. package/dist/services/handle.d.ts.map +1 -0
  64. package/dist/services/handle.js +99 -0
  65. package/dist/services/handle.js.map +1 -0
  66. package/dist/services/instances.d.ts +25 -0
  67. package/dist/services/instances.d.ts.map +1 -0
  68. package/dist/services/instances.js +137 -0
  69. package/dist/services/instances.js.map +1 -0
  70. package/dist/services/loopback.d.ts +5 -0
  71. package/dist/services/loopback.d.ts.map +1 -0
  72. package/dist/services/loopback.js +15 -0
  73. package/dist/services/loopback.js.map +1 -0
  74. package/dist/services/provider.d.ts +37 -0
  75. package/dist/services/provider.d.ts.map +1 -0
  76. package/dist/services/provider.js +472 -0
  77. package/dist/services/provider.js.map +1 -0
  78. package/dist/services/state-codec.d.ts +15 -0
  79. package/dist/services/state-codec.d.ts.map +1 -0
  80. package/dist/services/state-codec.js +115 -0
  81. package/dist/services/state-codec.js.map +1 -0
  82. package/dist/services/state-internals.d.ts +11 -0
  83. package/dist/services/state-internals.d.ts.map +1 -0
  84. package/dist/services/state-internals.js +10 -0
  85. package/dist/services/state-internals.js.map +1 -0
  86. package/dist/services/state.d.ts +23 -0
  87. package/dist/services/state.d.ts.map +1 -0
  88. package/dist/services/state.js +124 -0
  89. package/dist/services/state.js.map +1 -0
  90. package/dist/services/wire.d.ts +60 -0
  91. package/dist/services/wire.d.ts.map +1 -0
  92. package/dist/services/wire.js +173 -0
  93. package/dist/services/wire.js.map +1 -0
  94. package/dist/types.d.ts +203 -0
  95. package/dist/types.d.ts.map +1 -0
  96. package/dist/types.js +2 -0
  97. package/dist/types.js.map +1 -0
  98. package/package.json +72 -0
package/README.md ADDED
@@ -0,0 +1,205 @@
1
+ # @earendil-works/chord
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.
8
+
9
+ ## What Chord is for
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.
15
+
16
+ The design has a few connected pieces:
17
+
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*.
23
+
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.)
28
+
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 mutate the tracked `state` proxy and call
36
+ `publish(context)`; consumers receive complete immutable values. Chord flushes
37
+ one decoded operation batch per publication, while each remote client/state
38
+ stream owns independent path-codec state. Replicas become unready on disconnect
39
+ or replacement until they are rehydrated.
40
+
41
+ - **Delta tracking** derives compact operations from tracked plain JSON at
42
+ flush time. It preserves string append/front-truncation and array-append
43
+ behavior without retaining mutation history, supports durable base batches,
44
+ and validates untrusted operations as they are applied.
45
+
46
+ - **Remote service sources** advertise services available outside a facet host
47
+ and open bindings for the services its facets require. Bindings carry logical
48
+ calls and subscriptions through an application-supplied adapter. Chord
49
+ requires strict-JSON arguments, results, snapshots, updates, and catalogues,
50
+ but does not prescribe framing, routing, transport, or an application wire
51
+ envelope. `JsonRepresentation<T>` derives a wire-safe type for application data
52
+ with unknown payloads, while `isJsonValue()` validates received values at an
53
+ adapter boundary. Symmetric RPC peers are planned as one optional
54
+ implementation of this boundary.
55
+
56
+ - **Context** Chord provides a Go-like context system for cancellation and
57
+ invocation-scoped application values. Applications can carry permissions or
58
+ telemetry through those values without Chord depending on either.
59
+
60
+ The current runtime exports service tokens, singleton and keyed providers,
61
+ remote bindings, replicated state, facet hosts, and facet loaders from
62
+ `@earendil-works/chord`. Import public types and general runtime APIs from the
63
+ package root. Context constants and functions live in
64
+ `@earendil-works/chord/context` because their generic names should not pollute
65
+ the root API.
66
+ Chord-owned identifiers use the `chord.*` namespace and its reserved service
67
+ prefix is `$chord.*`.
68
+
69
+ ## Remote service adapters
70
+
71
+ Chord owns its transport-independent service wire grammar. Consumer adapters
72
+ use `createServiceCatalogueCall()`, `createServiceSubscribeCall()`, and
73
+ `createServiceUnsubscribeCall()` for `$chord.service` control calls.
74
+ `createRemoteServiceEndpoint()` handles those calls for one provider consumer,
75
+ including subscription activation and cleanup. `parseServiceCall()`,
76
+ `parseServiceCatalogue()`, and the decoded/wire snapshot and update parsers
77
+ validate Chord semantics after an adapter has established a strict-JSON
78
+ boundary. `RemoteServiceErrorCode` and `REMOTE_SERVICE_ERROR_CODES` define the
79
+ service errors that may cross that boundary.
80
+
81
+ Replicated state operations use one `createServiceStateEncoder()` at the
82
+ provider side and one `createServiceStateDecoder()` at the consumer side for
83
+ each subscription. Those registries create an independent Delta path dictionary
84
+ for every instance/member state and reset it on replacement, unavailability,
85
+ close, or fresh hydration. Applications may place these values inside any
86
+ routing, request, response, or event envelope; Chord does not prescribe that
87
+ outer protocol.
88
+
89
+ ## Tracking JSON deltas
90
+
91
+ Import the standalone delta primitive from `@earendil-works/chord/delta`:
92
+
93
+ ```ts
94
+ import { apply, track } from "@earendil-works/chord/delta";
95
+
96
+ const changes = track({ output: "", count: 0 });
97
+ changes.flush(); // opening base batch
98
+ changes.state.output += "done\n";
99
+ changes.state.count += 1;
100
+
101
+ const ops = changes.flush();
102
+ const replica = apply({ output: "", count: 0 }, ops);
103
+ ```
104
+
105
+ The first flush is always a complete base batch. Later flushes contain path-based
106
+ changes. `applyImmutable()` applies those batches while preserving prior replica
107
+ revisions. `replicatedState(initial)` uses tracking directly:
108
+
109
+ ```ts
110
+ const status = env.replicatedState({ output: "", count: 0 });
111
+ status.state.output += "done\n";
112
+ status.state.count += 1;
113
+ status.publish(context);
114
+ ```
115
+
116
+ `publish()` flushes once; remote connection plumbing encodes that operation batch
117
+ independently for every client/state pairing. String assignments preserve pure
118
+ appends and rolling-window movement as append and front-truncate operations;
119
+ unrelated rewrites fall back to a set. Values inserted into tracked state become
120
+ tracker-owned and must subsequently be mutated only through `state`. See the
121
+ [Delta guide](src/delta/README.md) for mutation, array, lifecycle, and
122
+ consumer-ownership rules.
123
+
124
+ ## Bundling and loading facets
125
+
126
+ `@earendil-works/chord/bundler` uses esbuild to turn ESM or TypeScript application
127
+ entries into independent, content-addressed CommonJS files. The package-level API
128
+ reads plugin identity and build configuration from `package.json`, then applies
129
+ facet path conventions supplied by the host application:
130
+
131
+ ```json
132
+ {
133
+ "name": "@example/my-plugin",
134
+ "version": "1.0.0",
135
+ "type": "module",
136
+ "peerDependencies": {
137
+ "@earendil-works/chord": "^0.84.4"
138
+ },
139
+ "chord": {
140
+ "facets": {
141
+ "worker": "./src/custom-worker.ts",
142
+ "presentation": false
143
+ }
144
+ }
145
+ }
146
+ ```
147
+
148
+ ```ts
149
+ import { bundleFacetPackage } from "@earendil-works/chord/bundler";
150
+
151
+ await bundleFacetPackage({
152
+ packagePath: "/path/to/my-plugin",
153
+ outdir: "/application-owned/plugin-builds/my-plugin",
154
+ defaultFacets: {
155
+ worker: "src/worker.ts",
156
+ presentation: "src/presentation.ts",
157
+ },
158
+ });
159
+ ```
160
+
161
+ Existing conventional files become entries unless `chord.facets` overrides or
162
+ disables them. Peer dependencies are externalized and resolved against the host
163
+ when loading. Chord never installs dependencies or runs package lifecycle
164
+ scripts. `bundleFacets()` remains available as the lower-level API for callers
165
+ that already have explicit plugin identity and entry mappings.
166
+
167
+ The output directory contains one `.cjs` file per entry plus
168
+ `chord-facets.json`. Load one application-selected entry through the Node-only
169
+ loader:
170
+
171
+ ```ts
172
+ import { createFacetBundleLoader } from "@earendil-works/chord/node";
173
+
174
+ const loader = createFacetBundleLoader({
175
+ manifestPath: "/application-owned/plugin-builds/my-plugin/chord-facets.json",
176
+ entry: "worker",
177
+ resolveExternal: (specifier) => import.meta.resolve(specifier),
178
+ });
179
+ const loaded = await loader.load();
180
+ ```
181
+
182
+ Each `load()` verifies SHA-256 integrity and compiles the CommonJS body directly
183
+ with `node:vm` instead of putting the plugin into Node's CommonJS or ESM module
184
+ cache. Externals are resolved by the host and loaded through a restricted
185
+ `require`; esbuild lowers dynamic imports so they use the same path. Disposing a
186
+ retired generation releases the loader's facet references, making its compiled
187
+ code eligible for garbage collection once plugin-owned resources are also gone.
188
+
189
+ For transport to another Node host, `readFacetBundleArtifact()` packages one
190
+ verified manifest entry with its source, and `createFacetBundleArtifactLoader()`
191
+ materializes fresh temporary generations while resolving externals against the
192
+ receiving host.
193
+
194
+ To reload, load a candidate, pass its facets to `FacetHost.reload()`, dispose the
195
+ candidate on failure, and dispose the retired `LoadedFacets` only after a
196
+ successful cutover. The host activates and validates the candidate while the old
197
+ providers remain routed, then replaces each singleton directly without an
198
+ unavailable interval. Stable service handles therefore do not become disconnected
199
+ during an ordinary reload. Keyed instances
200
+ remain incarnation-specific and replacements receive fresh generations. The
201
+ bundler writes a complete temporary directory before replacing the previous
202
+ output, so loaders do not observe partially built generations.
203
+
204
+ See [PLANNING.md](PLANNING.md) for the broader RPC and generation-loading
205
+ architecture.
package/dist/api.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ import type { Facet, FacetHost, FacetLoader, FacetOptions, MutableReplicatedState, RemoteServiceBinding, RemoteServiceBindingOptions, RemoteServiceContract, Service } from "./types.ts";
2
+ /** Create an active host for one complete set of facets. */
3
+ export declare function createFacetHost(options: FacetOptions): Promise<FacetHost>;
4
+ export declare function createStaticFacetLoader(facets: readonly Facet[]): FacetLoader;
5
+ export declare function combineFacetLoaders(loaders: readonly FacetLoader[]): FacetLoader;
6
+ export declare function defineFacet(facet: Facet): Facet;
7
+ export declare function defineService<T>(id: string, options: {
8
+ readonly local: true;
9
+ }): Service<T>;
10
+ export declare function defineService<T>(id: string, ...options: [RemoteServiceContract<T>] extends [never] ? readonly [options: never] : readonly [options?: {
11
+ readonly local?: false;
12
+ }]): Service<T>;
13
+ export declare function createRemoteServiceBinding(options: RemoteServiceBindingOptions): RemoteServiceBinding;
14
+ export declare function replicatedState<T extends object>(initial: T): MutableReplicatedState<T>;
15
+ //# sourceMappingURL=api.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACX,KAAK,EACL,SAAS,EACT,WAAW,EACX,YAAY,EAEZ,sBAAsB,EACtB,oBAAoB,EACpB,2BAA2B,EAC3B,qBAAqB,EACrB,OAAO,EACP,MAAM,YAAY,CAAC;AAEpB,4DAA4D;AAC5D,wBAAsB,eAAe,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,SAAS,CAAC,CAQ/E;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,SAAS,KAAK,EAAE,GAAG,WAAW,CAO7E;AAED,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,SAAS,WAAW,EAAE,GAAG,WAAW,CA0BhF;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,KAAK,GAAG,KAAK,CAE/C;AAED,wBAAgB,aAAa,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE;IAAE,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAA;CAAE,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;AAC5F,wBAAgB,aAAa,CAAC,CAAC,EAC9B,EAAE,EAAE,MAAM,EACV,GAAG,OAAO,EAAE,CAAC,qBAAqB,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,GACnD,SAAS,CAAC,OAAO,EAAE,KAAK,CAAC,GACzB,SAAS,CAAC,OAAO,CAAC,EAAE;IAAE,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAA;CAAE,CAAC,GAChD,OAAO,CAAC,CAAC,CAAC,CAAC;AAQd,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,2BAA2B,GAAG,oBAAoB,CAErG;AAED,wBAAgB,eAAe,CAAC,CAAC,SAAS,MAAM,EAAE,OAAO,EAAE,CAAC,GAAG,sBAAsB,CAAC,CAAC,CAAC,CAEvF","sourcesContent":["import { FacetKernel } from \"./facets/host.ts\";\nimport { disposeLoadedFacets } from \"./facets/loader.ts\";\nimport { RemoteServiceBindingImpl } from \"./services/consumer.ts\";\nimport { MutableReplicatedStateImpl } from \"./services/state.ts\";\nimport type {\n\tFacet,\n\tFacetHost,\n\tFacetLoader,\n\tFacetOptions,\n\tLoadedFacets,\n\tMutableReplicatedState,\n\tRemoteServiceBinding,\n\tRemoteServiceBindingOptions,\n\tRemoteServiceContract,\n\tService,\n} from \"./types.ts\";\n\n/** Create an active host for one complete set of facets. */\nexport async function createFacetHost(options: FacetOptions): Promise<FacetHost> {\n\tconst kernel = new FacetKernel(options);\n\tawait kernel.activate();\n\treturn Object.freeze({\n\t\tservices: kernel.provider,\n\t\treload: (facets: readonly Facet[]) => kernel.reload(facets),\n\t\tdispose: () => kernel.dispose(),\n\t});\n}\n\nexport function createStaticFacetLoader(facets: readonly Facet[]): FacetLoader {\n\tconst loadedFacets = Object.freeze([...facets]);\n\treturn {\n\t\tasync load() {\n\t\t\treturn { facets: loadedFacets, async dispose() {} };\n\t\t},\n\t};\n}\n\nexport function combineFacetLoaders(loaders: readonly FacetLoader[]): FacetLoader {\n\treturn {\n\t\tasync load() {\n\t\t\tconst loaded: LoadedFacets[] = [];\n\t\t\ttry {\n\t\t\t\tfor (const loader of loaders) loaded.push(await loader.load());\n\t\t\t} catch (error) {\n\t\t\t\tconst cleanupErrors = await disposeLoadedFacets(loaded.reverse());\n\t\t\t\tif (cleanupErrors.length > 0) {\n\t\t\t\t\tthrow new AggregateError([error, ...cleanupErrors], \"Facet loading and cleanup failed\");\n\t\t\t\t}\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t\tlet disposed = false;\n\t\t\treturn {\n\t\t\t\tfacets: Object.freeze(loaded.flatMap(({ facets }) => facets)),\n\t\t\t\tasync dispose() {\n\t\t\t\t\tif (disposed) return;\n\t\t\t\t\tdisposed = true;\n\t\t\t\t\tconst errors = await disposeLoadedFacets([...loaded].reverse());\n\t\t\t\t\tif (errors.length === 1) throw errors[0];\n\t\t\t\t\tif (errors.length > 1) throw new AggregateError(errors, \"Failed to dispose loaded facets\");\n\t\t\t\t},\n\t\t\t};\n\t\t},\n\t};\n}\n\nexport function defineFacet(facet: Facet): Facet {\n\treturn facet;\n}\n\nexport function defineService<T>(id: string, options: { readonly local: true }): Service<T>;\nexport function defineService<T>(\n\tid: string,\n\t...options: [RemoteServiceContract<T>] extends [never]\n\t\t? readonly [options: never]\n\t\t: readonly [options?: { readonly local?: false }]\n): Service<T>;\nexport function defineService(id: string, options?: { readonly local?: boolean }): Service<unknown> {\n\tif (id.length === 0) throw new TypeError(\"Service ID must not be empty\");\n\t// TODO: check if the reserved namespace should be part of Chord.\n\tif (id.startsWith(\"$chord.\")) throw new TypeError(\"Service IDs beginning with $chord. are reserved\");\n\treturn Object.freeze({ id, local: options?.local ?? false });\n}\n\nexport function createRemoteServiceBinding(options: RemoteServiceBindingOptions): RemoteServiceBinding {\n\treturn new RemoteServiceBindingImpl(options);\n}\n\nexport function replicatedState<T extends object>(initial: T): MutableReplicatedState<T> {\n\treturn new MutableReplicatedStateImpl(initial);\n}\n"]}
package/dist/api.js ADDED
@@ -0,0 +1,72 @@
1
+ import { FacetKernel } from "./facets/host.js";
2
+ import { disposeLoadedFacets } from "./facets/loader.js";
3
+ import { RemoteServiceBindingImpl } from "./services/consumer.js";
4
+ import { MutableReplicatedStateImpl } from "./services/state.js";
5
+ /** Create an active host for one complete set of facets. */
6
+ export async function createFacetHost(options) {
7
+ const kernel = new FacetKernel(options);
8
+ await kernel.activate();
9
+ return Object.freeze({
10
+ services: kernel.provider,
11
+ reload: (facets) => kernel.reload(facets),
12
+ dispose: () => kernel.dispose(),
13
+ });
14
+ }
15
+ export function createStaticFacetLoader(facets) {
16
+ const loadedFacets = Object.freeze([...facets]);
17
+ return {
18
+ async load() {
19
+ return { facets: loadedFacets, async dispose() { } };
20
+ },
21
+ };
22
+ }
23
+ export function combineFacetLoaders(loaders) {
24
+ return {
25
+ async load() {
26
+ const loaded = [];
27
+ try {
28
+ for (const loader of loaders)
29
+ loaded.push(await loader.load());
30
+ }
31
+ catch (error) {
32
+ const cleanupErrors = await disposeLoadedFacets(loaded.reverse());
33
+ if (cleanupErrors.length > 0) {
34
+ throw new AggregateError([error, ...cleanupErrors], "Facet loading and cleanup failed");
35
+ }
36
+ throw error;
37
+ }
38
+ let disposed = false;
39
+ return {
40
+ facets: Object.freeze(loaded.flatMap(({ facets }) => facets)),
41
+ async dispose() {
42
+ if (disposed)
43
+ return;
44
+ disposed = true;
45
+ const errors = await disposeLoadedFacets([...loaded].reverse());
46
+ if (errors.length === 1)
47
+ throw errors[0];
48
+ if (errors.length > 1)
49
+ throw new AggregateError(errors, "Failed to dispose loaded facets");
50
+ },
51
+ };
52
+ },
53
+ };
54
+ }
55
+ export function defineFacet(facet) {
56
+ return facet;
57
+ }
58
+ export function defineService(id, options) {
59
+ if (id.length === 0)
60
+ throw new TypeError("Service ID must not be empty");
61
+ // TODO: check if the reserved namespace should be part of Chord.
62
+ if (id.startsWith("$chord."))
63
+ throw new TypeError("Service IDs beginning with $chord. are reserved");
64
+ return Object.freeze({ id, local: options?.local ?? false });
65
+ }
66
+ export function createRemoteServiceBinding(options) {
67
+ return new RemoteServiceBindingImpl(options);
68
+ }
69
+ export function replicatedState(initial) {
70
+ return new MutableReplicatedStateImpl(initial);
71
+ }
72
+ //# sourceMappingURL=api.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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","sourcesContent":["import { FacetKernel } from \"./facets/host.ts\";\nimport { disposeLoadedFacets } from \"./facets/loader.ts\";\nimport { RemoteServiceBindingImpl } from \"./services/consumer.ts\";\nimport { MutableReplicatedStateImpl } from \"./services/state.ts\";\nimport type {\n\tFacet,\n\tFacetHost,\n\tFacetLoader,\n\tFacetOptions,\n\tLoadedFacets,\n\tMutableReplicatedState,\n\tRemoteServiceBinding,\n\tRemoteServiceBindingOptions,\n\tRemoteServiceContract,\n\tService,\n} from \"./types.ts\";\n\n/** Create an active host for one complete set of facets. */\nexport async function createFacetHost(options: FacetOptions): Promise<FacetHost> {\n\tconst kernel = new FacetKernel(options);\n\tawait kernel.activate();\n\treturn Object.freeze({\n\t\tservices: kernel.provider,\n\t\treload: (facets: readonly Facet[]) => kernel.reload(facets),\n\t\tdispose: () => kernel.dispose(),\n\t});\n}\n\nexport function createStaticFacetLoader(facets: readonly Facet[]): FacetLoader {\n\tconst loadedFacets = Object.freeze([...facets]);\n\treturn {\n\t\tasync load() {\n\t\t\treturn { facets: loadedFacets, async dispose() {} };\n\t\t},\n\t};\n}\n\nexport function combineFacetLoaders(loaders: readonly FacetLoader[]): FacetLoader {\n\treturn {\n\t\tasync load() {\n\t\t\tconst loaded: LoadedFacets[] = [];\n\t\t\ttry {\n\t\t\t\tfor (const loader of loaders) loaded.push(await loader.load());\n\t\t\t} catch (error) {\n\t\t\t\tconst cleanupErrors = await disposeLoadedFacets(loaded.reverse());\n\t\t\t\tif (cleanupErrors.length > 0) {\n\t\t\t\t\tthrow new AggregateError([error, ...cleanupErrors], \"Facet loading and cleanup failed\");\n\t\t\t\t}\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t\tlet disposed = false;\n\t\t\treturn {\n\t\t\t\tfacets: Object.freeze(loaded.flatMap(({ facets }) => facets)),\n\t\t\t\tasync dispose() {\n\t\t\t\t\tif (disposed) return;\n\t\t\t\t\tdisposed = true;\n\t\t\t\t\tconst errors = await disposeLoadedFacets([...loaded].reverse());\n\t\t\t\t\tif (errors.length === 1) throw errors[0];\n\t\t\t\t\tif (errors.length > 1) throw new AggregateError(errors, \"Failed to dispose loaded facets\");\n\t\t\t\t},\n\t\t\t};\n\t\t},\n\t};\n}\n\nexport function defineFacet(facet: Facet): Facet {\n\treturn facet;\n}\n\nexport function defineService<T>(id: string, options: { readonly local: true }): Service<T>;\nexport function defineService<T>(\n\tid: string,\n\t...options: [RemoteServiceContract<T>] extends [never]\n\t\t? readonly [options: never]\n\t\t: readonly [options?: { readonly local?: false }]\n): Service<T>;\nexport function defineService(id: string, options?: { readonly local?: boolean }): Service<unknown> {\n\tif (id.length === 0) throw new TypeError(\"Service ID must not be empty\");\n\t// TODO: check if the reserved namespace should be part of Chord.\n\tif (id.startsWith(\"$chord.\")) throw new TypeError(\"Service IDs beginning with $chord. are reserved\");\n\treturn Object.freeze({ id, local: options?.local ?? false });\n}\n\nexport function createRemoteServiceBinding(options: RemoteServiceBindingOptions): RemoteServiceBinding {\n\treturn new RemoteServiceBindingImpl(options);\n}\n\nexport function replicatedState<T extends object>(initial: T): MutableReplicatedState<T> {\n\treturn new MutableReplicatedStateImpl(initial);\n}\n"]}
@@ -0,0 +1,6 @@
1
+ export type { BundleFacetsOptions, BundleFacetsResult, FacetBundlePlatform, } from "./node/bundle.ts";
2
+ export { bundleFacets } from "./node/bundle.ts";
3
+ export type { FacetBundleEntry, FacetBundleManifest } from "./node/manifest.ts";
4
+ export type { BundleFacetPackageOptions, BundleFacetPackageResult } from "./node/package.ts";
5
+ export { bundleFacetPackage } from "./node/package.ts";
6
+ //# sourceMappingURL=bundler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bundler.d.ts","sourceRoot":"","sources":["../src/bundler.ts"],"names":[],"mappings":"AAAA,YAAY,EACX,mBAAmB,EACnB,kBAAkB,EAClB,mBAAmB,GACnB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAChD,YAAY,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAChF,YAAY,EAAE,yBAAyB,EAAE,wBAAwB,EAAE,MAAM,mBAAmB,CAAC;AAC7F,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC","sourcesContent":["export type {\n\tBundleFacetsOptions,\n\tBundleFacetsResult,\n\tFacetBundlePlatform,\n} from \"./node/bundle.ts\";\nexport { bundleFacets } from \"./node/bundle.ts\";\nexport type { FacetBundleEntry, FacetBundleManifest } from \"./node/manifest.ts\";\nexport type { BundleFacetPackageOptions, BundleFacetPackageResult } from \"./node/package.ts\";\nexport { bundleFacetPackage } from \"./node/package.ts\";\n"]}
@@ -0,0 +1,3 @@
1
+ export { bundleFacets } from "./node/bundle.js";
2
+ export { bundleFacetPackage } from "./node/package.js";
3
+ //# sourceMappingURL=bundler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bundler.js","sourceRoot":"","sources":["../src/bundler.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAGhD,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC","sourcesContent":["export type {\n\tBundleFacetsOptions,\n\tBundleFacetsResult,\n\tFacetBundlePlatform,\n} from \"./node/bundle.ts\";\nexport { bundleFacets } from \"./node/bundle.ts\";\nexport type { FacetBundleEntry, FacetBundleManifest } from \"./node/manifest.ts\";\nexport type { BundleFacetPackageOptions, BundleFacetPackageResult } from \"./node/package.ts\";\nexport { bundleFacetPackage } from \"./node/package.ts\";\n"]}
@@ -0,0 +1,24 @@
1
+ import type { Context, ContextKey } from "../types.ts";
2
+ export declare const BACKGROUND_CONTEXT: Context;
3
+ export declare const TODO_CONTEXT: Context;
4
+ export declare function createContextKey<T>(description: string): ContextKey<T>;
5
+ /** Derive a context containing one additional or replaced value. */
6
+ export declare function withContextValue<T>(key: ContextKey<T>, value: T, parent: Context): Context;
7
+ /**
8
+ * Derive a context cancelled by either the parent signal or the supplied signal.
9
+ * The parent context remains unchanged.
10
+ */
11
+ export declare function withAbortSignal(signal: AbortSignal, context: Context): Context;
12
+ /** Derive a context retaining all values except caller cancellation. Intended for mandatory cleanup only. */
13
+ export declare function withoutAbortSignal(context: Context): Context;
14
+ /** Derive an independently cancellable child context. */
15
+ export declare function withCancel(context: Context): {
16
+ readonly context: Context;
17
+ readonly cancel: (reason?: unknown) => void;
18
+ };
19
+ /**
20
+ * Observe a promise until it settles or the invocation is cancelled.
21
+ * Cancellation rejects only this waiter; it does not cancel the underlying promise.
22
+ */
23
+ export declare function awaitWithContext<T>(promise: Promise<T>, context: Context): Promise<T>;
24
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/context/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAsDvD,eAAO,MAAM,kBAAkB,EAAE,OAA0D,CAAC;AAC5F,eAAO,MAAM,YAAY,EAAE,OAAoD,CAAC;AAEhF,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,WAAW,EAAE,MAAM,GAAG,UAAU,CAAC,CAAC,CAAC,CAEtE;AAED,oEAAoE;AACpE,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,GAAG,EAAE,UAAU,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,OAAO,GAAG,OAAO,CAE1F;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,WAAW,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAI9E;AAED,6GAA6G;AAC7G,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAE5D;AAED,yDAAyD;AACzD,wBAAgB,UAAU,CAAC,OAAO,EAAE,OAAO,GAAG;IAC7C,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,CAAC,MAAM,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;CAC5C,CAMA;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC,CAkBrF","sourcesContent":["import type { Context, ContextKey } from \"../types.ts\";\n\nconst ABORT_SIGNAL_CONTEXT_KEY: ContextKey<AbortSignal | undefined> = Object.freeze({\n\ttoken: Symbol(\"chord.abortSignal\"),\n});\n\nabstract class BaseContext implements Context {\n\tabstract value<T>(key: ContextKey<T>): T | undefined;\n\tabstract toString(): string;\n\n\tget abortSignal(): AbortSignal | undefined {\n\t\treturn this.value(ABORT_SIGNAL_CONTEXT_KEY);\n\t}\n}\n\nclass EmptyContext extends BaseContext {\n\treadonly #name: string;\n\n\tconstructor(name: string) {\n\t\tsuper();\n\t\tthis.#name = name;\n\t}\n\n\tvalue<T>(_key: ContextKey<T>): T | undefined {\n\t\treturn undefined;\n\t}\n\n\ttoString(): string {\n\t\treturn this.#name;\n\t}\n}\n\nclass ContextValue<T> extends BaseContext {\n\treadonly #parent: Context;\n\treadonly #key: ContextKey<T>;\n\treadonly #value: T;\n\n\tconstructor(parent: Context, key: ContextKey<T>, value: T) {\n\t\tsuper();\n\t\tthis.#parent = parent;\n\t\tthis.#key = key;\n\t\tthis.#value = value;\n\t}\n\n\tvalue<Value>(key: ContextKey<Value>): Value | undefined {\n\t\tif (key.token === this.#key.token) return this.#value as unknown as Value;\n\t\treturn this.#parent.value(key);\n\t}\n\n\ttoString(): string {\n\t\treturn `${this.#parent}.WithValue(${this.#key.token.description ?? \"anonymous\"})`;\n\t}\n}\n\nexport const BACKGROUND_CONTEXT: Context = new EmptyContext(\"[Context BACKGROUND_CONTEXT]\");\nexport const TODO_CONTEXT: Context = new EmptyContext(\"[Context TODO_CONTEXT]\");\n\nexport function createContextKey<T>(description: string): ContextKey<T> {\n\treturn Object.freeze({ token: Symbol(description) });\n}\n\n/** Derive a context containing one additional or replaced value. */\nexport function withContextValue<T>(key: ContextKey<T>, value: T, parent: Context): Context {\n\treturn new ContextValue(parent, key, value);\n}\n\n/**\n * Derive a context cancelled by either the parent signal or the supplied signal.\n * The parent context remains unchanged.\n */\nexport function withAbortSignal(signal: AbortSignal, context: Context): Context {\n\tconst parentSignal = context.abortSignal;\n\tconst combined = parentSignal === undefined ? signal : AbortSignal.any([parentSignal, signal]);\n\treturn withContextValue(ABORT_SIGNAL_CONTEXT_KEY, combined, context);\n}\n\n/** Derive a context retaining all values except caller cancellation. Intended for mandatory cleanup only. */\nexport function withoutAbortSignal(context: Context): Context {\n\treturn withContextValue(ABORT_SIGNAL_CONTEXT_KEY, undefined, context);\n}\n\n/** Derive an independently cancellable child context. */\nexport function withCancel(context: Context): {\n\treadonly context: Context;\n\treadonly cancel: (reason?: unknown) => void;\n} {\n\tconst controller = new AbortController();\n\treturn {\n\t\tcontext: withAbortSignal(controller.signal, context),\n\t\tcancel: (reason?: unknown) => controller.abort(reason),\n\t};\n}\n\n/**\n * Observe a promise until it settles or the invocation is cancelled.\n * Cancellation rejects only this waiter; it does not cancel the underlying promise.\n */\nexport function awaitWithContext<T>(promise: Promise<T>, context: Context): Promise<T> {\n\tconst signal = context.abortSignal;\n\tif (signal === undefined) return promise;\n\tif (signal.aborted) return Promise.reject(abortError(signal));\n\treturn new Promise<T>((resolve, reject) => {\n\t\tconst onAbort = (): void => reject(abortError(signal));\n\t\tsignal.addEventListener(\"abort\", onAbort, { once: true });\n\t\tvoid promise.then(\n\t\t\t(value) => {\n\t\t\t\tsignal.removeEventListener(\"abort\", onAbort);\n\t\t\t\tresolve(value);\n\t\t\t},\n\t\t\t(error: unknown) => {\n\t\t\t\tsignal.removeEventListener(\"abort\", onAbort);\n\t\t\t\treject(error);\n\t\t\t},\n\t\t);\n\t});\n}\n\nfunction abortError(signal: AbortSignal): Error {\n\tconst reason: unknown = signal.reason;\n\treturn reason instanceof Error ? reason : new DOMException(\"The operation was aborted\", \"AbortError\");\n}\n"]}
@@ -0,0 +1,97 @@
1
+ const ABORT_SIGNAL_CONTEXT_KEY = Object.freeze({
2
+ token: Symbol("chord.abortSignal"),
3
+ });
4
+ class BaseContext {
5
+ get abortSignal() {
6
+ return this.value(ABORT_SIGNAL_CONTEXT_KEY);
7
+ }
8
+ }
9
+ class EmptyContext extends BaseContext {
10
+ #name;
11
+ constructor(name) {
12
+ super();
13
+ this.#name = name;
14
+ }
15
+ value(_key) {
16
+ return undefined;
17
+ }
18
+ toString() {
19
+ return this.#name;
20
+ }
21
+ }
22
+ class ContextValue extends BaseContext {
23
+ #parent;
24
+ #key;
25
+ #value;
26
+ constructor(parent, key, value) {
27
+ super();
28
+ this.#parent = parent;
29
+ this.#key = key;
30
+ this.#value = value;
31
+ }
32
+ value(key) {
33
+ if (key.token === this.#key.token)
34
+ return this.#value;
35
+ return this.#parent.value(key);
36
+ }
37
+ toString() {
38
+ return `${this.#parent}.WithValue(${this.#key.token.description ?? "anonymous"})`;
39
+ }
40
+ }
41
+ export const BACKGROUND_CONTEXT = new EmptyContext("[Context BACKGROUND_CONTEXT]");
42
+ export const TODO_CONTEXT = new EmptyContext("[Context TODO_CONTEXT]");
43
+ export function createContextKey(description) {
44
+ return Object.freeze({ token: Symbol(description) });
45
+ }
46
+ /** Derive a context containing one additional or replaced value. */
47
+ export function withContextValue(key, value, parent) {
48
+ return new ContextValue(parent, key, value);
49
+ }
50
+ /**
51
+ * Derive a context cancelled by either the parent signal or the supplied signal.
52
+ * The parent context remains unchanged.
53
+ */
54
+ export function withAbortSignal(signal, context) {
55
+ const parentSignal = context.abortSignal;
56
+ const combined = parentSignal === undefined ? signal : AbortSignal.any([parentSignal, signal]);
57
+ return withContextValue(ABORT_SIGNAL_CONTEXT_KEY, combined, context);
58
+ }
59
+ /** Derive a context retaining all values except caller cancellation. Intended for mandatory cleanup only. */
60
+ export function withoutAbortSignal(context) {
61
+ return withContextValue(ABORT_SIGNAL_CONTEXT_KEY, undefined, context);
62
+ }
63
+ /** Derive an independently cancellable child context. */
64
+ export function withCancel(context) {
65
+ const controller = new AbortController();
66
+ return {
67
+ context: withAbortSignal(controller.signal, context),
68
+ cancel: (reason) => controller.abort(reason),
69
+ };
70
+ }
71
+ /**
72
+ * Observe a promise until it settles or the invocation is cancelled.
73
+ * Cancellation rejects only this waiter; it does not cancel the underlying promise.
74
+ */
75
+ export function awaitWithContext(promise, context) {
76
+ const signal = context.abortSignal;
77
+ if (signal === undefined)
78
+ return promise;
79
+ if (signal.aborted)
80
+ return Promise.reject(abortError(signal));
81
+ return new Promise((resolve, reject) => {
82
+ const onAbort = () => reject(abortError(signal));
83
+ signal.addEventListener("abort", onAbort, { once: true });
84
+ void promise.then((value) => {
85
+ signal.removeEventListener("abort", onAbort);
86
+ resolve(value);
87
+ }, (error) => {
88
+ signal.removeEventListener("abort", onAbort);
89
+ reject(error);
90
+ });
91
+ });
92
+ }
93
+ function abortError(signal) {
94
+ const reason = signal.reason;
95
+ return reason instanceof Error ? reason : new DOMException("The operation was aborted", "AbortError");
96
+ }
97
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/context/index.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Context, ContextKey } from \"../types.ts\";\n\nconst ABORT_SIGNAL_CONTEXT_KEY: ContextKey<AbortSignal | undefined> = Object.freeze({\n\ttoken: Symbol(\"chord.abortSignal\"),\n});\n\nabstract class BaseContext implements Context {\n\tabstract value<T>(key: ContextKey<T>): T | undefined;\n\tabstract toString(): string;\n\n\tget abortSignal(): AbortSignal | undefined {\n\t\treturn this.value(ABORT_SIGNAL_CONTEXT_KEY);\n\t}\n}\n\nclass EmptyContext extends BaseContext {\n\treadonly #name: string;\n\n\tconstructor(name: string) {\n\t\tsuper();\n\t\tthis.#name = name;\n\t}\n\n\tvalue<T>(_key: ContextKey<T>): T | undefined {\n\t\treturn undefined;\n\t}\n\n\ttoString(): string {\n\t\treturn this.#name;\n\t}\n}\n\nclass ContextValue<T> extends BaseContext {\n\treadonly #parent: Context;\n\treadonly #key: ContextKey<T>;\n\treadonly #value: T;\n\n\tconstructor(parent: Context, key: ContextKey<T>, value: T) {\n\t\tsuper();\n\t\tthis.#parent = parent;\n\t\tthis.#key = key;\n\t\tthis.#value = value;\n\t}\n\n\tvalue<Value>(key: ContextKey<Value>): Value | undefined {\n\t\tif (key.token === this.#key.token) return this.#value as unknown as Value;\n\t\treturn this.#parent.value(key);\n\t}\n\n\ttoString(): string {\n\t\treturn `${this.#parent}.WithValue(${this.#key.token.description ?? \"anonymous\"})`;\n\t}\n}\n\nexport const BACKGROUND_CONTEXT: Context = new EmptyContext(\"[Context BACKGROUND_CONTEXT]\");\nexport const TODO_CONTEXT: Context = new EmptyContext(\"[Context TODO_CONTEXT]\");\n\nexport function createContextKey<T>(description: string): ContextKey<T> {\n\treturn Object.freeze({ token: Symbol(description) });\n}\n\n/** Derive a context containing one additional or replaced value. */\nexport function withContextValue<T>(key: ContextKey<T>, value: T, parent: Context): Context {\n\treturn new ContextValue(parent, key, value);\n}\n\n/**\n * Derive a context cancelled by either the parent signal or the supplied signal.\n * The parent context remains unchanged.\n */\nexport function withAbortSignal(signal: AbortSignal, context: Context): Context {\n\tconst parentSignal = context.abortSignal;\n\tconst combined = parentSignal === undefined ? signal : AbortSignal.any([parentSignal, signal]);\n\treturn withContextValue(ABORT_SIGNAL_CONTEXT_KEY, combined, context);\n}\n\n/** Derive a context retaining all values except caller cancellation. Intended for mandatory cleanup only. */\nexport function withoutAbortSignal(context: Context): Context {\n\treturn withContextValue(ABORT_SIGNAL_CONTEXT_KEY, undefined, context);\n}\n\n/** Derive an independently cancellable child context. */\nexport function withCancel(context: Context): {\n\treadonly context: Context;\n\treadonly cancel: (reason?: unknown) => void;\n} {\n\tconst controller = new AbortController();\n\treturn {\n\t\tcontext: withAbortSignal(controller.signal, context),\n\t\tcancel: (reason?: unknown) => controller.abort(reason),\n\t};\n}\n\n/**\n * Observe a promise until it settles or the invocation is cancelled.\n * Cancellation rejects only this waiter; it does not cancel the underlying promise.\n */\nexport function awaitWithContext<T>(promise: Promise<T>, context: Context): Promise<T> {\n\tconst signal = context.abortSignal;\n\tif (signal === undefined) return promise;\n\tif (signal.aborted) return Promise.reject(abortError(signal));\n\treturn new Promise<T>((resolve, reject) => {\n\t\tconst onAbort = (): void => reject(abortError(signal));\n\t\tsignal.addEventListener(\"abort\", onAbort, { once: true });\n\t\tvoid promise.then(\n\t\t\t(value) => {\n\t\t\t\tsignal.removeEventListener(\"abort\", onAbort);\n\t\t\t\tresolve(value);\n\t\t\t},\n\t\t\t(error: unknown) => {\n\t\t\t\tsignal.removeEventListener(\"abort\", onAbort);\n\t\t\t\treject(error);\n\t\t\t},\n\t\t);\n\t});\n}\n\nfunction abortError(signal: AbortSignal): Error {\n\tconst reason: unknown = signal.reason;\n\treturn reason instanceof Error ? reason : new DOMException(\"The operation was aborted\", \"AbortError\");\n}\n"]}
@@ -0,0 +1,119 @@
1
+ import type { JsonValue } from "../types.ts";
2
+ export type { JsonValue } from "../types.ts";
3
+ export type Seg = string | number;
4
+ export type Path = readonly Seg[];
5
+ export type NonEmptyPath = readonly [Seg, ...Seg[]];
6
+ /** A path inline, or an id assigned by the encoder on second use. */
7
+ export type PathRef<P extends Path = Path> = P | number;
8
+ /**
9
+ * Tuples are the form — in memory, on the wire, on disk.
10
+ *
11
+ * `r` is the ONLY op that replaces a whole value. `s`/`d`/`a`/`t` cannot target
12
+ * the root: the type forbids it. `p` may, and only because a tracked value can
13
+ * itself be an array — but a `p` that replaces its entire target is normalised to
14
+ * `r`/`s` at flush time, so a root `p` is always a partial modification.
15
+ *
16
+ * `Op` knows nothing about the path dictionary. Interning, id references and
17
+ * omitted paths live in `WireOp` and exist only between `encode` and `decode`.
18
+ */
19
+ export type Op = readonly ["r", JsonValue] | readonly ["s", NonEmptyPath, JsonValue] | readonly ["d", NonEmptyPath] | readonly ["a", NonEmptyPath, string] | readonly ["t", NonEmptyPath, number] | readonly ["p", Path, number, number, JsonValue[]];
20
+ /**
21
+ * What crosses a boundary. Adds two compressions and nothing else:
22
+ *
23
+ * ["#", id, path] defines an id, emitted on a path's SECOND use
24
+ * a numeric PathRef references a previously defined id
25
+ * a shortened tuple reuses the previous op's path; arity disambiguates
26
+ *
27
+ * ["r", value] carries no path, so it encodes to itself — which is why isBase
28
+ * works unchanged on either vocabulary.
29
+ */
30
+ export type WireOp = readonly ["r", JsonValue] | readonly ["s", PathRef<NonEmptyPath>, JsonValue] | readonly ["s", JsonValue] | readonly ["d", PathRef<NonEmptyPath>] | readonly ["d"] | readonly ["a", PathRef<NonEmptyPath>, string] | readonly ["a", string] | readonly ["t", PathRef<NonEmptyPath>, number] | readonly ["t", number] | readonly ["p", PathRef, number, number, JsonValue[]] | readonly ["p", number, number, JsonValue[]] | readonly ["#", number, Path];
31
+ export declare const isReplace: (op: Op | WireOp) => boolean;
32
+ /**
33
+ * A batch begins with a replacement. Flush guarantees `r` is at index 0 or absent,
34
+ * so this is exact rather than a heuristic.
35
+ */
36
+ export declare const isBase: (ops: readonly (Op | WireOp)[]) => boolean;
37
+ /**
38
+ * Longest suffix of `a` that is a prefix of `b`. Probes with indexOf and verifies
39
+ * exact substring equality, so the hot loops are native. A hand-written KMP is
40
+ * asymptotically equivalent and much slower in practice.
41
+ *
42
+ * Always correct: the returned n satisfies a.slice(a.length - n) === b.slice(0, n).
43
+ */
44
+ export declare function overlap(a: string, b: string, scan: number, probe?: number, maxCandidates?: number): number;
45
+ export interface TrackerOptions {
46
+ maxOverlapScan?: number;
47
+ }
48
+ export interface Tracker<T extends object> {
49
+ /**
50
+ * The tracked value. Mutate and read state only through this proxy. Values
51
+ * inserted into it are adopted: callers may retain read-only references, but
52
+ * must not mutate them outside this proxy.
53
+ */
54
+ state: T;
55
+ /** The untracked current value. Mutating it bypasses change tracking. */
56
+ readonly target: T;
57
+ flush(): Op[];
58
+ /** Make the next flush a complete base batch without changing the value. */
59
+ rebase(): void;
60
+ /** Accept pending mutations locally without emitting them. */
61
+ discard(): void;
62
+ readonly dirty: boolean;
63
+ }
64
+ export declare function track<T extends object>(root: T, options?: TrackerOptions): Tracker<T>;
65
+ /**
66
+ * Segments that reach the prototype chain.
67
+ *
68
+ * `JSON.parse` is safe on its own — it makes `__proto__` an own property. What is
69
+ * not safe is `parent[key] = value`, which is exactly what an applier does, and
70
+ * paths are data: `["s", ["__proto__", "isAdmin"], true]` pollutes
71
+ * `Object.prototype` for the whole process.
72
+ *
73
+ * Ops arrive from a facet, a plugin compartment, or a tool whose details may echo
74
+ * model output, so none of it is trusted input.
75
+ */
76
+ export declare const RESERVED_SEGMENTS: ReadonlySet<string>;
77
+ export declare class UnsafePathError extends Error {
78
+ readonly segment: Seg;
79
+ constructor(segment: Seg);
80
+ }
81
+ /**
82
+ * Verb, arity and payload shape for a **decoded** op: paths inline, no `#`, no
83
+ * short forms. `apply` uses this.
84
+ *
85
+ * Validating `Op` against the wire grammar would be laxer than the type: a
86
+ * two-element `["s", value]` would pass, and `apply` would then read the value as
87
+ * a path. Each vocabulary gets the validator that matches it.
88
+ */
89
+ export declare function assertValidOp(op: unknown): asserts op is Op;
90
+ /** The same, for the wire grammar: ids and short forms are legal here. */
91
+ export declare function assertValidWireOp(op: unknown): asserts op is WireOp;
92
+ export declare function assertSafePath(path: Path): void;
93
+ export declare class PathError extends Error {
94
+ readonly path: Path | number;
95
+ constructor(path: Path | number);
96
+ }
97
+ /**
98
+ * Apply ops to a plain mutable value. Returns the value, because `r` replaces it
99
+ * outright and cannot be done in place.
100
+ *
101
+ * Takes decoded ops. Path ids and omitted paths are a wire concern — run
102
+ * `decode` first if the ops came from a boundary.
103
+ */
104
+ export declare function apply<T>(target: T | undefined, ops: readonly Op[]): T;
105
+ /** Apply decoded operations without mutating the previous immutable value. */
106
+ export declare function applyImmutable<T>(target: T | undefined, ops: readonly Op[]): T;
107
+ export interface Encoder {
108
+ encode(ops: readonly Op[]): WireOp[];
109
+ }
110
+ /**
111
+ * Intern on SECOND use. A definition costs more than the path it replaces, so
112
+ * interning on first use loses on the many paths written exactly once.
113
+ */
114
+ export declare function encoder(): Encoder;
115
+ export interface Decoder {
116
+ decode(wire: readonly WireOp[]): Op[];
117
+ }
118
+ export declare function decoder(): Decoder;
119
+ //# sourceMappingURL=index.d.ts.map