Skip to content

Commit 2a5322a

Browse files
committed
edge cases and various todo items
1 parent 61ac59f commit 2a5322a

23 files changed

+494
-392
lines changed

packages/server/README.md

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@ Key points:
1515

1616
- `useServiceGraph(services: ServicesMap, options?: { sequential?: boolean }): ServiceRunner<ServicesMap>` — returns a _runner function_ which you call to start the graph: `const run = useServiceGraph(services, options); yield* run(subset?: string[] | string);`. By default services in the same topological layer run concurrently; pass `options.sequential = true` to run services in each layer serially.
1717
- `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 by default, or serially when `options.sequential` is true.
18+
- `dependsOn` — an optional object `{ startup: string[]; restart?: string[] }` listing service names this service depends on. Use `startup` to list services that must start before this service; use `restart` to list services that should trigger a restart of this service when they are restarted (for example, due to a watched file change). Services without dependencies in the same layer are started concurrently by default, or serially when `options.sequential` is true.
19+
20+
- `subset` (runner argument) — when calling the runner returned by `useServiceGraph` you may pass a subset (e.g. `yield* run(['serviceA'])` or `yield* run('serviceA')`) to start only a subset of services; any startup dependencies required by that subset are automatically included.
21+
22+
- Watching & restart propagation — pass `{ watch: true }` to `useServiceGraph` and define `watch` paths in each `ServiceDefinition` to enable file watching. The watcher will precompute transitive dependents (based on `dependsOn.restart`) and automatically emit restart updates for dependents when a watched path changes, so restarts propagate efficiently and deterministically.
1923
- Lifecycle hooks: `beforeStart`, `afterStart`, `beforeStop`, `afterStop` — each is an `Operation<void>` that runs at the appropriate time.
2024

2125
Example:
@@ -42,7 +46,7 @@ main(function* () {
4246
"B",
4347
"node --import tsx ./test/services/service-b.ts"
4448
),
45-
deps: ["A"],
49+
dependsOn: { startup: ["A"] },
4650
},
4751
});
4852
});
@@ -61,29 +65,28 @@ Each `ServiceDefinition` supports lifecycle hook operations. These hooks run in
6165
```ts
6266
const services = {
6367
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
68+
operation: (function* () {
69+
// start the service via useService or useChildSimulation
70+
yield* useService("A", "node --import tsx ./test/services/service-a.ts");
71+
// signal that the service is ready
72+
console.log("A has started");
73+
try {
74+
// keep running until cancelled
75+
yield* suspend();
76+
} finally {
77+
// cleanup runs automatically on scope cancellation
7678
console.log("A is stopping");
77-
})(),
79+
}
80+
})(),
7881
},
7982
};
8083
```
8184

8285
Notes:
8386

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+
- Use a try/finally in your `operation` to run cleanup logic when the service is stopped
88+
- This approach leverages Effection scopes and ensures cleanup runs in reverse dependency order when the graph is shut down
89+
- Use `useService` or `useChildSimulation` inside your operation as needed to start underlying processes
8790

8891
Try it
8992

@@ -132,5 +135,5 @@ node --import tsx ./example/basic-graph.ts
132135

133136
Previously services could expose their return value via a public `exportsOperation` that consumers could await. That mechanism has been removed in this branch as we move to a child-process-focused runner model. Provider-returned values are still delivered to dependent service factories internally, but no longer exposed as an operation on the public `services` map.
134137

135-
For convenience tests may use the `servicePorts` map exposed by the running graph to discover HTTP ports that services registered when they start.
138+
For convenience tests may use the `servicePorts` map exposed by the running graph to discover HTTP ports that services registered when they start. The `servicePorts` map is available on the object returned by the runner and contains service name => port when a service's `operation` returns an object with a `{ port: number }` property.
136139
````

packages/server/bin/run-simulation-child.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ main(function* () {
1010
const args = process.argv.slice(2);
1111
console.dir({ args });
1212
if (args.length < 1) {
13-
throw new Error("usage: run-simulation-child.js <modulePath> [jsonArgs]");
13+
throw new Error("usage: run-simulation-child.js <modulePath>");
1414
}
1515

1616
const modulePath = args[0];

packages/server/example/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ This folder contains runnable examples demonstrating `useServiceGraph` and `useS
44

55
There are two sets of examples:
66

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.
7+
- **use-service** (top-level files like `basic-graph.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.
88

99
- **operation** (under `operation/`) — these demonstrate `useChildSimulation()` which runs each service in a child process using a simulation factory. They show how to isolate simulations and start them as independent processes.
1010

@@ -25,3 +25,5 @@ node --import tsx ./example/operation/basic-graph.ts
2525
```
2626

2727
These examples make use of the small service implementations in `./example/services`.
28+
29+
Notes: the examples now use `dependsOn` with a `{ startup, restart? }` shape. To experiment with restart propagation, add a `watch` entry to a service and include dependents via `dependsOn.restart` — when a watched file changes the watcher will restart the affected service and its transitive dependents.

packages/server/example/concurrency-layers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ const servicesMap = {
1414
watch: ["./example/services/basic-sim-2.ts"],
1515
},
1616
dependent: {
17-
// deps: ["fast", "slow"] as const,
17+
dependsOn: { startup: ["fast", "slow"] as const },
1818
operation: resource<void>(function* (provide) {
1919
try {
2020
console.log("all deps started; running dependent service");

packages/server/example/lifecycle-hooks.ts

Lines changed: 0 additions & 56 deletions
This file was deleted.

packages/server/example/process-graph.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ const servicesMap = {
2828
),
2929
},
3030
B: {
31-
deps: ["A"] as const,
31+
dependsOn: { startup: ["A"] as const },
3232
operation: useService(
3333
"B",
3434
"node --import tsx ./example/services/basic-sim.ts",
Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,3 @@
1-
import {
2-
createFoundationSimulationServer,
3-
type FoundationSimulator,
4-
} from "@simulacrum/foundation-simulator";
1+
import { simulation as genSimulation } from "./gen-sim-factory.ts";
52

6-
export function simulation(
7-
port: number = 3301,
8-
startDelay: number = 10
9-
): FoundationSimulator<any> {
10-
const factory = createFoundationSimulationServer({
11-
port,
12-
extendRouter(router) {
13-
router.get("/status", (_req, res) => {
14-
res.status(200).send("ok");
15-
});
16-
},
17-
})();
18-
return factory;
19-
}
3+
export const simulation = genSimulation(3301, 10);
Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,3 @@
1-
import {
2-
createFoundationSimulationServer,
3-
type FoundationSimulator,
4-
} from "@simulacrum/foundation-simulator";
1+
import { simulation as genSimulation } from "./gen-sim-factory.ts";
52

6-
export function simulation(
7-
port: number = 3302,
8-
startDelay: number = 10
9-
): FoundationSimulator<any> {
10-
const factory = createFoundationSimulationServer({
11-
port,
12-
extendRouter(router) {
13-
router.get("/status", (_req, res) => {
14-
res.status(200).send("ok");
15-
});
16-
},
17-
})();
18-
return factory;
19-
}
3+
export const simulation = genSimulation(3302, 15);
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import {
2+
createFoundationSimulationServer,
3+
type FoundationSimulator,
4+
} from "@simulacrum/foundation-simulator";
5+
6+
/*
7+
Helper to create a basic foundation simulation server with a configurable
8+
start delay to simulate slow startups. You would export your simulator
9+
more directly instead of wrapping it like this in a real project.
10+
*/
11+
export function simulation(
12+
port: number = 3301,
13+
startDelay: number = 10
14+
): FoundationSimulator<any> {
15+
const factory = createFoundationSimulationServer({
16+
port,
17+
extendRouter(router) {
18+
router.get("/status", (_req, res) => {
19+
res.status(200).send("ok");
20+
});
21+
},
22+
})();
23+
24+
return {
25+
async listen(
26+
...args: Parameters<FoundationSimulator<any>["listen"]>
27+
): Promise<any> {
28+
if (startDelay > 0) {
29+
await new Promise((resolve) => setTimeout(resolve, startDelay));
30+
}
31+
// delegate to underlying factory listen
32+
return factory.listen(...args);
33+
},
34+
};
35+
}

packages/server/example/simulation-graph.ts

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,11 @@ import { simulationCLI } from "../src/cli.ts";
66

77
const servicesMap = {
88
A: {
9-
operation: useChildSimulation(
10-
"A",
11-
"./example/services/basic-sim.ts",
12-
[0, 10]
13-
),
9+
operation: useChildSimulation("A", "./example/services/basic-sim-1.ts"),
1410
},
1511
B: {
16-
deps: ["A"] as const,
17-
operation: useChildSimulation(
18-
"B",
19-
"./example/services/basic-sim.ts",
20-
[0, 20]
21-
),
12+
dependsOn: { startup: ["A"] as const },
13+
operation: useChildSimulation("B", "./example/services/basic-sim-2.ts"),
2214
},
2315
};
2416

0 commit comments

Comments
 (0)