feat: consolidate Chord public API

This commit is contained in:
Armin Ronacher 2026-08-29 18:56:26 +02:00
parent 4151e5754c
commit 6ec57e6e5f
25 changed files with 614 additions and 472 deletions

View file

@ -1,18 +1,19 @@
import { type Context, createContextKey, withContextValue } from "@earendil-works/chord/context";
import { NOOP_TELEMETRY_CONTEXT, type TelemetryContext } from "@earendil-works/pi-telemetry";
export {
awaitWithContext,
import {
BACKGROUND_CONTEXT,
type Context,
type ContextKey,
createContextKey,
INTERNAL_CONTEXT_OPERATIONS,
TODO_CONTEXT,
withAbortSignal,
withCancel,
withContextValue,
withoutAbortSignal,
} from "@earendil-works/chord/context";
import { NOOP_TELEMETRY_CONTEXT, type TelemetryContext } from "@earendil-works/pi-telemetry";
export { BACKGROUND_CONTEXT, type Context, type ContextKey, TODO_CONTEXT };
export const awaitWithContext = INTERNAL_CONTEXT_OPERATIONS.awaitWithContext;
export const createContextKey = INTERNAL_CONTEXT_OPERATIONS.createContextKey;
export const withAbortSignal = INTERNAL_CONTEXT_OPERATIONS.withAbortSignal;
export const withCancel = INTERNAL_CONTEXT_OPERATIONS.withCancel;
export const withContextValue = INTERNAL_CONTEXT_OPERATIONS.withContextValue;
export const withoutAbortSignal = INTERNAL_CONTEXT_OPERATIONS.withoutAbortSignal;
const TELEMETRY_CONTEXT_KEY = createContextKey<TelemetryContext>("pi.telemetryContext");

View file

@ -1,4 +1,4 @@
import type { JsonValue } from "@earendil-works/chord/json";
import type { JsonValue } from "@earendil-works/chord/types";
import type { AssistantMessage, StopReason, Usage } from "@earendil-works/pi-ai";
import type { AgentMessage, QueueMode, ThinkingLevel } from "../../types.ts";
import type { BranchPreparation } from "../compaction/branch-summarization.ts";
@ -7,7 +7,7 @@ import type { Context } from "../context.ts";
import type { AgentHarnessStreamOptions } from "../types.ts";
import type { ListElement, ListReadOptions, ListWrite, StoredValue, Value, ValueList, ValueWrite } from "./values.ts";
export type { JsonValue } from "@earendil-works/chord/json";
export type { JsonValue } from "@earendil-works/chord/types";
export type SettledAssistantMessage = AssistantMessage & {
stopReason: Exclude<StopReason, "pending">;

View file

@ -1,4 +1,4 @@
import type { JsonValue } from "@earendil-works/chord/json";
import type { JsonValue } from "@earendil-works/chord/types";
import type { TelemetryContext } from "@earendil-works/pi-telemetry";
import type { AnthropicOptions } from "./api/anthropic-messages.ts";
import type { AzureOpenAIResponsesOptions } from "./api/azure-openai-responses.ts";
@ -405,7 +405,7 @@ export interface Usage {
export type StopReason = "pending" | "stop" | "length" | "toolUse" | "error" | "aborted" | "deferred";
export type { JsonValue } from "@earendil-works/chord/json";
export type { JsonValue } from "@earendil-works/chord/types";
export interface DeferredHandle {
provider: string;

View file

@ -583,12 +583,15 @@ This is a planning aid, not a requirement to create all files immediately.
packages/chord/
src/
index.ts platform-neutral public API
context.ts neutral invocation context and cancellation
json.ts strict JSON types and validation
api.ts public functions
types.ts public types, including strict JSON values
context/
index.ts invocation context internals and compatibility entry
errors.ts lifecycle, service, and RPC errors
state.ts source and replica state primitives
services/
types.ts tokens, contracts, modes, snapshots
state.ts source and replica state primitives
state-internals.ts private replicated-state metadata
provider.ts singleton/keyed provider runtime
facade.ts stable local and remote facades
host-bindings.ts graph-facing service slots

View file

@ -48,8 +48,11 @@ The design has a few connected pieces:
The current runtime exports service tokens, singleton and keyed providers,
remote bindings, replicated state, facet hosts, and facet loaders from
`@earendil-works/chord`. Chord-owned identifiers use the `chord.*` namespace and
its reserved service prefix is `$chord.*`.
`@earendil-works/chord`. All public functions are collected in
`@earendil-works/chord/api`, while all public types are collected in
`@earendil-works/chord/types`; both are re-exported from the main entry point.
Chord-owned identifiers use the `chord.*` namespace and its reserved service
prefix is `$chord.*`.
See [PLANNING.md](PLANNING.md) for the broader RPC, generation-loading, and
bundling architecture.

View file

@ -11,15 +11,20 @@
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
"./context": {
"source": "./src/context.ts",
"types": "./dist/context.d.ts",
"import": "./dist/context.js"
"./api": {
"source": "./src/api.ts",
"types": "./dist/api.d.ts",
"import": "./dist/api.js"
},
"./json": {
"source": "./src/json.ts",
"types": "./dist/json.d.ts",
"import": "./dist/json.js"
"./context": {
"source": "./src/context/index.ts",
"types": "./dist/context/index.d.ts",
"import": "./dist/context/index.js"
},
"./types": {
"source": "./src/types.ts",
"types": "./dist/types.d.ts",
"import": "./dist/types.js"
},
"./package.json": "./package.json"
},

172
packages/chord/src/api.ts Normal file
View file

@ -0,0 +1,172 @@
import {
deriveCancellableContext,
deriveContextValue,
deriveContextWithAbortSignal,
deriveContextWithoutAbortSignal,
waitWithContext,
} from "./context/index.ts";
import { FacetKernel } from "./facets/host.ts";
import { disposeLoadedFacets } from "./facets/loader.ts";
import { RemoteServiceBindingImpl } from "./services/consumer.ts";
import { lookupServiceInstanceKey } from "./services/instances.ts";
import type { RemoteServiceProvider } from "./services/provider.ts";
import { createDeliveryContext, MutableReplicatedStateImpl } from "./services/state.ts";
import type {
Context,
ContextKey,
Facet,
FacetHost,
FacetLoader,
FacetOptions,
LoadedFacets,
MutableReplicatedState,
RemoteServiceBinding,
RemoteServiceBindingOptions,
RemoteServiceConnection,
RemoteServiceContract,
Service,
} from "./types.ts";
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 deriveContextValue(key, value, parent);
}
/**
* 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 {
return deriveContextWithAbortSignal(signal, context);
}
/** Derive a context retaining all values except caller cancellation. Intended for mandatory cleanup only. */
export function withoutAbortSignal(context: Context): Context {
return deriveContextWithoutAbortSignal(context);
}
/** Derive an independently cancellable child context. */
export function withCancel(context: Context): {
readonly context: Context;
readonly cancel: (reason?: unknown) => void;
} {
return deriveCancellableContext(context);
}
/**
* 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> {
return waitWithContext(promise, context);
}
/** 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 });
}
/** @publicApiReview Consider hiding this low-level remote binding factory behind facet connections. */
export function createRemoteServiceBinding(options: RemoteServiceBindingOptions): RemoteServiceBinding {
return new RemoteServiceBindingImpl(options);
}
/**
* Return the application key for a keyed service proxy, or undefined for a singleton.
*
* @publicApiReview Consider passing the key directly to keyed observers instead of exposing proxy metadata.
*/
export function getServiceInstanceKey(service: object): string | undefined {
return lookupServiceInstanceKey(service);
}
/** @publicApiReview Consider keeping this loopback transport adapter in testing support. */
export function createLoopbackServiceConnection(provider: RemoteServiceProvider): RemoteServiceConnection {
return {
invoke: (call, context) => provider.invoke(call, context),
subscribe: async (serviceId, mode, listener) => {
const subscription = provider.subscribe(serviceId, mode, (update) =>
listener(update, createDeliveryContext()),
);
return {
snapshot: subscription.snapshot,
activate: () => subscription.activate(),
close: () => subscription.close(),
};
},
};
}
export function replicatedState<T>(initial: T): MutableReplicatedState<T> {
return new MutableReplicatedStateImpl(initial);
}
/** @publicApiReview Delivery contexts may only be an internal service-replication concern. */
export function freshDeliveryContext(): Context {
return createDeliveryContext();
}

View file

@ -1,21 +1,11 @@
/** 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;
}
import type { Context, ContextKey } from "../types.ts";
const ABORT_SIGNAL_CONTEXT_KEY: ContextKey<AbortSignal | undefined> = Object.freeze({
export type { Context, ContextKey } from "../types.ts";
export const ABORT_SIGNAL_CONTEXT_KEY: ContextKey<AbortSignal | undefined> = Object.freeze({
token: Symbol("chord.abortSignal"),
});
/** Immutable invocation-scoped values passed explicitly through operations. */
export interface Context {
readonly abortSignal: AbortSignal | undefined;
value<T>(key: ContextKey<T>): T | undefined;
toString(): string;
}
abstract class BaseContext implements Context {
abstract value<T>(key: ContextKey<T>): T | undefined;
abstract toString(): string;
@ -42,7 +32,6 @@ class EmptyContext extends BaseContext {
}
}
/** One copy-on-write value layered over a parent context. */
class ContextValue<T> extends BaseContext {
readonly #parent: Context;
readonly #key: ContextKey<T>;
@ -65,53 +54,39 @@ class ContextValue<T> extends BaseContext {
}
}
/** Root context for work that has no caller. */
export const BACKGROUND_CONTEXT: Context = new EmptyContext("[Context BACKGROUND_CONTEXT]");
/** Temporary root context marking a call site that still needs context propagation. */
export const TODO_CONTEXT: Context = new EmptyContext("[Context TODO_CONTEXT]");
export function createContextKey<T>(description: string): ContextKey<T> {
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 {
export function deriveContextValue<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 {
export function deriveContextWithAbortSignal(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);
return deriveContextValue(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);
export function deriveContextWithoutAbortSignal(context: Context): Context {
return deriveContextValue(ABORT_SIGNAL_CONTEXT_KEY, undefined, context);
}
/** Derive an independently cancellable child context. */
export function withCancel(context: Context): {
export function deriveCancellableContext(context: Context): {
readonly context: Context;
readonly cancel: (reason?: unknown) => void;
} {
const controller = new AbortController();
return {
context: withAbortSignal(controller.signal, context),
context: deriveContextWithAbortSignal(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> {
export function waitWithContext<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));
@ -135,3 +110,13 @@ function abortError(signal: AbortSignal): Error {
const reason: unknown = signal.reason;
return reason instanceof Error ? reason : new DOMException("The operation was aborted", "AbortError");
}
/** @internal Narrow compatibility surface for the legacy context entry point. */
export const INTERNAL_CONTEXT_OPERATIONS = Object.freeze({
awaitWithContext: waitWithContext,
createContextKey,
withAbortSignal: deriveContextWithAbortSignal,
withCancel: deriveCancellableContext,
withContextValue: deriveContextValue,
withoutAbortSignal: deriveContextWithoutAbortSignal,
});

View file

@ -1,10 +1,20 @@
import { BACKGROUND_CONTEXT, type Context } from "../context.ts";
import type { RemoteServiceContract, RemoteServices, Service, ServiceMode } from "../services/contracts.ts";
import { BACKGROUND_CONTEXT } from "../context/index.ts";
import { ServiceHandle } from "../services/handle.ts";
import { InstanceDirectory, type InstanceDirectoryEntry } from "../services/instances.ts";
import { RemoteServiceProvider, validateRemoteServiceImplementation } from "../services/provider.ts";
import { replicatedState } from "../state.ts";
import type { Facet, FacetConnection, FacetEnvironment, FacetHost, FacetOptions, ServiceSpawner } from "./types.ts";
import { MutableReplicatedStateImpl } from "../services/state.ts";
import type {
Context,
Facet,
FacetConnection,
FacetEnvironment,
FacetOptions,
RemoteServiceContract,
RemoteServices,
Service,
ServiceMode,
ServiceSpawner,
} from "../types.ts";
interface FacetServiceReference {
readonly serviceId: string;
@ -315,19 +325,8 @@ type FacetProvision =
connectRemote(provider: RemoteServiceProvider): void;
};
/** 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(),
});
}
/** Private lifecycle and dependency kernel behind the atomic host entry point. */
class FacetKernel {
export class FacetKernel {
readonly #initialFacets: readonly Facet[];
readonly #connections: readonly FacetConnection[];
readonly #onError: (error: Error) => void;
@ -588,7 +587,7 @@ class FacetKernel {
},
replicatedState: <T>(initial: T) => {
lifecycle.assertRunning("create replicated state");
return replicatedState(initial);
return new MutableReplicatedStateImpl(initial);
},
own: (disposal) => lifecycle.own(disposal),
onActivate: (callback) => lifecycle.onActivate(callback),

View file

@ -1,52 +1,6 @@
import type { Facet } from "./types.ts";
import type { LoadedFacets } from "../types.ts";
export interface LoadedFacets {
readonly facets: readonly Facet[];
dispose(): Promise<void>;
}
export interface FacetLoader {
load(): Promise<LoadedFacets>;
}
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");
},
};
},
};
}
async function disposeLoadedFacets(loaded: readonly LoadedFacets[]): Promise<unknown[]> {
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

@ -1,61 +0,0 @@
import type { Context } from "../context.ts";
import type { RemoteServices, Service } from "../services/contracts.ts";
import type { ServiceCatalogueEntry } from "../services/protocol.ts";
import type { RemoteServiceProvider } from "../services/provider.ts";
import type { MutableReplicatedState } from "../state.ts";
export interface ServiceSpawner<T> {
spawn(key: string, implementation: T): () => 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>(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 function defineFacet(facet: Facet): Facet {
return facet;
}
export interface FacetConnection {
/** Whether this currently unavailable route 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 connections?: readonly FacetConnection[];
readonly onError?: (error: Error) => void;
}
export interface FacetHost {
readonly services: RemoteServiceProvider;
/** Replace active facets with matching IDs while preserving consumer service handles. */
reload(facets: readonly Facet[]): Promise<void>;
dispose(): Promise<void>;
}

View file

@ -3,42 +3,55 @@
*
* Its public API is under development.
*/
export * from "./context.ts";
export { createFacetHost } from "./facets/host.ts";
export {
awaitWithContext,
combineFacetLoaders,
createStaticFacetLoader,
type FacetLoader,
type LoadedFacets,
} from "./facets/loader.ts";
export {
defineFacet,
type Facet,
type FacetConnection,
type FacetEnvironment,
type FacetHost,
type FacetOptions,
type ServiceSpawner,
} from "./facets/types.ts";
export type { JsonValue } from "./json.ts";
export {
createContextKey,
createFacetHost,
createLoopbackServiceConnection,
createRemoteServiceBinding,
type RemoteServiceBinding,
type RemoteServiceBindingOptions,
} from "./services/consumer.ts";
export { defineService, type RemoteServices, type Service, type ServiceMode } from "./services/contracts.ts";
export { RemoteServiceError, type RemoteServiceErrorCode } from "./services/errors.ts";
export { getServiceInstanceKey } from "./services/metadata.ts";
createStaticFacetLoader,
defineFacet,
defineService,
freshDeliveryContext,
getServiceInstanceKey,
replicatedState,
withAbortSignal,
withCancel,
withContextValue,
withoutAbortSignal,
} from "./api.ts";
export { BACKGROUND_CONTEXT, TODO_CONTEXT } from "./context/index.ts";
export { RemoteServiceError } from "./services/errors.ts";
export { RemoteServiceProvider } from "./services/provider.ts";
export type {
Context,
ContextKey,
Facet,
FacetConnection,
FacetEnvironment,
FacetHost,
FacetLoader,
FacetOptions,
JsonValue,
LoadedFacets,
MutableReplicatedState,
RemoteServiceBinding,
RemoteServiceBindingOptions,
RemoteServiceConnection,
RemoteServiceContract,
RemoteServiceErrorCode,
RemoteServices,
ReplicatedState,
Service,
ServiceCall,
ServiceCatalogueEntry,
ServiceInstanceAddress,
ServiceInstanceSnapshot,
ServiceMemberSnapshot,
ServiceMode,
ServiceProviderUpdate,
ServiceSpawner,
ServiceSubscription,
ServiceSubscriptionSnapshot,
} from "./services/protocol.ts";
export { createLoopbackServiceConnection, RemoteServiceProvider } from "./services/provider.ts";
export { freshDeliveryContext, type MutableReplicatedState, type ReplicatedState, replicatedState } from "./state.ts";
} from "./types.ts";

View file

@ -1 +0,0 @@
export type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue };

View file

@ -1,29 +1,20 @@
import { awaitWithContext, BACKGROUND_CONTEXT, type Context } from "../context.ts";
import type { JsonValue } from "../json.ts";
import { freshDeliveryContext, ReplicatedStateReplica } from "../state.ts";
import type { RemoteServices, Service } from "./contracts.ts";
import { RemoteServiceError } from "./errors.ts";
import { InstanceDirectory, type InstanceDirectoryEntry } from "./instances.ts";
import { BACKGROUND_CONTEXT, waitWithContext } from "../context/index.ts";
import type {
Context,
JsonValue,
RemoteServiceBinding,
RemoteServiceBindingOptions,
RemoteServiceConnection,
Service,
ServiceInstanceAddress,
ServiceInstanceSnapshot,
ServiceMemberSnapshot,
ServiceProviderUpdate,
ServiceSubscription,
} from "./protocol.ts";
export interface RemoteServiceBindingOptions {
readonly services: readonly { readonly id: string }[];
readonly connection: RemoteServiceConnection;
readonly bound?: boolean;
readonly onError?: (error: Error) => void;
readonly assertAccess?: () => void;
}
export interface RemoteServiceBinding extends RemoteServices {
rebind(bound: boolean, context: Context): Promise<void>;
}
} from "../types.ts";
import { RemoteServiceError } from "./errors.ts";
import { InstanceDirectory, type InstanceDirectoryEntry } from "./instances.ts";
import { createDeliveryContext, ReplicatedStateReplica } from "./state.ts";
type ErrorReporter = (error: Error) => void;
type ServiceMemberKind = ServiceMemberSnapshot["kind"];
@ -355,7 +346,7 @@ class KeyedBinding<T> {
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, freshDeliveryContext());
for (const snapshot of subscription.snapshot.instances) this.#spawn(snapshot, createDeliveryContext());
this.#instances.ready();
subscription.activate();
}
@ -414,7 +405,7 @@ class KeyedBinding<T> {
}
}
class RemoteServiceBindingImpl implements RemoteServiceBinding {
export class RemoteServiceBindingImpl implements RemoteServiceBinding {
readonly #connection: RemoteServiceConnection;
readonly #allowlist = new Set<string>();
readonly #reportError: ErrorReporter;
@ -509,7 +500,7 @@ class RemoteServiceBindingImpl implements RemoteServiceBinding {
),
...[...this.#keyed.values()].map((binding) => binding.ready()),
];
await awaitWithContext(
await waitWithContext(
Promise.all(starts).then(() => undefined),
context,
);
@ -604,7 +595,7 @@ class RemoteServiceBindingImpl implements RemoteServiceBinding {
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]!, freshDeliveryContext());
binding.facade.install(snapshot.instances[0]!, createDeliveryContext());
subscription.activate();
}
@ -628,10 +619,6 @@ class RemoteServiceBindingImpl implements RemoteServiceBinding {
}
}
export function createRemoteServiceBinding(options: RemoteServiceBindingOptions): RemoteServiceBinding {
return new RemoteServiceBindingImpl(options);
}
function validateMembers(members: readonly ServiceMemberSnapshot[]): ReadonlyMap<string, ServiceMemberSnapshot> {
const result = new Map<string, ServiceMemberSnapshot>();
for (const member of members) {

View file

@ -1,80 +0,0 @@
import type { Context } from "../context.ts";
import type { JsonValue } from "../json.ts";
import type { MutableReplicatedState, ReplicatedState } from "../state.ts";
const SERVICE_TYPE = Symbol("chord.service.type");
export type ServiceMode = "singleton" | "keyed";
type InvalidJsonPart<T> = [T] extends [JsonValue]
? [JsonValue] extends [T]
? never
: InvalidJsonStructure<T>
: InvalidJsonStructure<T>;
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]-?: InvalidJsonPart<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;
/** Stable identity for one shared TypeScript service contract. */
export interface Service<T> {
// TODO: check if this should be part of Chord.
readonly id: string;
/** Process-local services accept unrestricted object contracts and are never published remotely. */
readonly local: boolean;
readonly [SERVICE_TYPE]?: (value: T) => T;
}
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 this 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 interface RemoteServices {
use<T>(service: Service<T>): T;
observe<T>(service: Service<T>, handler: (service: T, context: Context) => void | Promise<void>): () => void;
/** Activate this service set and wait for every acquired service to become ready. */
activate(context: Context): Promise<void>;
/** Wait until every currently acquired service has installed its initial snapshot. */
ready(context: Context): Promise<void>;
/** Install a host lifecycle guard for proxy member access. */
setAccessGuard(assertAccess: () => void): void;
dispose(context: Context): Promise<void>;
}
export type { MutableReplicatedState, ReplicatedState };

View file

@ -1,12 +1,4 @@
export type RemoteServiceErrorCode =
| "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";
import type { RemoteServiceErrorCode } from "../types.ts";
export class RemoteServiceError extends Error {
readonly code: RemoteServiceErrorCode;

View file

@ -1,5 +1,11 @@
import { BACKGROUND_CONTEXT, type Context, withCancel } from "../context.ts";
import { registerServiceInstance } from "./metadata.ts";
import { BACKGROUND_CONTEXT, deriveCancellableContext } from "../context/index.ts";
import type { Context } from "../types.ts";
const instanceKeys = new WeakMap<object, string>();
export function lookupServiceInstanceKey(service: object): string | undefined {
return instanceKeys.get(service);
}
export interface InstanceDirectoryEntry {
readonly key: string;
@ -128,7 +134,7 @@ export class InstanceDirectory<TEntry extends InstanceDirectoryEntry> {
#start(observer: Observer, entry: TEntry): void {
if (observer.closed || observer.tasks.has(entry)) return;
const { context, cancel } = withCancel(BACKGROUND_CONTEXT);
const { context, cancel } = deriveCancellableContext(BACKGROUND_CONTEXT);
observer.tasks.set(entry, { cancel });
try {
void Promise.resolve(observer.handler(entry.service, context)).catch((error: unknown) => {
@ -144,6 +150,10 @@ export class InstanceDirectory<TEntry extends InstanceDirectoryEntry> {
}
}
function registerServiceInstance(service: object, key: string): void {
instanceKeys.set(service, key);
}
function toError(error: unknown): Error {
return error instanceof Error ? error : new Error(String(error));
}

View file

@ -1,10 +0,0 @@
const instanceKeys = new WeakMap<object, string>();
export function registerServiceInstance(service: object, key: string): void {
instanceKeys.set(service, key);
}
/** Return the application key for a keyed service proxy, or undefined for a singleton. */
export function getServiceInstanceKey(service: object): string | undefined {
return instanceKeys.get(service);
}

View file

@ -1,73 +1,11 @@
import type { Context } from "../context.ts";
import type { JsonValue } from "../json.ts";
import type { ServiceMode } from "./contracts.ts";
// TODO: check if this should be part of Chord.
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 value: JsonValue };
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 value: JsonValue;
}
| { 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 RemoteServiceConnection {
invoke(call: ServiceCall, context: Context): Promise<JsonValue | undefined>;
subscribe(
serviceId: string,
mode: ServiceMode,
listener: (update: ServiceProviderUpdate, context: Context) => void,
context: Context,
): Promise<ServiceSubscription>;
}
export type {
RemoteServiceConnection,
ServiceCall,
ServiceCatalogueEntry,
ServiceInstanceAddress,
ServiceInstanceSnapshot,
ServiceMemberSnapshot,
ServiceProviderUpdate,
ServiceSubscription,
ServiceSubscriptionSnapshot,
} from "../types.ts";

View file

@ -1,20 +1,21 @@
import type { Context } from "../context.ts";
import type { JsonValue } from "../json.ts";
import { freshDeliveryContext } from "../state.ts";
import { getReplicatedStateInternals, type ReplicatedStateInternals } from "../state-internals.ts";
import type { RemoteServiceContract, Service, ServiceMode } from "./contracts.ts";
import { RemoteServiceError } from "./errors.ts";
import type {
RemoteServiceConnection,
Context,
JsonValue,
RemoteServiceContract,
Service,
ServiceCall,
ServiceCatalogueEntry,
ServiceInstanceAddress,
ServiceInstanceSnapshot,
ServiceMemberSnapshot,
ServiceMode,
ServiceProviderUpdate,
ServiceSubscription,
ServiceSubscriptionSnapshot,
} from "./protocol.ts";
} from "../types.ts";
import { RemoteServiceError } from "./errors.ts";
import { createDeliveryContext } from "./state.ts";
import { getReplicatedStateInternals, type ReplicatedStateInternals } from "./state-internals.ts";
type RemoteMethod = (...args: unknown[]) => unknown;
@ -363,7 +364,7 @@ export class RemoteServiceProvider {
#emit(registration: ServiceRegistration, update: ServiceProviderUpdate, context?: Context): void {
if (registration.subscribers.size === 0) return;
const deliveryContext = context ?? freshDeliveryContext();
const deliveryContext = context ?? createDeliveryContext();
for (const subscriber of registration.subscribers) {
if (subscriber.closed) continue;
const entry = { update, context: deliveryContext };
@ -418,17 +419,3 @@ function classifyRemoteServiceImplementation(
if (members.size === 0) throw new TypeError(`Remote service ${serviceId} has no members`);
return { implementation, members };
}
export function createLoopbackServiceConnection(provider: RemoteServiceProvider): RemoteServiceConnection {
return {
invoke: (call, context) => provider.invoke(call, context),
subscribe: async (serviceId, mode, listener) => {
const subscription = provider.subscribe(serviceId, mode, (update) => listener(update, freshDeliveryContext()));
return {
snapshot: subscription.snapshot,
activate: () => subscription.activate(),
close: () => subscription.close(),
};
},
};
}

View file

@ -1,4 +1,4 @@
import type { Context } from "./context.ts";
import type { Context } from "../types.ts";
export interface ReplicatedStateInternals {
readonly sequence: number;

View file

@ -1,21 +1,8 @@
import { BACKGROUND_CONTEXT, type Context, withCancel } from "./context.ts";
import type { JsonValue } from "./json.ts";
import { BACKGROUND_CONTEXT, deriveCancellableContext } from "../context/index.ts";
import type { Context, JsonValue, MutableReplicatedState, ReplicatedState } from "../types.ts";
import { registerReplicatedStateInternals } from "./state-internals.ts";
export interface ReplicatedState<T> {
/** Borrowed immutable value, or undefined until hydration. Do not mutate or retain it. */
readonly value: T | undefined;
/** Listener values are borrowed and must not be mutated or retained. */
subscribe(listener: (value: T, context: Context) => void): () => void;
}
export interface MutableReplicatedState<T> extends ReplicatedState<T> {
readonly value: T;
/** Transfers the JSON value to the state; the caller must not subsequently mutate it. */
set(value: T, context: Context): void;
}
class MutableReplicatedStateImpl<T> implements MutableReplicatedState<T> {
export class MutableReplicatedStateImpl<T> implements MutableReplicatedState<T> {
readonly #listeners = new Set<(value: T, context: Context) => void>();
readonly #sourceListeners = new Set<(value: T, sequence: number, context: Context) => void>();
#value: T;
@ -54,7 +41,7 @@ class MutableReplicatedStateImpl<T> implements MutableReplicatedState<T> {
subscribe(listener: (value: T, context: Context) => void): () => void {
this.#listeners.add(listener);
listener(this.value, freshDeliveryContext());
listener(this.value, createDeliveryContext());
return () => this.#listeners.delete(listener);
}
}
@ -76,7 +63,7 @@ export class ReplicatedStateReplica<T extends JsonValue = JsonValue> implements
subscribe(listener: (value: T, context: Context) => void): () => void {
this.#listeners.add(listener);
if (this.#value !== undefined) this.#deliver(listener, this.#value, freshDeliveryContext());
if (this.#value !== undefined) this.#deliver(listener, this.#value, createDeliveryContext());
return () => this.#listeners.delete(listener);
}
@ -114,12 +101,8 @@ export class ReplicatedStateReplica<T extends JsonValue = JsonValue> implements
}
}
export function replicatedState<T>(initial: T): MutableReplicatedState<T> {
return new MutableReplicatedStateImpl(initial);
}
export function freshDeliveryContext(): Context {
return withCancel(BACKGROUND_CONTEXT).context;
export function createDeliveryContext(): Context {
return deriveCancellableContext(BACKGROUND_CONTEXT).context;
}
function toError(error: unknown): Error {

261
packages/chord/src/types.ts Normal file
View file

@ -0,0 +1,261 @@
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 };
export interface ReplicatedState<T> {
/** Borrowed immutable value, or undefined until hydration. Do not mutate or retain it. */
readonly value: T | undefined;
/** Listener values are borrowed and must not be mutated or retained. */
subscribe(listener: (value: T, context: Context) => void): () => void;
}
export interface MutableReplicatedState<T> extends ReplicatedState<T> {
readonly value: T;
/** Transfers the JSON value to the state; the caller must not subsequently mutate it. */
set(value: T, 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> {
/** @publicApiReview Decide whether Chord should expose and own service identifiers. */
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> = [T] extends [JsonValue]
? [JsonValue] extends [T]
? never
: InvalidJsonStructure<T>
: InvalidJsonStructure<T>;
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]-?: InvalidJsonPart<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];
/** @publicApiReview Consider keeping this contract-validation helper internal. */
export type RemoteServiceContract<T> = InvalidRemoteMemberNames<T> extends never ? T : never;
export interface ServiceSpawner<T> {
spawn(key: string, implementation: T): () => void;
}
/**
* @publicApiReview This host-facing binding interface may be better hidden behind
* higher-level facet connection APIs.
*/
export interface RemoteServices {
use<T>(service: Service<T>): T;
observe<T>(service: Service<T>, handler: (service: T, context: Context) => void | Promise<void>): () => void;
/** Activate this service set and wait for every acquired service to become ready. */
activate(context: Context): Promise<void>;
/** Wait until every currently acquired service has installed its initial snapshot. */
ready(context: Context): Promise<void>;
/** Install a host lifecycle guard for proxy member access. */
setAccessGuard(assertAccess: () => void): void;
dispose(context: Context): Promise<void>;
}
/** @publicApiReview Consider hiding this low-level transport type behind the connection API. */
export type ServiceCatalogueEntry = {
readonly serviceId: string;
readonly mode: ServiceMode;
};
/** @publicApiReview Consider hiding this low-level transport type behind the connection API. */
export type ServiceInstanceAddress = {
readonly key: string;
readonly generation: number;
};
/** @publicApiReview Consider hiding this low-level transport type behind the connection API. */
export type ServiceMemberSnapshot =
| { readonly name: string; readonly kind: "method" }
| { readonly name: string; readonly kind: "state"; readonly sequence: number; readonly value: JsonValue };
/** @publicApiReview Consider hiding this low-level transport type behind the connection API. */
export type ServiceInstanceSnapshot = {
readonly instance?: ServiceInstanceAddress;
readonly members: readonly ServiceMemberSnapshot[];
};
/** @publicApiReview Consider hiding this low-level transport type behind the connection API. */
export type ServiceSubscriptionSnapshot = {
readonly serviceId: string;
readonly mode: ServiceMode;
readonly instances: readonly ServiceInstanceSnapshot[];
};
/** @publicApiReview Consider hiding this low-level transport type behind the connection API. */
export type ServiceProviderUpdate =
| {
readonly type: "state";
readonly instance?: ServiceInstanceAddress;
readonly member: string;
readonly sequence: number;
readonly value: JsonValue;
}
| { readonly type: "unavailable" }
| { readonly type: "replaced"; readonly snapshot: ServiceInstanceSnapshot }
| { readonly type: "spawned"; readonly instance: ServiceInstanceSnapshot }
| { readonly type: "closed"; readonly instance: ServiceInstanceAddress };
/** @publicApiReview Consider hiding this low-level transport type behind the connection API. */
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[];
};
/** @publicApiReview Consider hiding this low-level transport type behind the connection API. */
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.
*
* @publicApiReview Consider replacing this low-level transport boundary with a higher-level adapter API.
*/
export interface RemoteServiceConnection {
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 connection: RemoteServiceConnection;
readonly bound?: boolean;
readonly onError?: (error: Error) => void;
readonly assertAccess?: () => void;
}
export interface RemoteServiceBinding extends RemoteServices {
rebind(bound: boolean, context: Context): Promise<void>;
}
export type RemoteServiceErrorCode =
| "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";
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>(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 FacetConnection {
/** Whether this currently unavailable route 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 connections?: readonly FacetConnection[];
readonly onError?: (error: Error) => void;
}
export interface FacetHost {
readonly services: RemoteServiceProvider;
/** Replace active facets with matching IDs while preserving 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>;
}

View file

@ -1,7 +1,7 @@
import type { JsonValue } from "@earendil-works/chord/json";
import type { JsonValue } from "@earendil-works/chord/types";
import Type from "typebox";
/** Static JSON contract. Concrete serializers are responsible for rejecting unsupported runtime values. */
export const JsonValueSchema = Type.Unsafe<JsonValue>(Type.Unknown());
export type { JsonValue } from "@earendil-works/chord/json";
export type { JsonValue } from "@earendil-works/chord/types";

View file

@ -7,8 +7,9 @@
"paths": {
"*": ["./*"],
"@earendil-works/chord": ["./packages/chord/src/index.ts"],
"@earendil-works/chord/context": ["./packages/chord/src/context.ts"],
"@earendil-works/chord/json": ["./packages/chord/src/json.ts"],
"@earendil-works/chord/api": ["./packages/chord/src/api.ts"],
"@earendil-works/chord/context": ["./packages/chord/src/context/index.ts"],
"@earendil-works/chord/types": ["./packages/chord/src/types.ts"],
"@earendil-works/pi-telemetry": ["./packages/telemetry/src/index.ts"],
"@earendil-works/pi-telemetry/testing": ["./packages/telemetry/src/testing/index.ts"],
"@earendil-works/pi-ai": ["./packages/ai/src/index.ts"],