mirror of
https://github.com/trustgraph-ai/trustgraph.git
synced 2026-07-01 09:29:38 +02:00
Use HashMap for knowledge core state
This commit is contained in:
parent
451c6dbc58
commit
749f75715d
3 changed files with 78 additions and 61 deletions
|
|
@ -1983,6 +1983,28 @@ Notes:
|
|||
- `cd ts && bun run lint`
|
||||
- `git diff --check`
|
||||
|
||||
### 2026-06-04: KnowledgeCore HashMap State Slice
|
||||
|
||||
- Status: migrated and package-verified.
|
||||
- Completed:
|
||||
- `ts/packages/flow/src/cores/service.ts` now stores knowledge-core and
|
||||
document-core state in `HashMap` inside the existing `SynchronizedRef`.
|
||||
- Put/delete/load/get operations now read and update immutable `HashMap`
|
||||
snapshots with `HashMap.get`, `HashMap.set`, `HashMap.remove`, and
|
||||
`HashMap.has`.
|
||||
- Persistence and list-response helpers convert `HashMap` state to
|
||||
deterministic sorted records/arrays only at the API/JSON boundaries.
|
||||
- `ts/packages/flow/src/__tests__/knowledge-core-service.test.ts` now reads
|
||||
service state through `HashMap.get` and `Option`.
|
||||
- The focused scan for native map state in `cores/service.ts` is clean.
|
||||
- Verification:
|
||||
- `cd ts/packages/flow && bunx --bun vitest run src/__tests__/knowledge-core-service.test.ts`
|
||||
- `cd ts && bun run check:tsgo`
|
||||
- `cd ts && bun run build`
|
||||
- `cd ts && bun run test`
|
||||
- `cd ts && bun run lint`
|
||||
- `git diff --check`
|
||||
|
||||
## Subagent Findings To Preserve
|
||||
|
||||
- MCP/workbench:
|
||||
|
|
@ -2003,8 +2025,12 @@ Notes:
|
|||
workspace state are complete: the long-lived config store now uses
|
||||
`HashMap` inside `SynchronizedRef`, and plain records remain only at
|
||||
persistence/API boundaries.
|
||||
- KnowledgeCore service, FlowManager, and Librarian ref-backed state slices
|
||||
are complete. Follow-up service work should focus on scoped layers,
|
||||
- KnowledgeCore service operation dispatch, helper functions, and ref-backed
|
||||
core state are complete: `kgCores` and `deCores` now use `HashMap` inside
|
||||
`SynchronizedRef`, and plain records remain only at persistence/API
|
||||
boundaries.
|
||||
- FlowManager and Librarian ref-backed state slices are still valid larger
|
||||
collection targets. Follow-up service work should focus on scoped layers,
|
||||
schedules where polling semantics allow, and managed persistence providers
|
||||
rather than direct mutable service fields.
|
||||
- Flow service startup facades now consistently use `ManagedRuntime`, and
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import {mkdtemp, rm} from "node:fs/promises";
|
||||
import {tmpdir} from "node:os";
|
||||
import {join} from "node:path";
|
||||
import {Effect, SynchronizedRef} from "effect";
|
||||
import {Effect, HashMap, Option, SynchronizedRef} from "effect";
|
||||
import {describe, expect, it} from "vitest";
|
||||
import {
|
||||
topics,
|
||||
|
|
@ -96,7 +96,7 @@ describe("KnowledgeCoreService operations", () => {
|
|||
|
||||
await service.putKgCore(request, "put-1");
|
||||
const state = await Effect.runPromise(SynchronizedRef.get(service.state));
|
||||
const core = state.kgCores.get("alice:core-a");
|
||||
const core = Option.getOrUndefined(HashMap.get(state.kgCores, "alice:core-a"));
|
||||
|
||||
await service.getKgCore({
|
||||
operation: "get-kg-core",
|
||||
|
|
@ -166,7 +166,7 @@ describe("KnowledgeCoreService operations", () => {
|
|||
const state = await Effect.runPromise(SynchronizedRef.get(service.state));
|
||||
await rm(dir, {recursive: true, force: true});
|
||||
|
||||
expect(state.kgCores.get("alice:core-b")?.triples).toHaveLength(2);
|
||||
expect(Option.getOrUndefined(HashMap.get(state.kgCores, "alice:core-b"))?.triples).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("loads the legacy persisted knowledge shape with schema decoding", async () => {
|
||||
|
|
@ -187,6 +187,6 @@ describe("KnowledgeCoreService operations", () => {
|
|||
const state = await Effect.runPromise(SynchronizedRef.get(service.state));
|
||||
await rm(dir, {recursive: true, force: true});
|
||||
|
||||
expect(state.kgCores.get("alice:legacy")?.triples).toEqual([sampleTriple]);
|
||||
expect(Option.getOrUndefined(HashMap.get(state.kgCores, "alice:legacy"))?.triples).toEqual([sampleTriple]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ import {
|
|||
type Message,
|
||||
type ProcessorConfig,
|
||||
} from "@trustgraph/base";
|
||||
import {Duration, Effect, Layer, ManagedRuntime, Match, SynchronizedRef} from "effect";
|
||||
import {Duration, Effect, HashMap, Layer, ManagedRuntime, Match, SynchronizedRef} from "effect";
|
||||
import * as O from "effect/Option";
|
||||
import * as S from "effect/Schema";
|
||||
import {ensureDirectory, joinPath, readTextFile, writeTextFile} from "../runtime/effect-files.js";
|
||||
|
|
@ -81,8 +81,8 @@ const knowledgeCoreServiceError = (operation: string, cause: unknown): Knowledge
|
|||
message: errorMessage(cause),
|
||||
});
|
||||
|
||||
type KnowledgeCoreStore = Map<string, KnowledgeCore>;
|
||||
type DocumentCoreStore = Map<string, Array<DocumentEmbeddingsCore>>;
|
||||
type KnowledgeCoreStore = HashMap.HashMap<string, KnowledgeCore>;
|
||||
type DocumentCoreStore = HashMap.HashMap<string, Array<DocumentEmbeddingsCore>>;
|
||||
|
||||
interface KnowledgeCoreServiceState {
|
||||
readonly kgCores: KnowledgeCoreStore;
|
||||
|
|
@ -131,12 +131,15 @@ export interface KnowledgeCoreService extends AsyncProcessorRuntime<KnowledgeCor
|
|||
}
|
||||
|
||||
const initialState = (): KnowledgeCoreServiceState => ({
|
||||
kgCores: new Map<string, KnowledgeCore>(),
|
||||
deCores: new Map<string, Array<DocumentEmbeddingsCore>>(),
|
||||
kgCores: HashMap.empty<string, KnowledgeCore>(),
|
||||
deCores: HashMap.empty<string, Array<DocumentEmbeddingsCore>>(),
|
||||
consumer: null,
|
||||
responseProducer: null,
|
||||
});
|
||||
|
||||
const getHashMapValue = <K, V>(store: HashMap.HashMap<K, V>, key: K): V | undefined =>
|
||||
O.getOrUndefined(HashMap.get(store, key));
|
||||
|
||||
const cloneKnowledgeCore = (core: KnowledgeCore): KnowledgeCore => ({
|
||||
triples: Array.from(core.triples),
|
||||
graphEmbeddings: core.graphEmbeddings.map((entry) => ({
|
||||
|
|
@ -145,30 +148,17 @@ const cloneKnowledgeCore = (core: KnowledgeCore): KnowledgeCore => ({
|
|||
})),
|
||||
});
|
||||
|
||||
const cloneKgStore = (store: KnowledgeCoreStore): KnowledgeCoreStore => {
|
||||
const next = new Map<string, KnowledgeCore>();
|
||||
for (const [key, core] of store) {
|
||||
next.set(key, cloneKnowledgeCore(core));
|
||||
}
|
||||
return next;
|
||||
};
|
||||
|
||||
const cloneDeStore = (store: DocumentCoreStore): DocumentCoreStore => {
|
||||
const next = new Map<string, Array<DocumentEmbeddingsCore>>();
|
||||
for (const [key, cores] of store) {
|
||||
next.set(key, Array.from(cores));
|
||||
}
|
||||
return next;
|
||||
};
|
||||
const sortedEntries = <A>(store: HashMap.HashMap<string, A>): ReadonlyArray<readonly [string, A]> =>
|
||||
HashMap.toEntries(store).sort(([left], [right]) => left.localeCompare(right));
|
||||
|
||||
const toPersistedSnapshot = (state: KnowledgeCoreServiceState): PersistedKnowledgeSnapshot => {
|
||||
const kg: Record<string, KnowledgeCore> = {};
|
||||
const de: Record<string, Array<DocumentEmbeddingsCore>> = {};
|
||||
|
||||
for (const [key, core] of state.kgCores) {
|
||||
for (const [key, core] of sortedEntries(state.kgCores)) {
|
||||
kg[key] = cloneKnowledgeCore(core);
|
||||
}
|
||||
for (const [key, core] of state.deCores) {
|
||||
for (const [key, core] of sortedEntries(state.deCores)) {
|
||||
de[key] = Array.from(core);
|
||||
}
|
||||
|
||||
|
|
@ -176,9 +166,9 @@ const toPersistedSnapshot = (state: KnowledgeCoreServiceState): PersistedKnowled
|
|||
};
|
||||
|
||||
const kgStoreFromRecord = (record: LegacyKnowledgeSnapshot): KnowledgeCoreStore => {
|
||||
const store = new Map<string, KnowledgeCore>();
|
||||
let store = HashMap.empty<string, KnowledgeCore>();
|
||||
for (const [key, core] of Object.entries(record)) {
|
||||
store.set(key, cloneKnowledgeCore(core));
|
||||
store = HashMap.set(store, key, cloneKnowledgeCore(core));
|
||||
}
|
||||
return store;
|
||||
};
|
||||
|
|
@ -186,9 +176,9 @@ const kgStoreFromRecord = (record: LegacyKnowledgeSnapshot): KnowledgeCoreStore
|
|||
const deStoreFromRecord = (
|
||||
record: Record<string, Array<DocumentEmbeddingsCore>> | undefined,
|
||||
): DocumentCoreStore => {
|
||||
const store = new Map<string, Array<DocumentEmbeddingsCore>>();
|
||||
let store = HashMap.empty<string, Array<DocumentEmbeddingsCore>>();
|
||||
for (const [key, core] of Object.entries(record ?? {})) {
|
||||
store.set(key, Array.from(core));
|
||||
store = HashMap.set(store, key, Array.from(core));
|
||||
}
|
||||
return store;
|
||||
};
|
||||
|
|
@ -265,7 +255,7 @@ const readPersistedKnowledgeEffect = Effect.fn("KnowledgeCoreService.readPersist
|
|||
if (O.isSome(legacy)) {
|
||||
return {
|
||||
kgCores: kgStoreFromRecord(legacy.value),
|
||||
deCores: new Map<string, Array<DocumentEmbeddingsCore>>(),
|
||||
deCores: HashMap.empty<string, Array<DocumentEmbeddingsCore>>(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -304,14 +294,14 @@ const persistStateEffect = Effect.fn("KnowledgeCoreService.persistState")(
|
|||
),
|
||||
);
|
||||
|
||||
const listIds = (
|
||||
store: ReadonlyMap<string, unknown>,
|
||||
const listIds = <A>(
|
||||
store: HashMap.HashMap<string, A>,
|
||||
user: string,
|
||||
): Array<string> => {
|
||||
const prefix = user.length > 0 ? `${user}:` : "";
|
||||
const ids: Array<string> = [];
|
||||
|
||||
for (const key of store.keys()) {
|
||||
for (const [key] of sortedEntries(store)) {
|
||||
if (prefix.length === 0 || key.startsWith(prefix)) {
|
||||
ids.push(key.slice(prefix.length));
|
||||
}
|
||||
|
|
@ -413,7 +403,7 @@ const getKgCoreEffect = Effect.fn("getKgCoreEffect")(function* (
|
|||
requestId: string,
|
||||
) {
|
||||
const key = coreKey(request.user ?? "", request.id ?? "");
|
||||
const core = (yield* SynchronizedRef.get(stateRef)).kgCores.get(key);
|
||||
const core = getHashMapValue((yield* SynchronizedRef.get(stateRef)).kgCores, key);
|
||||
if (core === undefined) {
|
||||
return yield* knowledgeCoreServiceError("get-kg-core", `Knowledge core not found: ${key}`);
|
||||
}
|
||||
|
|
@ -452,11 +442,10 @@ const deleteKgCoreEffect = Effect.fn("deleteKgCoreEffect")(function* (
|
|||
requestId: string,
|
||||
) {
|
||||
const key = coreKey(request.user ?? "", request.id ?? "");
|
||||
const next = yield* SynchronizedRef.updateAndGet(stateRef, (state) => {
|
||||
const kgCores = cloneKgStore(state.kgCores);
|
||||
kgCores.delete(key);
|
||||
return {...state, kgCores};
|
||||
});
|
||||
const next = yield* SynchronizedRef.updateAndGet(stateRef, (state) => ({
|
||||
...state,
|
||||
kgCores: HashMap.remove(state.kgCores, key),
|
||||
}));
|
||||
|
||||
yield* persistStateEffect(persistPath, next);
|
||||
yield* Effect.log(`[KnowledgeCoreService] Deleted core: ${key}`);
|
||||
|
|
@ -471,8 +460,7 @@ const putKgCoreEffect = Effect.fn("putKgCoreEffect")(function* (
|
|||
) {
|
||||
const key = coreKey(request.user ?? "", request.id ?? "");
|
||||
const next = yield* SynchronizedRef.updateAndGet(stateRef, (state) => {
|
||||
const kgCores = cloneKgStore(state.kgCores);
|
||||
const existing = kgCores.get(key) ?? {triples: [], graphEmbeddings: []};
|
||||
const existing = getHashMapValue(state.kgCores, key) ?? {triples: [], graphEmbeddings: []};
|
||||
const core: KnowledgeCore = {
|
||||
triples: [
|
||||
...existing.triples,
|
||||
|
|
@ -486,11 +474,13 @@ const putKgCoreEffect = Effect.fn("putKgCoreEffect")(function* (
|
|||
})),
|
||||
],
|
||||
};
|
||||
kgCores.set(key, core);
|
||||
return {...state, kgCores};
|
||||
return {
|
||||
...state,
|
||||
kgCores: HashMap.set(state.kgCores, key, core),
|
||||
};
|
||||
});
|
||||
|
||||
const core = next.kgCores.get(key);
|
||||
const core = getHashMapValue(next.kgCores, key);
|
||||
yield* persistStateEffect(persistPath, next);
|
||||
yield* Effect.log(
|
||||
`[KnowledgeCoreService] Updated core ${key}: triples=${core?.triples.length ?? 0}, embeddings=${core?.graphEmbeddings.length ?? 0}`,
|
||||
|
|
@ -507,7 +497,7 @@ const loadKgCoreEffect = Effect.fn("loadKgCoreEffect")(function* (
|
|||
const user = request.user ?? "";
|
||||
const coreId = request.id ?? "";
|
||||
const key = coreKey(user, coreId);
|
||||
const core = (yield* SynchronizedRef.get(stateRef)).kgCores.get(key);
|
||||
const core = getHashMapValue((yield* SynchronizedRef.get(stateRef)).kgCores, key);
|
||||
if (core === undefined) {
|
||||
return yield* knowledgeCoreServiceError("load-kg-core", `Knowledge core not found: ${key}`);
|
||||
}
|
||||
|
|
@ -554,7 +544,7 @@ const getDeCoreEffect = Effect.fn("getDeCoreEffect")(function* (
|
|||
requestId: string,
|
||||
) {
|
||||
const key = coreKey(request.user ?? "", request.id ?? "");
|
||||
const core = (yield* SynchronizedRef.get(stateRef)).deCores.get(key);
|
||||
const core = getHashMapValue((yield* SynchronizedRef.get(stateRef)).deCores, key);
|
||||
if (core === undefined) {
|
||||
return yield* knowledgeCoreServiceError("get-de-core", `Document embeddings core not found: ${key}`);
|
||||
}
|
||||
|
|
@ -584,11 +574,10 @@ const deleteDeCoreEffect = Effect.fn("deleteDeCoreEffect")(function* (
|
|||
requestId: string,
|
||||
) {
|
||||
const key = coreKey(request.user ?? "", request.id ?? "");
|
||||
const next = yield* SynchronizedRef.updateAndGet(stateRef, (state) => {
|
||||
const deCores = cloneDeStore(state.deCores);
|
||||
deCores.delete(key);
|
||||
return {...state, deCores};
|
||||
});
|
||||
const next = yield* SynchronizedRef.updateAndGet(stateRef, (state) => ({
|
||||
...state,
|
||||
deCores: HashMap.remove(state.deCores, key),
|
||||
}));
|
||||
|
||||
yield* persistStateEffect(persistPath, next);
|
||||
yield* sendResponse(stateRef, {}, requestId);
|
||||
|
|
@ -606,11 +595,13 @@ const putDeCoreEffect = Effect.fn("putDeCoreEffect")(function* (
|
|||
}
|
||||
|
||||
const key = coreKey(request.user ?? "", request.id ?? "");
|
||||
const next = yield* SynchronizedRef.updateAndGet(stateRef, (state) => {
|
||||
const deCores = cloneDeStore(state.deCores);
|
||||
deCores.set(key, [...(deCores.get(key) ?? []), item]);
|
||||
return {...state, deCores};
|
||||
});
|
||||
const next = yield* SynchronizedRef.updateAndGet(stateRef, (state) => ({
|
||||
...state,
|
||||
deCores: HashMap.set(state.deCores, key, [
|
||||
...(getHashMapValue(state.deCores, key) ?? []),
|
||||
item,
|
||||
]),
|
||||
}));
|
||||
|
||||
yield* persistStateEffect(persistPath, next);
|
||||
yield* sendResponse(stateRef, {}, requestId);
|
||||
|
|
@ -622,7 +613,7 @@ const loadDeCoreEffect = Effect.fn("loadDeCoreEffect")(function* (
|
|||
requestId: string,
|
||||
) {
|
||||
const key = coreKey(request.user ?? "", request.id ?? "");
|
||||
const exists = (yield* SynchronizedRef.get(stateRef)).deCores.has(key);
|
||||
const exists = HashMap.has((yield* SynchronizedRef.get(stateRef)).deCores, key);
|
||||
if (!exists) {
|
||||
return yield* knowledgeCoreServiceError("load-de-core", `Document embeddings core not found: ${key}`);
|
||||
}
|
||||
|
|
@ -705,7 +696,7 @@ export function makeKnowledgeCoreService(config: KnowledgeCoreServiceConfig): Kn
|
|||
deCores: loaded.deCores,
|
||||
}));
|
||||
|
||||
yield* Effect.log(`[KnowledgeCoreService] Loaded persisted state (kg=${next.kgCores.size}, de=${next.deCores.size})`);
|
||||
yield* Effect.log(`[KnowledgeCoreService] Loaded persisted state (kg=${HashMap.size(next.kgCores)}, de=${HashMap.size(next.deCores)})`);
|
||||
});
|
||||
|
||||
service = Object.assign(base, {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue