chore(vendor): import pi 0.85.1

Imported as a linear vendor commit so forge rebases do not replay the upstream root tree.

git-subtree-dir: third_party/pi

git-subtree-split: d981de1229ef899957bbe968bc8dcda02a21f477
This commit is contained in:
2026-09-14 10:48:47 -04:00
parent 5ebc7018c3
commit 4def357d09
1669 changed files with 397627 additions and 0 deletions

View File

@@ -0,0 +1,90 @@
import { FacetKernel } from "./facets/host.ts";
import { disposeLoadedFacets } from "./facets/loader.ts";
import { RemoteServiceBindingImpl } from "./services/consumer.ts";
import { MutableReplicatedStateImpl } from "./services/state.ts";
import type {
Facet,
FacetHost,
FacetLoader,
FacetOptions,
LoadedFacets,
MutableReplicatedState,
RemoteServiceBinding,
RemoteServiceBindingOptions,
RemoteServiceContract,
Service,
} from "./types.ts";
/** Create an active host for one complete set of facets. */
export async function createFacetHost(options: FacetOptions): Promise<FacetHost> {
const kernel = new FacetKernel(options);
await kernel.activate();
return Object.freeze({
services: kernel.provider,
reload: (facets: readonly Facet[]) => kernel.reload(facets),
dispose: () => kernel.dispose(),
});
}
export function createStaticFacetLoader(facets: readonly Facet[]): FacetLoader {
const loadedFacets = Object.freeze([...facets]);
return {
async load() {
return { facets: loadedFacets, async dispose() {} };
},
};
}
export function combineFacetLoaders(loaders: readonly FacetLoader[]): FacetLoader {
return {
async load() {
const loaded: LoadedFacets[] = [];
try {
for (const loader of loaders) loaded.push(await loader.load());
} catch (error) {
const cleanupErrors = await disposeLoadedFacets(loaded.reverse());
if (cleanupErrors.length > 0) {
throw new AggregateError([error, ...cleanupErrors], "Facet loading and cleanup failed");
}
throw error;
}
let disposed = false;
return {
facets: Object.freeze(loaded.flatMap(({ facets }) => facets)),
async dispose() {
if (disposed) return;
disposed = true;
const errors = await disposeLoadedFacets([...loaded].reverse());
if (errors.length === 1) throw errors[0];
if (errors.length > 1) throw new AggregateError(errors, "Failed to dispose loaded facets");
},
};
},
};
}
export function defineFacet(facet: Facet): Facet {
return facet;
}
export function defineService<T>(id: string, options: { readonly local: true }): Service<T>;
export function defineService<T>(
id: string,
...options: [RemoteServiceContract<T>] extends [never]
? readonly [options: never]
: readonly [options?: { readonly local?: false }]
): Service<T>;
export function defineService(id: string, options?: { readonly local?: boolean }): Service<unknown> {
if (id.length === 0) throw new TypeError("Service ID must not be empty");
// TODO: check if the reserved namespace should be part of Chord.
if (id.startsWith("$chord.")) throw new TypeError("Service IDs beginning with $chord. are reserved");
return Object.freeze({ id, local: options?.local ?? false });
}
export function createRemoteServiceBinding(options: RemoteServiceBindingOptions): RemoteServiceBinding {
return new RemoteServiceBindingImpl(options);
}
export function replicatedState<T extends object>(initial: T): MutableReplicatedState<T> {
return new MutableReplicatedStateImpl(initial);
}

View File

@@ -0,0 +1,9 @@
export type {
BundleFacetsOptions,
BundleFacetsResult,
FacetBundlePlatform,
} from "./node/bundle.ts";
export { bundleFacets } from "./node/bundle.ts";
export type { FacetBundleEntry, FacetBundleManifest } from "./node/manifest.ts";
export type { BundleFacetPackageOptions, BundleFacetPackageResult } from "./node/package.ts";
export { bundleFacetPackage } from "./node/package.ts";

View File

@@ -0,0 +1,121 @@
import type { Context, ContextKey } from "../types.ts";
const ABORT_SIGNAL_CONTEXT_KEY: ContextKey<AbortSignal | undefined> = Object.freeze({
token: Symbol("chord.abortSignal"),
});
abstract class BaseContext implements Context {
abstract value<T>(key: ContextKey<T>): T | undefined;
abstract toString(): string;
get abortSignal(): AbortSignal | undefined {
return this.value(ABORT_SIGNAL_CONTEXT_KEY);
}
}
class EmptyContext extends BaseContext {
readonly #name: string;
constructor(name: string) {
super();
this.#name = name;
}
value<T>(_key: ContextKey<T>): T | undefined {
return undefined;
}
toString(): string {
return this.#name;
}
}
class ContextValue<T> extends BaseContext {
readonly #parent: Context;
readonly #key: ContextKey<T>;
readonly #value: T;
constructor(parent: Context, key: ContextKey<T>, value: T) {
super();
this.#parent = parent;
this.#key = key;
this.#value = value;
}
value<Value>(key: ContextKey<Value>): Value | undefined {
if (key.token === this.#key.token) return this.#value as unknown as Value;
return this.#parent.value(key);
}
toString(): string {
return `${this.#parent}.WithValue(${this.#key.token.description ?? "anonymous"})`;
}
}
export const BACKGROUND_CONTEXT: Context = new EmptyContext("[Context BACKGROUND_CONTEXT]");
export const TODO_CONTEXT: Context = new EmptyContext("[Context TODO_CONTEXT]");
export function createContextKey<T>(description: string): ContextKey<T> {
return Object.freeze({ token: Symbol(description) });
}
/** Derive a context containing one additional or replaced value. */
export function withContextValue<T>(key: ContextKey<T>, value: T, parent: Context): Context {
return new ContextValue(parent, key, value);
}
/**
* Derive a context cancelled by either the parent signal or the supplied signal.
* The parent context remains unchanged.
*/
export function withAbortSignal(signal: AbortSignal, context: Context): Context {
const parentSignal = context.abortSignal;
const combined = parentSignal === undefined ? signal : AbortSignal.any([parentSignal, signal]);
return withContextValue(ABORT_SIGNAL_CONTEXT_KEY, combined, context);
}
/** Derive a context retaining all values except caller cancellation. Intended for mandatory cleanup only. */
export function withoutAbortSignal(context: Context): Context {
return withContextValue(ABORT_SIGNAL_CONTEXT_KEY, undefined, context);
}
/** Derive an independently cancellable child context. */
export function withCancel(context: Context): {
readonly context: Context;
readonly cancel: (reason?: unknown) => void;
} {
const controller = new AbortController();
return {
context: withAbortSignal(controller.signal, context),
cancel: (reason?: unknown) => controller.abort(reason),
};
}
/**
* Observe a promise until it settles or the invocation is cancelled.
* Cancellation rejects only this waiter; it does not cancel the underlying promise.
*/
export function awaitWithContext<T>(promise: Promise<T>, context: Context): Promise<T> {
const signal = context.abortSignal;
if (signal === undefined) return promise;
if (signal.aborted) return Promise.reject(abortError(signal));
return new Promise<T>((resolve, reject) => {
const onAbort = (): void => reject(abortError(signal));
signal.addEventListener("abort", onAbort, { once: true });
void promise.then(
(value) => {
signal.removeEventListener("abort", onAbort);
resolve(value);
},
(error: unknown) => {
signal.removeEventListener("abort", onAbort);
reject(error);
},
);
});
}
function abortError(signal: AbortSignal): Error {
const reason: unknown = signal.reason;
return reason instanceof Error ? reason : new DOMException("The operation was aborted", "AbortError");
}

View File

@@ -0,0 +1,319 @@
# Chord Delta
Chord Delta synchronizes JSON values from an authoritative producer to an
ordered replica. It is available from `@earendil-works/chord/delta`.
A change is represented by an `Op`: a JSON tuple for replacing, setting,
deleting, updating a string, or splicing an array. Producers use `track()`;
replicas use `apply()` or `applyImmutable()`.
```ts
import { apply, track } from "@earendil-works/chord/delta";
const tracker = track({ output: "", entries: [] as string[] });
let replica = apply(undefined, tracker.flush());
tracker.state.output += "done\n";
tracker.state.entries.push("result");
replica = apply(replica, tracker.flush());
```
The first `flush()` returns one operation containing the complete value. Each
later flush returns the operations needed to transform the previously published
value into the current value. It returns `[]` when the value has not changed.
`applyImmutable()` copies only containers along changed paths and shares
unchanged subtrees. It does not mutate, clone, or freeze either complete input.
Chord's replicated-state producers mutate a tracked proxy and publish operation
batches; consumers still observe complete immutable values.
## Sending or storing changes
`flush()` produces decoded `Op[]` with complete paths. This is convenient for
local use but repeats long paths on the wire or disk.
`encoder()` compresses those paths and returns `WireOp[]`. `decoder()` validates
the encoded tuples, restores complete paths, and returns the `Op[]` required by
`apply()`:
```ts
import { apply, decoder, encoder, track } from "@earendil-works/chord/delta";
const tracker = track({ output: "" });
const enc = encoder(); // producer side
const dec = decoder(); // consumer side
let replica: { output: string } | undefined;
const send = () => {
const ops = tracker.flush();
const wire = enc.encode(ops); // serialize or store WireOp[] here
const received = dec.decode(wire);
replica = apply(replica, received);
};
```
Encoding is optional for local application. Never pass `WireOp[]` directly to
`apply()`.
An encoder and decoder are stateful. Use one pair for each ordered stream. The
encoder assigns numeric IDs to paths used across batches; the decoder remembers
the corresponding definitions. A complete-value operation resets both path
dictionaries, so replay can begin at that batch with a fresh decoder.
Path omission is local to one batch. Numeric path IDs may span batches. Each
independently hydrated replicated-state stream needs its own encoder and decoder.
Do not share a pair between state members or subscriptions, even when their
batches use the same ordered transport connection.
## Operation vocabulary
A path is an array of object keys and array indices:
```ts
["operation", "message", "content", 0, "text"]
```
### Decoded `Op`
`track().flush()` returns these tuples, and `apply()` accepts them:
| Tuple | Meaning |
| --- | --- |
| `["r", value]` | Replace the complete value. |
| `["s", path, value]` | Set a property or array element. |
| `["d", path]` | Delete an object property. |
| `["a", path, text]` | Append to a string. |
| `["t", path, count]` | Remove UTF-16 code units from a string's front. |
| `["p", path, index, remove, items]` | Splice an array. |
Except for `r`, every decoded operation carries its complete path. `s`, `d`,
`a`, and `t` cannot address the root. `p` may address a root array.
### Encoded `WireOp`
A `PathRef` is either an inline path or a non-negative numeric path ID.
`WireOp` supports the following tuples:
| Tuple | Meaning |
| --- | --- |
| `["r", value]` | Complete replacement; identical to decoded form. |
| `["#", id, path]` | Define a numeric path ID. |
| `["s", pathRef, value]` | Set with an inline or interned path. |
| `["s", value]` | Set using the previous path in this batch. |
| `["d", pathRef]` | Delete with an inline or interned path. |
| `["d"]` | Delete using the previous path. |
| `["a", pathRef, text]` | Append with an inline or interned path. |
| `["a", text]` | Append using the previous path. |
| `["t", pathRef, count]` | Front-truncate with an inline or interned path. |
| `["t", count]` | Front-truncate using the previous path. |
| `["p", pathRef, index, remove, items]` | Splice with an inline or interned path. |
| `["p", index, remove, items]` | Splice using the previous path. |
For example, adjacent decoded operations on one path:
```ts
[
["t", ["output"], 200],
["a", ["output"], "next chunk"],
]
```
encode to:
```ts
[
["t", ["output"], 200],
["a", "next chunk"], // reuses ["output"]
]
```
When `output` is used again in a later batch, the encoder defines an ID on its
second explicit use:
```ts
[
["#", 0, ["output"]],
["a", 0, "more"],
]
```
Later batches can use `0` directly until a complete-value operation resets the
dictionary.
## Producing changes
Read and mutate `tracker.state` as a normal object:
```ts
tracker.state.status = "running";
tracker.state.settings.theme = "dark";
tracker.state.messages.push(message);
delete tracker.state.retry;
```
Only the value at flush time is published:
```ts
tracker.state.status = "starting";
tracker.state.status = "running";
tracker.flush(); // one set to "running"
```
Replacing an object or array is valid. Delta compares its properties and elements
with the previously published value:
```ts
tracker.state.settings = {
...plainSettings,
theme: "dark",
};
```
Unchanged properties produce no operations. Changed nested strings and arrays
still use string and splice operations.
### Strings
Appending text produces an `a` operation:
```ts
tracker.state.output += "next line\n";
```
Moving a bounded text window forward produces `t` followed by `a` when the old
suffix matches the new prefix:
```ts
tracker.state.output = tracker.state.output.slice(200) + nextChunk;
```
An unrelated replacement produces `s`.
### Arrays
Use normal array methods:
```ts
tracker.state.messages.push(first);
tracker.state.messages.push(second);
tracker.state.messages.splice(3, 1, replacement);
```
All `push()` calls before one flush produce one tail `p`. Changes to older
elements remain separate, regardless of whether they happen before or after the
pushes. Changes to newly pushed elements are included in the pushed values.
Front or middle insertion, removal, sorting, reversing, `fill()`, and
`copyWithin()` are supported. A structural change combined with edits to elements
whose indices moved may compare and publish the retained suffix positionally:
```ts
tracker.state.items.shift();
tracker.state.items[0].status = "changed";
// A shift followed by push in the same flush has the same issue.
```
The emitted data can then scale with the retained suffix, or with the complete
array, rather than only the changed element. When batching is under your control,
flush the structural change before editing elements at their new indices.
Sparse arrays are unsupported. Writing beyond the next index throws. Increasing
`length` creates explicit `null` elements; decreasing it removes elements.
`fill()` and `copyWithin()` keep normal JavaScript reference semantics. Do not use
them to place one mutable object at multiple live paths.
### Optional properties
Optional object properties use absence. They do not require `null`:
```ts
type Settings = { label?: string; count: number };
const tracker = track<Settings>({ count: 0 });
tracker.state.label = "active";
tracker.state.label = undefined; // produces d
// `delete tracker.state.label` is equivalent
```
`undefined` is accepted only as assignment syntax for deleting an object
property. It is not a JSON value. Initial and assigned objects cannot contain own
`undefined` values, and array elements cannot be `undefined`. Use `null` when an
array position or explicit empty value must remain present.
## State ownership
The object passed to `track()` becomes tracker-owned. The same applies to objects
later assigned into state or inserted into arrays.
After insertion, a retained reference may be read but must not be mutated or
inserted at another live location. The tracker relies on this ownership rule; it
does not recursively validate values or detect aliases:
```ts
const item = { status: "new" };
tracker.state.item = item;
tracker.state.item.status = "ready"; // supported: tracked mutation
item.status = "broken"; // unsupported: bypasses tracking
tracker.state.other = item; // unsupported: one object at two live paths
```
The same restriction applies across separate array calls:
```ts
tracker.state.items.push(item);
tracker.state.items.push(item); // unsupported alias
```
Use distinct objects when values must appear at multiple paths. Perform
mutations through `tracker.state`; do not put a proxy read from `tracker.state`
back into tracked state.
Tracked state must be a mutable JSON tree:
- strings, booleans, finite numbers, `null`, arrays, and plain objects;
- no cycles or one mutable object stored at multiple locations;
- no sparse arrays, accessors, frozen objects, symbols, classes, functions,
`Map`, or `Set`.
Do not keep a child proxy across an array operation that changes indices. Read
the child again from its new index.
## Tracker lifecycle
```ts
tracker.flush(); // publish changes since the previous flush
tracker.rebase(); // make the next flush a complete replacement
tracker.discard(); // accept current changes without publishing them
tracker.state = replacement; // replace the root; next flush is complete
```
`discard()` intentionally prevents current changes from reaching existing
replicas. Use it only when those replicas do not need the discarded changes.
`apply()` adopts object and array payloads from its input batch. Do not freeze a
batch before applying it, and do not apply one in-memory batch to multiple
mutable replicas unless each replica owns that batch. A serialized and decoded
batch is already detached. `applyImmutable()` instead treats its previous value
and operation payloads as immutable, so one batch can safely fan out in-process.
A `decode()`, `apply()`, or `applyImmutable()` error terminates that stream.
Discard its decoder and replica, then recover from a later base batch. `apply()`
is not transactional; operations before the failing operation may already have
changed the replica.
## Limits
- Delta assumes one authoritative writer and ordered delivery. Sequence numbers,
gap detection, retries, and persistence policy belong to the surrounding
protocol or storage format.
- Object identity is not replicated. Tracked mutable state must be a tree;
immutable inputs may share references, but replicas need not preserve them.
- Object key insertion order is not replicated. Do not compare or hash replicas
using serialized key order.
- Array operations that change indices may publish a wider array region, as
described under Arrays.
- Object-valued keys named `__proto__`, `constructor`, or `prototype` can be read
and serialized, but cannot be mutated through that key. Replace the nearest
ordinarily named parent instead.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,906 @@
import { BACKGROUND_CONTEXT } from "../context/index.ts";
import { RemoteServiceBindingImpl } from "../services/consumer.ts";
import { ServiceSlot } from "../services/handle.ts";
import { InstanceDirectory, type InstanceDirectoryEntry } from "../services/instances.ts";
import { createLoopbackServiceTransport } from "../services/loopback.ts";
import { RemoteServiceProvider, validateRemoteServiceImplementation } from "../services/provider.ts";
import { MutableReplicatedStateImpl } from "../services/state.ts";
import type {
Context,
Facet,
FacetEnvironment,
FacetOptions,
RemoteServiceContract,
RemoteServiceSource,
RemoteServices,
Service,
ServiceMode,
ServiceSpawner,
} from "../types.ts";
interface FacetServiceReference {
readonly serviceId: string;
readonly service: Service<unknown>;
readonly mode: ServiceMode;
}
interface ExternalService {
readonly service: Service<unknown>;
readonly mode: ServiceMode;
readonly source: RemoteServiceSource;
}
interface FacetShape {
facetId: string;
requires: readonly FacetServiceReference[];
provides: readonly FacetServiceReference[];
}
interface FacetRuntime extends FacetShape {
requires: FacetServiceReference[];
provides: FacetServiceReference[];
readonly lifecycle: FacetLifecycle;
readonly provisions: FacetProvision[];
readonly singletonViews: Map<string, unknown>;
}
type LifecycleState = "setting_up" | "prepared" | "active" | "disposing" | "dead";
type GenerationPhase =
| "setup"
| "assembling"
| "connecting"
| "activating"
| "active"
| "reloading"
| "disposing"
| "dead";
type Disposal = () => void | Promise<void>;
class FacetLifecycle {
readonly id: string;
readonly #effects: Disposal[] = [];
readonly #observations: Array<() => Disposal> = [];
readonly #activate: Array<() => void | Promise<void>> = [];
#state: LifecycleState = "setting_up";
#serviceAccess = false;
constructor(id: string) {
this.id = id;
}
assertSettingUp(operation: string): void {
if (this.#state !== "setting_up") {
throw new Error(`Facet ${this.id} can ${operation} only during setup`);
}
}
assertRunning(operation: string): void {
if (this.#state !== "setting_up" && this.#state !== "active") {
throw new Error(`Facet ${this.id} cannot ${operation} while ${this.#state}`);
}
}
assertActive(operation: string): void {
if (this.#state !== "active") throw new Error(`Facet ${this.id} can ${operation} only while active`);
}
assertServiceAccess(): void {
if (!this.#serviceAccess) {
throw new Error(`Facet ${this.id} service handles cannot be used while ${this.#state}`);
}
}
revoke(): void {
this.#serviceAccess = false;
}
own(disposal: Disposal): void {
this.assertRunning("own resources");
this.#effects.push(disposal);
}
observe(start: () => Disposal): void {
this.assertSettingUp("observe services");
this.#observations.push(start);
}
onActivate(callback: () => void | Promise<void>): void {
this.assertSettingUp("register activation callbacks");
this.#activate.push(callback);
}
prepared(): void {
this.assertSettingUp("finish setup");
this.#state = "prepared";
}
async activate(): Promise<void> {
if (this.#state !== "prepared") throw new Error(`Facet ${this.id} is not prepared`);
this.#state = "active";
this.#serviceAccess = true;
for (const start of this.#observations) this.#effects.push(start());
for (const callback of this.#activate) await callback();
}
async dispose(): Promise<void> {
if (this.#state === "dead") return;
this.#state = "disposing";
const errors: unknown[] = [];
for (const effect of this.#effects.splice(0).reverse()) {
try {
await effect();
} catch (error) {
errors.push(error);
}
}
this.#observations.length = 0;
this.#activate.length = 0;
this.#serviceAccess = false;
this.#state = "dead";
if (errors.length === 1) throw errors[0];
if (errors.length > 1) throw new AggregateError(errors, `Failed to dispose facet ${this.id}`);
}
}
interface KeyedServiceSource {
observe<T>(service: Service<T>, handler: (service: T, context: Context) => void | Promise<void>): () => void;
}
interface LocalKeyedRegistration {
readonly generations: Map<string, number>;
readonly directory: InstanceDirectory<InstanceDirectoryEntry>;
}
class LocalKeyedServiceRegistry implements KeyedServiceSource {
readonly #registrations = new Map<string, LocalKeyedRegistration>();
#disposed = false;
constructor(services: readonly { readonly id: string }[], onError: (error: Error) => void) {
const ids = services.map(({ id }) => id);
if (new Set(ids).size !== ids.length) throw new TypeError("Local keyed service registry has duplicate IDs");
for (const serviceId of ids) {
this.#registrations.set(serviceId, {
generations: new Map(),
directory: new InstanceDirectory({ ready: true, onError }),
});
}
}
spawn<T>(service: Service<T>, key: string, implementation: T): () => void {
this.#assertActive();
if (key.length === 0) throw new TypeError("Local service instance key must not be empty");
if (typeof implementation !== "object" || implementation === null || Array.isArray(implementation)) {
throw new TypeError(`Local service ${service.id} implementation must be an object`);
}
const registration = this.#registration(service.id);
if (registration.directory.get(key) !== undefined) {
throw new Error(`Local service ${service.id} already has a live instance with key ${key}`);
}
const generation = (registration.generations.get(key) ?? 0) + 1;
registration.generations.set(key, generation);
const instance: InstanceDirectoryEntry = {
key,
generation,
service: implementation as object,
deactivate() {},
};
registration.directory.insert(instance);
let closed = false;
return () => {
if (closed) return;
closed = true;
registration.directory.remove(instance);
};
}
observe<T>(service: Service<T>, handler: (service: T, context: Context) => void | Promise<void>): () => void {
this.#assertActive();
return this.#registration(service.id).directory.observe(handler);
}
dispose(): void {
if (this.#disposed) return;
this.#disposed = true;
for (const registration of this.#registrations.values()) registration.directory.dispose();
this.#registrations.clear();
}
#registration(serviceId: string): LocalKeyedRegistration {
const registration = this.#registrations.get(serviceId);
if (registration === undefined) throw new Error(`Local keyed service ${serviceId} is not registered`);
return registration;
}
#assertActive(): void {
if (this.#disposed) throw new Error("Local keyed service registry is disposed");
}
}
class HostServiceSlots {
readonly #singletons = new Map<string, ServiceSlot>();
readonly #keyedSources = new Map<string, KeyedServiceSource>();
getSingleton<T>(service: Service<T>, assertAccess: () => void): T {
let slot = this.#singletons.get(service.id);
if (slot === undefined) {
slot = new ServiceSlot(service.id, !service.local);
this.#singletons.set(service.id, slot);
}
return slot.view<T>(assertAccess);
}
hasSingleton(serviceId: string): boolean {
return this.#singletons.has(serviceId);
}
observe<T>(
service: Service<T>,
assertAccess: () => void,
handler: (service: T, context: Context) => void | Promise<void>,
): Disposal {
const source = this.#keyedSources.get(service.id);
if (source === undefined) throw new Error(`Service ${service.id} is disconnected`);
let stopped = false;
const stop = source.observe(service, (target, context) => {
const slot = new ServiceSlot(service.id, !service.local);
slot.bind(target as object);
return handler(
slot.view<T>(() => {
assertAccess();
if (stopped || context.abortSignal?.aborted) {
throw new Error(`Keyed service ${service.id} observation is closed`);
}
}),
context,
);
});
return () => {
if (stopped) return;
stopped = true;
stop();
};
}
bindSingleton(serviceId: string, target: object): void {
this.#singletons.get(serviceId)?.bind(target);
}
bindKeyed(serviceId: string, services: KeyedServiceSource): void {
this.#keyedSources.set(serviceId, services);
}
dispose(): void {
for (const slot of this.#singletons.values()) slot.unbind();
this.#singletons.clear();
this.#keyedSources.clear();
}
}
type ServiceInstanceInstaller<T> = (key: string, implementation: T) => () => void;
interface StagedServiceInstance<T> {
readonly key: string;
readonly implementation: T;
release?: () => void;
}
class StagedServiceSpawner<T> implements ServiceSpawner<T> {
readonly #lifecycle: FacetLifecycle;
readonly #validate: (key: string, implementation: T) => void;
readonly #instances = new Map<string, StagedServiceInstance<T>>();
#installer: ServiceInstanceInstaller<T> | undefined;
constructor(lifecycle: FacetLifecycle, validate: (key: string, implementation: T) => void) {
this.#lifecycle = lifecycle;
this.#validate = validate;
}
connect(installer: ServiceInstanceInstaller<T>): void {
if (this.#installer !== undefined) throw new Error("Facet service provider is already connected");
this.#installer = installer;
for (const instance of this.#instances.values()) {
instance.release = installer(instance.key, instance.implementation);
}
}
spawn(key: string, implementation: T): () => void {
this.#lifecycle.assertActive("spawn service instances");
this.#validate(key, implementation);
if (this.#instances.has(key)) throw new Error(`Facet service already has a live instance with key ${key}`);
const instance: StagedServiceInstance<T> = { key, implementation };
this.#instances.set(key, instance);
if (this.#installer !== undefined) instance.release = this.#installer(key, implementation);
const close = (): void => {
if (this.#instances.get(key) !== instance) return;
this.#instances.delete(key);
instance.release?.();
};
this.#lifecycle.own(close);
return close;
}
}
type FacetProvision =
| {
readonly kind: "singleton";
readonly service: { readonly id: string; readonly local: boolean };
readonly implementation: object;
install(provider: RemoteServiceProvider): void;
validateReplacement(provider: RemoteServiceProvider): void;
replace(provider: RemoteServiceProvider): void;
}
| {
readonly kind: "keyed";
readonly service: { readonly id: string; readonly local: boolean };
connectLocal(registry: LocalKeyedServiceRegistry): void;
connectRemote(provider: RemoteServiceProvider): void;
};
/** Private lifecycle and dependency kernel behind the atomic host entry point. */
export class FacetKernel {
readonly #initialFacets: readonly Facet[];
readonly #serviceSources: readonly RemoteServiceSource[];
readonly #onError: (error: Error) => void;
readonly #facets = new Map<string, FacetRuntime>();
readonly #serviceSlots: HostServiceSlots;
readonly #sourceBindings = new Map<RemoteServiceSource, RemoteServices>();
#activationOrder: readonly string[] = [];
#provider: RemoteServiceProvider | undefined;
#internalServices: RemoteServices | undefined;
#localKeyedServices: LocalKeyedServiceRegistry | undefined;
#phase: GenerationPhase = "setup";
constructor(options: FacetOptions) {
const ids = options.facets.map((facet) => facet.id);
if (ids.some((id) => id.length === 0)) throw new Error("Facet ID must not be empty");
if (new Set(ids).size !== ids.length) throw new Error("Facet IDs must be unique within a generation");
this.#initialFacets = options.facets;
this.#serviceSources = options.serviceSources ?? [];
this.#onError = options.onError ?? (() => {});
this.#serviceSlots = new HostServiceSlots();
}
get provider(): RemoteServiceProvider {
if (this.#provider === undefined) throw new Error("Facet service provider is not assembled");
return this.#provider;
}
#createFacetRuntime(facetId: string): FacetRuntime {
return {
facetId,
requires: [],
provides: [],
lifecycle: new FacetLifecycle(facetId),
provisions: [],
singletonViews: new Map(),
};
}
#setupFacet(facet: Facet, record: FacetRuntime): void {
const result: unknown = facet.setup(this.#environment(record));
if (isPromiseLike(result)) {
void Promise.resolve(result).catch(() => {});
throw new Error(`Facet ${facet.id} setup must be synchronous`);
}
record.lifecycle.prepared();
}
async activate(): Promise<void> {
const records: FacetRuntime[] = [];
try {
for (const facet of this.#initialFacets) {
const record = this.#createFacetRuntime(facet.id);
this.#facets.set(facet.id, record);
this.#setupFacet(facet, record);
records.push(record);
}
this.#phase = "assembling";
const externalServices = await this.#resolveExternalServices(records);
this.#activationOrder = validateFacets(records, externalServices);
this.#assembleProviders();
this.#bindServices(externalServices);
this.#phase = "connecting";
await Promise.all(
[...this.#sourceBindings.values(), this.#internalServiceBinding].map((services) =>
services.ready(BACKGROUND_CONTEXT),
),
);
this.#phase = "activating";
for (const id of this.#activationOrder) await this.#facets.get(id)!.lifecycle.activate();
this.#phase = "active";
} catch (error) {
const cleanupErrors = await this.#terminate();
if (cleanupErrors.length > 0) {
throw new AggregateError([error, ...cleanupErrors], "Facet generation startup and cleanup failed");
}
throw error;
}
}
async reload(facets: readonly Facet[]): Promise<void> {
if (this.#phase !== "active") throw new Error(`Facet host cannot reload while ${this.#phase}`);
const ids = facets.map(({ id }) => id);
if (ids.some((id) => id.length === 0)) throw new Error("Facet ID must not be empty");
if (new Set(ids).size !== ids.length) throw new Error("Reloaded facet IDs must be unique");
for (const id of ids) {
if (!this.#facets.has(id)) throw new Error(`Facet ${id} is not active`);
}
this.#phase = "reloading";
const staged: FacetRuntime[] = [];
const candidates: FacetRuntime[] = [];
try {
for (const facet of facets) {
const record = this.#createFacetRuntime(facet.id);
staged.push(record);
this.#setupFacet(facet, record);
const previous = this.#facets.get(facet.id)!;
if (!sameFacetShape(previous, record)) {
throw new Error(`Reloaded facet ${facet.id} must preserve its service requirements and provisions`);
}
this.#validateReplacementProvisions(record.provisions);
candidates.push(record);
}
} catch (error) {
const cleanupErrors = await disposeFacetRecords(staged.reverse());
if (cleanupErrors.length > 0) {
const abortErrors = await this.#abort();
throw new AggregateError(
[error, ...cleanupErrors, ...abortErrors],
"Facet reload setup and cleanup failed",
);
}
this.#phase = "active";
throw error;
}
const replacements = new Map(candidates.map((record) => [record.facetId, record]));
const candidateOrder = this.#activationOrder.flatMap((id) => {
const candidate = replacements.get(id);
return candidate === undefined ? [] : [candidate];
});
try {
for (const candidate of candidateOrder) await candidate.lifecycle.activate();
for (const candidate of candidateOrder) this.#validateReplacementProvisions(candidate.provisions);
} catch (error) {
const cleanupErrors = await disposeFacetRecords([...candidateOrder].reverse());
if (cleanupErrors.length > 0) {
const abortErrors = await this.#abort();
throw new AggregateError(
[error, ...cleanupErrors, ...abortErrors],
"Facet reload activation and cleanup failed",
);
}
this.#phase = "active";
throw error;
}
const previous = candidateOrder.map(({ facetId }) => this.#facets.get(facetId)!);
for (const candidate of candidateOrder) this.#facets.set(candidate.facetId, candidate);
try {
for (const candidate of candidateOrder) {
for (const provision of candidate.provisions) {
if (provision.kind !== "singleton") continue;
if (provision.service.local) {
this.#serviceSlots.bindSingleton(provision.service.id, provision.implementation);
} else {
provision.replace(this.provider);
}
}
}
const retirementErrors = await disposeFacetRecords(previous.reverse());
if (retirementErrors.length === 1) throw retirementErrors[0];
if (retirementErrors.length > 1) {
throw new AggregateError(retirementErrors, "Failed to retire replaced facets");
}
for (const candidate of candidateOrder) {
for (const provision of candidate.provisions) {
if (provision.kind !== "keyed") continue;
if (provision.service.local) provision.connectLocal(this.#localKeyedRegistry);
else provision.connectRemote(this.provider);
}
}
} catch (error) {
const abortErrors = await this.#abort(previous);
throw new AggregateError([error, ...abortErrors], "Facet reload failed after cutover");
}
this.#phase = "active";
}
async dispose(): Promise<void> {
if (this.#phase === "dead") return;
if (this.#phase !== "active") throw new Error(`Facet host cannot be disposed while ${this.#phase}`);
const errors = await this.#terminate();
if (errors.length === 1) throw errors[0];
if (errors.length > 1) throw new AggregateError(errors, "Failed to dispose facet generation");
}
#validateReplacementProvisions(provisions: readonly FacetProvision[]): void {
for (const provision of provisions) {
if (provision.kind !== "singleton" || provision.service.local) continue;
provision.validateReplacement(this.provider);
}
}
#environment(runtime: FacetRuntime): FacetEnvironment {
const { lifecycle, provisions } = runtime;
return {
provide: <T>(service: Service<T>, implementation: NoInfer<T>): void => {
lifecycle.assertSettingUp("provide services");
if (typeof implementation !== "object" || implementation === null || Array.isArray(implementation)) {
throw new TypeError(`Service ${service.id} implementation must be an object`);
}
recordServiceReference(runtime.provides, service, "singleton");
provisions.push({
kind: "singleton",
service,
implementation,
install: (provider) => provider.provide(service, implementation as NoInfer<RemoteServiceContract<T>>),
validateReplacement: (provider) =>
provider.validateReplacement(service, implementation as NoInfer<RemoteServiceContract<T>>),
replace: (provider) => provider.replace(service, implementation as NoInfer<RemoteServiceContract<T>>),
});
},
provideMany: <T>(service: Service<T>): ServiceSpawner<T> => {
lifecycle.assertSettingUp("provide service instances");
recordServiceReference(runtime.provides, service, "keyed");
const instances = new StagedServiceSpawner<T>(lifecycle, (key, implementation) => {
if (key.length === 0) throw new TypeError("Facet service instance key must not be empty");
if (typeof implementation !== "object" || implementation === null || Array.isArray(implementation)) {
throw new TypeError(`Facet service ${service.id} implementation must be an object`);
}
if (!service.local) validateRemoteServiceImplementation(service.id, implementation);
});
provisions.push({
kind: "keyed",
service,
connectLocal: (registry) =>
instances.connect((key, implementation) => registry.spawn(service, key, implementation)),
connectRemote: (provider) =>
instances.connect((key, implementation) =>
provider.spawn(service, key, implementation as NoInfer<RemoteServiceContract<T>>),
),
});
return instances;
},
use: <T>(service: Service<T>): T => {
lifecycle.assertSettingUp("acquire services");
recordServiceReference(runtime.requires, service, "singleton");
let view = runtime.singletonViews.get(service.id);
if (view === undefined) {
view = this.#serviceSlots.getSingleton<T>(service, () => lifecycle.assertServiceAccess());
runtime.singletonViews.set(service.id, view);
}
return view as T;
},
observe: <T>(service: Service<T>, handler: (service: T, context: Context) => void | Promise<void>): void => {
lifecycle.assertSettingUp("observe services");
recordServiceReference(runtime.requires, service, "keyed");
lifecycle.observe(() =>
this.#serviceSlots.observe(service, () => lifecycle.assertServiceAccess(), handler),
);
},
replicatedState: <T extends object>(initial: T) => {
lifecycle.assertRunning("create replicated state");
return new MutableReplicatedStateImpl(initial);
},
own: (disposal) => lifecycle.own(disposal),
onActivate: (callback) => lifecycle.onActivate(callback),
onDeactivate: (callback) => lifecycle.own(callback),
};
}
async #resolveExternalServices(records: readonly FacetShape[]): Promise<ReadonlyMap<string, ExternalService>> {
const catalogues = await Promise.all(
this.#serviceSources.map(async (source) => ({
source,
entries: await source.catalogue(BACKGROUND_CONTEXT),
})),
);
const offered = new Map<string, { readonly mode: ServiceMode; readonly source: RemoteServiceSource }>();
for (const { source, entries } of catalogues) {
for (const { serviceId, mode } of entries) {
if (offered.has(serviceId)) {
throw new Error(`Facet host service ${serviceId} is offered by more than one source`);
}
offered.set(serviceId, { mode, source });
}
}
const local = new Set(records.flatMap(({ provides }) => provides.map(({ serviceId }) => serviceId)));
const external = new Map<string, ExternalService>();
for (const { requires } of records) {
for (const requirement of requires) {
if (local.has(requirement.serviceId) || external.has(requirement.serviceId)) continue;
let source = offered.get(requirement.serviceId);
if (source === undefined) {
const deferred = this.#serviceSources.filter(
({ acceptsUnavailableServices }) => acceptsUnavailableServices,
);
if (deferred.length > 1) {
throw new Error(`Facet host service ${requirement.serviceId} has more than one deferred source`);
}
if (deferred.length === 1) source = { mode: requirement.mode, source: deferred[0]! };
}
if (source !== undefined) {
external.set(requirement.serviceId, { ...source, service: requirement.service });
}
}
}
const serviceIdsBySource = new Map<RemoteServiceSource, string[]>();
for (const [serviceId, { source }] of external) {
let serviceIds = serviceIdsBySource.get(source);
if (serviceIds === undefined) {
serviceIds = [];
serviceIdsBySource.set(source, serviceIds);
}
serviceIds.push(serviceId);
}
for (const [source, serviceIds] of serviceIdsBySource) {
this.#sourceBindings.set(
source,
source.open({
services: serviceIds.map((id) => ({ id })),
assertAccess: () => this.#assertServiceTargetAccess(),
onError: this.#onError,
}),
);
}
return external;
}
#assembleProviders(): void {
const provisions = this.#provisions();
const remoteProvisions = provisions.filter(({ service }) => !service.local);
const provider = new RemoteServiceProvider(
remoteProvisions.map(({ service, kind }) => ({ service, mode: kind })),
);
const internalServices = new RemoteServiceBindingImpl({
services: remoteProvisions.map(({ service }) => service),
transport: createLoopbackServiceTransport(provider),
assertAccess: () => this.#assertServiceTargetAccess(),
onError: this.#onError,
});
const localKeyedServices = new LocalKeyedServiceRegistry(
provisions.flatMap((provision) =>
provision.kind === "keyed" && provision.service.local ? [provision.service] : [],
),
this.#onError,
);
this.#provider = provider;
this.#internalServices = internalServices;
this.#localKeyedServices = localKeyedServices;
for (const provision of provisions) {
if (provision.kind === "singleton") {
if (!provision.service.local) provision.install(provider);
} else if (provision.service.local) {
provision.connectLocal(localKeyedServices);
} else {
provision.connectRemote(provider);
}
}
}
#bindServices(externalServices: ReadonlyMap<string, ExternalService>): void {
for (const provision of this.#provisions()) {
if (provision.kind === "singleton") {
if (!this.#serviceSlots.hasSingleton(provision.service.id)) continue;
const target = provision.service.local
? provision.implementation
: (this.#internalServiceBinding.use(provision.service) as object);
this.#serviceSlots.bindSingleton(provision.service.id, target);
} else {
this.#serviceSlots.bindKeyed(
provision.service.id,
provision.service.local ? this.#localKeyedRegistry : this.#internalServiceBinding,
);
}
}
for (const [serviceId, { service, mode, source }] of externalServices) {
const services = this.#sourceBindings.get(source);
if (services === undefined) throw new Error(`Service source for ${serviceId} is not open`);
if (mode === "singleton") {
this.#serviceSlots.bindSingleton(serviceId, services.use(service) as object);
} else {
this.#serviceSlots.bindKeyed(serviceId, services);
}
}
}
#provisions(): FacetProvision[] {
return [...this.#facets.values()].flatMap(({ provisions }) => provisions);
}
get #localKeyedRegistry(): LocalKeyedServiceRegistry {
if (this.#localKeyedServices === undefined) throw new Error("Facet keyed services are not assembled");
return this.#localKeyedServices;
}
get #internalServiceBinding(): RemoteServices {
if (this.#internalServices === undefined) throw new Error("Facet remote services are not assembled");
return this.#internalServices;
}
async #disposeServiceBindings(): Promise<unknown[]> {
const bindings = [...this.#sourceBindings.values()];
this.#sourceBindings.clear();
if (this.#internalServices !== undefined) bindings.push(this.#internalServices);
this.#internalServices = undefined;
const results = await Promise.allSettled(bindings.map((services) => services.dispose(BACKGROUND_CONTEXT)));
return results.flatMap((result) => (result.status === "rejected" ? [result.reason] : []));
}
#assertServiceTargetAccess(): void {
if (
this.#phase !== "activating" &&
this.#phase !== "active" &&
this.#phase !== "reloading" &&
this.#phase !== "disposing"
) {
throw new Error(`Facet service targets cannot be used during ${this.#phase}`);
}
}
async #abort(extraRecords: readonly FacetRuntime[] = []): Promise<unknown[]> {
for (const record of this.#facets.values()) record.lifecycle.revoke();
for (const record of extraRecords) record.lifecycle.revoke();
return this.#terminate(extraRecords);
}
async #terminate(extraRecords: readonly FacetRuntime[] = []): Promise<unknown[]> {
this.#phase = "disposing";
const errors = await this.#disposeLifecycles();
errors.push(...(await disposeFacetRecords([...extraRecords].reverse())));
try {
this.#localKeyedServices?.dispose();
} catch (error) {
errors.push(error);
}
this.#localKeyedServices = undefined;
errors.push(...(await this.#disposeServiceBindings()));
try {
this.#serviceSlots.dispose();
} catch (error) {
errors.push(error);
}
try {
this.#provider?.dispose();
} catch (error) {
errors.push(error);
}
this.#phase = "dead";
return errors;
}
async #disposeLifecycles(): Promise<unknown[]> {
const errors: unknown[] = [];
const order =
this.#activationOrder.length > 0 ? [...this.#activationOrder].reverse() : [...this.#facets.keys()].reverse();
for (const id of order) {
const record = this.#facets.get(id);
if (record === undefined) continue;
this.#facets.delete(id);
try {
await record.lifecycle.dispose();
} catch (error) {
errors.push(error);
}
}
return errors;
}
}
async function disposeFacetRecords(records: readonly FacetRuntime[]): Promise<unknown[]> {
const errors: unknown[] = [];
for (const record of records) {
try {
await record.lifecycle.dispose();
} catch (error) {
errors.push(error);
}
}
return errors;
}
function validateFacets(
records: readonly FacetShape[],
externalServices: ReadonlyMap<string, { readonly mode: ServiceMode }>,
): string[] {
const providers = new Map<
string,
{ readonly facetId: string | undefined; readonly mode: ServiceMode | undefined }
>();
for (const [serviceId, { mode }] of externalServices) providers.set(serviceId, { facetId: undefined, mode });
for (const record of records) {
for (const provision of record.provides) {
const existing = providers.get(provision.serviceId);
if (existing !== undefined) {
if (existing.mode !== undefined && existing.mode !== provision.mode) {
throw new Error(`Service ${provision.serviceId} is provided as both singleton and keyed`);
}
if (existing.facetId === undefined) {
throw new Error(`Service ${provision.serviceId} is provided by both the host and ${record.facetId}`);
}
throw new Error(
`Service ${provision.serviceId} is provided by both ${existing.facetId} and ${record.facetId}`,
);
}
providers.set(provision.serviceId, { facetId: record.facetId, mode: provision.mode });
}
}
const dependencies = new Map(records.map((record) => [record.facetId, new Set<string>()]));
const dependents = new Map(records.map((record) => [record.facetId, new Set<string>()]));
for (const record of records) {
for (const requirement of record.requires) {
const provider = providers.get(requirement.serviceId);
if (provider === undefined) {
throw new Error(
`Facet ${record.facetId} requires local/${requirement.serviceId}/${requirement.mode}, but no facet provides it`,
);
}
if (provider.mode !== undefined && provider.mode !== requirement.mode) {
throw new Error(
`Facet ${record.facetId} requires ${requirement.serviceId} as ${requirement.mode}, but ${provider.facetId ?? "the host"} provides it as ${provider.mode}`,
);
}
if (provider.facetId === undefined || provider.facetId === record.facetId) continue;
dependencies.get(record.facetId)!.add(provider.facetId);
dependents.get(provider.facetId)!.add(record.facetId);
}
}
return topologicalOrder(records, dependencies, dependents);
}
function topologicalOrder(
records: readonly FacetShape[],
dependencies: ReadonlyMap<string, ReadonlySet<string>>,
dependents: ReadonlyMap<string, ReadonlySet<string>>,
): string[] {
const remaining = new Map([...dependencies].map(([id, values]) => [id, values.size]));
const ready = records.map((record) => record.facetId).filter((id) => remaining.get(id) === 0);
const order: string[] = [];
while (ready.length > 0) {
const id = ready.shift()!;
order.push(id);
for (const dependent of dependents.get(id) ?? []) {
const count = remaining.get(dependent)! - 1;
remaining.set(dependent, count);
if (count === 0) ready.push(dependent);
}
}
if (order.length !== records.length) {
const cycle = records.map((record) => record.facetId).filter((id) => remaining.get(id)! > 0);
throw new Error(`Facet dependency cycle: ${cycle.join(", ")}`);
}
return order;
}
function recordServiceReference(
target: FacetServiceReference[],
service: { readonly id: string },
mode: ServiceMode,
): void {
if (target.some((reference) => reference.serviceId === service.id && reference.mode === mode)) return;
target.push({ serviceId: service.id, service: service as Service<unknown>, mode });
}
function sameFacetShape(left: FacetShape, right: FacetShape): boolean {
return sameReferences(left.requires, right.requires) && sameReferences(left.provides, right.provides);
}
function sameReferences(left: readonly FacetServiceReference[], right: readonly FacetServiceReference[]): boolean {
return (
left.length === right.length &&
left.every((reference) =>
right.some((other) => other.serviceId === reference.serviceId && other.mode === reference.mode),
)
);
}
function isPromiseLike(value: unknown): value is PromiseLike<unknown> {
return typeof value === "object" && value !== null && "then" in value && typeof value.then === "function";
}

View File

@@ -0,0 +1,6 @@
import type { LoadedFacets } from "../types.ts";
export async function disposeLoadedFacets(loaded: readonly LoadedFacets[]): Promise<unknown[]> {
const results = await Promise.allSettled(loaded.map((entry) => entry.dispose()));
return results.flatMap((result) => (result.status === "rejected" ? [result.reason] : []));
}

View File

@@ -0,0 +1,79 @@
/**
* Chord is a standalone application-composition runtime for agentic applications.
*/
export {
combineFacetLoaders,
createFacetHost,
createRemoteServiceBinding,
createStaticFacetLoader,
defineFacet,
defineService,
replicatedState,
} from "./api.ts";
export { isJsonValue } from "./json.ts";
export {
isRemoteServiceErrorCode,
REMOTE_SERVICE_ERROR_CODES,
RemoteServiceError,
type RemoteServiceErrorCode,
} from "./services/errors.ts";
export {
createRemoteServiceEndpoint,
type RemoteServiceEndpoint,
RemoteServiceProvider,
type ServiceUpdatePublisher,
} from "./services/provider.ts";
export {
createServiceStateDecoder,
createServiceStateEncoder,
type ServiceStateDecoder,
type ServiceStateEncoder,
} from "./services/state-codec.ts";
export {
createServiceCatalogueCall,
createServiceSubscribeCall,
createServiceUnsubscribeCall,
decodeServiceControlCall,
parseServiceCall,
parseServiceCatalogue,
parseServiceProviderUpdate,
parseServiceSubscriptionSnapshot,
parseWireServiceProviderUpdate,
parseWireServiceSubscriptionSnapshot,
type ServiceControlCall,
type WireServiceInstanceSnapshot,
type WireServiceMemberSnapshot,
type WireServiceProviderUpdate,
type WireServiceSubscriptionSnapshot,
} from "./services/wire.ts";
export type {
Context,
ContextKey,
Facet,
FacetEnvironment,
FacetHost,
FacetLoader,
FacetOptions,
JsonRepresentation,
JsonValue,
LoadedFacets,
MutableReplicatedState,
RemoteServiceBinding,
RemoteServiceBindingOptions,
RemoteServiceSource,
RemoteServices,
RemoteServiceTransport,
ReplicatedState,
ReplicatedStateDelivery,
Service,
ServiceCall,
ServiceCatalogueEntry,
ServiceInstanceAddress,
ServiceInstanceSnapshot,
ServiceMemberSnapshot,
ServiceMode,
ServiceProviderUpdate,
ServiceSpawner,
ServiceSubscription,
ServiceSubscriptionSnapshot,
} from "./types.ts";

View File

@@ -0,0 +1,50 @@
import type { JsonValue } from "./types.ts";
/** Return whether a value is finite strict JSON with plain objects and no cycles. */
export function isJsonValue(value: unknown): value is JsonValue {
return check(value, new Set<object>(), 0);
}
function check(value: unknown, ancestors: Set<object>, depth: number): boolean {
if (depth > 512) return false;
if (value === null || typeof value === "string" || typeof value === "boolean") return true;
if (typeof value === "number") return Number.isFinite(value);
if (Array.isArray(value)) {
const keys = Reflect.ownKeys(value);
if (keys.length !== value.length + 1 || keys.some((key) => typeof key !== "string")) return false;
if (ancestors.has(value)) return false;
ancestors.add(value);
try {
for (let index = 0; index < value.length; index++) {
const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
if (
descriptor === undefined ||
!descriptor.enumerable ||
!("value" in descriptor) ||
!check(descriptor.value, ancestors, depth + 1)
) {
return false;
}
}
return true;
} finally {
ancestors.delete(value);
}
}
if (typeof value !== "object" || value === null) return false;
const prototype = Object.getPrototypeOf(value);
if (prototype !== Object.prototype && prototype !== null) return false;
if (Reflect.ownKeys(value).some((key) => typeof key !== "string")) return false;
if (ancestors.has(value)) return false;
ancestors.add(value);
try {
for (const descriptor of Object.values(Object.getOwnPropertyDescriptors(value))) {
if (!descriptor.enumerable || !("value" in descriptor) || !check(descriptor.value, ancestors, depth + 1)) {
return false;
}
}
return true;
} finally {
ancestors.delete(value);
}
}

View File

@@ -0,0 +1,24 @@
export type {
FacetBundleArtifactLoaderOptions,
FacetBundleExternalResolver,
FacetBundleLoaderOptions,
} from "./node/bundle-loader.ts";
export {
createFacetBundleArtifactLoader,
createFacetBundleLoader,
readFacetBundleArtifact,
readFacetBundleManifest,
} from "./node/bundle-loader.ts";
export type {
FacetBundleArtifact,
FacetBundleEntry,
FacetBundleManifest,
FacetBundlePlugin,
} from "./node/manifest.ts";
export {
FACET_BUNDLE_ARTIFACT_FORMAT,
FACET_BUNDLE_ARTIFACT_FORMAT_VERSION,
FACET_BUNDLE_FORMAT,
FACET_BUNDLE_FORMAT_VERSION,
FACET_BUNDLE_MANIFEST_FILE,
} from "./node/manifest.ts";

View File

@@ -0,0 +1,416 @@
import { createHash } from "node:crypto";
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { createRequire, isBuiltin } from "node:module";
import { tmpdir } from "node:os";
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { compileFunction } from "node:vm";
import type { Facet, FacetLoader, LoadedFacets } from "../types.ts";
import {
FACET_BUNDLE_ARTIFACT_FORMAT,
FACET_BUNDLE_ARTIFACT_FORMAT_VERSION,
FACET_BUNDLE_FORMAT,
FACET_BUNDLE_FORMAT_VERSION,
FACET_BUNDLE_MANIFEST_FILE,
type FacetBundleArtifact,
type FacetBundleEntry,
type FacetBundleManifest,
} from "./manifest.ts";
export type FacetBundleExternalResolver = (specifier: string) => string | URL | undefined;
export interface FacetBundleLoaderOptions {
readonly manifestPath: string | URL;
readonly entry: string;
/** Verify the entry's SHA-256 integrity before evaluating it. Defaults to true. */
readonly verifyIntegrity?: boolean;
/** Resolve host-provided external imports when the bundle is outside the host's package tree. */
readonly resolveExternal?: FacetBundleExternalResolver;
}
export interface FacetBundleArtifactLoaderOptions {
readonly artifact: unknown;
/** Resolve host-provided external imports against the receiving application. */
readonly resolveExternal?: FacetBundleExternalResolver;
/** Parent directory for materialized module generations. Defaults to the operating system temp directory. */
readonly temporaryDirectory?: string;
}
interface CommonJsModule {
exports: unknown;
}
/** Read and validate a versioned facet bundle manifest. */
export async function readFacetBundleManifest(path: string | URL): Promise<FacetBundleManifest> {
const manifestPath = toFilePath(path);
let parsed: unknown;
try {
parsed = JSON.parse(await readFile(manifestPath, "utf8"));
} catch (error) {
throw new Error(`Could not read facet bundle manifest ${manifestPath}`, { cause: error });
}
return validateManifest(parsed, manifestPath);
}
/** Read and verify one transportable entry from a facet bundle on disk. */
export async function readFacetBundleArtifact(options: {
readonly manifestPath: string | URL;
readonly entry: string;
}): Promise<FacetBundleArtifact> {
if (options.entry.length === 0) throw new TypeError("Facet bundle entry name must not be empty");
const manifestPath = toFilePath(options.manifestPath);
const manifest = await readFacetBundleManifest(manifestPath);
const entry = manifest.entries[options.entry];
if (entry === undefined) throw new Error(`Facet bundle ${manifest.plugin.id} has no entry named ${options.entry}`);
const modulePath = resolveBundleFile(manifestPath, entry.file, "entry");
const source = await readFile(modulePath, "utf8");
verifySource(source, entry);
const sourceMapContents =
entry.sourceMap === undefined
? undefined
: await readFile(resolveBundleFile(manifestPath, entry.sourceMap, "source map"), "utf8");
return Object.freeze({
format: FACET_BUNDLE_ARTIFACT_FORMAT,
formatVersion: FACET_BUNDLE_ARTIFACT_FORMAT_VERSION,
plugin: manifest.plugin,
entryName: options.entry,
entry,
source,
...(sourceMapContents === undefined ? {} : { sourceMapContents }),
});
}
/** Materialize a transported artifact and create a fresh VM-compiled CommonJS generation for each load. */
export function createFacetBundleArtifactLoader(options: FacetBundleArtifactLoaderOptions): FacetLoader {
const artifact = validateArtifact(options.artifact);
const temporaryParent = resolve(options.temporaryDirectory ?? tmpdir());
return {
async load(): Promise<LoadedFacets> {
await mkdir(temporaryParent, { recursive: true });
const directory = await mkdtemp(join(temporaryParent, "chord-facet-"));
try {
await materializeArtifact(directory, artifact);
const loaded = await createFacetBundleLoader({
manifestPath: join(directory, FACET_BUNDLE_MANIFEST_FILE),
entry: artifact.entryName,
resolveExternal: options.resolveExternal,
}).load();
let disposed = false;
return {
get facets() {
return loaded.facets;
},
async dispose() {
if (disposed) return;
disposed = true;
const errors: unknown[] = [];
try {
await loaded.dispose();
} catch (error) {
errors.push(error);
}
try {
await rm(directory, { force: true, recursive: true });
} catch (error) {
errors.push(error);
}
if (errors.length === 1) throw errors[0];
if (errors.length > 1) throw new AggregateError(errors, "Failed to dispose facet bundle artifact");
},
};
} catch (error) {
try {
await rm(directory, { force: true, recursive: true });
} catch (cleanupError) {
throw new AggregateError([error, cleanupError], "Facet bundle artifact loading and cleanup failed");
}
throw error;
}
},
};
}
/** Create a reusable loader for one opaque entry in a facet bundle manifest. */
export function createFacetBundleLoader(options: FacetBundleLoaderOptions): FacetLoader {
if (options.entry.length === 0) throw new TypeError("Facet bundle entry name must not be empty");
const manifestPath = toFilePath(options.manifestPath);
return {
async load(): Promise<LoadedFacets> {
const manifest = await readFacetBundleManifest(manifestPath);
const entry = manifest.entries[options.entry];
if (entry === undefined) {
throw new Error(`Facet bundle ${manifest.plugin.id} has no entry named ${options.entry}`);
}
const modulePath = resolveBundleFile(manifestPath, entry.file, "entry");
try {
const source = await readFile(modulePath, "utf8");
if (options.verifyIntegrity !== false) verifySource(source, entry);
const exported = executeCommonJsModule(source, modulePath, entry.externalImports, options.resolveExternal);
let facets = facetsFromModule(exported, manifest.plugin.id, options.entry);
let disposed = false;
return {
get facets() {
return facets;
},
async dispose() {
if (disposed) return;
disposed = true;
facets = Object.freeze([]);
},
};
} catch (error) {
const detail = error instanceof Error ? `: ${error.message}` : "";
throw new Error(`Could not load facet bundle entry ${manifest.plugin.id}/${options.entry}${detail}`, {
cause: error,
});
}
},
};
}
function executeCommonJsModule(
source: string,
modulePath: string,
externalImports: readonly string[],
resolver?: FacetBundleExternalResolver,
): unknown {
const requireFromModule = createRequire(modulePath);
const externalTargets = new Map<string, string>();
for (const specifier of externalImports) {
if (!isBuiltin(specifier)) validatePackageSpecifier(specifier);
externalTargets.set(specifier, toRequireSpecifier(resolveExternalTarget(specifier, resolver)));
}
const targetFor = (specifier: string): string => {
const target = externalTargets.get(specifier);
if (target === undefined) throw new Error(`Facet bundle required undeclared external import: ${specifier}`);
return target;
};
const requireExternal = Object.assign((specifier: string): unknown => requireFromModule(targetFor(specifier)), {
resolve: (specifier: string): string => requireFromModule.resolve(targetFor(specifier)),
});
const commonJsModule: CommonJsModule = { exports: Object.create(null) };
const compiled = compileFunction(source, ["exports", "require", "module", "__filename", "__dirname"], {
filename: modulePath,
});
Reflect.apply(compiled, commonJsModule.exports, [
commonJsModule.exports,
requireExternal,
commonJsModule,
modulePath,
dirname(modulePath),
]);
return commonJsModule.exports;
}
function toRequireSpecifier(target: string): string {
if (isAbsolute(target) || isBuiltin(target)) return target;
let url: URL;
try {
url = new URL(target);
} catch {
return target;
}
if (url.protocol === "file:") return fileURLToPath(url);
if (url.protocol === "node:") return url.href;
throw new Error(`Facet bundle external target must be a file, package, or built-in module: ${target}`);
}
function toFilePath(path: string | URL): string {
if (typeof path === "string") return resolve(path);
if (path.protocol !== "file:") throw new TypeError(`Facet bundle manifest must be a file URL, not ${path.protocol}`);
return fileURLToPath(path);
}
function resolveBundleFile(manifestPath: string, file: string, label: string): string {
if (file.length === 0 || isAbsolute(file) || basename(file) !== file || file === "." || file === "..") {
throw new Error(`Facet bundle ${label} must be a filename relative to its manifest`);
}
return resolve(dirname(manifestPath), file);
}
function verifySource(source: string, entry: FacetBundleEntry): void {
const expected = parseIntegrity(entry.integrity);
const actual = createHash("sha256").update(source, "utf8").digest("base64");
if (actual !== expected) throw new Error(`Facet bundle integrity check failed for ${entry.file}`);
}
function parseIntegrity(integrity: string): string {
const prefix = "sha256-";
if (!integrity.startsWith(prefix) || integrity.length === prefix.length) {
throw new Error("Facet bundle entry has an invalid SHA-256 integrity value");
}
return integrity.slice(prefix.length);
}
function facetsFromModule(imported: unknown, pluginId: string, entryName: string): readonly Facet[] {
if (!isRecord(imported)) throw new Error(`Facet bundle entry ${pluginId}/${entryName} did not export a module`);
const exported = imported.default;
const candidates: readonly unknown[] = Array.isArray(exported) ? (exported as readonly unknown[]) : [exported];
if (candidates.length === 0) {
throw new Error(`Facet bundle entry ${pluginId}/${entryName} exported no facets`);
}
const facets: Facet[] = [];
for (const candidate of candidates) {
if (!isRecord(candidate) || typeof candidate.id !== "string" || candidate.id.length === 0) {
throw new Error(`Facet bundle entry ${pluginId}/${entryName} has a facet with an invalid ID`);
}
if (typeof candidate.setup !== "function") {
throw new Error(`Facet bundle entry ${pluginId}/${entryName} facet ${candidate.id} has no setup function`);
}
facets.push(candidate as unknown as Facet);
}
const ids = facets.map(({ id }) => id);
if (new Set(ids).size !== ids.length) {
throw new Error(`Facet bundle entry ${pluginId}/${entryName} exports duplicate facet IDs`);
}
return Object.freeze(facets);
}
function validateArtifact(value: unknown): FacetBundleArtifact {
if (
!isRecord(value) ||
value.format !== FACET_BUNDLE_ARTIFACT_FORMAT ||
value.formatVersion !== FACET_BUNDLE_ARTIFACT_FORMAT_VERSION ||
typeof value.entryName !== "string" ||
value.entryName.length === 0 ||
typeof value.source !== "string"
) {
throw new Error("Invalid facet bundle artifact");
}
const manifest = validateManifest(
{
format: FACET_BUNDLE_FORMAT,
formatVersion: FACET_BUNDLE_FORMAT_VERSION,
plugin: value.plugin,
entries: { [value.entryName]: value.entry },
},
"facet bundle artifact",
);
const entry = manifest.entries[value.entryName]!;
if (entry.sourceMap === undefined) {
if (value.sourceMapContents !== undefined) {
throw new Error("Facet bundle artifact has source map contents without a source map");
}
} else if (typeof value.sourceMapContents !== "string") {
throw new Error("Facet bundle artifact is missing its source map contents");
}
verifySource(value.source, entry);
return Object.freeze({
format: FACET_BUNDLE_ARTIFACT_FORMAT,
formatVersion: FACET_BUNDLE_ARTIFACT_FORMAT_VERSION,
plugin: manifest.plugin,
entryName: value.entryName,
entry,
source: value.source,
...(value.sourceMapContents === undefined ? {} : { sourceMapContents: value.sourceMapContents }),
});
}
async function materializeArtifact(directory: string, artifact: FacetBundleArtifact): Promise<void> {
const manifest: FacetBundleManifest = {
format: FACET_BUNDLE_FORMAT,
formatVersion: FACET_BUNDLE_FORMAT_VERSION,
plugin: artifact.plugin,
entries: { [artifact.entryName]: artifact.entry },
};
await Promise.all([
writeFile(join(directory, artifact.entry.file), artifact.source, "utf8"),
writeFile(join(directory, FACET_BUNDLE_MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}\n`, "utf8"),
...(artifact.entry.sourceMap === undefined
? []
: [writeFile(join(directory, artifact.entry.sourceMap), artifact.sourceMapContents!, "utf8")]),
]);
}
function resolveExternalTarget(specifier: string, resolver?: FacetBundleExternalResolver): string {
const resolved = resolver?.(specifier);
if (resolved !== undefined) return typeof resolved === "string" ? resolved : resolved.href;
const extension = import.meta.url.endsWith(".ts") ? "ts" : "js";
if (specifier === "@earendil-works/chord") return new URL(`../index.${extension}`, import.meta.url).href;
if (specifier === "@earendil-works/chord/context") {
return new URL(`../context/index.${extension}`, import.meta.url).href;
}
if (specifier === "@earendil-works/chord/node") return new URL(`../node.${extension}`, import.meta.url).href;
return import.meta.resolve(specifier);
}
function validatePackageSpecifier(specifier: string): void {
const segments = specifier.split("/");
const packageParts = specifier.startsWith("@") ? 2 : 1;
if (
specifier.startsWith(".") ||
specifier.startsWith("/") ||
specifier.startsWith("#") ||
specifier.includes(":") ||
specifier.includes("\\") ||
segments.length < packageParts ||
segments.some((part) => part.length === 0 || part === "." || part === "..")
) {
throw new Error(`Facet bundle artifact has an unsupported external import: ${specifier}`);
}
}
function validateManifest(value: unknown, path: string): FacetBundleManifest {
if (!isRecord(value) || value.format !== FACET_BUNDLE_FORMAT) {
throw new Error(`Invalid facet bundle manifest format in ${path}`);
}
if (value.formatVersion !== FACET_BUNDLE_FORMAT_VERSION) {
throw new Error(`Unsupported facet bundle manifest version in ${path}: ${String(value.formatVersion)}`);
}
if (!isRecord(value.plugin) || typeof value.plugin.id !== "string" || value.plugin.id.length === 0) {
throw new Error(`Facet bundle manifest has an invalid plugin identity in ${path}`);
}
if (
value.plugin.version !== undefined &&
(typeof value.plugin.version !== "string" || value.plugin.version.length === 0)
) {
throw new Error(`Facet bundle manifest has an invalid plugin version in ${path}`);
}
if (!isRecord(value.entries) || Object.keys(value.entries).length === 0) {
throw new Error(`Facet bundle manifest has no entries in ${path}`);
}
const entries: Record<string, FacetBundleEntry> = {};
for (const [name, candidate] of Object.entries(value.entries)) {
if (name.length === 0 || !isRecord(candidate)) {
throw new Error(`Facet bundle manifest has an invalid entry in ${path}`);
}
if (typeof candidate.file !== "string") throw new Error(`Facet bundle entry ${name} has no file`);
resolveBundleFile(path, candidate.file, `entry ${name}`);
if (typeof candidate.integrity !== "string") throw new Error(`Facet bundle entry ${name} has no integrity`);
parseIntegrity(candidate.integrity);
if (
!Array.isArray(candidate.externalImports) ||
candidate.externalImports.some((item) => typeof item !== "string")
) {
throw new Error(`Facet bundle entry ${name} has invalid external imports`);
}
const externalImports = Object.freeze([...(candidate.externalImports as readonly string[])]);
if (new Set(externalImports).size !== externalImports.length) {
throw new Error(`Facet bundle entry ${name} has duplicate external imports`);
}
if (candidate.sourceMap !== undefined) {
if (typeof candidate.sourceMap !== "string")
throw new Error(`Facet bundle entry ${name} has an invalid source map`);
resolveBundleFile(path, candidate.sourceMap, `entry ${name} source map`);
}
entries[name] = Object.freeze({
file: candidate.file,
integrity: candidate.integrity,
externalImports,
...(candidate.sourceMap === undefined ? {} : { sourceMap: candidate.sourceMap }),
});
}
return Object.freeze({
format: FACET_BUNDLE_FORMAT,
formatVersion: FACET_BUNDLE_FORMAT_VERSION,
plugin: Object.freeze({
id: value.plugin.id,
...(value.plugin.version === undefined ? {} : { version: value.plugin.version }),
}),
entries: Object.freeze(entries),
});
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}

View File

@@ -0,0 +1,222 @@
import { createHash, randomUUID } from "node:crypto";
import { mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
import { basename, dirname, extname, join, relative, resolve, sep } from "node:path";
import { type BuildOptions, build, type Message } from "esbuild";
import {
FACET_BUNDLE_FORMAT,
FACET_BUNDLE_FORMAT_VERSION,
FACET_BUNDLE_MANIFEST_FILE,
type FacetBundleEntry,
type FacetBundleManifest,
} from "./manifest.ts";
export type FacetBundlePlatform = "node" | "browser" | "neutral";
export interface BundleFacetsOptions {
readonly plugin: {
readonly id: string;
readonly version?: string;
};
/** Opaque application-selected entry names mapped to TypeScript or JavaScript source files. */
readonly entries: Readonly<Record<string, string>>;
readonly outdir: string;
readonly workingDirectory?: string;
/** Additional package imports intentionally left for the loading application to resolve. */
readonly external?: readonly string[];
readonly sourceMap?: boolean;
readonly minify?: boolean;
readonly define?: Readonly<Record<string, string>>;
readonly platform?: FacetBundlePlatform;
readonly target?: string | readonly string[];
}
export interface BundleFacetsResult {
readonly manifest: FacetBundleManifest;
readonly manifestPath: string;
}
/** Bundle each opaque facet entry into an independent content-addressed CommonJS file. */
export async function bundleFacets(options: BundleFacetsOptions): Promise<BundleFacetsResult> {
validateOptions(options);
const workingDirectory = resolve(options.workingDirectory ?? process.cwd());
const outputDirectory = resolve(workingDirectory, options.outdir);
const outputParent = dirname(outputDirectory);
await mkdir(outputParent, { recursive: true });
const temporaryDirectory = join(outputParent, `.${basename(outputDirectory)}.tmp-${randomUUID()}`);
await mkdir(temporaryDirectory);
try {
const entries: Record<string, FacetBundleEntry> = {};
for (const [entryName, source] of Object.entries(options.entries).sort(([left], [right]) =>
left.localeCompare(right),
)) {
entries[entryName] = await bundleEntry({
entryName,
source: resolve(workingDirectory, source),
temporaryDirectory,
workingDirectory,
options,
});
}
const manifest: FacetBundleManifest = Object.freeze({
format: FACET_BUNDLE_FORMAT,
formatVersion: FACET_BUNDLE_FORMAT_VERSION,
plugin: Object.freeze({
id: options.plugin.id,
...(options.plugin.version === undefined ? {} : { version: options.plugin.version }),
}),
entries: Object.freeze(entries),
});
await writeFile(join(temporaryDirectory, FACET_BUNDLE_MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}\n`);
await replaceDirectory(temporaryDirectory, outputDirectory);
return Object.freeze({
manifest,
manifestPath: join(outputDirectory, FACET_BUNDLE_MANIFEST_FILE),
});
} catch (error) {
await rm(temporaryDirectory, { force: true, recursive: true });
throw error;
}
}
async function bundleEntry(input: {
readonly entryName: string;
readonly source: string;
readonly temporaryDirectory: string;
readonly workingDirectory: string;
readonly options: BundleFacetsOptions;
}): Promise<FacetBundleEntry> {
const entryPrefix = `facet-${shortHash(input.entryName)}`;
const requestedPlatform = input.options.platform ?? "node";
const buildOptions: BuildOptions = {
absWorkingDir: input.workingDirectory,
banner: { js: '"use strict";' },
bundle: true,
define: input.options.define,
entryNames: `${entryPrefix}-[hash]`,
entryPoints: [input.source],
external: [...new Set(["@earendil-works/chord", "@earendil-works/chord/*", ...(input.options.external ?? [])])],
format: "cjs",
legalComments: "none",
logLevel: "silent",
metafile: true,
minify: input.options.minify ?? false,
outdir: input.temporaryDirectory,
outExtension: { ".js": ".cjs" },
platform: requestedPlatform,
sourcemap: input.options.sourceMap === true ? "external" : false,
supported: { "dynamic-import": false },
target:
input.options.target === undefined
? requestedPlatform === "node"
? "node22.19"
: "es2022"
: typeof input.options.target === "string"
? input.options.target
: [...input.options.target],
write: true,
};
let result: Awaited<ReturnType<typeof build>>;
try {
result = await build(buildOptions);
} catch (error) {
const diagnostics = esbuildMessages(error);
const detail = diagnostics.length === 0 ? "" : `\n${diagnostics.join("\n")}`;
throw new Error(`Could not bundle facet entry ${input.entryName}${detail}`, { cause: error });
}
const metafile = result.metafile;
if (metafile === undefined) throw new Error(`Facet entry ${input.entryName} produced no build metadata`);
const outputs = Object.entries(metafile.outputs).filter(
([path, output]) => output.entryPoint !== undefined && extname(path) === ".cjs",
);
if (outputs.length !== 1)
throw new Error(`Facet entry ${input.entryName} did not produce exactly one JavaScript file`);
const [outputPath, metadata] = outputs[0]!;
const absoluteOutputPath = resolve(input.workingDirectory, outputPath);
const file = relative(input.temporaryDirectory, absoluteOutputPath);
if (file.length === 0 || file.startsWith(`..${sep}`) || basename(file) !== file) {
throw new Error(`Facet entry ${input.entryName} produced an invalid output path`);
}
const sourceMap = input.options.sourceMap === true ? `${file}.map` : undefined;
const allowedOutputs = new Set([absoluteOutputPath]);
if (sourceMap !== undefined) allowedOutputs.add(join(input.temporaryDirectory, sourceMap));
const unexpectedOutputs = Object.keys(metafile.outputs)
.map((path) => resolve(input.workingDirectory, path))
.filter((path) => !allowedOutputs.has(path));
if (unexpectedOutputs.length > 0) {
throw new Error(`Facet entry ${input.entryName} produced files other than its JavaScript bundle and source map`);
}
const contents = await readFile(absoluteOutputPath);
if (sourceMap !== undefined) await stat(join(input.temporaryDirectory, sourceMap));
return Object.freeze({
file,
integrity: `sha256-${createHash("sha256").update(contents).digest("base64")}`,
externalImports: Object.freeze(
[...new Set(metadata.imports.filter(({ external }) => external).map(({ path }) => path))].sort(),
),
...(sourceMap === undefined ? {} : { sourceMap }),
});
}
function validateOptions(options: BundleFacetsOptions): void {
if (options.plugin.id.length === 0) throw new TypeError("Facet bundle plugin ID must not be empty");
if (options.plugin.version !== undefined && options.plugin.version.length === 0) {
throw new TypeError("Facet bundle plugin version must not be empty");
}
const entries = Object.entries(options.entries);
if (entries.length === 0) throw new TypeError("Facet bundle must contain at least one entry");
for (const [name, source] of entries) {
if (name.length === 0) throw new TypeError("Facet bundle entry name must not be empty");
if (source.length === 0) throw new TypeError(`Facet bundle entry ${name} must have a source path`);
}
for (const external of options.external ?? []) {
if (external.length === 0) throw new TypeError("Facet bundle external import must not be empty");
}
}
async function replaceDirectory(temporaryDirectory: string, outputDirectory: string): Promise<void> {
const backupDirectory = `${outputDirectory}.old-${randomUUID()}`;
let movedExisting = false;
try {
await rename(outputDirectory, backupDirectory);
movedExisting = true;
} catch (error) {
if (!isMissingPath(error)) throw error;
}
try {
await rename(temporaryDirectory, outputDirectory);
} catch (error) {
if (movedExisting) await rename(backupDirectory, outputDirectory);
throw error;
}
if (movedExisting) await rm(backupDirectory, { force: true, recursive: true });
}
function shortHash(value: string): string {
return createHash("sha256").update(value).digest("hex").slice(0, 12);
}
function esbuildMessages(error: unknown): string[] {
if (!isRecord(error) || !Array.isArray(error.errors)) return [];
return (error.errors as readonly unknown[]).flatMap((candidate) =>
isEsbuildMessage(candidate) ? [formatEsbuildMessage(candidate)] : [],
);
}
function formatEsbuildMessage(message: Message): string {
const location = message.location;
return location === null
? message.text
: `${location.file}:${location.line}:${location.column + 1}: ${message.text}`;
}
function isEsbuildMessage(value: unknown): value is Message {
return isRecord(value) && typeof value.text === "string" && (value.location === null || isRecord(value.location));
}
function isMissingPath(error: unknown): boolean {
return isRecord(error) && error.code === "ENOENT";
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}

View File

@@ -0,0 +1,39 @@
export const FACET_BUNDLE_FORMAT = "chord.facet-bundle";
export const FACET_BUNDLE_FORMAT_VERSION = 2;
export const FACET_BUNDLE_MANIFEST_FILE = "chord-facets.json";
export const FACET_BUNDLE_ARTIFACT_FORMAT = "chord.facet-bundle-artifact";
export const FACET_BUNDLE_ARTIFACT_FORMAT_VERSION = 2;
export interface FacetBundleEntry {
/** Content-addressed CommonJS filename relative to the manifest. */
readonly file: string;
/** SHA-256 subresource-integrity value for the JavaScript file. */
readonly integrity: string;
/** Imports intentionally left for the loading application to resolve. */
readonly externalImports: readonly string[];
/** Source map filename relative to the manifest, when emitted. */
readonly sourceMap?: string;
}
export interface FacetBundleManifest {
readonly format: typeof FACET_BUNDLE_FORMAT;
readonly formatVersion: typeof FACET_BUNDLE_FORMAT_VERSION;
readonly plugin: FacetBundlePlugin;
readonly entries: Readonly<Record<string, FacetBundleEntry>>;
}
export interface FacetBundlePlugin {
readonly id: string;
readonly version?: string;
}
/** One self-contained manifest entry suitable for storage or transport to another Node host. */
export interface FacetBundleArtifact {
readonly format: typeof FACET_BUNDLE_ARTIFACT_FORMAT;
readonly formatVersion: typeof FACET_BUNDLE_ARTIFACT_FORMAT_VERSION;
readonly plugin: FacetBundlePlugin;
readonly entryName: string;
readonly entry: FacetBundleEntry;
readonly source: string;
readonly sourceMapContents?: string;
}

View File

@@ -0,0 +1,232 @@
import { readFile, realpath, stat } from "node:fs/promises";
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
import { type BundleFacetsResult, bundleFacets } from "./bundle.ts";
export interface BundleFacetPackageOptions {
/** Plugin package directory or its package.json path. */
readonly packagePath: string;
readonly outdir: string;
/** Application conventions applied when the corresponding source file exists. */
readonly defaultFacets?: Readonly<Record<string, string>>;
}
export interface BundleFacetPackageResult extends BundleFacetsResult {
readonly packageDirectory: string;
readonly packageJsonPath: string;
}
interface FacetPackageMetadata {
readonly packageDirectory: string;
readonly packageJsonPath: string;
readonly name: string;
readonly version: string;
readonly peerDependencies: readonly string[];
readonly configuredFacets: Readonly<Record<string, string | false>>;
readonly external: readonly string[];
readonly sourceMap: boolean;
}
/** Build a plugin package using package.json metadata and application-provided facet conventions. */
export async function bundleFacetPackage(options: BundleFacetPackageOptions): Promise<BundleFacetPackageResult> {
const metadata = await readFacetPackageMetadata(options.packagePath);
const entries = await resolveFacetEntries(metadata, options.defaultFacets ?? {});
const external = [...metadata.peerDependencies, ...metadata.external].flatMap((specifier) => [
specifier,
`${specifier}/*`,
]);
const result = await bundleFacets({
plugin: { id: metadata.name, version: metadata.version },
entries,
outdir: options.outdir,
workingDirectory: metadata.packageDirectory,
external,
sourceMap: metadata.sourceMap,
});
return Object.freeze({
...result,
packageDirectory: metadata.packageDirectory,
packageJsonPath: metadata.packageJsonPath,
});
}
async function readFacetPackageMetadata(packagePath: string): Promise<FacetPackageMetadata> {
if (packagePath.length === 0) throw new TypeError("Facet package path must not be empty");
const candidate = resolve(packagePath);
let packageDirectory: string;
let packageJsonPath: string;
let candidateStats: Awaited<ReturnType<typeof stat>>;
try {
candidateStats = await stat(candidate);
} catch (error) {
throw new Error(`Could not access facet package ${candidate}`, { cause: error });
}
if (candidateStats.isDirectory()) {
packageDirectory = await realpath(candidate);
packageJsonPath = join(packageDirectory, "package.json");
} else if (candidateStats.isFile() && basename(candidate) === "package.json") {
packageJsonPath = await realpath(candidate);
packageDirectory = dirname(packageJsonPath);
} else {
throw new Error(`Facet package path must name a directory or package.json: ${candidate}`);
}
let parsed: unknown;
try {
parsed = JSON.parse(await readFile(packageJsonPath, "utf8"));
} catch (error) {
throw new Error(`Could not read facet package metadata ${packageJsonPath}`, { cause: error });
}
if (!isRecord(parsed)) throw new Error(`Facet package metadata must be an object: ${packageJsonPath}`);
if (typeof parsed.name !== "string" || parsed.name.length === 0) {
throw new Error(`Facet package must have a non-empty name: ${packageJsonPath}`);
}
if (typeof parsed.version !== "string" || parsed.version.length === 0) {
throw new Error(`Facet package must have a non-empty version: ${packageJsonPath}`);
}
const peerDependencies = parsePeerDependencies(parsed.peerDependencies, packageJsonPath);
const chord = parseChordConfiguration(parsed.chord, packageJsonPath);
return {
packageDirectory,
packageJsonPath,
name: parsed.name,
version: parsed.version,
peerDependencies,
configuredFacets: chord.facets,
external: chord.external,
sourceMap: chord.sourceMap,
};
}
function parsePeerDependencies(value: unknown, packageJsonPath: string): readonly string[] {
if (value === undefined) return [];
if (
!isRecord(value) ||
Object.entries(value).some(([name, version]) => name.length === 0 || typeof version !== "string")
) {
throw new Error(`Facet package has invalid peerDependencies: ${packageJsonPath}`);
}
return Object.freeze(Object.keys(value).sort());
}
function parseChordConfiguration(
value: unknown,
packageJsonPath: string,
): {
readonly facets: Readonly<Record<string, string | false>>;
readonly external: readonly string[];
readonly sourceMap: boolean;
} {
if (value === undefined) return { facets: Object.freeze({}), external: Object.freeze([]), sourceMap: true };
if (!isRecord(value)) throw new Error(`Facet package chord configuration must be an object: ${packageJsonPath}`);
if (Object.keys(value).some((key) => key !== "facets" && key !== "external" && key !== "sourceMap")) {
throw new Error(`Facet package chord configuration has an unknown field: ${packageJsonPath}`);
}
const facets: Record<string, string | false> = {};
if (value.facets !== undefined) {
if (!isRecord(value.facets)) {
throw new Error(`Facet package chord.facets must be an object: ${packageJsonPath}`);
}
for (const [name, source] of Object.entries(value.facets)) {
if (name.length === 0 || (typeof source !== "string" && source !== false) || source === "") {
throw new Error(`Facet package has an invalid chord.facets entry: ${packageJsonPath}`);
}
facets[name] = source;
}
}
let external: readonly string[] = [];
if (value.external !== undefined) {
if (
!Array.isArray(value.external) ||
value.external.some((specifier) => typeof specifier !== "string" || specifier.length === 0)
) {
throw new Error(`Facet package chord.external must contain non-empty strings: ${packageJsonPath}`);
}
external = Object.freeze([...new Set(value.external as readonly string[])].sort());
}
if (value.sourceMap !== undefined && typeof value.sourceMap !== "boolean") {
throw new Error(`Facet package chord.sourceMap must be a boolean: ${packageJsonPath}`);
}
return {
facets: Object.freeze(facets),
external,
sourceMap: value.sourceMap ?? true,
};
}
async function resolveFacetEntries(
metadata: FacetPackageMetadata,
defaultFacets: Readonly<Record<string, string>>,
): Promise<Readonly<Record<string, string>>> {
const entries: Record<string, string> = {};
for (const [name, source] of Object.entries(defaultFacets)) {
validateFacetMapping(name, source, "default");
const path = resolvePackageEntry(metadata.packageDirectory, source, name);
try {
const entryStats = await stat(path);
if (!entryStats.isFile()) throw new Error(`Default facet entry ${name} is not a file: ${path}`);
const canonicalPath = await realpath(path);
validateCanonicalPackageEntry(metadata.packageDirectory, canonicalPath, name);
entries[name] = canonicalPath;
} catch (error) {
if (isMissingPath(error)) continue;
throw error;
}
}
for (const [name, source] of Object.entries(metadata.configuredFacets)) {
if (source === false) {
delete entries[name];
continue;
}
validateFacetMapping(name, source, "configured");
const path = resolvePackageEntry(metadata.packageDirectory, source, name);
let entryStats: Awaited<ReturnType<typeof stat>>;
try {
entryStats = await stat(path);
} catch (error) {
throw new Error(`Could not access configured facet entry ${name}: ${path}`, { cause: error });
}
if (!entryStats.isFile()) throw new Error(`Configured facet entry ${name} is not a file: ${path}`);
const canonicalPath = await realpath(path);
validateCanonicalPackageEntry(metadata.packageDirectory, canonicalPath, name);
entries[name] = canonicalPath;
}
if (Object.keys(entries).length === 0) {
throw new Error(`Facet package ${metadata.name} has no configured or conventional facet entries`);
}
return Object.freeze(entries);
}
function validateFacetMapping(name: string, source: string, kind: string): void {
if (name.length === 0) throw new Error(`Facet package ${kind} entry name must not be empty`);
if (source.length === 0) throw new Error(`Facet package ${kind} entry ${name} must have a source path`);
}
function resolvePackageEntry(packageDirectory: string, source: string, name: string): string {
if (isAbsolute(source)) throw new Error(`Facet package entry ${name} must be relative to the package directory`);
const path = resolve(packageDirectory, source);
const relativePath = relative(packageDirectory, path);
if (
relativePath.length === 0 ||
relativePath === ".." ||
relativePath.startsWith(`..${sep}`) ||
isAbsolute(relativePath)
) {
throw new Error(`Facet package entry ${name} escapes the package directory`);
}
return path;
}
function validateCanonicalPackageEntry(packageDirectory: string, path: string, name: string): void {
const relativePath = relative(packageDirectory, path);
if (relativePath === ".." || relativePath.startsWith(`..${sep}`) || isAbsolute(relativePath)) {
throw new Error(`Facet package entry ${name} resolves outside the package directory`);
}
}
function isMissingPath(error: unknown): boolean {
return isRecord(error) && error.code === "ENOENT";
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}

View File

@@ -0,0 +1,660 @@
import { awaitWithContext, BACKGROUND_CONTEXT } from "../context/index.ts";
import type { Op } from "../delta/index.ts";
import type {
Context,
JsonValue,
RemoteServiceBinding,
RemoteServiceBindingOptions,
RemoteServiceTransport,
Service,
ServiceInstanceAddress,
ServiceInstanceSnapshot,
ServiceMemberSnapshot,
ServiceProviderUpdate,
ServiceSubscription,
} from "../types.ts";
import { RemoteServiceError } from "./errors.ts";
import { ServiceSlot } from "./handle.ts";
import { InstanceDirectory, type InstanceDirectoryEntry } from "./instances.ts";
import { ReplicatedStateReplica, serviceDeliveryContext } from "./state.ts";
type ErrorReporter = (error: Error) => void;
type ServiceMemberKind = ServiceMemberSnapshot["kind"];
class MemberSlot {
readonly #serviceId: string;
readonly #member: string;
readonly #invoke: (args: readonly JsonValue[], context: Context) => Promise<JsonValue | undefined>;
readonly #state: ReplicatedStateReplica;
readonly #isActive: () => boolean;
readonly #assertAccess: () => void;
readonly value: unknown;
#kind: ServiceMemberKind | undefined;
#expectedKind: ServiceMemberKind | undefined;
constructor(
serviceId: string,
member: string,
invoke: (args: readonly JsonValue[], context: Context) => Promise<JsonValue | undefined>,
isActive: () => boolean,
assertAccess: () => void,
reportError: ErrorReporter,
) {
this.#serviceId = serviceId;
this.#member = member;
this.#invoke = invoke;
this.#isActive = isActive;
this.#assertAccess = assertAccess;
this.#state = new ReplicatedStateReplica(reportError);
const callable = (): void => {};
this.value = new Proxy(callable, {
apply: (_target, _thisArg, args) => this.#call(args),
get: (_target, property) => {
if (property === "value") {
this.#assertAccess();
this.#expect("state");
return this.#state.value;
}
if (property === "subscribe") return this.#subscribe.bind(this);
if (property === Symbol.toStringTag) return "RemoteServiceMember";
if (property === "then") return undefined;
return undefined;
},
});
}
setDescription(kind: ServiceMemberKind): void {
if (this.#kind !== undefined && this.#kind !== kind) {
throw new Error(`Remote service member ${this.#serviceId}.${this.#member} changed kind`);
}
this.#kind = kind;
if (this.#expectedKind !== undefined && this.#expectedKind !== kind) {
throw new RemoteServiceError(
"service_member_mismatch",
`Remote service member ${this.#serviceId}.${this.#member} is ${kind}, not ${this.#expectedKind}`,
);
}
}
hydrate(sequence: number, ops: readonly Op[], context: Context): void {
this.setDescription("state");
this.#state.hydrate(sequence, ops, context);
}
update(sequence: number, ops: readonly Op[], context: Context): void {
this.setDescription("state");
this.#state.update(sequence, ops, context);
}
clear(): void {
this.#state.clear();
}
#subscribe(listener: (value: JsonValue, context: Context) => void): () => void {
this.#assertAccess();
if (typeof listener !== "function") {
throw new TypeError("Replicated state subscription listener must be a function");
}
this.#expect("state");
return this.#state.subscribe(listener);
}
#expect(kind: ServiceMemberKind): void {
if (this.#expectedKind !== undefined && this.#expectedKind !== kind) {
throw new RemoteServiceError(
"service_member_mismatch",
`Remote service member ${this.#serviceId}.${this.#member} was used as two different kinds`,
);
}
this.#expectedKind = kind;
if (this.#kind !== undefined && this.#kind !== kind) {
throw new RemoteServiceError(
"service_member_mismatch",
`Remote service member ${this.#serviceId}.${this.#member} is ${this.#kind}, not ${kind}`,
);
}
}
#call(args: unknown[]): Promise<JsonValue | undefined> {
this.#assertAccess();
this.#expect("method");
if (!this.#isActive()) {
return Promise.reject(
new RemoteServiceError("service_stale_instance", `Remote service ${this.#serviceId} binding is closed`),
);
}
const context = args.at(-1);
if (!isContext(context)) {
return Promise.reject(
new RemoteServiceError(
"service_invalid_value",
`Remote service method ${this.#serviceId}.${this.#member} requires a trailing Context`,
),
);
}
const businessArgs = args.slice(0, -1) as JsonValue[];
return this.#invoke(businessArgs, context);
}
}
class ServiceFacade {
readonly #serviceId: string;
readonly #address: ServiceInstanceAddress | undefined;
readonly #transport: RemoteServiceTransport;
readonly #reportError: ErrorReporter;
readonly #slots = new Map<string, MemberSlot>();
readonly #descriptions = new Map<string, ServiceMemberKind>();
readonly #isActive: () => boolean;
readonly #assertAccess: () => void;
readonly proxy: object;
constructor(
serviceId: string,
address: ServiceInstanceAddress | undefined,
transport: RemoteServiceTransport,
isActive: () => boolean,
assertAccess: () => void,
reportError: ErrorReporter,
) {
this.#serviceId = serviceId;
this.#address = address;
this.#transport = transport;
this.#isActive = isActive;
this.#assertAccess = assertAccess;
this.#reportError = reportError;
this.proxy = new Proxy(Object.create(null) as object, {
get: (_target, property) => {
if (typeof property !== "string") return undefined;
return this.#slot(property).value;
},
});
}
install(snapshot: ServiceInstanceSnapshot, context: Context): void {
if (!sameAddress(snapshot.instance, this.#address))
throw new Error("Remote service snapshot has the wrong address");
const members = validateMembers(snapshot.members);
for (const name of this.#slots.keys()) {
if (!members.has(name)) {
throw new RemoteServiceError(
"service_member_not_found",
`Unknown remote service member ${this.#serviceId}.${name}`,
);
}
}
this.#descriptions.clear();
for (const [name, member] of members) this.#descriptions.set(name, member.kind);
for (const member of members.values()) {
const slot = this.#slots.get(member.name);
if (member.kind === "state") {
(slot ?? this.#slot(member.name)).hydrate(member.sequence, member.ops, context);
} else {
slot?.setDescription(member.kind);
}
}
}
update(member: string, sequence: number, ops: readonly Op[], context: Context): void {
if (this.#descriptions.get(member) !== "state") {
throw new Error(`Remote service update targets non-state member ${this.#serviceId}.${member}`);
}
this.#slot(member).update(sequence, ops, context);
}
clear(): void {
for (const slot of this.#slots.values()) slot.clear();
}
#slot(member: string): MemberSlot {
let slot = this.#slots.get(member);
if (slot !== undefined) return slot;
slot = new MemberSlot(
this.#serviceId,
member,
(args, context) =>
this.#transport.invoke(
{
serviceId: this.#serviceId,
...(this.#address === undefined ? {} : { instance: this.#address }),
member,
args,
},
context,
),
this.#isActive,
this.#assertAccess,
this.#reportError,
);
const kind = this.#descriptions.get(member);
if (kind !== undefined) slot.setDescription(kind);
this.#slots.set(member, slot);
return slot;
}
}
interface SingletonBinding {
facade: ServiceFacade;
subscription?: ServiceSubscription;
starting?: Promise<void>;
active: boolean;
revision: number;
}
interface KeyedInstance extends InstanceDirectoryEntry {
readonly facade: ServiceFacade;
}
class KeyedBinding<T> {
readonly #service: Service<T>;
readonly #transport: RemoteServiceTransport;
readonly #reportError: ErrorReporter;
readonly #assertAccess: () => void;
readonly #onEmpty: () => void;
readonly #instances: InstanceDirectory<KeyedInstance>;
#subscription: ServiceSubscription | undefined;
#starting: Promise<void> | undefined;
#closed = false;
#bound: boolean;
#revision = 0;
constructor(
service: Service<T>,
transport: RemoteServiceTransport,
reportError: ErrorReporter,
assertAccess: () => void,
onEmpty: () => void,
bound: boolean,
) {
this.#service = service;
this.#transport = transport;
this.#reportError = reportError;
this.#assertAccess = assertAccess;
this.#onEmpty = onEmpty;
this.#bound = bound;
this.#instances = new InstanceDirectory({ ready: false, onError: reportError });
}
observe(handler: (service: T, context: Context) => void | Promise<void>): () => void {
if (this.#closed) throw new Error("Remote keyed service binding is closed");
let stopped = false;
const stop = this.#instances.observe<T>((service, context) => {
const slot = new ServiceSlot(this.#service.id, true);
void slot.bind(service as object);
return handler(
slot.view<T>(() => {
this.#assertAccess();
if (stopped || context.abortSignal?.aborted) {
throw new RemoteServiceError(
"service_stale_instance",
`Remote service ${this.#service.id} observation is closed`,
);
}
}),
context,
);
});
if (this.#bound && this.#starting === undefined) {
const revision = this.#revision;
const starting = this.#start(revision);
this.#starting = starting;
void starting.catch((error: unknown) => {
if (!this.#closed && this.#revision === revision && this.#bound) this.#reportError(toError(error));
});
}
return () => {
if (stopped) return;
stopped = true;
stop();
if (this.#instances.observerCount === 0) this.#onEmpty();
};
}
async rebind(bound: boolean, context: Context): Promise<void> {
if (this.#closed) return;
this.#bound = bound;
const revision = ++this.#revision;
await this.#reset(context, false);
if (this.#closed || this.#revision !== revision || this.#bound !== bound) return;
if (bound && this.#instances.observerCount > 0) {
const starting = this.#start(revision);
this.#starting = starting;
await starting;
}
}
ready(): Promise<void> {
return this.#starting ?? Promise.resolve();
}
async close(context: Context): Promise<void> {
if (this.#closed) return;
this.#closed = true;
this.#revision += 1;
await this.#reset(context, true);
this.#instances.dispose();
}
async #reset(context: Context, waitForStarting: boolean): Promise<void> {
this.#instances.reset();
const starting = this.#starting;
this.#starting = undefined;
const subscription = this.#subscription;
this.#subscription = undefined;
await Promise.all([
waitForStarting ? starting?.catch(() => {}) : undefined,
subscription === undefined ? undefined : subscription.close(context),
]);
}
async #start(revision: number): Promise<void> {
const subscription = await this.#transport.subscribe(
this.#service.id,
"keyed",
(update, context) => {
if (this.#revision === revision) this.#update(update, context);
},
BACKGROUND_CONTEXT,
);
if (this.#closed || !this.#bound || this.#revision !== revision) {
await subscription.close(BACKGROUND_CONTEXT);
return;
}
this.#subscription = subscription;
if (subscription.snapshot.mode !== "keyed" || subscription.snapshot.serviceId !== this.#service.id) {
throw new Error(`Remote service ${this.#service.id} returned the wrong keyed snapshot`);
}
for (const snapshot of subscription.snapshot.instances) this.#spawn(snapshot, serviceDeliveryContext());
subscription.activate();
this.#instances.ready();
}
#update(update: ServiceProviderUpdate, context: Context): void {
if (this.#closed) return;
try {
switch (update.type) {
case "unavailable":
case "replaced":
throw new Error("Keyed service received a singleton lifecycle update");
case "spawned":
this.#spawn(update.instance, context);
break;
case "closed": {
const instance = this.#instances.get(update.instance.key);
if (instance?.generation === update.instance.generation) this.#instances.remove(instance);
break;
}
case "state": {
if (update.instance === undefined) throw new Error("Keyed state update has no instance address");
const instance = this.#instances.get(update.instance.key);
if (instance?.generation !== update.instance.generation) return;
instance.facade.update(update.member, update.sequence, update.ops, context);
break;
}
}
} catch (error) {
this.#reportError(toError(error));
}
}
#spawn(snapshot: ServiceInstanceSnapshot, context: Context): void {
const address = snapshot.instance;
if (address === undefined) throw new Error("Keyed service instance snapshot has no address");
let active = true;
const facade = new ServiceFacade(
this.#service.id,
address,
this.#transport,
() => active && !this.#closed,
this.#assertAccess,
this.#reportError,
);
facade.install(snapshot, context);
this.#instances.replace({
key: address.key,
generation: address.generation,
service: facade.proxy,
facade,
deactivate: () => {
active = false;
facade.clear();
},
});
}
}
export class RemoteServiceBindingImpl implements RemoteServiceBinding {
readonly #transport: RemoteServiceTransport;
readonly #allowlist = new Set<string>();
readonly #reportError: ErrorReporter;
readonly #modes = new Map<string, "singleton" | "keyed">();
readonly #assertAccess: () => void;
readonly #singletons = new Map<string, SingletonBinding>();
readonly #keyed = new Map<string, KeyedBinding<unknown>>();
#bound: boolean;
#readinessRevision = 0;
#bindingTransition = Promise.resolve();
#disposed = false;
constructor(options: RemoteServiceBindingOptions) {
this.#transport = options.transport;
const ids = options.services.map(({ id }) => id);
if (new Set(ids).size !== ids.length) throw new TypeError("Remote service binding has duplicate service IDs");
for (const id of ids) this.#allowlist.add(id);
this.#reportError = options.onError ?? (() => {});
this.#assertAccess = options.assertAccess ?? (() => {});
this.#bound = options.bound ?? true;
}
use<T>(service: Service<T>): T {
this.#assertRemotable(service);
this.#assertAvailable(service.id, "singleton");
let binding = this.#singletons.get(service.id);
if (binding !== undefined) return binding.facade.proxy as T;
binding = { facade: undefined as unknown as ServiceFacade, active: true, revision: 0 };
binding.facade = new ServiceFacade(
service.id,
undefined,
this.#transport,
() => binding!.active && !this.#disposed && this.#bound,
() => this.#assertHandleAccess(),
this.#reportError,
);
this.#singletons.set(service.id, binding);
this.#readinessRevision += 1;
if (this.#bound) {
const revision = binding.revision;
const starting = this.#startSingleton(service.id, binding, revision);
binding.starting = starting;
void starting.catch((error: unknown) => {
if (binding.active && binding.revision === revision && !this.#disposed && this.#bound) {
this.#reportError(toError(error));
}
});
}
return binding.facade.proxy as T;
}
observe<T>(service: Service<T>, handler: (service: T, context: Context) => void | Promise<void>): () => void {
this.#assertRemotable(service);
this.#assertAvailable(service.id, "keyed");
let binding = this.#keyed.get(service.id) as KeyedBinding<T> | undefined;
if (binding === undefined) {
binding = new KeyedBinding(
service,
this.#transport,
this.#reportError,
() => this.#assertHandleAccess(),
() => {
if (this.#keyed.get(service.id) !== binding) return;
this.#keyed.delete(service.id);
this.#readinessRevision += 1;
void binding!.close(BACKGROUND_CONTEXT).catch(this.#reportError);
},
this.#bound,
);
this.#keyed.set(service.id, binding as KeyedBinding<unknown>);
this.#readinessRevision += 1;
}
return binding.observe(handler);
}
async ready(context: Context): Promise<void> {
if (this.#disposed) throw new Error("Remote service binding is disposed");
while (true) {
const revision = this.#readinessRevision;
const starts = [
this.#bindingTransition,
...[...this.#singletons.values()].flatMap((binding) =>
binding.starting === undefined ? [] : [binding.starting],
),
...[...this.#keyed.values()].map((binding) => binding.ready()),
];
await awaitWithContext(
Promise.all(starts).then(() => undefined),
context,
);
if (this.#disposed) throw new Error("Remote service binding is disposed");
if (revision === this.#readinessRevision) return;
}
}
async rebind(bound: boolean, context: Context): Promise<void> {
if (this.#disposed) throw new Error("Remote service binding is disposed");
this.#bound = bound;
this.#readinessRevision += 1;
const transitions: Promise<void>[] = [];
for (const [serviceId, binding] of this.#singletons) {
binding.revision += 1;
binding.facade.clear();
const subscription = binding.subscription;
delete binding.subscription;
const revision = binding.revision;
const starting = (async () => {
await subscription?.close(context);
if (bound) await this.#startSingleton(serviceId, binding, revision);
})();
binding.starting = starting;
transitions.push(starting);
}
for (const binding of this.#keyed.values()) transitions.push(binding.rebind(bound, context));
const completion = (async () => {
const results = await Promise.allSettled(transitions);
const errors = results.filter((result): result is PromiseRejectedResult => result.status === "rejected");
if (errors.length > 0)
throw new AggregateError(
errors.map(({ reason }) => reason),
"Failed to rebind services",
);
})();
this.#bindingTransition = completion;
await completion;
}
async dispose(context: Context): Promise<void> {
if (this.#disposed) return;
this.#disposed = true;
const closes: Promise<void>[] = [];
for (const binding of this.#singletons.values()) {
binding.active = false;
binding.facade.clear();
if (binding.starting) closes.push(binding.starting.catch(() => {}));
if (binding.subscription) closes.push(Promise.resolve(binding.subscription.close(context)));
}
for (const binding of this.#keyed.values()) closes.push(binding.close(context));
this.#singletons.clear();
this.#keyed.clear();
const results = await Promise.allSettled(closes);
const errors = results.filter((result): result is PromiseRejectedResult => result.status === "rejected");
if (errors.length > 0)
throw new AggregateError(
errors.map(({ reason }) => reason),
"Failed to dispose services",
);
}
async #startSingleton(serviceId: string, binding: SingletonBinding, revision: number): Promise<void> {
const subscription = await this.#transport.subscribe(
serviceId,
"singleton",
(update, context) => {
if (!binding.active || binding.revision !== revision) return;
try {
if (update.type === "unavailable") {
binding.facade.clear();
} else if (update.type === "replaced") {
if (update.snapshot.instance !== undefined) {
throw new Error("Singleton replacement has an instance address");
}
binding.facade.install(update.snapshot, context);
} else if (update.type === "state" && update.instance === undefined) {
binding.facade.update(update.member, update.sequence, update.ops, context);
}
} catch (error) {
this.#reportError(toError(error));
}
},
BACKGROUND_CONTEXT,
);
if (!binding.active || this.#disposed || !this.#bound || binding.revision !== revision) {
await subscription.close(BACKGROUND_CONTEXT);
return;
}
binding.subscription = subscription;
const snapshot = subscription.snapshot;
if (snapshot.mode !== "singleton" || snapshot.serviceId !== serviceId || snapshot.instances.length !== 1) {
throw new Error(`Remote service ${serviceId} returned an invalid singleton snapshot`);
}
binding.facade.install(snapshot.instances[0]!, serviceDeliveryContext());
subscription.activate();
}
#assertHandleAccess(): void {
if (this.#disposed) throw new Error("Remote service binding is disposed");
this.#assertAccess();
}
#assertRemotable(service: { readonly id: string; readonly local: boolean }): void {
if (service.local) throw new RemoteServiceError("service_not_allowed", `Service ${service.id} is process-local`);
}
#assertAvailable(serviceId: string, mode: "singleton" | "keyed"): void {
if (this.#disposed) throw new Error("Remote service binding is disposed");
if (!this.#allowlist.has(serviceId)) {
throw new RemoteServiceError("service_not_allowed", `Remote service ${serviceId} is not allowlisted`);
}
const existing = this.#modes.get(serviceId);
if (existing !== undefined && existing !== mode) {
throw new RemoteServiceError(
"service_mode_mismatch",
`Remote service ${serviceId} is already used as ${existing}`,
);
}
this.#modes.set(serviceId, mode);
}
}
function validateMembers(members: readonly ServiceMemberSnapshot[]): ReadonlyMap<string, ServiceMemberSnapshot> {
const result = new Map<string, ServiceMemberSnapshot>();
for (const member of members) {
if (member.name.length === 0 || result.has(member.name)) {
throw new Error("Remote service has invalid member descriptions");
}
result.set(member.name, member);
}
return result;
}
function sameAddress(left: ServiceInstanceAddress | undefined, right: ServiceInstanceAddress | undefined): boolean {
if (left === undefined || right === undefined) return left === right;
return left.key === right.key && left.generation === right.generation;
}
function isContext(value: unknown): value is Context {
if (typeof value !== "object" || value === null) return false;
const candidate = value as Partial<Context>;
return typeof candidate.value === "function" && typeof candidate.toString === "function";
}
function toError(error: unknown): Error {
return error instanceof Error ? error : new Error(String(error));
}

View File

@@ -0,0 +1,26 @@
export const REMOTE_SERVICE_ERROR_CODES = [
"service_not_allowed",
"service_not_found",
"service_mode_mismatch",
"service_member_not_found",
"service_member_mismatch",
"service_instance_not_found",
"service_stale_instance",
"service_invalid_value",
] as const;
export type RemoteServiceErrorCode = (typeof REMOTE_SERVICE_ERROR_CODES)[number];
export function isRemoteServiceErrorCode(value: unknown): value is RemoteServiceErrorCode {
return typeof value === "string" && REMOTE_SERVICE_ERROR_CODES.includes(value as RemoteServiceErrorCode);
}
export class RemoteServiceError extends Error {
readonly code: RemoteServiceErrorCode;
constructor(code: RemoteServiceErrorCode, message: string) {
super(message);
this.name = "RemoteServiceError";
this.code = code;
}
}

View File

@@ -0,0 +1,113 @@
interface ResolvedValue {
readonly value: unknown;
readonly receiver: object;
}
type ValueResolver = () => ResolvedValue;
/** Host-owned mutable target with consumer-owned guarded views. */
export class ServiceSlot {
readonly #serviceId: string;
readonly #wrapObjects: boolean;
#implementation: object | undefined;
constructor(serviceId: string, wrapObjects: boolean) {
this.#serviceId = serviceId;
this.#wrapObjects = wrapObjects;
}
view<T>(assertAccess: () => void): T {
return new ServiceView(this, assertAccess, this.#wrapObjects).proxy as T;
}
bind(implementation: object): void {
this.#implementation = implementation;
}
unbind(): void {
this.#implementation = undefined;
}
resolve(property: PropertyKey, assertAccess: () => void): ResolvedValue {
assertAccess();
const implementation = this.#implementation;
if (implementation === undefined) throw new Error(`Service ${this.#serviceId} is disconnected`);
return {
value: Reflect.get(implementation, property, implementation),
receiver: implementation,
};
}
}
class ServiceView {
readonly #slot: ServiceSlot;
readonly #assertAccess: () => void;
readonly #wrapObjects: boolean;
readonly #members = new Map<PropertyKey, ValueView>();
readonly proxy: object;
constructor(slot: ServiceSlot, assertAccess: () => void, wrapObjects: boolean) {
this.#slot = slot;
this.#assertAccess = assertAccess;
this.#wrapObjects = wrapObjects;
this.proxy = new Proxy(Object.create(null) as object, {
get: (_target, property) => this.#getMember(property),
});
}
#getMember(property: PropertyKey): unknown {
const resolve = (): ResolvedValue => this.#slot.resolve(property, this.#assertAccess);
const current = resolve().value;
if (!isObject(current) || (typeof current !== "function" && !this.#wrapObjects)) return current;
let member = this.#members.get(property);
if (member === undefined) {
member = new ValueView(resolve, typeof current === "function");
this.#members.set(property, member);
}
return member.proxy;
}
}
class ValueView {
readonly #resolve: ValueResolver;
readonly #children = new Map<PropertyKey, ValueView>();
readonly proxy: object;
constructor(resolve: ValueResolver, callable: boolean) {
this.#resolve = resolve;
const target: object = callable ? () => undefined : Object.create(null);
this.proxy = new Proxy(target, {
apply: (_target, _thisArg, args) => this.#invoke(args),
get: (_target, property) => this.#get(property),
});
}
#invoke(args: unknown[]): unknown {
const { value, receiver } = this.#resolve();
if (typeof value !== "function") throw new TypeError("Service member is not callable");
return Reflect.apply(value, receiver, args);
}
#get(property: PropertyKey): unknown {
const resolve = (): ResolvedValue => {
const parent = this.#resolve();
if (!isObject(parent.value)) throw new TypeError("Service member does not have properties");
return {
value: Reflect.get(parent.value, property, parent.value),
receiver: parent.value,
};
};
const current = resolve().value;
if (typeof current !== "function") return current;
let child = this.#children.get(property);
if (child === undefined) {
child = new ValueView(resolve, true);
this.#children.set(property, child);
}
return child.proxy;
}
}
function isObject(value: unknown): value is object {
return (typeof value === "object" && value !== null) || typeof value === "function";
}

View File

@@ -0,0 +1,147 @@
import { BACKGROUND_CONTEXT, withCancel } from "../context/index.ts";
import type { Context } from "../types.ts";
export interface InstanceDirectoryEntry {
readonly key: string;
readonly generation: number;
readonly service: object;
deactivate(): void;
}
interface Observer {
readonly handler: (service: object, context: Context) => void | Promise<void>;
readonly tasks: Map<InstanceDirectoryEntry, { readonly cancel: (reason?: unknown) => void }>;
closed: boolean;
}
/** Owns keyed instance lifetime and the cancellable tasks observing those instances. */
export class InstanceDirectory<TEntry extends InstanceDirectoryEntry> {
readonly #entries = new Map<string, TEntry>();
readonly #observers = new Set<Observer>();
readonly #reportError: (error: Error) => void;
#ready: boolean;
#disposed = false;
constructor(options: { readonly ready: boolean; readonly onError: (error: Error) => void }) {
this.#ready = options.ready;
this.#reportError = options.onError;
}
get observerCount(): number {
return this.#observers.size;
}
get(key: string): TEntry | undefined {
return this.#entries.get(key);
}
insert(entry: TEntry): void {
this.#assertActive();
if (this.#entries.has(entry.key)) {
throw new Error(`Keyed service already has a live instance with key ${entry.key}`);
}
this.#entries.set(entry.key, entry);
if (this.#ready) this.#startAll(entry);
}
replace(entry: TEntry): void {
this.#assertActive();
const previous = this.#entries.get(entry.key);
if (previous !== undefined) {
if (previous.generation === entry.generation) {
throw new Error("Keyed service repeated a live generation");
}
this.#remove(previous);
}
this.#entries.set(entry.key, entry);
if (this.#ready) this.#startAll(entry);
}
remove(entry: TEntry): void {
if (this.#entries.get(entry.key) !== entry) return;
this.#remove(entry);
}
ready(): void {
this.#assertActive();
if (this.#ready) return;
this.#ready = true;
for (const entry of this.#entries.values()) this.#startAll(entry);
}
reset(): void {
if (this.#disposed) return;
this.#ready = false;
for (const entry of [...this.#entries.values()]) this.#remove(entry);
}
observe<T>(handler: (service: T, context: Context) => void | Promise<void>): () => void {
this.#assertActive();
const observer: Observer = {
handler: handler as Observer["handler"],
tasks: new Map(),
closed: false,
};
this.#observers.add(observer);
if (this.#ready) {
for (const entry of this.#entries.values()) this.#start(observer, entry);
}
return () => {
if (observer.closed) return;
observer.closed = true;
for (const task of observer.tasks.values()) task.cancel();
observer.tasks.clear();
this.#observers.delete(observer);
};
}
dispose(): void {
if (this.#disposed) return;
this.#disposed = true;
for (const observer of this.#observers) {
observer.closed = true;
for (const task of observer.tasks.values()) task.cancel();
observer.tasks.clear();
}
this.#observers.clear();
for (const entry of [...this.#entries.values()]) {
entry.deactivate();
}
this.#entries.clear();
}
#remove(entry: TEntry): void {
if (this.#entries.get(entry.key) !== entry) return;
this.#entries.delete(entry.key);
entry.deactivate();
for (const observer of this.#observers) {
observer.tasks.get(entry)?.cancel();
observer.tasks.delete(entry);
}
}
#startAll(entry: TEntry): void {
for (const observer of this.#observers) this.#start(observer, entry);
}
#start(observer: Observer, entry: TEntry): void {
if (observer.closed || observer.tasks.has(entry)) return;
const { context, cancel } = withCancel(BACKGROUND_CONTEXT);
observer.tasks.set(entry, { cancel });
try {
void Promise.resolve(observer.handler(entry.service, context)).catch((error: unknown) => {
if (!context.abortSignal?.aborted) this.#reportError(toError(error));
});
} catch (error) {
if (!context.abortSignal?.aborted) this.#reportError(toError(error));
}
}
#assertActive(): void {
if (this.#disposed) throw new Error("Keyed service directory is disposed");
}
}
function toError(error: unknown): Error {
return error instanceof Error ? error : new Error(String(error));
}

View File

@@ -0,0 +1,17 @@
import type { RemoteServiceTransport } from "../types.ts";
import type { RemoteServiceProvider } from "./provider.ts";
/** Connects a provider to a binding without changing remote service semantics. */
export function createLoopbackServiceTransport(provider: RemoteServiceProvider): RemoteServiceTransport {
return {
invoke: (call, context) => provider.invoke(call, context),
subscribe: async (serviceId, mode, listener) => {
const subscription = provider.subscribe(serviceId, mode, listener);
return {
snapshot: subscription.snapshot,
activate: () => subscription.activate(),
close: () => subscription.close(),
};
},
};
}

View File

@@ -0,0 +1,586 @@
import type {
Context,
JsonValue,
RemoteServiceContract,
Service,
ServiceCall,
ServiceCatalogueEntry,
ServiceInstanceAddress,
ServiceInstanceSnapshot,
ServiceMemberSnapshot,
ServiceMode,
ServiceProviderUpdate,
ServiceSubscription,
ServiceSubscriptionSnapshot,
} from "../types.ts";
import { RemoteServiceError } from "./errors.ts";
import { serviceDeliveryContext } from "./state.ts";
import { getReplicatedStateInternals, type ReplicatedStateInternals } from "./state-internals.ts";
import { decodeServiceControlCall } from "./wire.ts";
type RemoteMethod = (...args: unknown[]) => unknown;
type InstanceMember =
| { readonly kind: "method"; readonly method: RemoteMethod }
| { readonly kind: "state"; readonly state: ReplicatedStateInternals };
type ServiceMemberKind = InstanceMember["kind"];
interface ClassifiedRemoteServiceImplementation {
readonly implementation: object;
readonly members: Map<string, InstanceMember>;
}
interface ProviderInstance {
readonly address?: ServiceInstanceAddress;
readonly implementation: object;
readonly members: ReadonlyMap<string, InstanceMember>;
readonly removeMemberListeners: readonly (() => void)[];
active: boolean;
}
interface ProviderSubscriber {
readonly listener: (update: ServiceProviderUpdate, context: Context) => void;
readonly buffer: { readonly update: ServiceProviderUpdate; readonly context: Context }[];
active: boolean;
terminated: boolean;
closed: boolean;
}
interface ServiceProviderDefinition {
readonly service: { readonly id: string; readonly local?: boolean };
readonly mode: ServiceMode;
}
interface ServiceRegistration {
readonly serviceId: string;
readonly mode: ServiceMode;
singleton?: ProviderInstance;
singletonShape?: ReadonlyMap<string, ServiceMemberKind>;
readonly instances: Map<string, ProviderInstance>;
readonly generations: Map<string, number>;
readonly subscribers: Set<ProviderSubscriber>;
}
export type ServiceUpdatePublisher = (
subscriptionId: string,
update: ServiceProviderUpdate,
context: Context,
) => void | Promise<void>;
/** Hosts one provider for one remote consumer and owns that consumer's subscriptions. */
export interface RemoteServiceEndpoint {
invoke(call: ServiceCall, publish: ServiceUpdatePublisher, context: Context): Promise<JsonValue | undefined>;
dispose(): void;
}
export class RemoteServiceProvider {
readonly #catalogue: readonly ServiceCatalogueEntry[];
readonly #registrations = new Map<string, ServiceRegistration>();
#disposed = false;
constructor(entries: readonly (ServiceProviderDefinition | { readonly id: string })[]) {
const definitions = entries.map(
(entry): ServiceProviderDefinition => ("service" in entry ? entry : { service: entry, mode: "singleton" }),
);
for (const { service } of definitions) {
if ("local" in service && service.local === true) {
throw new TypeError(`Local service ${service.id} cannot be published remotely`);
}
}
const ids = definitions.map(({ service }) => service.id);
if (new Set(ids).size !== ids.length) throw new TypeError("Remote service catalogue contains duplicate IDs");
this.#catalogue = Object.freeze(
definitions.map(({ service, mode }) => Object.freeze({ serviceId: service.id, mode })),
);
for (const { service, mode } of definitions) {
this.#registrations.set(service.id, {
serviceId: service.id,
mode,
instances: new Map(),
generations: new Map(),
subscribers: new Set(),
});
}
}
get catalogue(): readonly ServiceCatalogueEntry[] {
return this.#catalogue;
}
provide<T>(service: Service<T>, implementation: NoInfer<RemoteServiceContract<T>>): void {
this.#assertActive();
this.#assertRemotable(service);
this.#assertAllowed(service.id);
const registration = this.#registration(service.id, "singleton");
if (registration.singleton !== undefined) {
throw new RemoteServiceError("service_mode_mismatch", `Remote service ${service.id} already has a provider`);
}
const classified = classifyRemoteServiceImplementation(registration.serviceId, implementation);
const shape = serviceMemberShape(classified.members);
this.#assertSingletonShape(registration, shape);
registration.singleton = this.#createInstance(registration, classified, undefined);
registration.singletonShape = shape;
}
/** Disconnect one singleton while preserving active subscriptions and remote facades. */
withdraw<T>(service: Service<T>): void {
this.#assertActive();
this.#assertRemotable(service);
this.#assertAllowed(service.id);
const registration = this.#registration(service.id, "singleton");
const previous = registration.singleton;
if (previous === undefined) return;
previous.active = false;
for (const remove of previous.removeMemberListeners) remove();
delete registration.singleton;
this.#emit(registration, { type: "unavailable" });
}
/** Check a singleton replacement without changing the active provider. */
validateReplacement<T>(service: Service<T>, implementation: NoInfer<RemoteServiceContract<T>>): void {
this.#assertActive();
this.#assertRemotable(service);
this.#assertAllowed(service.id);
const registration = this.#registration(service.id, "singleton");
const classified = classifyRemoteServiceImplementation(registration.serviceId, implementation);
this.#assertSingletonShape(registration, serviceMemberShape(classified.members));
}
/** Replace one singleton without making its stable remote facade unavailable. */
replace<T>(service: Service<T>, implementation: NoInfer<RemoteServiceContract<T>>): void {
this.#assertActive();
this.#assertRemotable(service);
this.#assertAllowed(service.id);
const registration = this.#registration(service.id, "singleton");
const classified = classifyRemoteServiceImplementation(registration.serviceId, implementation);
const shape = serviceMemberShape(classified.members);
this.#assertSingletonShape(registration, shape);
const replacement = this.#createInstance(registration, classified, undefined);
const previous = registration.singleton;
if (previous !== undefined) {
previous.active = false;
for (const remove of previous.removeMemberListeners) remove();
}
registration.singleton = replacement;
registration.singletonShape = shape;
this.#emit(registration, { type: "replaced", snapshot: this.#snapshotInstance(replacement) });
}
use<T>(service: Service<T>): T {
this.#assertActive();
this.#assertRemotable(service);
this.#assertAllowed(service.id);
const registration = this.#registrations.get(service.id);
if (registration?.mode !== "singleton" || registration.singleton === undefined) {
throw new RemoteServiceError("service_not_found", `Remote service ${service.id} has no local provider`);
}
return registration.singleton.implementation as T;
}
spawn<T>(service: Service<T>, key: string, implementation: NoInfer<RemoteServiceContract<T>>): () => void {
this.#assertActive();
this.#assertRemotable(service);
this.#assertAllowed(service.id);
if (key.length === 0) throw new TypeError("Remote service instance key must not be empty");
const registration = this.#registration(service.id, "keyed");
if (registration.instances.has(key)) {
throw new RemoteServiceError(
"service_mode_mismatch",
`Remote service ${service.id} already has a live instance with key ${key}`,
);
}
const generation = (registration.generations.get(key) ?? 0) + 1;
registration.generations.set(key, generation);
const address = { key, generation } satisfies ServiceInstanceAddress;
const classified = classifyRemoteServiceImplementation(registration.serviceId, implementation);
const instance = this.#createInstance(registration, classified, address);
registration.instances.set(key, instance);
this.#emit(registration, { type: "spawned", instance: this.#snapshotInstance(instance) });
let closed = false;
return () => {
if (closed) return;
closed = true;
if (registration.instances.get(key) !== instance) return;
instance.active = false;
for (const remove of instance.removeMemberListeners) remove();
registration.instances.delete(key);
this.#emit(registration, { type: "closed", instance: address });
};
}
async invoke(call: ServiceCall, context: Context): Promise<JsonValue | undefined> {
this.#assertActive();
this.#assertAllowed(call.serviceId);
const registration = this.#registrations.get(call.serviceId);
if (registration === undefined) {
throw new RemoteServiceError("service_not_found", `Unknown remote service ${call.serviceId}`);
}
const instance = this.#resolveInstance(registration, call.instance);
const member = instance.members.get(call.member);
if (member === undefined) {
throw new RemoteServiceError(
"service_member_not_found",
`Unknown remote service member ${call.serviceId}.${call.member}`,
);
}
if (member.kind !== "method") {
throw new RemoteServiceError(
"service_member_mismatch",
`Remote service member ${call.serviceId}.${call.member} is not a method`,
);
}
const result: unknown = await Reflect.apply(member.method, instance.implementation, [...call.args, context]);
return result as JsonValue | undefined;
}
subscribe(
serviceId: string,
mode: ServiceMode,
listener: (update: ServiceProviderUpdate, context: Context) => void,
): ServiceSubscription {
this.#assertActive();
this.#assertAllowed(serviceId);
const registration = this.#registration(serviceId, mode);
if (registration.mode === "singleton" && registration.singleton === undefined) {
throw new RemoteServiceError("service_not_found", `Remote service ${serviceId} has no provider`);
}
const subscriber: ProviderSubscriber = {
listener,
buffer: [],
active: false,
terminated: false,
closed: false,
};
this.#publishPending(registration);
registration.subscribers.add(subscriber);
const snapshot = this.#snapshot(registration);
return {
snapshot,
activate: () => {
if (subscriber.closed || subscriber.active) return;
subscriber.active = true;
const errors: unknown[] = [];
try {
for (const entry of subscriber.buffer.splice(0)) {
try {
listener(entry.update, entry.context);
} catch (error) {
errors.push(error);
}
}
} finally {
if (subscriber.terminated) subscriber.closed = true;
}
throwCollectedErrors(errors, "Failed to activate remote service subscription");
},
close: () => {
if (subscriber.closed) return;
subscriber.closed = true;
subscriber.buffer.length = 0;
registration.subscribers.delete(subscriber);
},
};
}
dispose(): void {
if (this.#disposed) return;
this.#disposed = true;
const errors: unknown[] = [];
for (const registration of this.#registrations.values()) {
const singleton = registration.singleton;
if (singleton !== undefined) {
singleton.active = false;
for (const remove of singleton.removeMemberListeners) remove();
delete registration.singleton;
try {
this.#emit(registration, { type: "unavailable" });
} catch (error) {
errors.push(error);
}
}
for (const [key, instance] of [...registration.instances]) {
instance.active = false;
for (const remove of instance.removeMemberListeners) remove();
registration.instances.delete(key);
try {
this.#emit(registration, { type: "closed", instance: instance.address! });
} catch (error) {
errors.push(error);
}
}
for (const subscriber of registration.subscribers) {
if (subscriber.active) {
subscriber.closed = true;
subscriber.buffer.length = 0;
} else {
subscriber.terminated = true;
}
}
registration.subscribers.clear();
}
this.#registrations.clear();
if (errors.length === 1) throw errors[0];
if (errors.length > 1) throw new AggregateError(errors, "Failed to dispose remote service provider");
}
#registration(serviceId: string, mode: ServiceMode): ServiceRegistration {
const registration = this.#registrations.get(serviceId);
if (registration === undefined) {
throw new RemoteServiceError("service_not_found", `Unknown remote service ${serviceId}`);
}
if (registration.mode !== mode) {
throw new RemoteServiceError(
"service_mode_mismatch",
`Remote service ${serviceId} is ${registration.mode}, not ${mode}`,
);
}
return registration;
}
#createInstance(
registration: ServiceRegistration,
classified: ClassifiedRemoteServiceImplementation,
address: ServiceInstanceAddress | undefined,
): ProviderInstance {
const removeMemberListeners: (() => void)[] = [];
const instance: ProviderInstance = {
...(address === undefined ? {} : { address }),
implementation: classified.implementation,
members: classified.members,
removeMemberListeners,
active: true,
};
for (const [name, member] of classified.members) {
if (member.kind !== "state") continue;
removeMemberListeners.push(
member.state.subscribe((ops, sequence, context) => {
if (!instance.active) return;
this.#emit(
registration,
{
type: "state",
...(address === undefined ? {} : { instance: address }),
member: name,
sequence,
ops,
},
context,
);
}),
);
}
return instance;
}
#assertSingletonShape(registration: ServiceRegistration, replacement: ReadonlyMap<string, ServiceMemberKind>): void {
const current = registration.singletonShape;
if (current === undefined || sameServiceMemberShape(current, replacement)) return;
throw new RemoteServiceError(
"service_member_mismatch",
`Remote service ${registration.serviceId} replacement must preserve its member shape`,
);
}
#resolveInstance(registration: ServiceRegistration, address: ServiceInstanceAddress | undefined): ProviderInstance {
if (registration.mode === "singleton") {
if (address !== undefined) {
throw new RemoteServiceError(
"service_mode_mismatch",
`Remote service ${registration.serviceId} is singleton`,
);
}
if (registration.singleton === undefined) {
throw new RemoteServiceError(
"service_not_found",
`Remote service ${registration.serviceId} has no provider`,
);
}
return registration.singleton;
}
if (address === undefined) {
throw new RemoteServiceError("service_mode_mismatch", `Remote service ${registration.serviceId} is keyed`);
}
const instance = registration.instances.get(address.key);
if (instance === undefined) {
throw new RemoteServiceError(
"service_instance_not_found",
`Remote service ${registration.serviceId} has no instance ${address.key}`,
);
}
if (instance.address?.generation !== address.generation) {
throw new RemoteServiceError(
"service_stale_instance",
`Remote service ${registration.serviceId} instance ${address.key} is stale`,
);
}
return instance;
}
#publishPending(registration: ServiceRegistration): void {
const context = serviceDeliveryContext();
const instances =
registration.mode === "singleton"
? registration.singleton === undefined
? []
: [registration.singleton]
: registration.instances.values();
for (const instance of instances) {
for (const member of instance.members.values()) {
if (member.kind === "state") member.state.publish(context);
}
}
}
#snapshot(registration: ServiceRegistration): ServiceSubscriptionSnapshot {
const instances =
registration.mode === "singleton"
? registration.singleton
? [this.#snapshotInstance(registration.singleton)]
: []
: [...registration.instances.values()]
.sort((left, right) => left.address!.key.localeCompare(right.address!.key))
.map((instance) => this.#snapshotInstance(instance));
return { serviceId: registration.serviceId, mode: registration.mode, instances };
}
#snapshotInstance(instance: ProviderInstance): ServiceInstanceSnapshot {
const members: ServiceMemberSnapshot[] = [];
for (const [name, member] of instance.members) {
if (member.kind === "method") {
members.push({ name, kind: "method" });
} else {
members.push({
name,
kind: "state",
sequence: member.state.sequence,
ops: [["r", member.state.value as JsonValue]],
});
}
}
return {
...(instance.address === undefined ? {} : { instance: instance.address }),
members,
};
}
#emit(registration: ServiceRegistration, update: ServiceProviderUpdate, context?: Context): void {
if (registration.subscribers.size === 0) return;
const deliveryContext = context ?? serviceDeliveryContext();
const errors: unknown[] = [];
for (const subscriber of registration.subscribers) {
if (subscriber.closed) continue;
const entry = { update, context: deliveryContext };
if (!subscriber.active) {
subscriber.buffer.push(entry);
continue;
}
try {
subscriber.listener(entry.update, entry.context);
} catch (error) {
errors.push(error);
}
}
throwCollectedErrors(errors, `Failed to publish remote service ${registration.serviceId} update`);
}
#assertRemotable(service: { readonly id: string; readonly local: boolean }): void {
if (service.local) throw new RemoteServiceError("service_not_allowed", `Service ${service.id} is process-local`);
}
#assertAllowed(serviceId: string): void {
if (!this.#registrations.has(serviceId)) {
throw new RemoteServiceError("service_not_allowed", `Remote service ${serviceId} is not allowlisted`);
}
}
#assertActive(): void {
if (this.#disposed) throw new Error("Remote service provider is disposed");
}
}
export function createRemoteServiceEndpoint(provider: RemoteServiceProvider): RemoteServiceEndpoint {
const subscriptions = new Map<string, ServiceSubscription>();
let disposed = false;
return {
async invoke(call, publish, context) {
if (disposed) throw new Error("Remote service endpoint is disposed");
const control = decodeServiceControlCall(call);
if (control?.type === "catalogue") return provider.catalogue as unknown as JsonValue;
if (control?.type === "subscribe") {
if (subscriptions.has(control.subscriptionId)) {
throw new Error("Service subscription ID is already active");
}
const subscription = provider.subscribe(control.serviceId, control.mode, (update, updateContext) => {
void Promise.resolve(publish(control.subscriptionId, update, updateContext)).catch(() => {});
});
subscriptions.set(control.subscriptionId, subscription);
subscription.activate();
return subscription.snapshot as unknown as JsonValue;
}
if (control?.type === "unsubscribe") {
const subscription = subscriptions.get(control.subscriptionId);
if (subscription === undefined) throw new Error("Service subscription was not found");
subscription.close();
subscriptions.delete(control.subscriptionId);
return undefined;
}
return provider.invoke(call, context);
},
dispose() {
if (disposed) return;
disposed = true;
for (const subscription of subscriptions.values()) subscription.close();
subscriptions.clear();
},
};
}
export function validateRemoteServiceImplementation(serviceId: string, implementation: unknown): void {
void classifyRemoteServiceImplementation(serviceId, implementation);
}
function classifyRemoteServiceImplementation(
serviceId: string,
implementation: unknown,
): ClassifiedRemoteServiceImplementation {
if (typeof implementation !== "object" || implementation === null || Array.isArray(implementation)) {
throw new TypeError(`Remote service ${serviceId} implementation must be an object`);
}
const members = new Map<string, InstanceMember>();
for (const name of Object.keys(implementation).sort()) {
const descriptor = Object.getOwnPropertyDescriptor(implementation, name);
if (descriptor === undefined || !("value" in descriptor)) {
throw new TypeError(`Remote service member ${serviceId}.${name} must be a data property`);
}
if (typeof descriptor.value === "function") {
members.set(name, { kind: "method", method: descriptor.value as RemoteMethod });
continue;
}
const state = getReplicatedStateInternals(descriptor.value);
if (state !== undefined) {
members.set(name, { kind: "state", state });
continue;
}
throw new TypeError(`Remote service member ${serviceId}.${name} is not remotely exposable`);
}
if (members.size === 0) throw new TypeError(`Remote service ${serviceId} has no members`);
return { implementation, members };
}
function throwCollectedErrors(errors: readonly unknown[], message: string): void {
if (errors.length === 1) throw errors[0];
if (errors.length > 1) throw new AggregateError(errors, message);
}
function serviceMemberShape(members: ReadonlyMap<string, InstanceMember>): ReadonlyMap<string, ServiceMemberKind> {
return new Map([...members].map(([name, member]) => [name, member.kind]));
}
function sameServiceMemberShape(
left: ReadonlyMap<string, ServiceMemberKind>,
right: ReadonlyMap<string, ServiceMemberKind>,
): boolean {
return left.size === right.size && [...left].every(([name, kind]) => right.get(name) === kind);
}

View File

@@ -0,0 +1,158 @@
import { type Decoder, decoder, type Encoder, encoder } from "../delta/index.ts";
import type {
ServiceInstanceAddress,
ServiceInstanceSnapshot,
ServiceProviderUpdate,
ServiceSubscriptionSnapshot,
} from "../types.ts";
import type { WireServiceProviderUpdate, WireServiceSubscriptionSnapshot } from "./wire.ts";
/** Stateful operation encoders for every replicated state in one service subscription. */
export interface ServiceStateEncoder {
encodeSnapshot(snapshot: ServiceSubscriptionSnapshot): WireServiceSubscriptionSnapshot;
encodeUpdate(update: ServiceProviderUpdate): WireServiceProviderUpdate;
}
/** Stateful operation decoders for every replicated state in one service subscription. */
export interface ServiceStateDecoder {
decodeSnapshot(snapshot: WireServiceSubscriptionSnapshot): ServiceSubscriptionSnapshot;
decodeUpdate(update: WireServiceProviderUpdate): ServiceProviderUpdate;
}
interface CodecEntry<C> {
readonly instance: ServiceInstanceAddress | undefined;
readonly codec: C;
}
class StateCodecRegistry<C> {
readonly #create: () => C;
readonly #entries = new Map<string, CodecEntry<C>>();
constructor(create: () => C) {
this.#create = create;
}
reset(): void {
this.#entries.clear();
}
add(instance: ServiceInstanceAddress | undefined, member: string): C {
const key = stateKey(instance, member);
if (this.#entries.has(key)) throw new Error(`Duplicate service state ${describeState(instance, member)}`);
const codec = this.#create();
this.#entries.set(key, { instance, codec });
return codec;
}
get(instance: ServiceInstanceAddress | undefined, member: string): C {
const codec = this.#entries.get(stateKey(instance, member))?.codec;
if (codec === undefined) throw new Error(`Unknown service state ${describeState(instance, member)}`);
return codec;
}
removeInstance(instance: ServiceInstanceAddress): void {
for (const [key, entry] of this.#entries) {
if (sameAddress(entry.instance, instance)) this.#entries.delete(key);
}
}
}
export function createServiceStateEncoder(): ServiceStateEncoder {
const codecs = new StateCodecRegistry<Encoder>(encoder);
return {
encodeSnapshot(snapshot) {
codecs.reset();
return {
...snapshot,
instances: snapshot.instances.map((instance) => encodeInstance(instance, codecs)),
};
},
encodeUpdate(update) {
switch (update.type) {
case "state":
return { ...update, ops: codecs.get(update.instance, update.member).encode(update.ops) };
case "replaced":
codecs.reset();
return { ...update, snapshot: encodeInstance(update.snapshot, codecs) };
case "spawned":
return { ...update, instance: encodeInstance(update.instance, codecs) };
case "unavailable":
codecs.reset();
return update;
case "closed":
codecs.removeInstance(update.instance);
return update;
}
},
};
}
export function createServiceStateDecoder(): ServiceStateDecoder {
const codecs = new StateCodecRegistry<Decoder>(decoder);
return {
decodeSnapshot(snapshot) {
codecs.reset();
return {
...snapshot,
instances: snapshot.instances.map((instance) => decodeInstance(instance, codecs)),
};
},
decodeUpdate(update) {
switch (update.type) {
case "state":
return { ...update, ops: codecs.get(update.instance, update.member).decode(update.ops) };
case "replaced":
codecs.reset();
return { ...update, snapshot: decodeInstance(update.snapshot, codecs) };
case "spawned":
return { ...update, instance: decodeInstance(update.instance, codecs) };
case "unavailable":
codecs.reset();
return update;
case "closed":
codecs.removeInstance(update.instance);
return update;
}
},
};
}
function encodeInstance(
instance: ServiceInstanceSnapshot,
codecs: StateCodecRegistry<Encoder>,
): WireServiceSubscriptionSnapshot["instances"][number] {
return {
...instance,
members: instance.members.map((member) =>
member.kind === "state"
? { ...member, ops: codecs.add(instance.instance, member.name).encode(member.ops) }
: member,
),
};
}
function decodeInstance(
instance: WireServiceSubscriptionSnapshot["instances"][number],
codecs: StateCodecRegistry<Decoder>,
): ServiceInstanceSnapshot {
return {
...instance,
members: instance.members.map((member) =>
member.kind === "state"
? { ...member, ops: codecs.add(instance.instance, member.name).decode(member.ops) }
: member,
),
};
}
function stateKey(instance: ServiceInstanceAddress | undefined, member: string): string {
return JSON.stringify([instance?.key ?? null, instance?.generation ?? null, member]);
}
function sameAddress(left: ServiceInstanceAddress | undefined, right: ServiceInstanceAddress): boolean {
return left?.key === right.key && left.generation === right.generation;
}
function describeState(instance: ServiceInstanceAddress | undefined, member: string): string {
return instance === undefined ? member : `${instance.key}@${instance.generation}.${member}`;
}

View File

@@ -0,0 +1,20 @@
import type { Op } from "../delta/index.ts";
import type { Context } from "../types.ts";
export interface ReplicatedStateInternals {
readonly sequence: number;
readonly value: unknown;
publish(context: Context): void;
subscribe(listener: (ops: readonly Op[], sequence: number, context: Context) => void): () => void;
}
const sources = new WeakMap<object, ReplicatedStateInternals>();
export function registerReplicatedStateInternals(value: object, internals: ReplicatedStateInternals): void {
sources.set(value, internals);
}
export function getReplicatedStateInternals(value: unknown): ReplicatedStateInternals | undefined {
if (typeof value !== "object" || value === null) return undefined;
return sources.get(value);
}

View File

@@ -0,0 +1,139 @@
import { BACKGROUND_CONTEXT } from "../context/index.ts";
import { applyImmutable, isBase, type Op, type Tracker, track } from "../delta/index.ts";
import type { Context, JsonValue, MutableReplicatedState, ReplicatedState, ReplicatedStateDelivery } from "../types.ts";
import { registerReplicatedStateInternals } from "./state-internals.ts";
export class MutableReplicatedStateImpl<T extends object> implements MutableReplicatedState<T> {
readonly #listeners = new Set<(value: T, context: Context, delivery: ReplicatedStateDelivery) => void>();
readonly #sourceListeners = new Set<(ops: readonly Op[], sequence: number, context: Context) => void>();
readonly #tracker: Tracker<T>;
#publishedValue: T;
#sequence = 0;
constructor(initial: T) {
this.#tracker = track(initial);
this.#publishedValue = applyImmutable(undefined, this.#tracker.flush()) as unknown as T;
const thisSource = this;
registerReplicatedStateInternals(this, {
get sequence() {
return thisSource.#sequence;
},
get value() {
return thisSource.#publishedValue;
},
publish: (context) => thisSource.publish(context),
subscribe: (listener) => {
thisSource.#sourceListeners.add(listener);
return () => thisSource.#sourceListeners.delete(listener);
},
});
}
get value(): T {
return this.#publishedValue;
}
get state(): T {
return this.#tracker.state;
}
publish(context: Context): void {
const ops = this.#tracker.flush();
if (ops.length === 0) return;
this.#sequence += 1;
this.#publishedValue = applyImmutable(this.#publishedValue as unknown as JsonValue, ops) as unknown as T;
for (const listener of [...this.#sourceListeners]) listener(ops, this.#sequence, context);
const delivery = { kind: "update", sequence: this.#sequence } as const;
for (const listener of [...this.#listeners]) listener(this.#publishedValue, context, delivery);
}
subscribe(listener: (value: T, context: Context, delivery: ReplicatedStateDelivery) => void): () => void {
const context = serviceDeliveryContext();
this.publish(context);
this.#listeners.add(listener);
listener(this.#publishedValue, context, { kind: "hydrate", sequence: this.#sequence });
return () => this.#listeners.delete(listener);
}
}
/** A cold read-only state used by service consumers until a complete snapshot arrives. */
export class ReplicatedStateReplica<T extends JsonValue = JsonValue> implements ReplicatedState<T> {
readonly #listeners = new Set<(value: T, context: Context, delivery: ReplicatedStateDelivery) => void>();
readonly #reportError: (error: Error) => void;
#value: T | undefined;
#sequence: number | undefined;
constructor(reportError: (error: Error) => void) {
this.#reportError = reportError;
}
get value(): T | undefined {
return this.#value;
}
subscribe(listener: (value: T, context: Context, delivery: ReplicatedStateDelivery) => void): () => void {
this.#listeners.add(listener);
if (this.#value !== undefined) {
this.#deliver(listener, this.#value, serviceDeliveryContext(), {
kind: "hydrate",
sequence: this.#sequence!,
});
}
return () => this.#listeners.delete(listener);
}
hydrate(sequence: number, ops: readonly Op[], context: Context): void {
if (!isBase(ops)) throw new Error("Replicated state snapshot is not a base operation batch");
const value = applyImmutable<T>(undefined, ops);
this.#sequence = sequence;
this.#value = value;
this.#deliverAll(context, { kind: "hydrate", sequence });
}
update(sequence: number, ops: readonly Op[], context: Context): void {
if (this.#sequence === undefined || this.#value === undefined) {
throw new Error("Replicated state received an update before hydration");
}
if (sequence !== this.#sequence + 1) {
this.clear();
throw new Error("Replicated state update sequence has a gap");
}
const value = applyImmutable(this.#value, ops);
this.#sequence = sequence;
this.#value = value;
this.#deliverAll(context, { kind: "update", sequence });
}
clear(): void {
this.#value = undefined;
this.#sequence = undefined;
}
#deliverAll(context: Context, delivery: ReplicatedStateDelivery): void {
if (this.#value === undefined) return;
for (const listener of this.#listeners) this.#deliver(listener, this.#value, context, delivery);
}
#deliver(
listener: (value: T, context: Context, delivery: ReplicatedStateDelivery) => void,
value: T,
context: Context,
delivery: ReplicatedStateDelivery,
): void {
try {
listener(value, context, delivery);
} catch (error) {
this.#reportError(toError(error));
}
}
}
/** @internal Context for synthetic service deliveries without a caller. */
export function serviceDeliveryContext(): Context {
// TODO: Add delivery-scoped cancellation or metadata if deliveries gain an owned lifecycle.
return BACKGROUND_CONTEXT;
}
function toError(error: unknown): Error {
return error instanceof Error ? error : new Error(String(error));
}

View File

@@ -0,0 +1,234 @@
import { assertValidOp, assertValidWireOp, type WireOp } from "../delta/index.ts";
import type {
ServiceCall,
ServiceCatalogueEntry,
ServiceInstanceAddress,
ServiceMode,
ServiceProviderUpdate,
ServiceSubscriptionSnapshot,
} from "../types.ts";
export type WireServiceMemberSnapshot =
| { readonly name: string; readonly kind: "method" }
| { readonly name: string; readonly kind: "state"; readonly sequence: number; readonly ops: readonly WireOp[] };
export type WireServiceInstanceSnapshot = {
readonly instance?: ServiceInstanceAddress;
readonly members: readonly WireServiceMemberSnapshot[];
};
export type WireServiceSubscriptionSnapshot = {
readonly serviceId: string;
readonly mode: ServiceMode;
readonly instances: readonly WireServiceInstanceSnapshot[];
};
export type WireServiceProviderUpdate =
| {
readonly type: "state";
readonly instance?: ServiceInstanceAddress;
readonly member: string;
readonly sequence: number;
readonly ops: readonly WireOp[];
}
| { readonly type: "unavailable" }
| { readonly type: "replaced"; readonly snapshot: WireServiceInstanceSnapshot }
| { readonly type: "spawned"; readonly instance: WireServiceInstanceSnapshot }
| { readonly type: "closed"; readonly instance: ServiceInstanceAddress };
const SERVICE_CONTROL_ID = "$chord.service";
const SERVICE_CATALOGUE_MEMBER = "catalogue";
const SERVICE_SUBSCRIBE_MEMBER = "subscribe";
const SERVICE_UNSUBSCRIBE_MEMBER = "unsubscribe";
export type ServiceControlCall =
| { readonly type: "catalogue" }
| {
readonly type: "subscribe";
readonly subscriptionId: string;
readonly serviceId: string;
readonly mode: ServiceMode;
}
| { readonly type: "unsubscribe"; readonly subscriptionId: string };
export function createServiceCatalogueCall(): ServiceCall {
return { serviceId: SERVICE_CONTROL_ID, member: SERVICE_CATALOGUE_MEMBER, args: [] };
}
export function createServiceSubscribeCall(subscriptionId: string, serviceId: string, mode: ServiceMode): ServiceCall {
return { serviceId: SERVICE_CONTROL_ID, member: SERVICE_SUBSCRIBE_MEMBER, args: [subscriptionId, serviceId, mode] };
}
export function createServiceUnsubscribeCall(subscriptionId: string): ServiceCall {
return { serviceId: SERVICE_CONTROL_ID, member: SERVICE_UNSUBSCRIBE_MEMBER, args: [subscriptionId] };
}
export function decodeServiceControlCall(call: ServiceCall): ServiceControlCall | undefined {
if (call.serviceId !== SERVICE_CONTROL_ID || call.instance !== undefined) return undefined;
if (call.member === SERVICE_CATALOGUE_MEMBER && call.args.length === 0) return { type: "catalogue" };
if (
call.member === SERVICE_SUBSCRIBE_MEMBER &&
call.args.length === 3 &&
isId(call.args[0]) &&
isId(call.args[1]) &&
(call.args[2] === "singleton" || call.args[2] === "keyed")
) {
return {
type: "subscribe",
subscriptionId: call.args[0],
serviceId: call.args[1],
mode: call.args[2],
};
}
if (call.member === SERVICE_UNSUBSCRIBE_MEMBER && call.args.length === 1 && isId(call.args[0])) {
return { type: "unsubscribe", subscriptionId: call.args[0] };
}
return undefined;
}
export function parseServiceCall(value: unknown): ServiceCall {
const call = record(value, "service call");
assertKeys(call, ["serviceId", "member", "args"], ["instance"], "service call");
if (!isId(call.serviceId) || !isId(call.member) || !Array.isArray(call.args)) {
throw new TypeError("Invalid service call");
}
if (call.instance !== undefined) assertAddress(call.instance);
return value as ServiceCall;
}
export function parseServiceCatalogue(value: unknown): readonly ServiceCatalogueEntry[] {
if (!Array.isArray(value)) throw new TypeError("Invalid service catalogue");
const ids = new Set<string>();
for (const candidate of value) {
const entry = record(candidate, "service catalogue entry");
assertKeys(entry, ["serviceId", "mode"], [], "service catalogue entry");
if (!isId(entry.serviceId) || !isMode(entry.mode) || ids.has(entry.serviceId)) {
throw new TypeError("Invalid service catalogue");
}
ids.add(entry.serviceId);
}
return value as unknown as readonly ServiceCatalogueEntry[];
}
export function parseServiceSubscriptionSnapshot(value: unknown): ServiceSubscriptionSnapshot {
assertSubscriptionSnapshot(value, assertValidOp);
return value as ServiceSubscriptionSnapshot;
}
export function parseWireServiceSubscriptionSnapshot(value: unknown): WireServiceSubscriptionSnapshot {
assertSubscriptionSnapshot(value, assertValidWireOp);
return value as WireServiceSubscriptionSnapshot;
}
export function parseServiceProviderUpdate(value: unknown): ServiceProviderUpdate {
assertProviderUpdate(value, assertValidOp);
return value as ServiceProviderUpdate;
}
export function parseWireServiceProviderUpdate(value: unknown): WireServiceProviderUpdate {
assertProviderUpdate(value, assertValidWireOp);
return value as WireServiceProviderUpdate;
}
function assertSubscriptionSnapshot(value: unknown, assertOp: (value: unknown) => void): void {
const snapshot = record(value, "service subscription snapshot");
assertKeys(snapshot, ["serviceId", "mode", "instances"], [], "service subscription snapshot");
if (!isId(snapshot.serviceId) || !isMode(snapshot.mode) || !Array.isArray(snapshot.instances)) {
throw new TypeError("Invalid service subscription snapshot");
}
for (const instance of snapshot.instances) assertInstance(instance, assertOp);
}
function assertProviderUpdate(value: unknown, assertOp: (value: unknown) => void): void {
const update = record(value, "service provider update");
switch (update.type) {
case "state":
assertKeys(update, ["type", "member", "sequence", "ops"], ["instance"], "state update");
if (!isId(update.member) || !isInteger(update.sequence, 1) || !Array.isArray(update.ops)) {
throw new TypeError("Invalid service state update");
}
if (update.instance !== undefined) assertAddress(update.instance);
for (const op of update.ops) assertOp(op);
return;
case "unavailable":
assertKeys(update, ["type"], [], "unavailable update");
return;
case "replaced":
assertKeys(update, ["type", "snapshot"], [], "replacement update");
assertInstance(update.snapshot, assertOp);
return;
case "spawned":
assertKeys(update, ["type", "instance"], [], "spawn update");
assertInstance(update.instance, assertOp);
return;
case "closed":
assertKeys(update, ["type", "instance"], [], "close update");
assertAddress(update.instance);
return;
default:
throw new TypeError("Invalid service provider update");
}
}
function assertInstance(value: unknown, assertOp: (value: unknown) => void): void {
const instance = record(value, "service instance snapshot");
assertKeys(instance, ["members"], ["instance"], "service instance snapshot");
if (instance.instance !== undefined) assertAddress(instance.instance);
if (!Array.isArray(instance.members)) throw new TypeError("Invalid service instance snapshot");
for (const candidate of instance.members) {
const member = record(candidate, "service member snapshot");
if (member.kind === "method") {
assertKeys(member, ["name", "kind"], [], "service method snapshot");
if (!isId(member.name)) throw new TypeError("Invalid service method snapshot");
continue;
}
if (member.kind === "state") {
assertKeys(member, ["name", "kind", "sequence", "ops"], [], "service state snapshot");
if (!isId(member.name) || !isInteger(member.sequence, 0) || !Array.isArray(member.ops)) {
throw new TypeError("Invalid service state snapshot");
}
for (const op of member.ops) assertOp(op);
continue;
}
throw new TypeError("Invalid service member snapshot");
}
}
function assertAddress(value: unknown): asserts value is ServiceInstanceAddress {
const address = record(value, "service instance address");
assertKeys(address, ["key", "generation"], [], "service instance address");
if (!isId(address.key) || !isInteger(address.generation, 1)) {
throw new TypeError("Invalid service instance address");
}
}
function record(value: unknown, description: string): Record<string, unknown> {
if (typeof value !== "object" || value === null || Array.isArray(value)) {
throw new TypeError(`Invalid ${description}`);
}
return value as Record<string, unknown>;
}
function assertKeys(
value: Record<string, unknown>,
required: readonly string[],
optional: readonly string[],
description: string,
): void {
const allowed = new Set([...required, ...optional]);
if (required.some((key) => !Object.hasOwn(value, key)) || Object.keys(value).some((key) => !allowed.has(key))) {
throw new TypeError(`Invalid ${description}`);
}
}
function isId(value: unknown): value is string {
return typeof value === "string" && value.length > 0;
}
function isMode(value: unknown): value is ServiceMode {
return value === "singleton" || value === "keyed";
}
function isInteger(value: unknown, minimum: number): value is number {
return typeof value === "number" && Number.isInteger(value) && value >= minimum;
}

View File

@@ -0,0 +1,261 @@
import type { Op } from "./delta/index.ts";
import type { RemoteServiceProvider } from "./services/provider.ts";
export type { RemoteServiceError } from "./services/errors.ts";
export type { RemoteServiceProvider } from "./services/provider.ts";
/** Typed identity for one value carried by a {@link Context}. */
export interface ContextKey<T> {
readonly token: symbol;
/** Type-only marker that prevents keys with different value types from being interchangeable. */
readonly valueType?: (value: T) => T;
}
/** Immutable invocation-scoped values passed explicitly through operations. */
export interface Context {
readonly abortSignal: AbortSignal | undefined;
value<T>(key: ContextKey<T>): T | undefined;
toString(): string;
}
export type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue };
type IsAny<T> = 0 extends 1 & T ? true : false;
/** Strict-JSON representation of an application data type. Unknown payloads become JsonValue. */
export type JsonRepresentation<T> = IsAny<T> extends true
? JsonValue
: unknown extends T
? JsonValue
: T extends null | boolean | number | string
? T
: T extends readonly (infer TItem)[]
? JsonRepresentation<TItem>[]
: T extends object
? { [TKey in keyof T]: JsonRepresentation<T[TKey]> }
: never;
export interface ReplicatedStateDelivery {
readonly kind: "hydrate" | "update";
readonly sequence: number;
}
export interface ReplicatedState<T> {
/** Immutable value, or undefined until hydration. Later updates do not mutate previously returned values. */
readonly value: T | undefined;
/** Listener values are immutable and may structurally share unchanged data with other revisions. */
subscribe(listener: (value: T, context: Context, delivery: ReplicatedStateDelivery) => void): () => void;
}
export interface MutableReplicatedState<T extends object> extends ReplicatedState<T> {
readonly value: T;
/** Mutable tracked state. All writes must go through this proxy. */
readonly state: T;
/** Publish the changes made through {@link state} since the previous publication. */
publish(context: Context): void;
}
declare const SERVICE_TYPE: unique symbol;
export type ServiceMode = "singleton" | "keyed";
/** Stable identity for one shared TypeScript service contract. */
export interface Service<T> {
readonly id: string;
/** Process-local services accept unrestricted object contracts and are never published remotely. */
readonly local: boolean;
readonly [SERVICE_TYPE]?: (value: T) => T;
}
type InvalidJsonPart<T> = IsAny<T> extends true
? T
: unknown extends T
? never
: [T] extends [JsonValue]
? [JsonValue] extends [T]
? never
: InvalidJsonStructure<T>
: InvalidJsonStructure<T>;
type InvalidJsonProperty<T> = [Exclude<T, undefined>] extends [never] ? T : InvalidJsonPart<Exclude<T, undefined>>;
type InvalidJsonStructure<T> = T extends null | boolean | number | string
? never
: T extends readonly (infer TItem)[]
? InvalidJsonPart<TItem>
: T extends (...args: never[]) => unknown
? T
: T extends object
? { [TKey in keyof T]-?: InvalidJsonProperty<T[TKey]> }[keyof T]
: T;
type InvalidRemoteMember<T> = T extends ReplicatedState<infer TValue>
? InvalidJsonPart<TValue> extends never
? never
: "state value is not JSON"
: T extends (...args: [...infer TArgs, Context]) => Promise<infer TResult>
? InvalidJsonPart<TArgs[number]> extends never
? TResult extends void
? never
: InvalidJsonPart<TResult> extends never
? never
: "method result is not JSON or void"
: "method argument is not JSON"
: "member is not a remote method or ReplicatedState";
type InvalidRemoteMemberNames<T> = {
[TKey in keyof T]-?: InvalidRemoteMember<T[TKey]> extends never ? never : TKey;
}[keyof T];
export type RemoteServiceContract<T> = InvalidRemoteMemberNames<T> extends never ? T : never;
export interface ServiceSpawner<T> {
spawn(key: string, implementation: T): () => void;
}
export interface RemoteServices {
use<T>(service: Service<T>): T;
observe<T>(service: Service<T>, handler: (service: T, context: Context) => void | Promise<void>): () => void;
/** Wait until every currently acquired service has installed its initial snapshot. */
ready(context: Context): Promise<void>;
dispose(context: Context): Promise<void>;
}
export type ServiceCatalogueEntry = {
readonly serviceId: string;
readonly mode: ServiceMode;
};
export type ServiceInstanceAddress = {
readonly key: string;
readonly generation: number;
};
export type ServiceMemberSnapshot =
| { readonly name: string; readonly kind: "method" }
| { readonly name: string; readonly kind: "state"; readonly sequence: number; readonly ops: readonly Op[] };
export type ServiceInstanceSnapshot = {
readonly instance?: ServiceInstanceAddress;
readonly members: readonly ServiceMemberSnapshot[];
};
export type ServiceSubscriptionSnapshot = {
readonly serviceId: string;
readonly mode: ServiceMode;
readonly instances: readonly ServiceInstanceSnapshot[];
};
export type ServiceProviderUpdate =
| {
readonly type: "state";
readonly instance?: ServiceInstanceAddress;
readonly member: string;
readonly sequence: number;
readonly ops: readonly Op[];
}
| { readonly type: "unavailable" }
| { readonly type: "replaced"; readonly snapshot: ServiceInstanceSnapshot }
| { readonly type: "spawned"; readonly instance: ServiceInstanceSnapshot }
| { readonly type: "closed"; readonly instance: ServiceInstanceAddress };
export type ServiceCall = {
readonly serviceId: string;
readonly instance?: ServiceInstanceAddress;
readonly member: string;
/** Borrowed immutable values. Chord validates but does not clone them. */
readonly args: readonly JsonValue[];
};
export interface ServiceSubscription {
readonly snapshot: ServiceSubscriptionSnapshot;
activate(): void;
close(context?: Context): void | Promise<void>;
}
/**
* Pluggable wire boundary consumed by a remote service binding.
*
* Implementations choose transport, framing, routing, and envelope encoding. Values crossing this
* boundary must remain strict JSON. Chord does not clone values or require a particular application wire protocol;
* adapters own serialization and any isolation copies they require.
*
*/
export interface RemoteServiceTransport {
invoke(call: ServiceCall, context: Context): Promise<JsonValue | undefined>;
subscribe(
serviceId: string,
mode: ServiceMode,
listener: (update: ServiceProviderUpdate, context: Context) => void,
context: Context,
): Promise<ServiceSubscription>;
}
export interface RemoteServiceBindingOptions {
readonly services: readonly { readonly id: string }[];
readonly transport: RemoteServiceTransport;
readonly bound?: boolean;
readonly onError?: (error: Error) => void;
readonly assertAccess?: () => void;
}
export interface RemoteServiceBinding extends RemoteServices {
rebind(bound: boolean, context: Context): Promise<void>;
}
export interface FacetEnvironment {
/** Declare a hard dependency on one singleton service and return its stable handle. */
use<T>(service: Service<T>): T;
/** Declare a hard dependency on a keyed service and observe each live instance. */
observe<T>(service: Service<T>, handler: (service: T, context: Context) => void | Promise<void>): void;
/** Declare and install this facet's singleton implementation of a service. */
provide<T>(service: Service<T>, implementation: NoInfer<T>): void;
/** Declare ownership of a multi-instance service and return its deferred spawning capability. */
provideMany<T>(service: Service<T>): ServiceSpawner<T>;
/** Create initialized mutable state suitable for exposing through a service implementation. */
replicatedState<T extends object>(initial: T): MutableReplicatedState<T>;
/** Give the facet ownership of a resource cleanup function. */
own(disposal: () => void | Promise<void>): void;
/** Register asynchronous initialization after dependencies are bound and ready. */
onActivate(callback: () => void | Promise<void>): void;
/** Register final facet teardown. */
onDeactivate(callback: () => void | Promise<void>): void;
}
export interface Facet {
readonly id: string;
setup(env: FacetEnvironment): void;
}
export interface RemoteServiceSource {
/** Whether this currently unavailable source may provisionally own absent requirements. */
readonly acceptsUnavailableServices: boolean;
catalogue(context: Context): Promise<readonly ServiceCatalogueEntry[]>;
open(options: {
readonly services: readonly { readonly id: string }[];
assertAccess(): void;
onError(error: Error): void;
}): RemoteServices;
}
export interface FacetOptions {
readonly facets: readonly Facet[];
readonly serviceSources?: readonly RemoteServiceSource[];
readonly onError?: (error: Error) => void;
}
export interface FacetHost {
readonly services: RemoteServiceProvider;
/** Activate and replace facets with matching IDs without disconnecting consumer service handles. */
reload(facets: readonly Facet[]): Promise<void>;
dispose(): Promise<void>;
}
export interface LoadedFacets {
readonly facets: readonly Facet[];
dispose(): Promise<void>;
}
export interface FacetLoader {
load(): Promise<LoadedFacets>;
}