Squashed 'third_party/pi/' content from commit d981de122
git-subtree-dir: third_party/pi git-subtree-split: d981de1229ef899957bbe968bc8dcda02a21f477
This commit is contained in:
673
packages/chord/test/facets.test.ts
Normal file
673
packages/chord/test/facets.test.ts
Normal file
@@ -0,0 +1,673 @@
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import { BACKGROUND_CONTEXT } from "../src/context/index.ts";
|
||||
import {
|
||||
type Context,
|
||||
createFacetHost,
|
||||
createRemoteServiceBinding,
|
||||
defineFacet,
|
||||
defineService,
|
||||
type MutableReplicatedState,
|
||||
RemoteServiceProvider,
|
||||
type RemoteServiceSource,
|
||||
type ReplicatedState,
|
||||
} from "../src/index.ts";
|
||||
import { createLoopbackServiceTransport } from "./helpers.ts";
|
||||
|
||||
interface Source {
|
||||
read(context: Context): Promise<string>;
|
||||
}
|
||||
|
||||
interface Projection {
|
||||
read(context: Context): Promise<string>;
|
||||
}
|
||||
|
||||
interface KeyedValue {
|
||||
read(context: Context): Promise<string>;
|
||||
}
|
||||
|
||||
interface Watched {
|
||||
readonly state: ReplicatedState<{ value: number }>;
|
||||
}
|
||||
|
||||
interface HostValues {
|
||||
readonly name: string;
|
||||
readonly use: string;
|
||||
}
|
||||
|
||||
interface LocalKeyedValue {
|
||||
readonly metadata: Map<string, string>;
|
||||
read(): string;
|
||||
}
|
||||
|
||||
interface LeftValue {
|
||||
read(context: Context): Promise<string>;
|
||||
}
|
||||
|
||||
interface RightValue {
|
||||
read(context: Context): Promise<string>;
|
||||
}
|
||||
|
||||
interface CombinedValue {
|
||||
read(context: Context): Promise<string>;
|
||||
}
|
||||
|
||||
const Source = defineService<Source>("test.experimental.source");
|
||||
const Projection = defineService<Projection>("test.experimental.projection");
|
||||
const KeyedValue = defineService<KeyedValue>("test.experimental.keyed-value");
|
||||
const Watched = defineService<Watched>("test.experimental.watched");
|
||||
const HostValues = defineService<HostValues>("test.experimental.host-values", { local: true });
|
||||
const LocalKeyedValue = defineService<LocalKeyedValue>("test.experimental.local-keyed-value", { local: true });
|
||||
const LeftValue = defineService<LeftValue>("test.experimental.left-value");
|
||||
const RightValue = defineService<RightValue>("test.experimental.right-value");
|
||||
const CombinedValue = defineService<CombinedValue>("test.experimental.combined-value");
|
||||
|
||||
describe("facet host", () => {
|
||||
test("discovers setup dependencies before connecting stable service handles", async () => {
|
||||
const trace: string[] = [];
|
||||
let sourceHandle: Source | undefined;
|
||||
const projection = defineFacet({
|
||||
id: "projection",
|
||||
setup(env) {
|
||||
trace.push("setup projection");
|
||||
sourceHandle = env.use(Source);
|
||||
expect(() => sourceHandle!.read(BACKGROUND_CONTEXT)).toThrow(
|
||||
"Facet projection service handles cannot be used while setting_up",
|
||||
);
|
||||
env.provide(Projection, {
|
||||
read: (context) => sourceHandle!.read(context),
|
||||
});
|
||||
env.onActivate(() => {
|
||||
trace.push("activate projection");
|
||||
});
|
||||
env.onDeactivate(() => {
|
||||
trace.push("dispose projection");
|
||||
});
|
||||
},
|
||||
});
|
||||
const source = defineFacet({
|
||||
id: "source",
|
||||
setup(env) {
|
||||
trace.push("setup source");
|
||||
env.provide(Source, {
|
||||
async read() {
|
||||
return "value";
|
||||
},
|
||||
});
|
||||
env.onActivate(() => {
|
||||
trace.push("activate source");
|
||||
});
|
||||
env.onDeactivate(() => {
|
||||
trace.push("dispose source");
|
||||
});
|
||||
},
|
||||
});
|
||||
const host = await createFacetHost({ facets: [projection, source] });
|
||||
|
||||
expect(trace).toEqual(["setup projection", "setup source", "activate source", "activate projection"]);
|
||||
expect(await sourceHandle!.read(BACKGROUND_CONTEXT)).toBe("value");
|
||||
expect(await host.services.use(Projection).read(BACKGROUND_CONTEXT)).toBe("value");
|
||||
|
||||
await host.dispose();
|
||||
expect(trace.slice(-2)).toEqual(["dispose projection", "dispose source"]);
|
||||
});
|
||||
|
||||
test("connects keyed observations only when the observing facet activates", async () => {
|
||||
const trace: string[] = [];
|
||||
const observer = defineFacet({
|
||||
id: "observer",
|
||||
setup(env) {
|
||||
env.observe(KeyedValue, async (service, context) => {
|
||||
trace.push(`observe ${await service.read(context)}`);
|
||||
});
|
||||
env.onActivate(() => {
|
||||
trace.push("activate observer");
|
||||
});
|
||||
},
|
||||
});
|
||||
const provider = defineFacet({
|
||||
id: "provider",
|
||||
setup(env) {
|
||||
const values = env.provideMany(KeyedValue);
|
||||
env.onActivate(() => {
|
||||
trace.push("activate provider");
|
||||
values.spawn("one", {
|
||||
async read() {
|
||||
return "one";
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
const host = await createFacetHost({ facets: [observer, provider] });
|
||||
await vi.waitFor(() => expect(trace).toContain("observe one"));
|
||||
|
||||
expect(trace).toEqual(["activate provider", "activate observer", "observe one"]);
|
||||
const remoteServices = createRemoteServiceBinding({
|
||||
services: [KeyedValue],
|
||||
transport: createLoopbackServiceTransport(host.services),
|
||||
});
|
||||
const remoteValues: string[] = [];
|
||||
remoteServices.observe(KeyedValue, async (service, context) => {
|
||||
remoteValues.push(await service.read(context));
|
||||
});
|
||||
await remoteServices.ready(BACKGROUND_CONTEXT);
|
||||
await vi.waitFor(() => expect(remoteValues).toEqual(["one"]));
|
||||
|
||||
await remoteServices.dispose(BACKGROUND_CONTEXT);
|
||||
await host.dispose();
|
||||
});
|
||||
|
||||
test("routes remotely exposable keyed services through the host provider", async () => {
|
||||
const observed: Array<{ service: KeyedValue; context: Context }> = [];
|
||||
const consumer = defineFacet({
|
||||
id: "remote-keyed-consumer",
|
||||
setup(env) {
|
||||
env.observe(KeyedValue, (service, context) => {
|
||||
observed.push({ service, context });
|
||||
});
|
||||
},
|
||||
});
|
||||
const provider = (value: string) =>
|
||||
defineFacet({
|
||||
id: "remote-keyed-provider",
|
||||
setup(env) {
|
||||
const values = env.provideMany(KeyedValue);
|
||||
env.onActivate(() => {
|
||||
values.spawn("current", {
|
||||
async read() {
|
||||
return value;
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
const host = await createFacetHost({ facets: [consumer, provider("A")] });
|
||||
await vi.waitFor(() => expect(observed).toHaveLength(1));
|
||||
const first = observed[0]!;
|
||||
await expect(first.service.read(first.context)).resolves.toBe("A");
|
||||
|
||||
await host.reload([provider("B")]);
|
||||
await vi.waitFor(() => expect(observed).toHaveLength(2));
|
||||
expect(first.context.abortSignal?.aborted).toBe(true);
|
||||
expect(() => first.service.read(first.context)).toThrow("observation is closed");
|
||||
await expect(observed[1]!.service.read(observed[1]!.context)).resolves.toBe("B");
|
||||
|
||||
await host.dispose();
|
||||
expect(observed[1]!.context.abortSignal?.aborted).toBe(true);
|
||||
});
|
||||
|
||||
test("terminates the host when keyed replacement publication fails", async () => {
|
||||
const failure = new Error("spawn publication failed");
|
||||
const provider = (value: string) =>
|
||||
defineFacet({
|
||||
id: "failing-keyed-provider",
|
||||
setup(env) {
|
||||
const values = env.provideMany(KeyedValue);
|
||||
env.onActivate(() => {
|
||||
values.spawn("current", {
|
||||
async read() {
|
||||
return value;
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
const host = await createFacetHost({ facets: [provider("A")] });
|
||||
const subscription = host.services.subscribe(KeyedValue.id, "keyed", (update) => {
|
||||
if (update.type === "spawned") throw failure;
|
||||
});
|
||||
subscription.activate();
|
||||
|
||||
await expect(host.reload([provider("B")])).rejects.toThrow("Facet reload failed after cutover");
|
||||
await expect(host.reload([])).rejects.toThrow("Facet host cannot reload while dead");
|
||||
expect(() => host.services.use(KeyedValue)).toThrow("Remote service provider is disposed");
|
||||
await host.dispose();
|
||||
});
|
||||
|
||||
test("terminates the host when keyed retirement publication fails", async () => {
|
||||
const failure = new Error("close publication failed");
|
||||
const provider = (value: string) =>
|
||||
defineFacet({
|
||||
id: "failing-keyed-retirement-provider",
|
||||
setup(env) {
|
||||
const values = env.provideMany(KeyedValue);
|
||||
env.onActivate(() => {
|
||||
values.spawn("current", {
|
||||
async read() {
|
||||
return value;
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
const host = await createFacetHost({ facets: [provider("A")] });
|
||||
const subscription = host.services.subscribe(KeyedValue.id, "keyed", (update) => {
|
||||
if (update.type === "closed") throw failure;
|
||||
});
|
||||
subscription.activate();
|
||||
|
||||
await expect(host.reload([provider("B")])).rejects.toThrow("Facet reload failed after cutover");
|
||||
await expect(host.reload([])).rejects.toThrow("Facet host cannot reload while dead");
|
||||
await host.dispose();
|
||||
});
|
||||
|
||||
test("keeps unrestricted local keyed services process-local across provider reloads", async () => {
|
||||
const observed: Array<{ service: LocalKeyedValue; context: Context }> = [];
|
||||
const consumer = defineFacet({
|
||||
id: "local-keyed-consumer",
|
||||
setup(env) {
|
||||
env.observe(LocalKeyedValue, (service, context) => {
|
||||
observed.push({ service, context });
|
||||
});
|
||||
},
|
||||
});
|
||||
const provider = (value: string) =>
|
||||
defineFacet({
|
||||
id: "local-keyed-provider",
|
||||
setup(env) {
|
||||
const values = env.provideMany(LocalKeyedValue);
|
||||
env.onActivate(() => {
|
||||
values.spawn("current", {
|
||||
metadata: new Map([["value", value]]),
|
||||
read: () => value,
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
const host = await createFacetHost({ facets: [consumer, provider("A")] });
|
||||
await vi.waitFor(() => expect(observed).toHaveLength(1));
|
||||
const first = observed[0]!;
|
||||
expect(first.service.read()).toBe("A");
|
||||
expect(first.service.metadata.get("value")).toBe("A");
|
||||
expect(host.services.catalogue).not.toContainEqual({ serviceId: LocalKeyedValue.id, mode: "keyed" });
|
||||
expect(() => host.services.use(LocalKeyedValue)).toThrow("process-local");
|
||||
|
||||
await host.reload([provider("B")]);
|
||||
await vi.waitFor(() => expect(observed).toHaveLength(2));
|
||||
expect(first.context.abortSignal?.aborted).toBe(true);
|
||||
expect(() => first.service.read()).toThrow(`Keyed service ${LocalKeyedValue.id} observation is closed`);
|
||||
expect(observed[1]!.service.read()).toBe("B");
|
||||
expect(observed[1]!.service.metadata.get("value")).toBe("B");
|
||||
|
||||
await host.dispose();
|
||||
expect(observed[1]!.context.abortSignal?.aborted).toBe(true);
|
||||
});
|
||||
|
||||
test("keeps remotely exposable local state replicas stable across provider reloads", async () => {
|
||||
const sources: MutableReplicatedState<{ value: number }>[] = [];
|
||||
const revisions: number[] = [];
|
||||
let watched: Watched | undefined;
|
||||
const consumer = defineFacet({
|
||||
id: "state-consumer",
|
||||
setup(env) {
|
||||
watched = env.use(Watched);
|
||||
env.onActivate(() => {
|
||||
env.own(watched!.state.subscribe(({ value }) => revisions.push(value)));
|
||||
});
|
||||
},
|
||||
});
|
||||
const provider = (value: number) =>
|
||||
defineFacet({
|
||||
id: "state-provider",
|
||||
setup(env) {
|
||||
const state = env.replicatedState({ value });
|
||||
sources.push(state);
|
||||
env.provide(Watched, { state });
|
||||
},
|
||||
});
|
||||
const host = await createFacetHost({ facets: [consumer, provider(1)] });
|
||||
const retainedState = watched!.state;
|
||||
expect(retainedState.value).toEqual({ value: 1 });
|
||||
expect(revisions).toEqual([1]);
|
||||
|
||||
await host.reload([provider(2)]);
|
||||
expect(watched!.state).toBe(retainedState);
|
||||
expect(retainedState.value).toEqual({ value: 2 });
|
||||
expect(revisions).toEqual([1, 2]);
|
||||
sources[0]!.state.value = 3;
|
||||
sources[0]!.publish(BACKGROUND_CONTEXT);
|
||||
expect(retainedState.value).toEqual({ value: 2 });
|
||||
expect(revisions).toEqual([1, 2]);
|
||||
|
||||
await host.dispose();
|
||||
expect(() => retainedState.value).toThrow("Facet state-consumer service handles cannot be used while dead");
|
||||
});
|
||||
|
||||
test("scopes singleton service views to each facet lifecycle", async () => {
|
||||
const consumerHandles: Source[] = [];
|
||||
const cleanupValues: string[] = [];
|
||||
let peerHandle: Source | undefined;
|
||||
const consumer = (generation: string) =>
|
||||
defineFacet({
|
||||
id: "scoped-consumer",
|
||||
setup(env) {
|
||||
const source = env.use(Source);
|
||||
expect(env.use(Source)).toBe(source);
|
||||
consumerHandles.push(source);
|
||||
expect(() => source.read(BACKGROUND_CONTEXT)).toThrow(
|
||||
"Facet scoped-consumer service handles cannot be used while setting_up",
|
||||
);
|
||||
env.onDeactivate(async () => {
|
||||
cleanupValues.push(`${generation}:${await source.read(BACKGROUND_CONTEXT)}`);
|
||||
});
|
||||
},
|
||||
});
|
||||
const peer = defineFacet({
|
||||
id: "peer-consumer",
|
||||
setup(env) {
|
||||
peerHandle = env.use(Source);
|
||||
},
|
||||
});
|
||||
const provider = defineFacet({
|
||||
id: "scoped-provider",
|
||||
setup(env) {
|
||||
env.provide(Source, {
|
||||
async read() {
|
||||
return "value";
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
const host = await createFacetHost({ facets: [consumer("A"), peer, provider] });
|
||||
expect(consumerHandles[0]).not.toBe(peerHandle);
|
||||
const retainedOldRead = consumerHandles[0]!.read;
|
||||
|
||||
await host.reload([consumer("B")]);
|
||||
expect(cleanupValues).toEqual(["A:value"]);
|
||||
expect(Object.is(consumerHandles[1], consumerHandles[0])).toBe(false);
|
||||
expect(() => consumerHandles[0]!.read(BACKGROUND_CONTEXT)).toThrow(
|
||||
"Facet scoped-consumer service handles cannot be used while dead",
|
||||
);
|
||||
expect(() => retainedOldRead(BACKGROUND_CONTEXT)).toThrow(
|
||||
"Facet scoped-consumer service handles cannot be used while dead",
|
||||
);
|
||||
await expect(consumerHandles[1]!.read(BACKGROUND_CONTEXT)).resolves.toBe("value");
|
||||
await expect(peerHandle!.read(BACKGROUND_CONTEXT)).resolves.toBe("value");
|
||||
|
||||
await host.dispose();
|
||||
expect(cleanupValues).toEqual(["A:value", "B:value"]);
|
||||
expect(() => consumerHandles[1]!.read(BACKGROUND_CONTEXT)).toThrow(
|
||||
"Facet scoped-consumer service handles cannot be used while dead",
|
||||
);
|
||||
expect(() => peerHandle!.read(BACKGROUND_CONTEXT)).toThrow(
|
||||
"Facet peer-consumer service handles cannot be used while dead",
|
||||
);
|
||||
});
|
||||
|
||||
test("owns resources registered during activation", async () => {
|
||||
let state: MutableReplicatedState<{ value: number }> | undefined;
|
||||
let deliveries = 0;
|
||||
const consumer = defineFacet({
|
||||
id: "consumer",
|
||||
setup(env) {
|
||||
const watched = env.use(Watched);
|
||||
env.onActivate(() => {
|
||||
env.own(
|
||||
watched.state.subscribe(() => {
|
||||
deliveries += 1;
|
||||
}),
|
||||
);
|
||||
});
|
||||
},
|
||||
});
|
||||
const provider = defineFacet({
|
||||
id: "provider",
|
||||
setup(env) {
|
||||
state = env.replicatedState({ value: 0 });
|
||||
env.provide(Watched, { state });
|
||||
},
|
||||
});
|
||||
const host = await createFacetHost({ facets: [consumer, provider] });
|
||||
expect(deliveries).toBe(1);
|
||||
state!.state.value = 1;
|
||||
state!.publish(BACKGROUND_CONTEXT);
|
||||
expect(deliveries).toBe(2);
|
||||
|
||||
await host.dispose();
|
||||
state!.state.value = 2;
|
||||
state!.publish(BACKGROUND_CONTEXT);
|
||||
expect(deliveries).toBe(2);
|
||||
});
|
||||
|
||||
test("provides arbitrary host services through the facet graph", async () => {
|
||||
const values: HostValues = { name: "session", use: "host value" };
|
||||
const consumer = defineFacet({
|
||||
id: "host-service-consumer",
|
||||
setup(env) {
|
||||
const hostValues = env.use(HostValues);
|
||||
expect(() => hostValues.use).toThrow(
|
||||
"Facet host-service-consumer service handles cannot be used while setting_up",
|
||||
);
|
||||
env.onActivate(() => {
|
||||
expect(hostValues).not.toBe(values);
|
||||
expect(hostValues.name).toBe("session");
|
||||
expect(hostValues.use).toBe("host value");
|
||||
});
|
||||
},
|
||||
});
|
||||
const provider = defineFacet({
|
||||
id: "host-service-provider",
|
||||
setup(env) {
|
||||
env.provide(HostValues, values);
|
||||
},
|
||||
});
|
||||
const host = await createFacetHost({ facets: [consumer, provider] });
|
||||
expect(() => host.services.use(HostValues)).toThrow("Service test.experimental.host-values is process-local");
|
||||
await host.dispose();
|
||||
});
|
||||
|
||||
test("combines connected services and facet-provided services in one host", async () => {
|
||||
const leftProvider = new RemoteServiceProvider([LeftValue]);
|
||||
leftProvider.provide(LeftValue, {
|
||||
async read() {
|
||||
return "left";
|
||||
},
|
||||
});
|
||||
const rightProvider = new RemoteServiceProvider([RightValue]);
|
||||
rightProvider.provide(RightValue, {
|
||||
async read() {
|
||||
return "right";
|
||||
},
|
||||
});
|
||||
const leftNamespace = createRemoteServiceBinding({
|
||||
services: [LeftValue],
|
||||
transport: createLoopbackServiceTransport(leftProvider),
|
||||
bound: false,
|
||||
});
|
||||
const rightNamespace = createRemoteServiceBinding({
|
||||
services: [RightValue],
|
||||
transport: createLoopbackServiceTransport(rightProvider),
|
||||
bound: false,
|
||||
});
|
||||
const serviceSources = [
|
||||
{ namespace: leftNamespace, provider: leftProvider },
|
||||
{ namespace: rightNamespace, provider: rightProvider },
|
||||
].map(({ namespace, provider }) => {
|
||||
const ready = namespace.ready.bind(namespace);
|
||||
return Object.assign(namespace, {
|
||||
acceptsUnavailableServices: false,
|
||||
async catalogue() {
|
||||
return provider.catalogue;
|
||||
},
|
||||
open() {
|
||||
return namespace;
|
||||
},
|
||||
async ready(context: Context) {
|
||||
await namespace.rebind(true, context);
|
||||
await ready(context);
|
||||
},
|
||||
});
|
||||
});
|
||||
const facet = defineFacet({
|
||||
id: "combined",
|
||||
setup(env) {
|
||||
const left = env.use(LeftValue);
|
||||
const right = env.use(RightValue);
|
||||
env.provide(CombinedValue, {
|
||||
async read(context) {
|
||||
return `${await left.read(context)} ${await right.read(context)}`;
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const host = await createFacetHost({ facets: [facet], serviceSources });
|
||||
await expect(host.services.use(CombinedValue).read(BACKGROUND_CONTEXT)).resolves.toBe("left right");
|
||||
|
||||
await host.dispose();
|
||||
await Promise.all([leftNamespace.dispose(BACKGROUND_CONTEXT), rightNamespace.dispose(BACKGROUND_CONTEXT)]);
|
||||
leftProvider.dispose();
|
||||
rightProvider.dispose();
|
||||
});
|
||||
|
||||
test("rejects a service offered by multiple sources", async () => {
|
||||
const duplicate = {
|
||||
acceptsUnavailableServices: false,
|
||||
async catalogue() {
|
||||
return [{ serviceId: LeftValue.id, mode: "singleton" as const }];
|
||||
},
|
||||
open() {
|
||||
throw new Error("Ambiguous sources must not open");
|
||||
},
|
||||
} satisfies RemoteServiceSource;
|
||||
const consumer = defineFacet({
|
||||
id: "duplicate-consumer",
|
||||
setup(env) {
|
||||
env.use(LeftValue);
|
||||
},
|
||||
});
|
||||
await expect(createFacetHost({ facets: [consumer], serviceSources: [duplicate, duplicate] })).rejects.toThrow(
|
||||
`Facet host service ${LeftValue.id} is offered by more than one source`,
|
||||
);
|
||||
});
|
||||
|
||||
test("reopens source bindings from changed catalogues for a replacement generation", async () => {
|
||||
const leftProvider = new RemoteServiceProvider([LeftValue]);
|
||||
leftProvider.provide(LeftValue, {
|
||||
async read() {
|
||||
return "left";
|
||||
},
|
||||
});
|
||||
const rightProvider = new RemoteServiceProvider([RightValue]);
|
||||
rightProvider.provide(RightValue, {
|
||||
async read() {
|
||||
return "right";
|
||||
},
|
||||
});
|
||||
let currentProvider = leftProvider;
|
||||
let opened = 0;
|
||||
let disposed = 0;
|
||||
const source = {
|
||||
acceptsUnavailableServices: false,
|
||||
async catalogue() {
|
||||
return currentProvider.catalogue;
|
||||
},
|
||||
open(options) {
|
||||
opened += 1;
|
||||
const namespace = createRemoteServiceBinding({
|
||||
services: options.services,
|
||||
transport: createLoopbackServiceTransport(currentProvider),
|
||||
bound: false,
|
||||
assertAccess: options.assertAccess,
|
||||
onError: options.onError,
|
||||
});
|
||||
const ready = namespace.ready.bind(namespace);
|
||||
const dispose = namespace.dispose.bind(namespace);
|
||||
return Object.assign(namespace, {
|
||||
async ready(context: Context) {
|
||||
await namespace.rebind(true, context);
|
||||
await ready(context);
|
||||
},
|
||||
async dispose(context: Context) {
|
||||
disposed += 1;
|
||||
await dispose(context);
|
||||
},
|
||||
});
|
||||
},
|
||||
} satisfies RemoteServiceSource;
|
||||
const values: string[] = [];
|
||||
const leftConsumer = defineFacet({
|
||||
id: "left-consumer",
|
||||
setup(env) {
|
||||
const left = env.use(LeftValue);
|
||||
env.onActivate(async () => {
|
||||
values.push(await left.read(BACKGROUND_CONTEXT));
|
||||
});
|
||||
},
|
||||
});
|
||||
const first = await createFacetHost({ facets: [leftConsumer], serviceSources: [source] });
|
||||
await first.dispose();
|
||||
|
||||
currentProvider = rightProvider;
|
||||
const rightConsumer = defineFacet({
|
||||
id: "right-consumer",
|
||||
setup(env) {
|
||||
const right = env.use(RightValue);
|
||||
env.onActivate(async () => {
|
||||
values.push(await right.read(BACKGROUND_CONTEXT));
|
||||
});
|
||||
},
|
||||
});
|
||||
const second = await createFacetHost({ facets: [rightConsumer], serviceSources: [source] });
|
||||
await second.dispose();
|
||||
|
||||
expect(values).toEqual(["left", "right"]);
|
||||
expect(opened).toBe(2);
|
||||
expect(disposed).toBe(2);
|
||||
leftProvider.dispose();
|
||||
rightProvider.dispose();
|
||||
});
|
||||
|
||||
test("rejects missing dependencies, cycles, and asynchronous setup", async () => {
|
||||
let activated = false;
|
||||
const missing = defineFacet({
|
||||
id: "missing",
|
||||
setup(env) {
|
||||
env.use(Source);
|
||||
env.onActivate(() => {
|
||||
activated = true;
|
||||
});
|
||||
},
|
||||
});
|
||||
await expect(createFacetHost({ facets: [missing] })).rejects.toThrow(
|
||||
"Facet missing requires local/test.experimental.source/singleton, but no facet provides it",
|
||||
);
|
||||
expect(activated).toBe(false);
|
||||
|
||||
const first = defineFacet({
|
||||
id: "first",
|
||||
setup(env) {
|
||||
env.use(Projection);
|
||||
env.provide(Source, {
|
||||
async read() {
|
||||
return "first";
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
const second = defineFacet({
|
||||
id: "second",
|
||||
setup(env) {
|
||||
env.use(Source);
|
||||
env.provide(Projection, {
|
||||
async read() {
|
||||
return "second";
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
await expect(createFacetHost({ facets: [first, second] })).rejects.toThrow(
|
||||
"Facet dependency cycle: first, second",
|
||||
);
|
||||
|
||||
const asynchronous = defineFacet({
|
||||
id: "asynchronous",
|
||||
async setup() {
|
||||
await Promise.resolve();
|
||||
},
|
||||
});
|
||||
await expect(createFacetHost({ facets: [asynchronous] })).rejects.toThrow(
|
||||
"Facet asynchronous setup must be synchronous",
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user