Gateway: abstract task registry storage#56927
Conversation
dd9e26b to
8db9b86
Compare
|
Merged via squash.
Thanks @mbelinky! |
Greptile SummaryThis PR cleanly extracts the task registry's persistence layer behind a Key points from the review:
Confidence Score: 5/5Safe to merge — no user-visible behavior change and no current correctness issues; all findings are P2 improvement suggestions for future-proofing. All remaining findings are P2: the live map reference concern is only relevant for future async store implementations, and the empty-snapshot hook silencing is a design choice with no current downstream consumers. The abstraction is correctly implemented and well-tested. No files require special attention; src/tasks/task-registry.ts has two P2 notes worth considering before the next storage backend is introduced.
|
| Filename | Overview |
|---|---|
| src/tasks/task-registry.store.ts | New abstraction layer introducing TaskRegistryStore and TaskRegistryHooks interfaces, with module-level singletons and configure/reset helpers. Well-structured; the "hooks" in params check correctly handles null to distinguish clearing hooks from omitting them. |
| src/tasks/task-registry.store.json.ts | JSON persistence logic extracted verbatim from the old store file. Validation, versioning, and test-environment path isolation all carried over correctly. |
| src/tasks/task-registry.ts | Registry updated to delegate persistence through the new store interface and emit incremental hook events on restore, upsert, and delete. Lazy closure pattern for hook events is correctly guarded, keeping the hot path allocation-free when no hooks are registered. |
| src/tasks/task-registry.store.test.ts | New tests cover store injection and all three hook event kinds (restored, upserted, deleted). afterEach cleanup correctly resets both the registry and the runtime via resetTaskRegistryForTests. |
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/tasks/task-registry.ts
Line: 72-73
Comment:
**Live map reference passed to `saveSnapshot`**
`persistTaskRegistry()` passes the module-level `tasks` map directly to `saveSnapshot`. The parameter is typed as `ReadonlyMap`, which prevents mutations through the reference, but the underlying map continues to be mutated by the registry between calls. The current JSON store is safe because it iterates and serializes immediately, but any future store implementation that defers reading (e.g., async batching/flush) would observe a later, modified state rather than the snapshot captured at write time.
Since this PR explicitly positions `TaskRegistryStore` as an infrastructure seam for future backends, it's worth documenting the contract — or passing a snapshot at call time:
```ts
function persistTaskRegistry() {
// Pass a shallow copy to give store implementations a stable snapshot.
// The live map is mutable; any async store must copy immediately on save.
getTaskRegistryStore().saveSnapshot(new Map(tasks));
}
```
Alternatively, document in the `TaskRegistryStore` type that `saveSnapshot` must read the map synchronously before returning.
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: src/tasks/task-registry.ts
Line: 283-297
Comment:
**`restored` hook event is silently skipped for empty snapshots**
When `loadSnapshot()` returns an empty map, the function returns early on line 284 without emitting any hook event. A consumer that relies on the `restored` event to know the registry has finished initializing (e.g., to start indexing or drain a queue) will never receive it on a fresh or empty store.
If the intention is "no event on empty restore", that's a reasonable choice, but it should be documented in the `TaskRegistryHooks` or `TaskRegistryHookEvent` types. Alternatively, emit the event unconditionally (with an empty `tasks` array) so the hook contract is predictable:
```ts
// always emit restored so consumers know initialization is done
emitTaskRegistryHookEvent(() => ({
kind: "restored",
tasks: snapshotTaskRecords(tasks),
}));
```
How can I resolve this? If you propose a fix, please make it concise.Reviews (1): Last reviewed commit: "Gateway: tighten task store hooks" | Re-trigger Greptile
| getTaskRegistryStore().saveSnapshot(tasks); | ||
| } |
There was a problem hiding this comment.
Live map reference passed to
saveSnapshot
persistTaskRegistry() passes the module-level tasks map directly to saveSnapshot. The parameter is typed as ReadonlyMap, which prevents mutations through the reference, but the underlying map continues to be mutated by the registry between calls. The current JSON store is safe because it iterates and serializes immediately, but any future store implementation that defers reading (e.g., async batching/flush) would observe a later, modified state rather than the snapshot captured at write time.
Since this PR explicitly positions TaskRegistryStore as an infrastructure seam for future backends, it's worth documenting the contract — or passing a snapshot at call time:
function persistTaskRegistry() {
// Pass a shallow copy to give store implementations a stable snapshot.
// The live map is mutable; any async store must copy immediately on save.
getTaskRegistryStore().saveSnapshot(new Map(tasks));
}Alternatively, document in the TaskRegistryStore type that saveSnapshot must read the map synchronously before returning.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/tasks/task-registry.ts
Line: 72-73
Comment:
**Live map reference passed to `saveSnapshot`**
`persistTaskRegistry()` passes the module-level `tasks` map directly to `saveSnapshot`. The parameter is typed as `ReadonlyMap`, which prevents mutations through the reference, but the underlying map continues to be mutated by the registry between calls. The current JSON store is safe because it iterates and serializes immediately, but any future store implementation that defers reading (e.g., async batching/flush) would observe a later, modified state rather than the snapshot captured at write time.
Since this PR explicitly positions `TaskRegistryStore` as an infrastructure seam for future backends, it's worth documenting the contract — or passing a snapshot at call time:
```ts
function persistTaskRegistry() {
// Pass a shallow copy to give store implementations a stable snapshot.
// The live map is mutable; any async store must copy immediately on save.
getTaskRegistryStore().saveSnapshot(new Map(tasks));
}
```
Alternatively, document in the `TaskRegistryStore` type that `saveSnapshot` must read the map synchronously before returning.
How can I resolve this? If you propose a fix, please make it concise.| const restored = getTaskRegistryStore().loadSnapshot(); | ||
| if (restored.size === 0) { | ||
| return; | ||
| } | ||
| for (const [taskId, task] of restored.entries()) { | ||
| tasks.set(taskId, task); | ||
| } | ||
| rebuildRunIdIndex(); | ||
| emitTaskRegistryHookEvent(() => ({ | ||
| kind: "restored", | ||
| tasks: snapshotTaskRecords(tasks), | ||
| })); | ||
| } catch (error) { | ||
| log.warn("Failed to restore task registry", { error }); | ||
| } |
There was a problem hiding this comment.
restored hook event is silently skipped for empty snapshots
When loadSnapshot() returns an empty map, the function returns early on line 284 without emitting any hook event. A consumer that relies on the restored event to know the registry has finished initializing (e.g., to start indexing or drain a queue) will never receive it on a fresh or empty store.
If the intention is "no event on empty restore", that's a reasonable choice, but it should be documented in the TaskRegistryHooks or TaskRegistryHookEvent types. Alternatively, emit the event unconditionally (with an empty tasks array) so the hook contract is predictable:
// always emit restored so consumers know initialization is done
emitTaskRegistryHookEvent(() => ({
kind: "restored",
tasks: snapshotTaskRecords(tasks),
}));Prompt To Fix With AI
This is a comment left during a code review.
Path: src/tasks/task-registry.ts
Line: 283-297
Comment:
**`restored` hook event is silently skipped for empty snapshots**
When `loadSnapshot()` returns an empty map, the function returns early on line 284 without emitting any hook event. A consumer that relies on the `restored` event to know the registry has finished initializing (e.g., to start indexing or drain a queue) will never receive it on a fresh or empty store.
If the intention is "no event on empty restore", that's a reasonable choice, but it should be documented in the `TaskRegistryHooks` or `TaskRegistryHookEvent` types. Alternatively, emit the event unconditionally (with an empty `tasks` array) so the hook contract is predictable:
```ts
// always emit restored so consumers know initialization is done
emitTaskRegistryHookEvent(() => ({
kind: "restored",
tasks: snapshotTaskRecords(tasks),
}));
```
How can I resolve this? If you propose a fix, please make it concise.Merged via squash. Prepared head SHA: 8db9b86 Co-authored-by: mbelinky <[email protected]> Co-authored-by: mbelinky <[email protected]> Reviewed-by: @mbelinky
Merged via squash. Prepared head SHA: 8db9b86 Co-authored-by: mbelinky <[email protected]> Co-authored-by: mbelinky <[email protected]> Reviewed-by: @mbelinky
Merged via squash. Prepared head SHA: 8db9b86 Co-authored-by: mbelinky <[email protected]> Co-authored-by: mbelinky <[email protected]> Reviewed-by: @mbelinky
Summary
TaskRegistryStoreinterface.task-registry.store.json.tsmodule.restored,upserted,deleted) so follow-up work can attach indexing or compaction logic without reopening the lifecycle code.Why
The lifecycle PR proves the background-task contract and channel behavior. This follow-up separates snapshot persistence from registry observation so we can harden storage later without rewriting the task logic.
Scope
Design Notes
TaskRegistryStoreowns snapshot load/save.Validation
Ran on
mb-server:pnpm exec vitest run src/tasks/task-registry.store.test.ts src/tasks/task-registry.test.tspnpm build