/** * ProviderInstanceRegistryLive — runtime implementation of * `ProviderInstanceRegistry` plus its sibling mutator. * * Materializes every entry in a `ProviderInstanceConfigMap`: * * - When the entry's `driver` matches a registered driver, the registry * decodes the opaque `config` envelope through `driver.configSchema` * and calls `driver.create()` inside a fresh child scope. The * resulting `ProviderInstance` is stored keyed by instance id, * alongside its scope so the entry can be torn down independently. * - When the entry's `driver` is unknown to this build (fork, rollback, * in-flight PR branch), the registry emits an `"unavailable"` shadow * `ServerProvider` snapshot instead of failing. This is what makes * downgrades and fork-hopping safe per the * `forward/backward compatibility invariant` in * `packages/contracts/src/providerInstance.ts`. * - When the entry's config fails schema decode, the registry logs and * emits a shadow snapshot with the schema detail — same bucket as an * unknown driver. * * Unlike the pre-Slice-D layer, the registry now holds mutable state * (`Ref`s + `PubSub`) and exposes an internal mutator * (`ProviderInstanceRegistryMutator`) whose `reconcile` method diffs a * fresh config map against the live state, tearing down removed instances * and building new ones without disturbing unaffected instances. * * Every live instance runs inside its own child `Scope`. The registry's * own scope owns all child scopes via finalizers, so closing the registry * tears every instance down in reverse order; closing a single instance * (via `reconcile` removing it) leaves the rest untouched. * * @module provider/Layers/ProviderInstanceRegistryLive */import { defaultInstanceIdForDriver, providerInstanceConfigEnabledFlag, ProviderInstanceId, type ProviderInstanceConfig, type ProviderInstanceConfigMap, type ProviderDriverKind, type ServerProvider,} from "@t3tools/contracts";import * as Context from "effect/Context";import * as Effect from "effect/Effect";import * as Equal from "effect/Equal";import * as Exit from "effect/Exit";import * as Layer from "effect/Layer";import * as PubSub from "effect/PubSub";import * as Ref from "effect/Ref";import * as Schema from "effect/Schema";import * as Scope from "effect/Scope";import * as Stream from "effect/Stream"; import { buildUnavailableProviderSnapshot } from "../unavailableProviderSnapshot.ts";import { ProviderInstanceRegistry, type ProviderInstanceRegistryShape,} from "../Services/ProviderInstanceRegistry.ts";import { ProviderInstanceRegistryMutator, type ProviderInstanceRegistryMutatorShape,} from "../Services/ProviderInstanceRegistryMutator.ts";import type { AnyProviderDriver, ProviderInstance } from "../ProviderDriver.ts"; /** * Live registry entry: the materialized `ProviderInstance` + the fresh * child scope its `create` effect ran in + the original `entry` envelope * so `reconcile` can cheaply detect "no-op" updates. */interface LiveEntry { readonly instance: ProviderInstance; readonly scope: Scope.Closeable; readonly entry: ProviderInstanceConfig;} /** * Internal state shared between the public registry service and the * mutator service. Both services are thin shells around these refs. */interface RegistryState { readonly entries: Ref.Ref<ReadonlyMap<ProviderInstanceId, LiveEntry>>; readonly unavailable: Ref.Ref<ReadonlyMap<ProviderInstanceId, ServerProvider>>; readonly changes: PubSub.PubSub<void>;} /** * Structural equality on `ProviderInstanceConfig` envelopes. Used by * `reconcile` to skip rebuilds when settings arrive unchanged. Config * payloads are opaque `unknown` at the envelope layer; `Equal.equals` * falls back to structural equality for plain records, which matches how * the schema decode output is constructed. */const entryEqual = (a: ProviderInstanceConfig, b: ProviderInstanceConfig): boolean => Equal.equals(a, b); /** * Resolve an entry's enabled state. An explicit false on either the * envelope or the raw config blob wins (most restrictive) — old settings * files can carry both flags with conflicting values, and a user's disable * must never be silently undone. Otherwise the envelope flag wins, then the * decoded config's flag (which carries the driver schema's default for * built-ins and forks alike), then enabled by default. */const resolveEntryEnabled = (entry: ProviderInstanceConfig, typedConfig: unknown): boolean => { const rawConfigEnabled = providerInstanceConfigEnabledFlag(entry.config); if (entry.enabled === false || rawConfigEnabled === false) { return false; } return entry.enabled ?? providerInstanceConfigEnabledFlag(typedConfig) ?? true;}; /** * Build one live entry from a raw config envelope. Returns either a * `LiveEntry` plus undefined unavailable shadow, or a shadow snapshot and * undefined entry — callers dispatch to the appropriate Ref bucket. */const buildEntry = <R>(input: { readonly driversById: ReadonlyMap<ProviderDriverKind, AnyProviderDriver<R>>; readonly parentScope: Scope.Scope; readonly instanceId: ProviderInstanceId; readonly rawInstanceId: string; readonly entry: ProviderInstanceConfig;}): Effect.Effect< | { readonly kind: "live"; readonly live: LiveEntry } | { readonly kind: "unavailable"; readonly snapshot: ServerProvider }, never, R> => Effect.gen(function* () { const { driversById, parentScope, instanceId, rawInstanceId, entry } = input; const driver = driversById.get(entry.driver); if (!driver) { return { kind: "unavailable" as const, snapshot: yield* buildUnavailableProviderSnapshot({ driverKind: entry.driver, instanceId, displayName: entry.displayName, accentColor: entry.accentColor, reason: `Driver '${entry.driver}' is not registered in this build.`, }), }; } const decoder = Schema.decodeUnknownEffect(driver.configSchema); const decodeResult = yield* decoder(entry.config ?? driver.defaultConfig()).pipe(Effect.result); if (decodeResult._tag === "Failure") { const issue = decodeResult.failure; const detail = issue.message ?? String(issue); yield* Effect.logError("Failed to decode provider instance config", { instanceId: rawInstanceId, driver: entry.driver, detail, }); return { kind: "unavailable" as const, snapshot: yield* buildUnavailableProviderSnapshot({ driverKind: entry.driver, instanceId, displayName: entry.displayName, accentColor: entry.accentColor, reason: `Invalid config for instance '${rawInstanceId}': ${detail}`, }), }; } const typedConfig = decodeResult.success; const childScope = yield* Scope.make(); // Attach the child scope to the registry's parent scope: if the // registry scope closes, each surviving instance's child scope is // closed through this finalizer. `reconcile` manually closes the // child scope on remove/replace; subsequent close via the parent's // finalizer is a no-op because `Scope.close` is idempotent. yield* Scope.addFinalizer(parentScope, Scope.close(childScope, Exit.void).pipe(Effect.ignore)); const createResult = yield* driver .create({ instanceId, displayName: entry.displayName, accentColor: entry.accentColor, environment: entry.environment ?? [], enabled: resolveEntryEnabled(entry, typedConfig), config: typedConfig, }) .pipe(Effect.provideService(Scope.Scope, childScope), Effect.result); if (createResult._tag === "Failure") { yield* Effect.logError("Failed to create provider instance", { instanceId: rawInstanceId, driver: entry.driver, detail: createResult.failure.detail, }); yield* Scope.close(childScope, Exit.void).pipe(Effect.ignore); return { kind: "unavailable" as const, snapshot: yield* buildUnavailableProviderSnapshot({ driverKind: entry.driver, instanceId, displayName: entry.displayName, accentColor: entry.accentColor, reason: `Driver '${entry.driver}' failed to create instance: ${createResult.failure.detail}`, }), }; } return { kind: "live" as const, live: { instance: createResult.success, scope: childScope, entry, }, }; }); /** * Reconcile-only implementation of the mutator. Exposed to the hydration * layer; never called directly by the rest of the server. */const makeReconcile = <R>(input: { readonly state: RegistryState; readonly driversById: ReadonlyMap<ProviderDriverKind, AnyProviderDriver<R>>; readonly parentScope: Scope.Scope;}): ((configMap: ProviderInstanceConfigMap) => Effect.Effect<void, never, R>) => { const { state, driversById, parentScope } = input; return (configMap: ProviderInstanceConfigMap) => Effect.gen(function* () { const previousEntries = yield* Ref.get(state.entries); const previousUnavailable = yield* Ref.get(state.unavailable); const nextRaw = Object.entries(configMap); const nextKeys = new Set<ProviderInstanceId>( nextRaw.map(([raw]) => ProviderInstanceId.make(raw)), ); // 1. Close scopes for instances that disappeared or whose config // changed. Do this BEFORE creating replacements so ids map 1-to-1 // to live scopes at all times. const removedIds: Array<ProviderInstanceId> = []; const replacedIds = new Set<ProviderInstanceId>(); for (const [instanceId, live] of previousEntries) { if (!nextKeys.has(instanceId)) { removedIds.push(instanceId); continue; } const nextEntry = configMap[instanceId]; if (nextEntry !== undefined && !entryEqual(live.entry, nextEntry)) { replacedIds.add(instanceId); } } for (const id of [...removedIds, ...replacedIds]) { const live = previousEntries.get(id); if (live) { yield* Scope.close(live.scope, Exit.void).pipe(Effect.ignore); } } // 2. Build additions and replacements. Walk `nextRaw` so the final // entry order follows settings-author order. const builtEntries = new Map<ProviderInstanceId, LiveEntry>(); const builtUnavailable = new Map<ProviderInstanceId, ServerProvider>(); let orderChanged = false; const previousOrder = [...previousEntries.keys()]; const nextOrder: Array<ProviderInstanceId> = []; for (const [rawInstanceId, entry] of nextRaw) { const instanceId = ProviderInstanceId.make(rawInstanceId); nextOrder.push(instanceId); const existing = previousEntries.get(instanceId); if (existing !== undefined && !replacedIds.has(instanceId)) { // No-op update: keep the existing live entry and scope. builtEntries.set(instanceId, existing); continue; } const result = yield* buildEntry({ driversById, parentScope, instanceId, rawInstanceId, entry, }); if (result.kind === "live") { builtEntries.set(instanceId, result.live); } else { builtUnavailable.set(instanceId, result.snapshot); } } if (previousOrder.length === nextOrder.length) { for (let i = 0; i < previousOrder.length; i++) { if (previousOrder[i] !== nextOrder[i]) { orderChanged = true; break; } } } else { orderChanged = true; } const entriesChanged = orderChanged || removedIds.length > 0 || replacedIds.size > 0 || builtEntries.size !== previousEntries.size; const unavailableChanged = builtUnavailable.size !== previousUnavailable.size || [...builtUnavailable].some(([id, snapshot]) => { const prev = previousUnavailable.get(id); return prev === undefined || !Equal.equals(prev, snapshot); }) || [...previousUnavailable].some(([id]) => !builtUnavailable.has(id)); yield* Ref.set(state.entries, builtEntries); yield* Ref.set(state.unavailable, builtUnavailable); if (entriesChanged || unavailableChanged) { yield* PubSub.publish(state.changes, undefined); } });}; /** * Build the registry's runtime state from a concrete configMap. Returns a * record containing: * * - `registry`: the read-only `ProviderInstanceRegistryShape` to expose * under `ProviderInstanceRegistry`. * - `mutator`: the `ProviderInstanceRegistryMutatorShape` to expose * under `ProviderInstanceRegistryMutator`. * - `reconcile`: the raw reconcile function, provided for convenience so * boot-time layers can hydrate an initial map before publishing the * services. * * The scope that this effect runs in owns every per-instance child scope * created during `reconcile`. Closing that scope closes every live * instance. */export const makeProviderInstanceRegistry = <R>(input: { readonly drivers: ReadonlyArray<AnyProviderDriver<R>>; readonly configMap: ProviderInstanceConfigMap;}): Effect.Effect< { readonly registry: ProviderInstanceRegistryShape; readonly mutator: ProviderInstanceRegistryMutatorShape; }, never, R | Scope.Scope> => Effect.gen(function* () { const driversById = new Map<ProviderDriverKind, AnyProviderDriver<R>>( input.drivers.map((driver) => [driver.driverKind, driver]), ); // Capture the enclosing scope so per-instance child scopes can be // attached to it at `reconcile` time. Without this, `reconcile` // called later (e.g. from the hydration layer) would attach child // scopes to the *caller's* scope instead of the registry's. const parentScope = yield* Scope.Scope; // Capture the driver R context at construction time so `reconcile` // can be invoked later without re-providing driver dependencies. // The service tag's declared `reconcile: Effect<void>` hides R from // consumers — we materialize that here. const driverContext = yield* Effect.context<R>(); const entries = yield* Ref.make<ReadonlyMap<ProviderInstanceId, LiveEntry>>(new Map()); const unavailable = yield* Ref.make<ReadonlyMap<ProviderInstanceId, ServerProvider>>(new Map()); const changes = yield* PubSub.unbounded<void>(); yield* Effect.addFinalizer(() => PubSub.shutdown(changes)); const state: RegistryState = { entries, unavailable, changes }; const reconcileWithR = makeReconcile({ state, driversById, parentScope }); const reconcile: ProviderInstanceRegistryMutatorShape["reconcile"] = (configMap) => reconcileWithR(configMap).pipe(Effect.provideContext(driverContext)); // Hydrate the initial configMap synchronously so callers can read // `listInstances` immediately after this effect completes. yield* reconcile(input.configMap); const registry: ProviderInstanceRegistryShape = { getInstance: (id) => Ref.get(entries).pipe(Effect.map((map) => map.get(id)?.instance)), listInstances: Ref.get(entries).pipe( Effect.map( (map) => Array.from(map.values(), (live) => live.instance) as ReadonlyArray<ProviderInstance>, ), ), listUnavailable: Ref.get(unavailable).pipe( Effect.map((map) => Array.from(map.values()) as ReadonlyArray<ServerProvider>), ), // Getters: each read constructs a fresh Stream / Effect descriptor // so multiple consumers don't share a single already-started // Channel or subscription. Matches the pattern `ProviderRegistry` // uses for its own `streamChanges`. get streamChanges() { return Stream.fromPubSub(changes); }, // Synchronous subscribe — callers that need to consume changes // from a forked fibre must acquire the subscription in their own // fibre first (via `yield* registry.subscribeChanges`) and only // then fork a consumer loop on `Stream.fromSubscription(...)` / // `PubSub.take(...)`. See the shape docs for the race this avoids. get subscribeChanges() { return PubSub.subscribe(changes); }, }; const mutator: ProviderInstanceRegistryMutatorShape = { reconcile }; return { registry, mutator }; }); /** * Assemble a `ProviderInstanceRegistry` Layer bound to a fixed set of * drivers and a pre-resolved `ProviderInstanceConfigMap`. Used by tests * that want explicit control over the registry's source-of-truth without * wiring up the settings watcher. * * Only exposes the public registry tag — hot-reload consumers should use * `ProviderInstanceRegistryMutableLayer` (below) or the hydration layer. */export const ProviderInstanceRegistryLayer = <R>(input: { readonly drivers: ReadonlyArray<AnyProviderDriver<R>>; readonly configMap: ProviderInstanceConfigMap;}): Layer.Layer<ProviderInstanceRegistry, never, R> => Layer.effect( ProviderInstanceRegistry, makeProviderInstanceRegistry(input).pipe(Effect.map((built) => built.registry)), ) as Layer.Layer<ProviderInstanceRegistry, never, R>; /** * Layer variant that also exposes the mutator tag. Consumed by * `ProviderInstanceRegistryHydrationLive` to reconcile on settings * changes. Tests that exercise the mutator directly can pair this Layer * with a test-local `ServerSettingsService`. */export const ProviderInstanceRegistryMutableLayer = <R>(input: { readonly drivers: ReadonlyArray<AnyProviderDriver<R>>; readonly configMap: ProviderInstanceConfigMap;}): Layer.Layer<ProviderInstanceRegistry | ProviderInstanceRegistryMutator, never, R> => Layer.effectContext( makeProviderInstanceRegistry(input).pipe( Effect.map(({ registry, mutator }) => Context.make(ProviderInstanceRegistry, registry).pipe( Context.add(ProviderInstanceRegistryMutator, mutator), ), ), ), ) as Layer.Layer<ProviderInstanceRegistry | ProviderInstanceRegistryMutator, never, R>; export { defaultInstanceIdForDriver };
/**
* ProviderInstanceRegistryLive — runtime implementation of
* `ProviderInstanceRegistry` plus its sibling mutator.
*
* Materializes every entry in a `ProviderInstanceConfigMap`:
*
* - When the entry's `driver` matches a registered driver, the registry
* decodes the opaque `config` envelope through `driver.configSchema`
* and calls `driver.create()` inside a fresh child scope. The
* resulting `ProviderInstance` is stored keyed by instance id,
* alongside its scope so the entry can be torn down independently.
* - When the entry's `driver` is unknown to this build (fork, rollback,
* in-flight PR branch), the registry emits an `"unavailable"` shadow
* `ServerProvider` snapshot instead of failing. This is what makes
* downgrades and fork-hopping safe per the
* `forward/backward compatibility invariant` in
* `packages/contracts/src/providerInstance.ts`.
* - When the entry's config fails schema decode, the registry logs and
* emits a shadow snapshot with the schema detail — same bucket as an
* unknown driver.
*
* Unlike the pre-Slice-D layer, the registry now holds mutable state
* (`Ref`s + `PubSub`) and exposes an internal mutator
* (`ProviderInstanceRegistryMutator`) whose `reconcile` method diffs a
* fresh config map against the live state, tearing down removed instances
* and building new ones without disturbing unaffected instances.
*
* Every live instance runs inside its own child `Scope`. The registry's
* own scope owns all child scopes via finalizers, so closing the registry
* tears every instance down in reverse order; closing a single instance
* (via `reconcile` removing it) leaves the rest untouched.
*
* @module provider/Layers/ProviderInstanceRegistryLive
*/
import {
defaultInstanceIdForDriver,
providerInstanceConfigEnabledFlag,
ProviderInstanceId,
type ProviderInstanceConfig,
type ProviderInstanceConfigMap,
type ProviderDriverKind,
type ServerProvider,
} from "@t3tools/contracts";
import * as Context from "effect/Context";
import * as Effect from "effect/Effect";
import * as Equal from "effect/Equal";
import * as Exit from "effect/Exit";
import * as Layer from "effect/Layer";
import * as PubSub from "effect/PubSub";
import * as Ref from "effect/Ref";
import * as Schema from "effect/Schema";
import * as Scope from "effect/Scope";
import * as Stream from "effect/Stream";
import { buildUnavailableProviderSnapshot } from "../unavailableProviderSnapshot.ts";
import {
ProviderInstanceRegistry,
type ProviderInstanceRegistryShape,
} from "../Services/ProviderInstanceRegistry.ts";
import {
ProviderInstanceRegistryMutator,
type ProviderInstanceRegistryMutatorShape,
} from "../Services/ProviderInstanceRegistryMutator.ts";
import type { AnyProviderDriver, ProviderInstance } from "../ProviderDriver.ts";
/**
* Live registry entry: the materialized `ProviderInstance` + the fresh
* child scope its `create` effect ran in + the original `entry` envelope
* so `reconcile` can cheaply detect "no-op" updates.
*/
interface LiveEntry {
readonly instance: ProviderInstance;
readonly scope: Scope.Closeable;
readonly entry: ProviderInstanceConfig;
}
/**
* Internal state shared between the public registry service and the
* mutator service. Both services are thin shells around these refs.
*/
interface RegistryState {
readonly entries: Ref.Ref<ReadonlyMap<ProviderInstanceId, LiveEntry>>;
readonly unavailable: Ref.Ref<ReadonlyMap<ProviderInstanceId, ServerProvider>>;
readonly changes: PubSub.PubSub<void>;
}
/**
* Structural equality on `ProviderInstanceConfig` envelopes. Used by
* `reconcile` to skip rebuilds when settings arrive unchanged. Config
* payloads are opaque `unknown` at the envelope layer; `Equal.equals`
* falls back to structural equality for plain records, which matches how
* the schema decode output is constructed.
*/
const entryEqual = (a: ProviderInstanceConfig, b: ProviderInstanceConfig): boolean =>
Equal.equals(a, b);
/**
* Resolve an entry's enabled state. An explicit false on either the
* envelope or the raw config blob wins (most restrictive) — old settings
* files can carry both flags with conflicting values, and a user's disable
* must never be silently undone. Otherwise the envelope flag wins, then the
* decoded config's flag (which carries the driver schema's default for
* built-ins and forks alike), then enabled by default.
*/
const resolveEntryEnabled = (entry: ProviderInstanceConfig, typedConfig: unknown): boolean => {
const rawConfigEnabled = providerInstanceConfigEnabledFlag(entry.config);
if (entry.enabled === false || rawConfigEnabled === false) {
return false;
}
return entry.enabled ?? providerInstanceConfigEnabledFlag(typedConfig) ?? true;
};
/**
* Build one live entry from a raw config envelope. Returns either a
* `LiveEntry` plus undefined unavailable shadow, or a shadow snapshot and
* undefined entry — callers dispatch to the appropriate Ref bucket.
*/
const buildEntry = <R>(input: {
readonly driversById: ReadonlyMap<ProviderDriverKind, AnyProviderDriver<R>>;
readonly parentScope: Scope.Scope;
readonly instanceId: ProviderInstanceId;
readonly rawInstanceId: string;
readonly entry: ProviderInstanceConfig;
}): Effect.Effect<
| { readonly kind: "live"; readonly live: LiveEntry }
| { readonly kind: "unavailable"; readonly snapshot: ServerProvider },
never,
R
> =>
Effect.gen(function* () {
const { driversById, parentScope, instanceId, rawInstanceId, entry } = input;
const driver = driversById.get(entry.driver);
if (!driver) {
return {
kind: "unavailable" as const,
snapshot: yield* buildUnavailableProviderSnapshot({
driverKind: entry.driver,
instanceId,
displayName: entry.displayName,
accentColor: entry.accentColor,
reason: `Driver '${entry.driver}' is not registered in this build.`,
}),
};
}
const decoder = Schema.decodeUnknownEffect(driver.configSchema);
const decodeResult = yield* decoder(entry.config ?? driver.defaultConfig()).pipe(Effect.result);
if (decodeResult._tag === "Failure") {
const issue = decodeResult.failure;
const detail = issue.message ?? String(issue);
yield* Effect.logError("Failed to decode provider instance config", {
instanceId: rawInstanceId,
driver: entry.driver,
detail,
});
return {
kind: "unavailable" as const,
snapshot: yield* buildUnavailableProviderSnapshot({
driverKind: entry.driver,
instanceId,
displayName: entry.displayName,
accentColor: entry.accentColor,
reason: `Invalid config for instance '${rawInstanceId}': ${detail}`,
}),
};
}
const typedConfig = decodeResult.success;
const childScope = yield* Scope.make();
// Attach the child scope to the registry's parent scope: if the
// registry scope closes, each surviving instance's child scope is
// closed through this finalizer. `reconcile` manually closes the
// child scope on remove/replace; subsequent close via the parent's
// finalizer is a no-op because `Scope.close` is idempotent.
yield* Scope.addFinalizer(parentScope, Scope.close(childScope, Exit.void).pipe(Effect.ignore));
const createResult = yield* driver
.create({
instanceId,
displayName: entry.displayName,
accentColor: entry.accentColor,
environment: entry.environment ?? [],
enabled: resolveEntryEnabled(entry, typedConfig),
config: typedConfig,
})
.pipe(Effect.provideService(Scope.Scope, childScope), Effect.result);
if (createResult._tag === "Failure") {
yield* Effect.logError("Failed to create provider instance", {
instanceId: rawInstanceId,
driver: entry.driver,
detail: createResult.failure.detail,
});
yield* Scope.close(childScope, Exit.void).pipe(Effect.ignore);
return {
kind: "unavailable" as const,
snapshot: yield* buildUnavailableProviderSnapshot({
driverKind: entry.driver,
instanceId,
displayName: entry.displayName,
accentColor: entry.accentColor,
reason: `Driver '${entry.driver}' failed to create instance: ${createResult.failure.detail}`,
}),
};
}
return {
kind: "live" as const,
live: {
instance: createResult.success,
scope: childScope,
entry,
},
};
});
/**
* Reconcile-only implementation of the mutator. Exposed to the hydration
* layer; never called directly by the rest of the server.
*/
const makeReconcile = <R>(input: {
readonly state: RegistryState;
readonly driversById: ReadonlyMap<ProviderDriverKind, AnyProviderDriver<R>>;
readonly parentScope: Scope.Scope;
}): ((configMap: ProviderInstanceConfigMap) => Effect.Effect<void, never, R>) => {
const { state, driversById, parentScope } = input;
return (configMap: ProviderInstanceConfigMap) =>
Effect.gen(function* () {
const previousEntries = yield* Ref.get(state.entries);
const previousUnavailable = yield* Ref.get(state.unavailable);
const nextRaw = Object.entries(configMap);
const nextKeys = new Set<ProviderInstanceId>(
nextRaw.map(([raw]) => ProviderInstanceId.make(raw)),
);
// 1. Close scopes for instances that disappeared or whose config
// changed. Do this BEFORE creating replacements so ids map 1-to-1
// to live scopes at all times.
const removedIds: Array<ProviderInstanceId> = [];
const replacedIds = new Set<ProviderInstanceId>();
for (const [instanceId, live] of previousEntries) {
if (!nextKeys.has(instanceId)) {
removedIds.push(instanceId);
continue;
}
const nextEntry = configMap[instanceId];
if (nextEntry !== undefined && !entryEqual(live.entry, nextEntry)) {
replacedIds.add(instanceId);
}
}
for (const id of [...removedIds, ...replacedIds]) {
const live = previousEntries.get(id);
if (live) {
yield* Scope.close(live.scope, Exit.void).pipe(Effect.ignore);
}
}
// 2. Build additions and replacements. Walk `nextRaw` so the final
// entry order follows settings-author order.
const builtEntries = new Map<ProviderInstanceId, LiveEntry>();
const builtUnavailable = new Map<ProviderInstanceId, ServerProvider>();
let orderChanged = false;
const previousOrder = [...previousEntries.keys()];
const nextOrder: Array<ProviderInstanceId> = [];
for (const [rawInstanceId, entry] of nextRaw) {
const instanceId = ProviderInstanceId.make(rawInstanceId);
nextOrder.push(instanceId);
const existing = previousEntries.get(instanceId);
if (existing !== undefined && !replacedIds.has(instanceId)) {
// No-op update: keep the existing live entry and scope.
builtEntries.set(instanceId, existing);
continue;
}
const result = yield* buildEntry({
driversById,
parentScope,
instanceId,
rawInstanceId,
entry,
});
if (result.kind === "live") {
builtEntries.set(instanceId, result.live);
} else {
builtUnavailable.set(instanceId, result.snapshot);
}
}
if (previousOrder.length === nextOrder.length) {
for (let i = 0; i < previousOrder.length; i++) {
if (previousOrder[i] !== nextOrder[i]) {
orderChanged = true;
break;
}
}
} else {
orderChanged = true;
}
const entriesChanged =
orderChanged ||
removedIds.length > 0 ||
replacedIds.size > 0 ||
builtEntries.size !== previousEntries.size;
const unavailableChanged =
builtUnavailable.size !== previousUnavailable.size ||
[...builtUnavailable].some(([id, snapshot]) => {
const prev = previousUnavailable.get(id);
return prev === undefined || !Equal.equals(prev, snapshot);
}) ||
[...previousUnavailable].some(([id]) => !builtUnavailable.has(id));
yield* Ref.set(state.entries, builtEntries);
yield* Ref.set(state.unavailable, builtUnavailable);
if (entriesChanged || unavailableChanged) {
yield* PubSub.publish(state.changes, undefined);
}
});
};
/**
* Build the registry's runtime state from a concrete configMap. Returns a
* record containing:
*
* - `registry`: the read-only `ProviderInstanceRegistryShape` to expose
* under `ProviderInstanceRegistry`.
* - `mutator`: the `ProviderInstanceRegistryMutatorShape` to expose
* under `ProviderInstanceRegistryMutator`.
* - `reconcile`: the raw reconcile function, provided for convenience so
* boot-time layers can hydrate an initial map before publishing the
* services.
*
* The scope that this effect runs in owns every per-instance child scope
* created during `reconcile`. Closing that scope closes every live
* instance.
*/
export const makeProviderInstanceRegistry = <R>(input: {
readonly drivers: ReadonlyArray<AnyProviderDriver<R>>;
readonly configMap: ProviderInstanceConfigMap;
}): Effect.Effect<
{
readonly registry: ProviderInstanceRegistryShape;
readonly mutator: ProviderInstanceRegistryMutatorShape;
},
never,
R | Scope.Scope
> =>
Effect.gen(function* () {
const driversById = new Map<ProviderDriverKind, AnyProviderDriver<R>>(
input.drivers.map((driver) => [driver.driverKind, driver]),
);
// Capture the enclosing scope so per-instance child scopes can be
// attached to it at `reconcile` time. Without this, `reconcile`
// called later (e.g. from the hydration layer) would attach child
// scopes to the *caller's* scope instead of the registry's.
const parentScope = yield* Scope.Scope;
// Capture the driver R context at construction time so `reconcile`
// can be invoked later without re-providing driver dependencies.
// The service tag's declared `reconcile: Effect<void>` hides R from
// consumers — we materialize that here.
const driverContext = yield* Effect.context<R>();
const entries = yield* Ref.make<ReadonlyMap<ProviderInstanceId, LiveEntry>>(new Map());
const unavailable = yield* Ref.make<ReadonlyMap<ProviderInstanceId, ServerProvider>>(new Map());
const changes = yield* PubSub.unbounded<void>();
yield* Effect.addFinalizer(() => PubSub.shutdown(changes));
const state: RegistryState = { entries, unavailable, changes };
const reconcileWithR = makeReconcile({ state, driversById, parentScope });
const reconcile: ProviderInstanceRegistryMutatorShape["reconcile"] = (configMap) =>
reconcileWithR(configMap).pipe(Effect.provideContext(driverContext));
// Hydrate the initial configMap synchronously so callers can read
// `listInstances` immediately after this effect completes.
yield* reconcile(input.configMap);
const registry: ProviderInstanceRegistryShape = {
getInstance: (id) => Ref.get(entries).pipe(Effect.map((map) => map.get(id)?.instance)),
listInstances: Ref.get(entries).pipe(
Effect.map(
(map) =>
Array.from(map.values(), (live) => live.instance) as ReadonlyArray<ProviderInstance>,
),
),
listUnavailable: Ref.get(unavailable).pipe(
Effect.map((map) => Array.from(map.values()) as ReadonlyArray<ServerProvider>),
),
// Getters: each read constructs a fresh Stream / Effect descriptor
// so multiple consumers don't share a single already-started
// Channel or subscription. Matches the pattern `ProviderRegistry`
// uses for its own `streamChanges`.
get streamChanges() {
return Stream.fromPubSub(changes);
},
// Synchronous subscribe — callers that need to consume changes
// from a forked fibre must acquire the subscription in their own
// fibre first (via `yield* registry.subscribeChanges`) and only
// then fork a consumer loop on `Stream.fromSubscription(...)` /
// `PubSub.take(...)`. See the shape docs for the race this avoids.
get subscribeChanges() {
return PubSub.subscribe(changes);
},
};
const mutator: ProviderInstanceRegistryMutatorShape = { reconcile };
return { registry, mutator };
});
/**
* Assemble a `ProviderInstanceRegistry` Layer bound to a fixed set of
* drivers and a pre-resolved `ProviderInstanceConfigMap`. Used by tests
* that want explicit control over the registry's source-of-truth without
* wiring up the settings watcher.
*
* Only exposes the public registry tag — hot-reload consumers should use
* `ProviderInstanceRegistryMutableLayer` (below) or the hydration layer.
*/
export const ProviderInstanceRegistryLayer = <R>(input: {
readonly drivers: ReadonlyArray<AnyProviderDriver<R>>;
readonly configMap: ProviderInstanceConfigMap;
}): Layer.Layer<ProviderInstanceRegistry, never, R> =>
Layer.effect(
ProviderInstanceRegistry,
makeProviderInstanceRegistry(input).pipe(Effect.map((built) => built.registry)),
) as Layer.Layer<ProviderInstanceRegistry, never, R>;
/**
* Layer variant that also exposes the mutator tag. Consumed by
* `ProviderInstanceRegistryHydrationLive` to reconcile on settings
* changes. Tests that exercise the mutator directly can pair this Layer
* with a test-local `ServerSettingsService`.
*/
export const ProviderInstanceRegistryMutableLayer = <R>(input: {
readonly drivers: ReadonlyArray<AnyProviderDriver<R>>;
readonly configMap: ProviderInstanceConfigMap;
}): Layer.Layer<ProviderInstanceRegistry | ProviderInstanceRegistryMutator, never, R> =>
Layer.effectContext(
makeProviderInstanceRegistry(input).pipe(
Effect.map(({ registry, mutator }) =>
Context.make(ProviderInstanceRegistry, registry).pipe(
Context.add(ProviderInstanceRegistryMutator, mutator),
),
),
),
) as Layer.Layer<ProviderInstanceRegistry | ProviderInstanceRegistryMutator, never, R>;
export { defaultInstanceIdForDriver };