Skip to content

Commit 785a24e

Browse files
committed
tests and service filter
1 parent 418b9ca commit 785a24e

26 files changed

+1088
-13
lines changed

packages/server/README.md

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,152 @@ https://github.com/thefrontside/simulacrum
66

77
> [!WARNING]
88
> The server is undergoing a refactor, and this may not be required for your use case. The refactor includes allow for more simply running single simulators so this package will be primarily useful as a control plane for cases where there are many simulators under test and in use. For the previous iterations, see the `v0` branch which contain the previous functionality.
9+
10+
## Operation-based service orchestration
11+
12+
`@simulacrum/server` provides operations to start and manage services with lifecycle hooks. The recommended pattern is to create `Operation<void>` instances for each service (typically via `useService`) and pass them to `useServiceGraph` which starts the services respecting a dependency DAG and provides lifecycle hooks for startup and shutdown.
13+
14+
Key points:
15+
16+
- `useServiceGraph(services: ServicesMap): Operation<void>` — starts a DAG of services. Each service in the map is declared as a `ServiceDefinition`.
17+
- `ServiceDefinition.operation` (required) — an `Operation<void>` which indicates the service has started. This operation may be long-lived (e.g. `useService`) or may return once the service is ready while a background child keeps the service running. See the example below.
18+
- `deps` — an optional list of service names this service depends on; services without dependencies in the same layer are started concurrently.
19+
- Lifecycle hooks: `beforeStart`, `afterStart`, `beforeStop`, `afterStop` — each is an `Operation<void>` that runs at the appropriate time.
20+
21+
Example:
22+
23+
```ts
24+
import { main, spawn, sleep } from "effection";
25+
import { useServiceGraph, useService } from "@simulacrum/server";
26+
27+
main(function* () {
28+
yield* spawn(function* () {
29+
// In many situations, pass `useService` directly: it returns once the
30+
// process is spawned and, if a wellnessCheck is provided, once the
31+
// wellnessCheck passes. The service is automatically shut down by
32+
// effection when the operation goes out of scope.
33+
yield* useServiceGraph({
34+
A: {
35+
operation: useService(
36+
"A",
37+
"node --import tsx ./test/services/service-a.ts"
38+
),
39+
},
40+
B: {
41+
operation: useService(
42+
"B",
43+
"node --import tsx ./test/services/service-b.ts"
44+
),
45+
deps: ["A"],
46+
},
47+
});
48+
});
49+
});
50+
```
51+
52+
Notes:
53+
54+
- `useServiceGraph` returns an `Operation<void>` that holds while services run and only cancels on parent scope termination.
55+
- If you want to start services sequentially or add more advanced concurrency control, compose operations yourself and use `spawn` to control how operations run.
56+
57+
### Lifecycle hooks
58+
59+
Each `ServiceDefinition` supports lifecycle hook operations. These hooks run in the parent scope and are useful for performing orchestration tasks, logging, or writing sentinel files for integration tests. Hooks are `Operation<void>` as well.
60+
61+
```ts
62+
const services = {
63+
A: {
64+
operation: useService(
65+
"A",
66+
"node --import tsx ./test/services/service-a.ts"
67+
),
68+
afterStart: () =>
69+
(function* () {
70+
// runs after the operation returns
71+
console.log("A has started");
72+
})(),
73+
beforeStop: () =>
74+
(function* () {
75+
// runs during shutdown in reverse order
76+
console.log("A is stopping");
77+
})(),
78+
},
79+
};
80+
```
81+
82+
Notes:
83+
84+
- `afterStart` runs after `operation` returns (service is ready)
85+
- `beforeStop` runs during cleanup in reverse-order of startup
86+
- Hooks are optional and can be used together with a passed `operation` or a custom operation
87+
88+
Try it
89+
90+
```bash
91+
# Run the server package tests
92+
cd packages/server
93+
npm test
94+
```
95+
96+
## Examples
97+
98+
The `example` folder contains runnable examples demonstrating `useServiceGraph` and `useService`.
99+
100+
Run the basic dependency example:
101+
102+
```bash
103+
cd packages/server
104+
npm run example:basic
105+
```
106+
107+
Run lifecycle hooks example:
108+
109+
```bash
110+
cd packages/server
111+
npm run example:lifecycle
112+
```
113+
114+
Run concurrency layers example:
115+
116+
````bash
117+
cd packages/server
118+
npm run example:concurrency
119+
120+
Run examples directly (each example has its own npm script). You can also run the TypeScript module with `tsx`.
121+
122+
```bash
123+
cd packages/server
124+
npm run example:basic
125+
npm run example:lifecycle
126+
npm run example:concurrency
127+
# or run a module directly:
128+
node --import tsx ./example/basic-graph.ts
129+
```
130+
131+
### Typed exports between services 💡
132+
133+
Services may return a value from their `operation`. That value is exposed to dependent services via an `exportsOperation` on the provider.
134+
135+
```ts
136+
const services = {
137+
provider: {
138+
// provider operation returns { url: string }
139+
operation: useService<{ url: string }>(
140+
"provider",
141+
"node --import tsx ./example/services/provider.ts"
142+
),
143+
},
144+
consumer: {
145+
deps: ["provider"],
146+
operation() {
147+
return (function* () {
148+
// access provider exports via the `services` variable
149+
const providerExports = yield* services.provider.exportsOperation;
150+
console.log("provider url:", providerExports.url);
151+
})();
152+
},
153+
},
154+
};
155+
156+
// pass `services` to `useServiceGraph` or `simulationCLI`
157+
````

packages/server/example/README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Server package examples
2+
3+
This folder contains runnable examples demonstrating `useServiceGraph` and `useService`.
4+
5+
There are two sets of examples:
6+
7+
- **use-service** (top-level files like `basic-graph.ts`, `lifecycle-hooks.ts`, `concurrency-layers.ts`) — these spawn separate processes using `useService` (e.g. `node --import tsx ./example/services/*.ts`). Use these to exercise the process-based behavior.
8+
9+
- **operation** (under `operation/`) — these use the `httpServer()` operation directly and run entirely in-process. They are faster and more deterministic for tests and quick iteration.
10+
11+
Quick commands:
12+
13+
Run the basic dependency example (use-service):
14+
15+
```bash
16+
cd packages/server
17+
node --import tsx ./example/basic-graph.ts
18+
```
19+
20+
Run the basic dependency example (operation):
21+
22+
```bash
23+
cd packages/server
24+
node --import tsx ./example/operation/basic-graph.ts
25+
```
26+
27+
These examples make use of the small service implementations in `./example/services`.
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
#!/usr/bin/env node
2+
import { sleep, each, type Stream } from "effection";
3+
import { useService } from "../src/service.ts";
4+
import { useServiceGraph } from "../src/services.ts";
5+
import { simulationCLI } from "../src/cli.ts";
6+
7+
const services = {
8+
A: {
9+
operation: useService("A", "node --import tsx ./example/services/a.ts", {
10+
wellnessCheck: {
11+
frequency: 10,
12+
*operation(stdio: Stream<string, void>) {
13+
for (let line of yield* each<string>(stdio)) {
14+
if (line.includes("started")) {
15+
console.log("A ready (wellnessCheck)");
16+
return { ok: true } as any;
17+
}
18+
yield* each.next();
19+
}
20+
},
21+
},
22+
}),
23+
},
24+
B: {
25+
operation: useService("B", "node --import tsx ./example/services/b.ts", {
26+
wellnessCheck: {
27+
frequency: 10,
28+
*operation(stdio: Stream<string, void>) {
29+
for (let line of yield* each<string>(stdio)) {
30+
if (line.includes("started")) {
31+
console.log("B ready (wellnessCheck)");
32+
return { ok: true } as any;
33+
}
34+
yield* each.next();
35+
}
36+
},
37+
},
38+
}),
39+
deps: ["A"],
40+
},
41+
};
42+
43+
export function example(opts: { duration?: number } = {}) {
44+
return (function* () {
45+
yield* useServiceGraph(services as any);
46+
yield* sleep(opts.duration ?? 300);
47+
console.log(`Basic example complete`);
48+
})();
49+
}
50+
51+
import { fileURLToPath } from "node:url";
52+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
53+
// run via CLI when executed directly
54+
simulationCLI(services);
55+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
#!/usr/bin/env node
2+
import { sleep, each, type Stream } from "effection";
3+
import { useServiceGraph } from "../src/services.ts";
4+
import { useService } from "../src/service.ts";
5+
import { simulationCLI } from "../src/cli.ts";
6+
7+
const services = {
8+
fast: {
9+
operation: useService(
10+
"fast",
11+
"node --import tsx ./example/services/fast.ts",
12+
{
13+
wellnessCheck: {
14+
frequency: 10,
15+
*operation(stdio: Stream<string, void>) {
16+
for (let line of yield* each<string>(stdio)) {
17+
if (line.includes("started")) {
18+
console.log("fast ready");
19+
return { ok: true } as any;
20+
}
21+
yield* each.next();
22+
}
23+
},
24+
},
25+
}
26+
),
27+
},
28+
slow: {
29+
operation: useService(
30+
"slow",
31+
"node --import tsx ./example/services/slow.ts",
32+
{
33+
wellnessCheck: {
34+
frequency: 10,
35+
*operation(stdio: Stream<string, void>) {
36+
for (let line of yield* each<string>(stdio)) {
37+
if (line.includes("started")) {
38+
console.log("slow ready");
39+
return { ok: true } as any;
40+
}
41+
yield* each.next();
42+
}
43+
},
44+
},
45+
}
46+
),
47+
},
48+
dependent: {
49+
deps: ["fast", "slow"],
50+
operation: (function* () {
51+
console.log("dependent: all deps started; running dependent logic");
52+
yield* sleep(50);
53+
})(),
54+
},
55+
};
56+
57+
import { fileURLToPath } from "node:url";
58+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
59+
simulationCLI(services as any);
60+
}
61+
62+
export function example(opts: { duration?: number } = {}) {
63+
return (function* () {
64+
yield* useServiceGraph(services as any);
65+
yield* sleep(opts.duration ?? 300);
66+
console.log(`Concurrency example complete`);
67+
})();
68+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { run, spawn, sleep } from "effection";
2+
3+
run(function* () {
4+
yield* spawn(function* () {
5+
console.log("debug - spawn start");
6+
yield* sleep(100);
7+
console.log("debug - spawn done");
8+
});
9+
console.log("debug - after spawn (should only print after spawn completes)");
10+
});
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
#!/usr/bin/env node
2+
import { sleep, each, type Stream } from "effection";
3+
import { useService } from "../src/service.ts";
4+
import { useServiceGraph } from "../src/services.ts";
5+
import { simulationCLI } from "../src/cli.ts";
6+
7+
const services = {
8+
provider: {
9+
operation: useService(
10+
"provider",
11+
"node --import tsx ./example/services/fast.ts",
12+
{
13+
wellnessCheck: {
14+
frequency: 10,
15+
*operation(stdio: Stream<string, void>) {
16+
for (let line of yield* each<string>(stdio)) {
17+
if (line.includes("started")) {
18+
return { ok: true } as any;
19+
}
20+
yield* each.next();
21+
}
22+
},
23+
},
24+
}
25+
),
26+
afterStart() {
27+
return (function* () {
28+
console.log("provider: afterStart");
29+
})();
30+
},
31+
beforeStop() {
32+
return (function* () {
33+
console.log("provider: beforeStop");
34+
})();
35+
},
36+
},
37+
consumer: {
38+
deps: ["provider"],
39+
operation: useService(
40+
"consumer",
41+
"node --import tsx ./example/services/a.ts",
42+
{
43+
wellnessCheck: {
44+
frequency: 10,
45+
*operation(stdio: Stream<string, void>) {
46+
for (let line of yield* each<string>(stdio)) {
47+
if (line.includes("started")) {
48+
return { ok: true } as any;
49+
}
50+
yield* each.next();
51+
}
52+
},
53+
},
54+
}
55+
),
56+
afterStart() {
57+
return (function* () {
58+
console.log("consumer: afterStart");
59+
})();
60+
},
61+
beforeStop() {
62+
return (function* () {
63+
console.log("consumer: beforeStop");
64+
})();
65+
},
66+
},
67+
};
68+
69+
import { fileURLToPath } from "node:url";
70+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
71+
simulationCLI(services);
72+
}
73+
74+
export function example(opts: { duration?: number } = {}) {
75+
return (function* () {
76+
console.log(`Starting lifecycle hooks example`);
77+
yield* useServiceGraph(services as any);
78+
yield* sleep(opts.duration ?? 150);
79+
console.log(`Lifecycle example complete`);
80+
})();
81+
}

0 commit comments

Comments
 (0)