mirror of
https://github.com/trustgraph-ai/trustgraph.git
synced 2026-06-30 17:09:38 +02:00
Migrate config service to ref-backed Effect runtime
This commit is contained in:
parent
b4ee2b691f
commit
88db18fbda
4 changed files with 907 additions and 755 deletions
|
|
@ -12,23 +12,23 @@ Verified source roots:
|
|||
- Effect v4 subtree: `/home/elpresidank/YeeBois/projects/beep-effect2/.repos/effect-v4`
|
||||
- Installed Effect beta used by this workspace: `ts/node_modules/effect`
|
||||
|
||||
Current signal counts from `ts/packages` after the 2026-06-02 strict tsgo
|
||||
slice:
|
||||
Current signal counts from `ts/packages` after the 2026-06-02 config service
|
||||
runtime slice:
|
||||
|
||||
| Signal | Count |
|
||||
| --- | ---: |
|
||||
| `Effect.runPromise` | 208 |
|
||||
| `Map<` | 58 |
|
||||
| `WebSocket` | 45 |
|
||||
| `new Map` | 45 |
|
||||
| `Effect.runPromise` | 207 |
|
||||
| `Map<` | 65 |
|
||||
| `WebSocket` | 51 |
|
||||
| `new Map` | 47 |
|
||||
| `toPromiseRequestor` | 19 |
|
||||
| `makeAsyncProcessor` | 19 |
|
||||
| `receive(` | 18 |
|
||||
| `while (` | 13 |
|
||||
| `while (` | 12 |
|
||||
| `new Error` | 14 |
|
||||
| `new Promise` | 10 |
|
||||
| `JSON.parse` | 8 |
|
||||
| `localStorage` | 8 |
|
||||
| `localStorage` | 9 |
|
||||
| `JSON.stringify` | 6 |
|
||||
| `setTimeout` | 4 |
|
||||
| `process.env` | 3 |
|
||||
|
|
@ -78,7 +78,7 @@ Notes:
|
|||
|
||||
### 2026-06-02: Strict Base, CLI, MCP, And tsgo Slice
|
||||
|
||||
- Status: migrated, root-verified, ready to commit.
|
||||
- Status: migrated, root-verified, committed, and pushed.
|
||||
- Completed:
|
||||
- Base messaging, NATS backend, producer, consumer, subscriber,
|
||||
request/response, runtime factories, processor programs, flow specs, and
|
||||
|
|
@ -102,6 +102,37 @@ Notes:
|
|||
- `cd ts && bun run build`
|
||||
- `git diff --check`
|
||||
|
||||
### 2026-06-02: ConfigService Ref-Backed State Slice
|
||||
|
||||
- Status: migrated and root-verified.
|
||||
- Completed:
|
||||
- `ts/packages/flow/src/config/service.ts` now models runtime state as a
|
||||
`SynchronizedRef<ConfigServiceState>` instead of adding mutable
|
||||
`store`, `version`, consumer, and producer fields onto the processor
|
||||
object.
|
||||
- Config operations have Effect-returning handlers with Promise facades only
|
||||
on the exported compatibility methods.
|
||||
- Request narrowing now uses `effect/Predicate` rather than request-record
|
||||
type assertions.
|
||||
- Persistence remains schema-backed and now reads/writes snapshots from the
|
||||
ref-backed state.
|
||||
- The consume loop now uses `Effect.whileLoop`; the remaining
|
||||
`consumer.receive(2000)` call is a pubsub boundary for this service.
|
||||
- Service startup now exposes `runMain()` through `NodeRuntime.runMain`.
|
||||
The legacy `run()` Promise facade uses `ManagedRuntime`, and
|
||||
`ts/scripts/run-config.ts` delegates directly to `runMain()` instead of
|
||||
owning its own catch/process-exit wrapper.
|
||||
- Config-service tests cover tagged invalid mutation errors, workspace
|
||||
persistence, legacy load, concurrent ref-backed mutations, and push
|
||||
publishing from the stored producer handle.
|
||||
- Verification:
|
||||
- `bun run --cwd ts/packages/flow build`
|
||||
- `bun run --cwd ts/packages/flow test`
|
||||
- `cd ts && bun run check`
|
||||
- `cd ts && bun run build`
|
||||
- `cd ts && bun run test`
|
||||
- `git diff --check`
|
||||
|
||||
## Subagent Findings To Preserve
|
||||
|
||||
- MCP/workbench:
|
||||
|
|
@ -111,8 +142,9 @@ Notes:
|
|||
the client API is less Promise-first.
|
||||
- MCP env is now Config-backed; continue that policy for future MCP settings.
|
||||
- Flow stateful services:
|
||||
- Config, librarian, cores, and flow-manager still have mutable poller
|
||||
service objects. These remain good candidates for `Context` services,
|
||||
- Config service ref-backed state is complete. Librarian, cores, and
|
||||
flow-manager still have mutable poller service objects. These remain good
|
||||
candidates for `Context` services,
|
||||
scoped layers, `Ref`/`SynchronizedRef`, `Schedule`, and managed
|
||||
persistence.
|
||||
- Persistence IO should move toward `FileSystem` or `KeyValueStore` where
|
||||
|
|
@ -140,10 +172,9 @@ Notes:
|
|||
|
||||
## Ranked Findings
|
||||
|
||||
### P0: Convert Stateful Flow Services To Scoped Effect Services
|
||||
### P0: Continue Stateful Flow Services To Scoped Effect Services
|
||||
|
||||
- TrustGraph evidence:
|
||||
- `ts/packages/flow/src/config/service.ts`
|
||||
- `ts/packages/flow/src/librarian/service.ts`
|
||||
- `ts/packages/flow/src/cores/service.ts`
|
||||
- `ts/packages/flow/src/flow-manager/service.ts`
|
||||
|
|
@ -152,8 +183,12 @@ Notes:
|
|||
`Effect.addFinalizer`, `Config`, `Schema`, `FileSystem`,
|
||||
`KeyValueStore`.
|
||||
- Rewrite shape:
|
||||
- Model one service at a time as a `Context` service plus scoped layer.
|
||||
- Model one remaining service at a time as a `Context` service plus scoped
|
||||
layer or ref-backed state slice.
|
||||
- Store mutable service state in `Ref` or `SynchronizedRef`.
|
||||
- Run service main programs with platform runtime entrypoints such as
|
||||
`NodeRuntime.runMain`; keep `ManagedRuntime` only for compatibility
|
||||
Promise facades.
|
||||
- Replace polling sleep loops with schedules where behavior allows.
|
||||
- Decode persisted payloads and config with schemas at boundaries.
|
||||
- Tests:
|
||||
|
|
@ -267,8 +302,8 @@ Notes:
|
|||
|
||||
## Recommended PR Order
|
||||
|
||||
1. Config service scoped state migration.
|
||||
2. RAG and agent requestor bridge removal.
|
||||
1. RAG and agent requestor bridge removal.
|
||||
2. Librarian, cores, or flow-manager scoped state migration.
|
||||
3. Client RPC managed runtime/scoped layer cleanup.
|
||||
4. Base processor registry and constructor shim redesign.
|
||||
5. Gateway RPC callback and client streaming completion cleanup.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { Effect, SynchronizedRef } from "effect";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { topics } from "@trustgraph/base";
|
||||
import {
|
||||
ConfigServiceError,
|
||||
makeConfigService,
|
||||
|
|
@ -16,9 +18,15 @@ import type {
|
|||
} from "@trustgraph/base";
|
||||
|
||||
class NoopPubSub implements PubSubBackend {
|
||||
async createProducer<T>(_options: CreateProducerOptions): Promise<BackendProducer<T>> {
|
||||
readonly sentByTopic = new Map<string, Array<unknown>>();
|
||||
|
||||
async createProducer<T>(options: CreateProducerOptions<T>): Promise<BackendProducer<T>> {
|
||||
return {
|
||||
send: async () => undefined,
|
||||
send: async (message) => {
|
||||
const sent = this.sentByTopic.get(options.topic) ?? [];
|
||||
sent.push(message);
|
||||
this.sentByTopic.set(options.topic, sent);
|
||||
},
|
||||
flush: async () => undefined,
|
||||
close: async () => undefined,
|
||||
};
|
||||
|
|
@ -48,10 +56,12 @@ const makeService = (persistPath?: string) =>
|
|||
describe("ConfigService operations", () => {
|
||||
it("uses tagged errors for invalid mutations", async () => {
|
||||
const service = makeService();
|
||||
const putRequest: ConfigRequest = { operation: "put" };
|
||||
const deleteRequest: ConfigRequest = { operation: "delete" };
|
||||
|
||||
const putError = await service.handlePut({ operation: "put" } as ConfigRequest)
|
||||
const putError = await service.handlePut(putRequest)
|
||||
.catch((caught: unknown) => caught);
|
||||
const deleteError = await service.handleDelete({ operation: "delete" } as ConfigRequest)
|
||||
const deleteError = await service.handleDelete(deleteRequest)
|
||||
.catch((caught: unknown) => caught);
|
||||
|
||||
expect(putError).toBeInstanceOf(ConfigServiceError);
|
||||
|
|
@ -64,13 +74,14 @@ describe("ConfigService operations", () => {
|
|||
const dir = await mkdtemp(join(tmpdir(), "trustgraph-config-service-"));
|
||||
const persistPath = join(dir, "config.json");
|
||||
const service = makeService(persistPath);
|
||||
|
||||
await service.handlePut({
|
||||
const putRequest: ConfigRequest = {
|
||||
operation: "put",
|
||||
values: [
|
||||
{ workspace: "alpha", type: "prompt", key: "system", value: "hello" },
|
||||
],
|
||||
} as ConfigRequest);
|
||||
};
|
||||
|
||||
await service.handlePut(putRequest);
|
||||
|
||||
const persisted = await Bun.file(persistPath).json();
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
|
|
@ -97,10 +108,11 @@ describe("ConfigService operations", () => {
|
|||
const service = makeService(persistPath);
|
||||
|
||||
await service.loadFromDisk();
|
||||
const response = service.handleGet({
|
||||
const getRequest: ConfigRequest = {
|
||||
operation: "get",
|
||||
keys: ["prompt", "system"],
|
||||
} as ConfigRequest);
|
||||
};
|
||||
const response = service.handleGet(getRequest);
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
|
||||
expect(response).toEqual({
|
||||
|
|
@ -110,4 +122,58 @@ describe("ConfigService operations", () => {
|
|||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("serializes concurrent mutations through ref-backed state", async () => {
|
||||
const service = makeService();
|
||||
const requests: Array<ConfigRequest> = [
|
||||
{ operation: "put", values: [{ type: "prompt", key: "a", value: "one" }] },
|
||||
{ operation: "put", values: [{ type: "prompt", key: "b", value: "two" }] },
|
||||
{ operation: "put", values: [{ workspace: "beta", type: "prompt", key: "c", value: "three" }] },
|
||||
];
|
||||
|
||||
await Promise.all(requests.map((request) => service.handlePut(request)));
|
||||
|
||||
expect(service.handleGet({ operation: "get", keys: ["prompt"] })).toEqual({
|
||||
version: 3,
|
||||
values: {
|
||||
a: "one",
|
||||
b: "two",
|
||||
},
|
||||
});
|
||||
expect(service.handleGetValuesAllWorkspaces({ operation: "getvalues-all-ws", keys: ["prompt"] }).values).toEqual([
|
||||
{ workspace: "default", type: "prompt", key: "a", value: "one" },
|
||||
{ workspace: "default", type: "prompt", key: "b", value: "two" },
|
||||
{ workspace: "beta", type: "prompt", key: "c", value: "three" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("pushes config from the stored producer handle", async () => {
|
||||
const backend = new NoopPubSub();
|
||||
const service = makeConfigService({
|
||||
id: "config-test",
|
||||
manageProcessSignals: false,
|
||||
pubsub: backend,
|
||||
});
|
||||
const pushProducer = await backend.createProducer<{
|
||||
readonly version: number;
|
||||
readonly config: Record<string, unknown>;
|
||||
}>({ topic: topics.configPush });
|
||||
|
||||
await Effect.runPromise(
|
||||
SynchronizedRef.update(service.state, (state) => ({
|
||||
...state,
|
||||
pushProducer,
|
||||
})),
|
||||
);
|
||||
await service.pushConfig();
|
||||
await service.handlePut({
|
||||
operation: "put",
|
||||
values: [{ type: "prompt", key: "system", value: "hello" }],
|
||||
});
|
||||
|
||||
expect(backend.sentByTopic.get(topics.configPush)).toEqual([
|
||||
{ version: 0, config: {} },
|
||||
{ version: 1, config: { prompt: { system: "hello" } } },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -7,9 +7,6 @@
|
|||
* NATS_URL (default: nats://localhost:4222)
|
||||
* CONFIG_PERSIST_PATH (optional, e.g., ./data/config.json)
|
||||
*/
|
||||
import { run } from "../packages/flow/src/config/service.js";
|
||||
import {runMain} from "../packages/flow/src/config/service.js";
|
||||
|
||||
run().catch((err) => {
|
||||
console.error("Config service failed:", err);
|
||||
process.exit(1);
|
||||
});
|
||||
runMain();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue