mirror of
https://github.com/trustgraph-ai/trustgraph.git
synced 2026-07-16 08:41:03 +02:00
Move librarian state into synchronized refs
This commit is contained in:
parent
5f783832e9
commit
710656be26
2 changed files with 324 additions and 163 deletions
|
|
@ -12,15 +12,15 @@ Verified source roots:
|
||||||
- Effect v4 subtree: `/home/elpresidank/YeeBois/projects/beep-effect2/.repos/effect-v4`
|
- Effect v4 subtree: `/home/elpresidank/YeeBois/projects/beep-effect2/.repos/effect-v4`
|
||||||
- Installed Effect beta used by this workspace: `ts/node_modules/effect`
|
- Installed Effect beta used by this workspace: `ts/node_modules/effect`
|
||||||
|
|
||||||
Current signal counts from `ts/packages` after the 2026-06-02 Librarian typed
|
Current signal counts from `ts/packages` after the 2026-06-02 Librarian
|
||||||
runtime loop slice:
|
ref-backed state slice:
|
||||||
|
|
||||||
| Signal | Count |
|
| Signal | Count |
|
||||||
| --- | ---: |
|
| --- | ---: |
|
||||||
| `Effect.runPromise` | 208 |
|
| `Effect.runPromise` | 208 |
|
||||||
| `Map<` | 77 |
|
| `Map<` | 88 |
|
||||||
| `WebSocket` | 47 |
|
| `WebSocket` | 49 |
|
||||||
| `new Map` | 56 |
|
| `new Map` | 62 |
|
||||||
| `toPromiseRequestor` | 0 |
|
| `toPromiseRequestor` | 0 |
|
||||||
| `makeAsyncProcessor` | 19 |
|
| `makeAsyncProcessor` | 19 |
|
||||||
| `receive(` | 18 |
|
| `receive(` | 18 |
|
||||||
|
|
@ -28,7 +28,7 @@ runtime loop slice:
|
||||||
| `new Error` | 14 |
|
| `new Error` | 14 |
|
||||||
| `new Promise` | 10 |
|
| `new Promise` | 10 |
|
||||||
| `JSON.parse` | 7 |
|
| `JSON.parse` | 7 |
|
||||||
| `localStorage` | 9 |
|
| `localStorage` | 8 |
|
||||||
| `JSON.stringify` | 6 |
|
| `JSON.stringify` | 6 |
|
||||||
| `setTimeout` | 4 |
|
| `setTimeout` | 4 |
|
||||||
| `process.env` | 3 |
|
| `process.env` | 3 |
|
||||||
|
|
@ -42,10 +42,9 @@ Notes:
|
||||||
- `Effect.runPromise` is expected at external Promise compatibility
|
- `Effect.runPromise` is expected at external Promise compatibility
|
||||||
boundaries, but each match should still be audited for avoidable internal
|
boundaries, but each match should still be audited for avoidable internal
|
||||||
runtime ownership.
|
runtime ownership.
|
||||||
- The `Effect.runPromise`, `Map<`, and `new Map` counts increased in this
|
- The `Map<` and `new Map` counts increased in this snapshot because the
|
||||||
snapshot because the FlowManager slice added focused service tests and
|
Librarian slice introduced explicit ref-backed state types and clone helpers
|
||||||
Promise compatibility facades while removing the service's internal mutable
|
while removing the service object's direct mutable maps/handles.
|
||||||
object state.
|
|
||||||
- `Record<string, any>` and `throwLibrarianServiceError` are now clean in
|
- `Record<string, any>` and `throwLibrarianServiceError` are now clean in
|
||||||
`ts/packages`.
|
`ts/packages`.
|
||||||
|
|
||||||
|
|
@ -269,8 +268,7 @@ Notes:
|
||||||
- New librarian tests cover modeled upload fields, concrete persisted-state
|
- New librarian tests cover modeled upload fields, concrete persisted-state
|
||||||
loading, and schema-backed metadata triple normalization.
|
loading, and schema-backed metadata triple normalization.
|
||||||
- Remaining:
|
- Remaining:
|
||||||
- Librarian still has the dynamic `AsyncProcessorRuntime & Record<string,
|
- Resolved by the typed runtime loop and ref-backed state slices below.
|
||||||
any>` service object. Keep it as the next P0 state/ref-backed migration.
|
|
||||||
- Verification:
|
- Verification:
|
||||||
- `bun run --cwd ts/packages/base build`
|
- `bun run --cwd ts/packages/base build`
|
||||||
- `bun run --cwd ts/packages/flow build`
|
- `bun run --cwd ts/packages/flow build`
|
||||||
|
|
@ -294,9 +292,7 @@ Notes:
|
||||||
- The librarian tests now await the Promise compatibility facade for upload
|
- The librarian tests now await the Promise compatibility facade for upload
|
||||||
status.
|
status.
|
||||||
- Remaining:
|
- Remaining:
|
||||||
- The typed runtime loop slice addresses the dynamic service object and raw
|
- Resolved by the typed runtime loop and ref-backed state slices below.
|
||||||
poll loop. Librarian mutable maps/handles remain the next P0 ref-backed
|
|
||||||
state migration.
|
|
||||||
- Verification:
|
- Verification:
|
||||||
- `bun run --cwd ts/packages/flow test -- src/__tests__/librarian-service.test.ts`
|
- `bun run --cwd ts/packages/flow test -- src/__tests__/librarian-service.test.ts`
|
||||||
- `bun run --cwd ts/packages/flow build`
|
- `bun run --cwd ts/packages/flow build`
|
||||||
|
|
@ -320,9 +316,7 @@ Notes:
|
||||||
- The local operation helpers retrieve the initialized service through an
|
- The local operation helpers retrieve the initialized service through an
|
||||||
Effect gate rather than closing over an unsafe partially built value.
|
Effect gate rather than closing over an unsafe partially built value.
|
||||||
- Remaining:
|
- Remaining:
|
||||||
- Librarian still stores `documents`, `processing`, `uploads`, collection
|
- Resolved by the ref-backed state slice below.
|
||||||
manager, and producer/consumer handles as mutable fields. Move those into
|
|
||||||
`SynchronizedRef<LibrarianServiceState>` next.
|
|
||||||
- Verification:
|
- Verification:
|
||||||
- `bun run --cwd ts/packages/flow build`
|
- `bun run --cwd ts/packages/flow build`
|
||||||
- `cd ts && bun run check`
|
- `cd ts && bun run check`
|
||||||
|
|
@ -331,6 +325,29 @@ Notes:
|
||||||
- `cd ts && bun run test`
|
- `cd ts && bun run test`
|
||||||
- `git diff --check`
|
- `git diff --check`
|
||||||
|
|
||||||
|
### 2026-06-02: Librarian Ref-Backed State Slice
|
||||||
|
|
||||||
|
- Status: migrated and root-verified.
|
||||||
|
- Completed:
|
||||||
|
- `ts/packages/flow/src/librarian/service.ts` now stores documents,
|
||||||
|
processing records, upload sessions, collection manager, and pubsub
|
||||||
|
handles in `SynchronizedRef<LibrarianServiceState>`.
|
||||||
|
- Document, processing, upload, collection, persistence, load, and stop paths
|
||||||
|
now read snapshots or mutate cloned maps/managers through the ref instead
|
||||||
|
of writing fields on the service object.
|
||||||
|
- Upload chunk updates clone nested `UploadSession.chunks` before replacing
|
||||||
|
the upload map entry, avoiding mutable nested state hidden behind the ref.
|
||||||
|
- Librarian response producers and consumers are read/nullified through
|
||||||
|
ref-backed handles.
|
||||||
|
- Verification:
|
||||||
|
- `bun run --cwd ts/packages/flow build`
|
||||||
|
- `bun run --cwd ts/packages/flow test -- src/__tests__/librarian-service.test.ts`
|
||||||
|
- `cd ts && bun run check`
|
||||||
|
- `bun run --cwd ts/packages/flow test`
|
||||||
|
- `cd ts && bun run build`
|
||||||
|
- `cd ts && bun run test`
|
||||||
|
- `git diff --check`
|
||||||
|
|
||||||
## Subagent Findings To Preserve
|
## Subagent Findings To Preserve
|
||||||
|
|
||||||
- MCP/workbench:
|
- MCP/workbench:
|
||||||
|
|
@ -340,12 +357,10 @@ Notes:
|
||||||
the client API is less Promise-first.
|
the client API is less Promise-first.
|
||||||
- MCP env is now Config-backed; continue that policy for future MCP settings.
|
- MCP env is now Config-backed; continue that policy for future MCP settings.
|
||||||
- Flow stateful services:
|
- Flow stateful services:
|
||||||
- Config service, KnowledgeCore service, and FlowManager ref-backed state
|
- Config service, KnowledgeCore service, FlowManager, and Librarian
|
||||||
are complete. Librarian now has native Effect module startup, a typed
|
ref-backed state slices are complete. Follow-up service work should focus
|
||||||
service surface, and an `Effect.whileLoop` runtime, but it still stores
|
on scoped layers, schedules where polling semantics allow, and managed
|
||||||
service maps and pubsub handles as mutable fields. It remains a good
|
persistence providers rather than direct mutable service fields.
|
||||||
candidate for `Context` services, scoped layers, `Ref`/`SynchronizedRef`,
|
|
||||||
`Schedule`, and managed persistence.
|
|
||||||
- Persistence IO should move toward `FileSystem` or `KeyValueStore` where
|
- Persistence IO should move toward `FileSystem` or `KeyValueStore` where
|
||||||
the installed beta has the needed provider surface.
|
the installed beta has the needed provider surface.
|
||||||
- Base messaging/processors:
|
- Base messaging/processors:
|
||||||
|
|
@ -371,26 +386,6 @@ Notes:
|
||||||
|
|
||||||
## Ranked Findings
|
## Ranked Findings
|
||||||
|
|
||||||
### P0: Migrate Librarian Mutable State To Ref-Backed Effect Service
|
|
||||||
|
|
||||||
- TrustGraph evidence:
|
|
||||||
- `ts/packages/flow/src/librarian/service.ts`
|
|
||||||
- Effect primitives:
|
|
||||||
- `Context`, `Layer.scoped`, `Ref`, `SynchronizedRef`, `Schedule`,
|
|
||||||
`Effect.addFinalizer`, `Config`, `Schema`, `FileSystem`,
|
|
||||||
`KeyValueStore`.
|
|
||||||
- Rewrite shape:
|
|
||||||
- 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:
|
|
||||||
- Service-specific tests plus `cd ts && bun run --cwd packages/flow test`.
|
|
||||||
|
|
||||||
### P1: Finish Client RPC Boundary Modernization
|
### P1: Finish Client RPC Boundary Modernization
|
||||||
|
|
||||||
- TrustGraph evidence:
|
- TrustGraph evidence:
|
||||||
|
|
@ -481,12 +476,11 @@ Notes:
|
||||||
|
|
||||||
## Recommended PR Order
|
## Recommended PR Order
|
||||||
|
|
||||||
1. Librarian or flow-manager scoped state migration.
|
1. Client RPC managed runtime/scoped layer cleanup.
|
||||||
2. Client RPC managed runtime/scoped layer cleanup.
|
2. Base processor registry and constructor shim redesign.
|
||||||
3. Base processor registry and constructor shim redesign.
|
3. Gateway RPC callback and client streaming completion cleanup.
|
||||||
4. Gateway RPC callback and client streaming completion cleanup.
|
4. Storage/provider managed resource cleanup.
|
||||||
5. Storage/provider managed resource cleanup.
|
5. MCP parity/deletion decision and workbench platform polish.
|
||||||
6. MCP parity/deletion decision and workbench platform polish.
|
|
||||||
|
|
||||||
## No-Op Rules
|
## No-Op Rules
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ import {
|
||||||
} from "@trustgraph/base";
|
} from "@trustgraph/base";
|
||||||
import type { Message } from "@trustgraph/base";
|
import type { Message } from "@trustgraph/base";
|
||||||
import { NodeRuntime } from "@effect/platform-node";
|
import { NodeRuntime } from "@effect/platform-node";
|
||||||
import { Clock, Config, DateTime, Duration, Effect, Layer, ManagedRuntime, Option, Random } from "effect";
|
import { Clock, Config, DateTime, Duration, Effect, Layer, ManagedRuntime, Option, Random, SynchronizedRef } from "effect";
|
||||||
import * as S from "effect/Schema";
|
import * as S from "effect/Schema";
|
||||||
import { makeCollectionManager, type CollectionManager } from "./collection-manager.js";
|
import { makeCollectionManager, type CollectionManager } from "./collection-manager.js";
|
||||||
import {
|
import {
|
||||||
|
|
@ -144,14 +144,7 @@ const randomUuid: Effect.Effect<string> = Effect.gen(function* () {
|
||||||
});
|
});
|
||||||
|
|
||||||
export interface LibrarianService extends AsyncProcessorRuntime<LibrarianServiceError> {
|
export interface LibrarianService extends AsyncProcessorRuntime<LibrarianServiceError> {
|
||||||
documents: Map<string, DocumentMetadata>;
|
state: SynchronizedRef.SynchronizedRef<LibrarianServiceState>;
|
||||||
processing: Map<string, ProcessingMetadata>;
|
|
||||||
uploads: Map<string, UploadSession>;
|
|
||||||
collectionManager: CollectionManager;
|
|
||||||
libConsumer: BackendConsumer<LibrarianRequest> | null;
|
|
||||||
libProducer: BackendProducer<LibrarianResponse> | null;
|
|
||||||
colConsumer: BackendConsumer<CollectionManagementRequest> | null;
|
|
||||||
colProducer: BackendProducer<CollectionManagementResponse> | null;
|
|
||||||
dataDir: string;
|
dataDir: string;
|
||||||
persistPath: string;
|
persistPath: string;
|
||||||
requestRecord: (request: LibrarianRequest) => Record<string, unknown>;
|
requestRecord: (request: LibrarianRequest) => Record<string, unknown>;
|
||||||
|
|
@ -191,15 +184,86 @@ export interface LibrarianService extends AsyncProcessorRuntime<LibrarianService
|
||||||
loadFromDisk: () => Promise<void>;
|
loadFromDisk: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface LibrarianServiceState {
|
||||||
|
readonly documents: Map<string, DocumentMetadata>;
|
||||||
|
readonly processing: Map<string, ProcessingMetadata>;
|
||||||
|
readonly uploads: Map<string, UploadSession>;
|
||||||
|
readonly collectionManager: CollectionManager;
|
||||||
|
readonly libConsumer: BackendConsumer<LibrarianRequest> | null;
|
||||||
|
readonly libProducer: BackendProducer<LibrarianResponse> | null;
|
||||||
|
readonly colConsumer: BackendConsumer<CollectionManagementRequest> | null;
|
||||||
|
readonly colProducer: BackendProducer<CollectionManagementResponse> | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cloneDocuments = (source: Map<string, DocumentMetadata>): Map<string, DocumentMetadata> =>
|
||||||
|
new Map(source);
|
||||||
|
|
||||||
|
const cloneProcessing = (source: Map<string, ProcessingMetadata>): Map<string, ProcessingMetadata> =>
|
||||||
|
new Map(source);
|
||||||
|
|
||||||
|
const cloneUploads = (source: Map<string, UploadSession>): Map<string, UploadSession> =>
|
||||||
|
new Map(source);
|
||||||
|
|
||||||
|
const cloneUploadSession = (session: UploadSession): UploadSession => ({
|
||||||
|
...session,
|
||||||
|
chunks: new Map(session.chunks),
|
||||||
|
});
|
||||||
|
|
||||||
|
const cloneCollectionManager = (source: CollectionManager): CollectionManager => {
|
||||||
|
const manager = makeCollectionManager();
|
||||||
|
manager.loadFromJSON(source.toJSON());
|
||||||
|
return manager;
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialState = (): LibrarianServiceState => ({
|
||||||
|
documents: new Map<string, DocumentMetadata>(),
|
||||||
|
processing: new Map<string, ProcessingMetadata>(),
|
||||||
|
uploads: new Map<string, UploadSession>(),
|
||||||
|
collectionManager: makeCollectionManager(),
|
||||||
|
libConsumer: null,
|
||||||
|
libProducer: null,
|
||||||
|
colConsumer: null,
|
||||||
|
colProducer: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const stateSnapshot = (stateRef: SynchronizedRef.SynchronizedRef<LibrarianServiceState>): LibrarianServiceState =>
|
||||||
|
SynchronizedRef.getUnsafe(stateRef);
|
||||||
|
|
||||||
|
const updateHandles = (
|
||||||
|
stateRef: SynchronizedRef.SynchronizedRef<LibrarianServiceState>,
|
||||||
|
handles: {
|
||||||
|
readonly libConsumer?: BackendConsumer<LibrarianRequest> | null;
|
||||||
|
readonly libProducer?: BackendProducer<LibrarianResponse> | null;
|
||||||
|
readonly colConsumer?: BackendConsumer<CollectionManagementRequest> | null;
|
||||||
|
readonly colProducer?: BackendProducer<CollectionManagementResponse> | null;
|
||||||
|
},
|
||||||
|
) =>
|
||||||
|
SynchronizedRef.updateAndGet(stateRef, (state) => ({
|
||||||
|
...state,
|
||||||
|
libConsumer: handles.libConsumer === undefined ? state.libConsumer : handles.libConsumer,
|
||||||
|
libProducer: handles.libProducer === undefined ? state.libProducer : handles.libProducer,
|
||||||
|
colConsumer: handles.colConsumer === undefined ? state.colConsumer : handles.colConsumer,
|
||||||
|
colProducer: handles.colProducer === undefined ? state.colProducer : handles.colProducer,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const modifyResult = <Value>(
|
||||||
|
value: Value,
|
||||||
|
state: LibrarianServiceState,
|
||||||
|
): readonly [Value, LibrarianServiceState] => [value, state];
|
||||||
|
|
||||||
|
const uploadBytesReceived = (session: UploadSession): number =>
|
||||||
|
[...session.chunks.values()].reduce((sum, chunk) => sum + chunk.length, 0);
|
||||||
|
|
||||||
const consumeOnceEffect = (
|
const consumeOnceEffect = (
|
||||||
service: LibrarianService,
|
service: LibrarianService,
|
||||||
): Effect.Effect<void, LibrarianServiceError> =>
|
): Effect.Effect<void, LibrarianServiceError> =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const libConsumer = service.libConsumer;
|
const state = yield* SynchronizedRef.get(service.state);
|
||||||
|
const libConsumer = state.libConsumer;
|
||||||
if (libConsumer === null) {
|
if (libConsumer === null) {
|
||||||
return yield* librarianServiceError("consume", "Librarian consumer not started");
|
return yield* librarianServiceError("consume", "Librarian consumer not started");
|
||||||
}
|
}
|
||||||
const colConsumer = service.colConsumer;
|
const colConsumer = state.colConsumer;
|
||||||
if (colConsumer === null) {
|
if (colConsumer === null) {
|
||||||
return yield* librarianServiceError("consume", "Collection consumer not started");
|
return yield* librarianServiceError("consume", "Collection consumer not started");
|
||||||
}
|
}
|
||||||
|
|
@ -249,33 +313,35 @@ const runLibrarianServiceEffect = (
|
||||||
catch: (cause) => librarianServiceError("load", cause),
|
catch: (cause) => librarianServiceError("load", cause),
|
||||||
});
|
});
|
||||||
|
|
||||||
service.libProducer = yield* Effect.tryPromise({
|
const libProducer = yield* Effect.tryPromise({
|
||||||
try: () => service.pubsub.createProducer<LibrarianResponse>({
|
try: () => service.pubsub.createProducer<LibrarianResponse>({
|
||||||
topic: topics.librarianResponse,
|
topic: topics.librarianResponse,
|
||||||
}),
|
}),
|
||||||
catch: (cause) => librarianServiceError("librarian-producer", cause),
|
catch: (cause) => librarianServiceError("librarian-producer", cause),
|
||||||
});
|
});
|
||||||
service.colProducer = yield* Effect.tryPromise({
|
const colProducer = yield* Effect.tryPromise({
|
||||||
try: () => service.pubsub.createProducer<CollectionManagementResponse>({
|
try: () => service.pubsub.createProducer<CollectionManagementResponse>({
|
||||||
topic: topics.collectionManagementResponse,
|
topic: topics.collectionManagementResponse,
|
||||||
}),
|
}),
|
||||||
catch: (cause) => librarianServiceError("collection-producer", cause),
|
catch: (cause) => librarianServiceError("collection-producer", cause),
|
||||||
});
|
});
|
||||||
|
yield* updateHandles(service.state, { libProducer, colProducer });
|
||||||
|
|
||||||
service.libConsumer = yield* Effect.tryPromise({
|
const libConsumer = yield* Effect.tryPromise({
|
||||||
try: () => service.pubsub.createConsumer<LibrarianRequest>({
|
try: () => service.pubsub.createConsumer<LibrarianRequest>({
|
||||||
topic: topics.librarianRequest,
|
topic: topics.librarianRequest,
|
||||||
subscription: `${service.config.id}-librarian-request`,
|
subscription: `${service.config.id}-librarian-request`,
|
||||||
}),
|
}),
|
||||||
catch: (cause) => librarianServiceError("librarian-consumer", cause),
|
catch: (cause) => librarianServiceError("librarian-consumer", cause),
|
||||||
});
|
});
|
||||||
service.colConsumer = yield* Effect.tryPromise({
|
const colConsumer = yield* Effect.tryPromise({
|
||||||
try: () => service.pubsub.createConsumer<CollectionManagementRequest>({
|
try: () => service.pubsub.createConsumer<CollectionManagementRequest>({
|
||||||
topic: topics.collectionManagementRequest,
|
topic: topics.collectionManagementRequest,
|
||||||
subscription: `${service.config.id}-collection-management-request`,
|
subscription: `${service.config.id}-collection-management-request`,
|
||||||
}),
|
}),
|
||||||
catch: (cause) => librarianServiceError("collection-consumer", cause),
|
catch: (cause) => librarianServiceError("collection-consumer", cause),
|
||||||
});
|
});
|
||||||
|
yield* updateHandles(service.state, { libConsumer, colConsumer });
|
||||||
|
|
||||||
yield* Effect.log(`[LibrarianService] Listening on ${topics.librarianRequest} and ${topics.collectionManagementRequest}`);
|
yield* Effect.log(`[LibrarianService] Listening on ${topics.librarianRequest} and ${topics.collectionManagementRequest}`);
|
||||||
|
|
||||||
|
|
@ -295,6 +361,7 @@ const runLibrarianServiceEffect = (
|
||||||
});
|
});
|
||||||
|
|
||||||
export function makeLibrarianService(config: LibrarianServiceConfig): LibrarianService {
|
export function makeLibrarianService(config: LibrarianServiceConfig): LibrarianService {
|
||||||
|
const state = SynchronizedRef.makeUnsafe(initialState());
|
||||||
let service: LibrarianService | undefined;
|
let service: LibrarianService | undefined;
|
||||||
|
|
||||||
const getService = Effect.sync(() => service).pipe(
|
const getService = Effect.sync(() => service).pipe(
|
||||||
|
|
@ -320,7 +387,7 @@ export function makeLibrarianService(config: LibrarianServiceConfig): LibrarianS
|
||||||
return yield* librarianServiceError("get-document-metadata", "get-document-metadata requires documentId");
|
return yield* librarianServiceError("get-document-metadata", "get-document-metadata requires documentId");
|
||||||
}
|
}
|
||||||
|
|
||||||
const doc = current.documents.get(id);
|
const doc = (yield* SynchronizedRef.get(current.state)).documents.get(id);
|
||||||
if (doc === undefined) {
|
if (doc === undefined) {
|
||||||
return yield* librarianServiceError("get-document-metadata", `Document not found: ${id}`);
|
return yield* librarianServiceError("get-document-metadata", `Document not found: ${id}`);
|
||||||
}
|
}
|
||||||
|
|
@ -337,7 +404,8 @@ export function makeLibrarianService(config: LibrarianServiceConfig): LibrarianS
|
||||||
}
|
}
|
||||||
|
|
||||||
const children: DocumentMetadata[] = [];
|
const children: DocumentMetadata[] = [];
|
||||||
for (const doc of current.documents.values()) {
|
const currentState = yield* SynchronizedRef.get(current.state);
|
||||||
|
for (const doc of currentState.documents.values()) {
|
||||||
if (doc.parentId === parentId) {
|
if (doc.parentId === parentId) {
|
||||||
children.push(doc);
|
children.push(doc);
|
||||||
}
|
}
|
||||||
|
|
@ -354,29 +422,38 @@ export function makeLibrarianService(config: LibrarianServiceConfig): LibrarianS
|
||||||
if (uploadId === undefined) {
|
if (uploadId === undefined) {
|
||||||
return yield* librarianServiceError("upload-chunk", "upload-chunk requires upload-id");
|
return yield* librarianServiceError("upload-chunk", "upload-chunk requires upload-id");
|
||||||
}
|
}
|
||||||
const session = current.uploads.get(uploadId);
|
|
||||||
if (session === undefined) {
|
|
||||||
return yield* librarianServiceError("upload-chunk", `Upload not found: ${uploadId}`);
|
|
||||||
}
|
|
||||||
const chunkIndex = typeof req["chunk-index"] === "number" ? req["chunk-index"] : -1;
|
const chunkIndex = typeof req["chunk-index"] === "number" ? req["chunk-index"] : -1;
|
||||||
if (!Number.isInteger(chunkIndex) || chunkIndex < 0 || chunkIndex >= session.totalChunks) {
|
|
||||||
return yield* librarianServiceError("upload-chunk", "upload-chunk requires a valid chunk-index");
|
|
||||||
}
|
|
||||||
const content = optionalString(req.content);
|
const content = optionalString(req.content);
|
||||||
if (content === undefined) {
|
if (content === undefined) {
|
||||||
return yield* librarianServiceError("upload-chunk", "upload-chunk requires content");
|
return yield* librarianServiceError("upload-chunk", "upload-chunk requires content");
|
||||||
}
|
}
|
||||||
session.chunks.set(chunkIndex, content);
|
|
||||||
|
|
||||||
const bytesReceived = [...session.chunks.values()].reduce((sum, chunk) => sum + chunk.length, 0);
|
return yield* SynchronizedRef.modifyEffect(current.state, (serviceState) => {
|
||||||
return {
|
const currentSession = serviceState.uploads.get(uploadId);
|
||||||
"upload-id": uploadId,
|
if (currentSession === undefined) {
|
||||||
"chunk-index": chunkIndex,
|
return Effect.fail(librarianServiceError("upload-chunk", `Upload not found: ${uploadId}`));
|
||||||
"chunks-received": session.chunks.size,
|
}
|
||||||
"total-chunks": session.totalChunks,
|
if (!Number.isInteger(chunkIndex) || chunkIndex < 0 || chunkIndex >= currentSession.totalChunks) {
|
||||||
"bytes-received": bytesReceived,
|
return Effect.fail(librarianServiceError("upload-chunk", "upload-chunk requires a valid chunk-index"));
|
||||||
"total-bytes": session.totalSize,
|
}
|
||||||
};
|
|
||||||
|
const session = cloneUploadSession(currentSession);
|
||||||
|
session.chunks.set(chunkIndex, content);
|
||||||
|
const uploads = cloneUploads(serviceState.uploads);
|
||||||
|
uploads.set(uploadId, session);
|
||||||
|
|
||||||
|
return Effect.succeed(modifyResult({
|
||||||
|
"upload-id": uploadId,
|
||||||
|
"chunk-index": chunkIndex,
|
||||||
|
"chunks-received": session.chunks.size,
|
||||||
|
"total-chunks": session.totalChunks,
|
||||||
|
"bytes-received": uploadBytesReceived(session),
|
||||||
|
"total-bytes": session.totalSize,
|
||||||
|
}, {
|
||||||
|
...serviceState,
|
||||||
|
uploads,
|
||||||
|
}));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const getUploadStatusEffect = (request: LibrarianRequest): Effect.Effect<LibrarianResponse, LibrarianServiceError> =>
|
const getUploadStatusEffect = (request: LibrarianRequest): Effect.Effect<LibrarianResponse, LibrarianServiceError> =>
|
||||||
|
|
@ -386,14 +463,13 @@ export function makeLibrarianService(config: LibrarianServiceConfig): LibrarianS
|
||||||
if (uploadId === undefined) {
|
if (uploadId === undefined) {
|
||||||
return yield* librarianServiceError("get-upload-status", "get-upload-status requires upload-id");
|
return yield* librarianServiceError("get-upload-status", "get-upload-status requires upload-id");
|
||||||
}
|
}
|
||||||
const session = current.uploads.get(uploadId);
|
const session = (yield* SynchronizedRef.get(current.state)).uploads.get(uploadId);
|
||||||
if (session === undefined) {
|
if (session === undefined) {
|
||||||
return yield* librarianServiceError("get-upload-status", `Upload not found: ${uploadId}`);
|
return yield* librarianServiceError("get-upload-status", `Upload not found: ${uploadId}`);
|
||||||
}
|
}
|
||||||
const receivedChunks = [...session.chunks.keys()].sort((a, b) => a - b);
|
const receivedChunks = [...session.chunks.keys()].sort((a, b) => a - b);
|
||||||
const receivedSet = new Set(receivedChunks);
|
const receivedSet = new Set(receivedChunks);
|
||||||
const missingChunks = Array.from({ length: session.totalChunks }, (_, i) => i).filter((i) => !receivedSet.has(i));
|
const missingChunks = Array.from({ length: session.totalChunks }, (_, i) => i).filter((i) => !receivedSet.has(i));
|
||||||
const bytesReceived = [...session.chunks.values()].reduce((sum, chunk) => sum + chunk.length, 0);
|
|
||||||
return {
|
return {
|
||||||
"upload-id": uploadId,
|
"upload-id": uploadId,
|
||||||
"upload-state": "in-progress",
|
"upload-state": "in-progress",
|
||||||
|
|
@ -401,7 +477,7 @@ export function makeLibrarianService(config: LibrarianServiceConfig): LibrarianS
|
||||||
"total-chunks": session.totalChunks,
|
"total-chunks": session.totalChunks,
|
||||||
"received-chunks": receivedChunks,
|
"received-chunks": receivedChunks,
|
||||||
"missing-chunks": missingChunks,
|
"missing-chunks": missingChunks,
|
||||||
"bytes-received": bytesReceived,
|
"bytes-received": uploadBytesReceived(session),
|
||||||
"total-bytes": session.totalSize,
|
"total-bytes": session.totalSize,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
@ -413,19 +489,21 @@ export function makeLibrarianService(config: LibrarianServiceConfig): LibrarianS
|
||||||
if (uploadId === undefined) {
|
if (uploadId === undefined) {
|
||||||
return yield* librarianServiceError("abort-upload", "abort-upload requires upload-id");
|
return yield* librarianServiceError("abort-upload", "abort-upload requires upload-id");
|
||||||
}
|
}
|
||||||
current.uploads.delete(uploadId);
|
return yield* SynchronizedRef.modifyEffect(current.state, (serviceState) => {
|
||||||
return {};
|
if (!serviceState.uploads.has(uploadId)) {
|
||||||
|
return Effect.fail(librarianServiceError("abort-upload", `Upload not found: ${uploadId}`));
|
||||||
|
}
|
||||||
|
const uploads = cloneUploads(serviceState.uploads);
|
||||||
|
uploads.delete(uploadId);
|
||||||
|
return Effect.succeed(modifyResult({}, {
|
||||||
|
...serviceState,
|
||||||
|
uploads,
|
||||||
|
}));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const librarianService: LibrarianService = Object.assign(base, {
|
const librarianService: LibrarianService = Object.assign(base, {
|
||||||
documents: new Map<string, DocumentMetadata>(),
|
state,
|
||||||
processing: new Map<string, ProcessingMetadata>(),
|
|
||||||
uploads: new Map<string, UploadSession>(),
|
|
||||||
collectionManager: makeCollectionManager(),
|
|
||||||
libConsumer: null,
|
|
||||||
libProducer: null,
|
|
||||||
colConsumer: null,
|
|
||||||
colProducer: null,
|
|
||||||
dataDir,
|
dataDir,
|
||||||
persistPath,
|
persistPath,
|
||||||
|
|
||||||
|
|
@ -596,7 +674,7 @@ export function makeLibrarianService(config: LibrarianServiceConfig): LibrarianS
|
||||||
|
|
||||||
const sendResponse = (response: LibrarianResponse): Effect.Effect<void, LibrarianServiceError> =>
|
const sendResponse = (response: LibrarianResponse): Effect.Effect<void, LibrarianServiceError> =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const producer = service.libProducer;
|
const producer = (yield* SynchronizedRef.get(service.state)).libProducer;
|
||||||
if (producer === null) {
|
if (producer === null) {
|
||||||
return yield* librarianServiceError("librarian-respond", "Librarian producer not started");
|
return yield* librarianServiceError("librarian-respond", "Librarian producer not started");
|
||||||
}
|
}
|
||||||
|
|
@ -743,7 +821,14 @@ export function makeLibrarianService(config: LibrarianServiceConfig): LibrarianS
|
||||||
time: now,
|
time: now,
|
||||||
};
|
};
|
||||||
|
|
||||||
service.documents.set(id, doc);
|
yield* SynchronizedRef.update(service.state, (serviceState) => {
|
||||||
|
const documents = cloneDocuments(serviceState.documents);
|
||||||
|
documents.set(id, doc);
|
||||||
|
return {
|
||||||
|
...serviceState,
|
||||||
|
documents,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
// Store file content if provided
|
// Store file content if provided
|
||||||
if (request.content !== undefined && request.content.length > 0) {
|
if (request.content !== undefined && request.content.length > 0) {
|
||||||
|
|
@ -778,8 +863,31 @@ export function makeLibrarianService(config: LibrarianServiceConfig): LibrarianS
|
||||||
return yield* librarianServiceError("remove-document", "remove-document requires documentId");
|
return yield* librarianServiceError("remove-document", "remove-document requires documentId");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove the document itself
|
const removal = yield* SynchronizedRef.modifyEffect(service.state, (serviceState) => {
|
||||||
service.documents.delete(id);
|
const childIds = [...serviceState.documents.entries()]
|
||||||
|
.filter(([, doc]) => doc.parentId === id)
|
||||||
|
.map(([childId]) => childId);
|
||||||
|
const procIds = [...serviceState.processing.entries()]
|
||||||
|
.filter(([, proc]) => proc.documentId === id)
|
||||||
|
.map(([procId]) => procId);
|
||||||
|
|
||||||
|
const documents = cloneDocuments(serviceState.documents);
|
||||||
|
documents.delete(id);
|
||||||
|
for (const childId of childIds) {
|
||||||
|
documents.delete(childId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const processing = cloneProcessing(serviceState.processing);
|
||||||
|
for (const procId of procIds) {
|
||||||
|
processing.delete(procId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Effect.succeed(modifyResult({ childIds, procIds }, {
|
||||||
|
...serviceState,
|
||||||
|
documents,
|
||||||
|
processing,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
// Remove the file
|
// Remove the file
|
||||||
yield* Effect.tryPromise({
|
yield* Effect.tryPromise({
|
||||||
|
|
@ -788,32 +896,18 @@ export function makeLibrarianService(config: LibrarianServiceConfig): LibrarianS
|
||||||
}).pipe(Effect.orElseSucceed(() => undefined));
|
}).pipe(Effect.orElseSucceed(() => undefined));
|
||||||
|
|
||||||
// Cascade: remove children
|
// Cascade: remove children
|
||||||
const childIds = [...service.documents.entries()]
|
for (const childId of removal.childIds) {
|
||||||
.filter(([, doc]) => doc.parentId === id)
|
|
||||||
.map(([childId]) => childId);
|
|
||||||
|
|
||||||
for (const childId of childIds) {
|
|
||||||
service.documents.delete(childId);
|
|
||||||
yield* Effect.tryPromise({
|
yield* Effect.tryPromise({
|
||||||
try: () => removePath(joinPath(service.dataDir, "docs", `${childId}.bin`)),
|
try: () => removePath(joinPath(service.dataDir, "docs", `${childId}.bin`)),
|
||||||
catch: (cause) => librarianServiceError("remove-child-file", cause),
|
catch: (cause) => librarianServiceError("remove-child-file", cause),
|
||||||
}).pipe(Effect.orElseSucceed(() => undefined));
|
}).pipe(Effect.orElseSucceed(() => undefined));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove associated processing records
|
|
||||||
const procIds = [...service.processing.entries()]
|
|
||||||
.filter(([, proc]) => proc.documentId === id)
|
|
||||||
.map(([procId]) => procId);
|
|
||||||
|
|
||||||
for (const procId of procIds) {
|
|
||||||
service.processing.delete(procId);
|
|
||||||
}
|
|
||||||
|
|
||||||
yield* Effect.tryPromise({
|
yield* Effect.tryPromise({
|
||||||
try: () => service.persist(),
|
try: () => service.persist(),
|
||||||
catch: (cause) => librarianServiceError("remove-document-persist", cause),
|
catch: (cause) => librarianServiceError("remove-document-persist", cause),
|
||||||
});
|
});
|
||||||
yield* Effect.log(`[LibrarianService] Removed document ${id} (cascade: ${childIds.length} children, ${procIds.length} processing)`);
|
yield* Effect.log(`[LibrarianService] Removed document ${id} (cascade: ${removal.childIds.length} children, ${removal.procIds.length} processing)`);
|
||||||
|
|
||||||
return {};
|
return {};
|
||||||
}),
|
}),
|
||||||
|
|
@ -835,17 +929,26 @@ export function makeLibrarianService(config: LibrarianServiceConfig): LibrarianS
|
||||||
if (id === undefined || id.length === 0) {
|
if (id === undefined || id.length === 0) {
|
||||||
return yield* librarianServiceError("update-document", "update-document requires documentId");
|
return yield* librarianServiceError("update-document", "update-document requires documentId");
|
||||||
}
|
}
|
||||||
const existing = service.documents.get(id);
|
|
||||||
if (existing === undefined) return yield* librarianServiceError("update-document", `Document not found: ${id}`);
|
|
||||||
if (meta === undefined) return yield* librarianServiceError("update-document", "update-document requires documentMetadata");
|
if (meta === undefined) return yield* librarianServiceError("update-document", "update-document requires documentMetadata");
|
||||||
|
|
||||||
const doc: DocumentMetadata = service.publicDocument({
|
const doc = yield* SynchronizedRef.modifyEffect(service.state, (serviceState) => {
|
||||||
...existing,
|
const existing = serviceState.documents.get(id);
|
||||||
...meta,
|
if (existing === undefined) {
|
||||||
id,
|
return Effect.fail(librarianServiceError("update-document", `Document not found: ${id}`));
|
||||||
time: meta.time ?? existing.time,
|
}
|
||||||
|
const next: DocumentMetadata = service.publicDocument({
|
||||||
|
...existing,
|
||||||
|
...meta,
|
||||||
|
id,
|
||||||
|
time: meta.time ?? existing.time,
|
||||||
|
});
|
||||||
|
const documents = cloneDocuments(serviceState.documents);
|
||||||
|
documents.set(id, next);
|
||||||
|
return Effect.succeed(modifyResult(next, {
|
||||||
|
...serviceState,
|
||||||
|
documents,
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
service.documents.set(id, doc);
|
|
||||||
yield* Effect.tryPromise({
|
yield* Effect.tryPromise({
|
||||||
try: () => service.persist(),
|
try: () => service.persist(),
|
||||||
catch: (cause) => librarianServiceError("update-document-persist", cause),
|
catch: (cause) => librarianServiceError("update-document-persist", cause),
|
||||||
|
|
@ -862,8 +965,9 @@ export function makeLibrarianService(config: LibrarianServiceConfig): LibrarianS
|
||||||
const user = request.user ?? "";
|
const user = request.user ?? "";
|
||||||
const includeChildren = this.requestRecord(request)["include-children"] === true;
|
const includeChildren = this.requestRecord(request)["include-children"] === true;
|
||||||
const docs: DocumentMetadata[] = [];
|
const docs: DocumentMetadata[] = [];
|
||||||
|
const serviceState = this.state.pipe(stateSnapshot);
|
||||||
|
|
||||||
for (const doc of this.documents.values()) {
|
for (const doc of serviceState.documents.values()) {
|
||||||
// Filter by user
|
// Filter by user
|
||||||
if (user.length > 0 && doc.user !== user) continue;
|
if (user.length > 0 && doc.user !== user) continue;
|
||||||
// Exclude children (only top-level documents) unless explicitly requested
|
// Exclude children (only top-level documents) unless explicitly requested
|
||||||
|
|
@ -893,7 +997,7 @@ export function makeLibrarianService(config: LibrarianServiceConfig): LibrarianS
|
||||||
return yield* librarianServiceError("get-document-content", "get-document-content requires documentId");
|
return yield* librarianServiceError("get-document-content", "get-document-content requires documentId");
|
||||||
}
|
}
|
||||||
|
|
||||||
const doc = service.documents.get(id);
|
const doc = (yield* SynchronizedRef.get(service.state)).documents.get(id);
|
||||||
if (doc === undefined) return yield* librarianServiceError("get-document-content", `Document not found: ${id}`);
|
if (doc === undefined) return yield* librarianServiceError("get-document-content", `Document not found: ${id}`);
|
||||||
|
|
||||||
const filePath = joinPath(service.dataDir, "docs", `${id}.bin`);
|
const filePath = joinPath(service.dataDir, "docs", `${id}.bin`);
|
||||||
|
|
@ -924,11 +1028,7 @@ export function makeLibrarianService(config: LibrarianServiceConfig): LibrarianS
|
||||||
if (meta.parentId === undefined || meta.parentId.length === 0) {
|
if (meta.parentId === undefined || meta.parentId.length === 0) {
|
||||||
return yield* librarianServiceError("add-child-document", "add-child-document requires parentId in metadata");
|
return yield* librarianServiceError("add-child-document", "add-child-document requires parentId in metadata");
|
||||||
}
|
}
|
||||||
|
const parentId = meta.parentId;
|
||||||
// Verify parent exists
|
|
||||||
if (Boolean(service.documents.has(meta.parentId)) === false) {
|
|
||||||
return yield* librarianServiceError("add-child-document", `Parent document not found: ${meta.parentId}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const id = meta.id;
|
const id = meta.id;
|
||||||
const now = yield* currentEpochSeconds;
|
const now = yield* currentEpochSeconds;
|
||||||
|
|
@ -939,7 +1039,17 @@ export function makeLibrarianService(config: LibrarianServiceConfig): LibrarianS
|
||||||
time: now,
|
time: now,
|
||||||
};
|
};
|
||||||
|
|
||||||
service.documents.set(id, doc);
|
yield* SynchronizedRef.modifyEffect(service.state, (serviceState) => {
|
||||||
|
if (Boolean(serviceState.documents.has(parentId)) === false) {
|
||||||
|
return Effect.fail(librarianServiceError("add-child-document", `Parent document not found: ${parentId}`));
|
||||||
|
}
|
||||||
|
const documents = cloneDocuments(serviceState.documents);
|
||||||
|
documents.set(id, doc);
|
||||||
|
return Effect.succeed(modifyResult(undefined, {
|
||||||
|
...serviceState,
|
||||||
|
documents,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
// Store file content if provided
|
// Store file content if provided
|
||||||
if (request.content !== undefined && request.content.length > 0) {
|
if (request.content !== undefined && request.content.length > 0) {
|
||||||
|
|
@ -955,7 +1065,7 @@ export function makeLibrarianService(config: LibrarianServiceConfig): LibrarianS
|
||||||
try: () => service.persist(),
|
try: () => service.persist(),
|
||||||
catch: (cause) => librarianServiceError("add-child-document-persist", cause),
|
catch: (cause) => librarianServiceError("add-child-document-persist", cause),
|
||||||
});
|
});
|
||||||
yield* Effect.log(`[LibrarianService] Added child document ${id} (parent: ${meta.parentId})`);
|
yield* Effect.log(`[LibrarianService] Added child document ${id} (parent: ${parentId})`);
|
||||||
|
|
||||||
return service.documentResponse(doc);
|
return service.documentResponse(doc);
|
||||||
}),
|
}),
|
||||||
|
|
@ -991,7 +1101,14 @@ export function makeLibrarianService(config: LibrarianServiceConfig): LibrarianS
|
||||||
time: now,
|
time: now,
|
||||||
};
|
};
|
||||||
|
|
||||||
service.processing.set(id, record);
|
yield* SynchronizedRef.update(service.state, (serviceState) => {
|
||||||
|
const processing = cloneProcessing(serviceState.processing);
|
||||||
|
processing.set(id, record);
|
||||||
|
return {
|
||||||
|
...serviceState,
|
||||||
|
processing,
|
||||||
|
};
|
||||||
|
});
|
||||||
yield* Effect.tryPromise({
|
yield* Effect.tryPromise({
|
||||||
try: () => service.persist(),
|
try: () => service.persist(),
|
||||||
catch: (cause) => librarianServiceError("add-processing-persist", cause),
|
catch: (cause) => librarianServiceError("add-processing-persist", cause),
|
||||||
|
|
@ -1015,7 +1132,14 @@ export function makeLibrarianService(config: LibrarianServiceConfig): LibrarianS
|
||||||
return yield* librarianServiceError("remove-processing", "remove-processing requires processingId");
|
return yield* librarianServiceError("remove-processing", "remove-processing requires processingId");
|
||||||
}
|
}
|
||||||
|
|
||||||
service.processing.delete(id);
|
yield* SynchronizedRef.update(service.state, (serviceState) => {
|
||||||
|
const processing = cloneProcessing(serviceState.processing);
|
||||||
|
processing.delete(id);
|
||||||
|
return {
|
||||||
|
...serviceState,
|
||||||
|
processing,
|
||||||
|
};
|
||||||
|
});
|
||||||
yield* Effect.tryPromise({
|
yield* Effect.tryPromise({
|
||||||
try: () => service.persist(),
|
try: () => service.persist(),
|
||||||
catch: (cause) => librarianServiceError("remove-processing-persist", cause),
|
catch: (cause) => librarianServiceError("remove-processing-persist", cause),
|
||||||
|
|
@ -1032,8 +1156,9 @@ export function makeLibrarianService(config: LibrarianServiceConfig): LibrarianS
|
||||||
listProcessing: function(this: LibrarianService, request: LibrarianRequest): LibrarianResponse {
|
listProcessing: function(this: LibrarianService, request: LibrarianRequest): LibrarianResponse {
|
||||||
const documentId = this.documentId(request);
|
const documentId = this.documentId(request);
|
||||||
const records: ProcessingMetadata[] = [];
|
const records: ProcessingMetadata[] = [];
|
||||||
|
const serviceState = this.state.pipe(stateSnapshot);
|
||||||
|
|
||||||
for (const proc of this.processing.values()) {
|
for (const proc of serviceState.processing.values()) {
|
||||||
const procDocumentId = proc.documentId ?? proc["document-id"];
|
const procDocumentId = proc.documentId ?? proc["document-id"];
|
||||||
if (documentId !== undefined && documentId.length > 0 && procDocumentId !== documentId) {
|
if (documentId !== undefined && documentId.length > 0 && procDocumentId !== documentId) {
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -1066,7 +1191,7 @@ export function makeLibrarianService(config: LibrarianServiceConfig): LibrarianS
|
||||||
const uploadId = yield* randomUuid;
|
const uploadId = yield* randomUuid;
|
||||||
const createdAt = yield* currentIsoString;
|
const createdAt = yield* currentIsoString;
|
||||||
|
|
||||||
service.uploads.set(uploadId, {
|
const session: UploadSession = {
|
||||||
id: uploadId,
|
id: uploadId,
|
||||||
documentMetadata: meta,
|
documentMetadata: meta,
|
||||||
totalSize,
|
totalSize,
|
||||||
|
|
@ -1075,6 +1200,15 @@ export function makeLibrarianService(config: LibrarianServiceConfig): LibrarianS
|
||||||
createdAt,
|
createdAt,
|
||||||
chunks: new Map<number, string>(),
|
chunks: new Map<number, string>(),
|
||||||
user: meta.user ?? optionalString(req.user) ?? "default",
|
user: meta.user ?? optionalString(req.user) ?? "default",
|
||||||
|
};
|
||||||
|
|
||||||
|
yield* SynchronizedRef.update(service.state, (serviceState) => {
|
||||||
|
const uploads = cloneUploads(serviceState.uploads);
|
||||||
|
uploads.set(uploadId, session);
|
||||||
|
return {
|
||||||
|
...serviceState,
|
||||||
|
uploads,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -1102,7 +1236,7 @@ export function makeLibrarianService(config: LibrarianServiceConfig): LibrarianS
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const uploadId = optionalString(service.requestRecord(request)["upload-id"]);
|
const uploadId = optionalString(service.requestRecord(request)["upload-id"]);
|
||||||
if (uploadId === undefined) return yield* librarianServiceError("complete-upload", "complete-upload requires upload-id");
|
if (uploadId === undefined) return yield* librarianServiceError("complete-upload", "complete-upload requires upload-id");
|
||||||
const session = service.uploads.get(uploadId);
|
const session = (yield* SynchronizedRef.get(service.state)).uploads.get(uploadId);
|
||||||
if (session === undefined) return yield* librarianServiceError("complete-upload", `Upload not found: ${uploadId}`);
|
if (session === undefined) return yield* librarianServiceError("complete-upload", `Upload not found: ${uploadId}`);
|
||||||
if (session.chunks.size !== session.totalChunks) {
|
if (session.chunks.size !== session.totalChunks) {
|
||||||
return yield* librarianServiceError("complete-upload", `Upload incomplete: ${session.chunks.size}/${session.totalChunks} chunks received`);
|
return yield* librarianServiceError("complete-upload", `Upload incomplete: ${session.chunks.size}/${session.totalChunks} chunks received`);
|
||||||
|
|
@ -1119,7 +1253,14 @@ export function makeLibrarianService(config: LibrarianServiceConfig): LibrarianS
|
||||||
}),
|
}),
|
||||||
catch: (cause) => librarianServiceError("complete-upload-add-document", cause),
|
catch: (cause) => librarianServiceError("complete-upload-add-document", cause),
|
||||||
});
|
});
|
||||||
service.uploads.delete(uploadId);
|
yield* SynchronizedRef.update(service.state, (serviceState) => {
|
||||||
|
const uploads = cloneUploads(serviceState.uploads);
|
||||||
|
uploads.delete(uploadId);
|
||||||
|
return {
|
||||||
|
...serviceState,
|
||||||
|
uploads,
|
||||||
|
};
|
||||||
|
});
|
||||||
const documentId = response.documentMetadata?.id ?? response["document-metadata"]?.id ?? session.documentMetadata.id;
|
const documentId = response.documentMetadata?.id ?? response["document-metadata"]?.id ?? session.documentMetadata.id;
|
||||||
return {
|
return {
|
||||||
...response,
|
...response,
|
||||||
|
|
@ -1153,7 +1294,8 @@ export function makeLibrarianService(config: LibrarianServiceConfig): LibrarianS
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const user = optionalString(service.requestRecord(request).user);
|
const user = optionalString(service.requestRecord(request).user);
|
||||||
const sessions = [];
|
const sessions = [];
|
||||||
for (const session of service.uploads.values()) {
|
const serviceState = yield* SynchronizedRef.get(service.state);
|
||||||
|
for (const session of serviceState.uploads.values()) {
|
||||||
if (user !== undefined && session.user !== user) continue;
|
if (user !== undefined && session.user !== user) continue;
|
||||||
const documentMetadataJson = yield* encodeJsonString(
|
const documentMetadataJson = yield* encodeJsonString(
|
||||||
"list-uploads-document-metadata",
|
"list-uploads-document-metadata",
|
||||||
|
|
@ -1229,7 +1371,7 @@ export function makeLibrarianService(config: LibrarianServiceConfig): LibrarianS
|
||||||
|
|
||||||
const sendResponse = (response: CollectionManagementResponse): Effect.Effect<void, LibrarianServiceError> =>
|
const sendResponse = (response: CollectionManagementResponse): Effect.Effect<void, LibrarianServiceError> =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const producer = service.colProducer;
|
const producer = (yield* SynchronizedRef.get(service.state)).colProducer;
|
||||||
if (producer === null) {
|
if (producer === null) {
|
||||||
return yield* librarianServiceError("collection-respond", "Collection producer not started");
|
return yield* librarianServiceError("collection-respond", "Collection producer not started");
|
||||||
}
|
}
|
||||||
|
|
@ -1266,7 +1408,7 @@ export function makeLibrarianService(config: LibrarianServiceConfig): LibrarianS
|
||||||
switch (request.operation) {
|
switch (request.operation) {
|
||||||
case "list-collections": {
|
case "list-collections": {
|
||||||
const user = request.user ?? "";
|
const user = request.user ?? "";
|
||||||
const collections = service.collectionManager.listCollections(user);
|
const collections = (yield* SynchronizedRef.get(service.state)).collectionManager.listCollections(user);
|
||||||
return { collections };
|
return { collections };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1277,13 +1419,19 @@ export function makeLibrarianService(config: LibrarianServiceConfig): LibrarianS
|
||||||
const description = request.description ?? "";
|
const description = request.description ?? "";
|
||||||
const tags = request.tags ?? [];
|
const tags = request.tags ?? [];
|
||||||
|
|
||||||
service.collectionManager.updateCollection(user, collection, name, description, tags);
|
const collections = yield* SynchronizedRef.modifyEffect(service.state, (serviceState) => {
|
||||||
|
const collectionManager = cloneCollectionManager(serviceState.collectionManager);
|
||||||
|
collectionManager.updateCollection(user, collection, name, description, tags);
|
||||||
|
return Effect.succeed(modifyResult(collectionManager.listCollections(user), {
|
||||||
|
...serviceState,
|
||||||
|
collectionManager,
|
||||||
|
}));
|
||||||
|
});
|
||||||
yield* Effect.tryPromise({
|
yield* Effect.tryPromise({
|
||||||
try: () => service.persist(),
|
try: () => service.persist(),
|
||||||
catch: (cause) => librarianServiceError("update-collection-persist", cause),
|
catch: (cause) => librarianServiceError("update-collection-persist", cause),
|
||||||
});
|
});
|
||||||
|
|
||||||
const collections = service.collectionManager.listCollections(user);
|
|
||||||
return { collections };
|
return { collections };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1291,7 +1439,14 @@ export function makeLibrarianService(config: LibrarianServiceConfig): LibrarianS
|
||||||
const user = request.user ?? "";
|
const user = request.user ?? "";
|
||||||
const collection = request.collection ?? "";
|
const collection = request.collection ?? "";
|
||||||
|
|
||||||
service.collectionManager.deleteCollection(user, collection);
|
yield* SynchronizedRef.update(service.state, (serviceState) => {
|
||||||
|
const collectionManager = cloneCollectionManager(serviceState.collectionManager);
|
||||||
|
collectionManager.deleteCollection(user, collection);
|
||||||
|
return {
|
||||||
|
...serviceState,
|
||||||
|
collectionManager,
|
||||||
|
};
|
||||||
|
});
|
||||||
yield* Effect.tryPromise({
|
yield* Effect.tryPromise({
|
||||||
try: () => service.persist(),
|
try: () => service.persist(),
|
||||||
catch: (cause) => librarianServiceError("delete-collection-persist", cause),
|
catch: (cause) => librarianServiceError("delete-collection-persist", cause),
|
||||||
|
|
@ -1316,10 +1471,11 @@ export function makeLibrarianService(config: LibrarianServiceConfig): LibrarianS
|
||||||
const service = this;
|
const service = this;
|
||||||
return Effect.runPromise(
|
return Effect.runPromise(
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
|
const serviceState = yield* SynchronizedRef.get(service.state);
|
||||||
const data = {
|
const data = {
|
||||||
documents: Object.fromEntries(service.documents),
|
documents: Object.fromEntries(serviceState.documents),
|
||||||
processing: Object.fromEntries(service.processing),
|
processing: Object.fromEntries(serviceState.processing),
|
||||||
collections: service.collectionManager.toJSON(),
|
collections: serviceState.collectionManager.toJSON(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const json = yield* encodeJsonString("persist-encode", data);
|
const json = yield* encodeJsonString("persist-encode", data);
|
||||||
|
|
@ -1358,26 +1514,34 @@ export function makeLibrarianService(config: LibrarianServiceConfig): LibrarianS
|
||||||
|
|
||||||
if (parsed === null) return;
|
if (parsed === null) return;
|
||||||
|
|
||||||
service.documents.clear();
|
const documents = new Map<string, DocumentMetadata>();
|
||||||
if (parsed.documents !== undefined) {
|
if (parsed.documents !== undefined) {
|
||||||
for (const [id, doc] of Object.entries(parsed.documents)) {
|
for (const [id, doc] of Object.entries(parsed.documents)) {
|
||||||
service.documents.set(id, service.publicDocument(doc));
|
documents.set(id, service.publicDocument(doc));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
service.processing.clear();
|
const processing = new Map<string, ProcessingMetadata>();
|
||||||
if (parsed.processing !== undefined) {
|
if (parsed.processing !== undefined) {
|
||||||
for (const [id, proc] of Object.entries(parsed.processing)) {
|
for (const [id, proc] of Object.entries(parsed.processing)) {
|
||||||
service.processing.set(id, service.publicProcessing(proc));
|
processing.set(id, service.publicProcessing(proc));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const collectionManager = makeCollectionManager();
|
||||||
if (parsed.collections !== undefined) {
|
if (parsed.collections !== undefined) {
|
||||||
service.collectionManager.loadFromJSON(parsed.collections);
|
collectionManager.loadFromJSON(parsed.collections);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
yield* SynchronizedRef.update(service.state, (serviceState) => ({
|
||||||
|
...serviceState,
|
||||||
|
documents,
|
||||||
|
processing,
|
||||||
|
collectionManager,
|
||||||
|
}));
|
||||||
|
|
||||||
yield* Effect.log(
|
yield* Effect.log(
|
||||||
`[LibrarianService] Loaded persisted state (documents=${service.documents.size}, processing=${service.processing.size})`,
|
`[LibrarianService] Loaded persisted state (documents=${documents.size}, processing=${processing.size})`,
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
@ -1390,38 +1554,41 @@ export function makeLibrarianService(config: LibrarianServiceConfig): LibrarianS
|
||||||
const service = this;
|
const service = this;
|
||||||
return Effect.runPromise(
|
return Effect.runPromise(
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const libConsumer = service.libConsumer;
|
const serviceState = yield* SynchronizedRef.get(service.state);
|
||||||
|
const libConsumer = serviceState.libConsumer;
|
||||||
if (libConsumer !== null) {
|
if (libConsumer !== null) {
|
||||||
yield* Effect.tryPromise({
|
yield* Effect.tryPromise({
|
||||||
try: () => libConsumer.close(),
|
try: () => libConsumer.close(),
|
||||||
catch: (cause) => librarianServiceError("close-librarian-consumer", cause),
|
catch: (cause) => librarianServiceError("close-librarian-consumer", cause),
|
||||||
});
|
});
|
||||||
service.libConsumer = null;
|
|
||||||
}
|
}
|
||||||
const libProducer = service.libProducer;
|
const libProducer = serviceState.libProducer;
|
||||||
if (libProducer !== null) {
|
if (libProducer !== null) {
|
||||||
yield* Effect.tryPromise({
|
yield* Effect.tryPromise({
|
||||||
try: () => libProducer.close(),
|
try: () => libProducer.close(),
|
||||||
catch: (cause) => librarianServiceError("close-librarian-producer", cause),
|
catch: (cause) => librarianServiceError("close-librarian-producer", cause),
|
||||||
});
|
});
|
||||||
service.libProducer = null;
|
|
||||||
}
|
}
|
||||||
const colConsumer = service.colConsumer;
|
const colConsumer = serviceState.colConsumer;
|
||||||
if (colConsumer !== null) {
|
if (colConsumer !== null) {
|
||||||
yield* Effect.tryPromise({
|
yield* Effect.tryPromise({
|
||||||
try: () => colConsumer.close(),
|
try: () => colConsumer.close(),
|
||||||
catch: (cause) => librarianServiceError("close-collection-consumer", cause),
|
catch: (cause) => librarianServiceError("close-collection-consumer", cause),
|
||||||
});
|
});
|
||||||
service.colConsumer = null;
|
|
||||||
}
|
}
|
||||||
const colProducer = service.colProducer;
|
const colProducer = serviceState.colProducer;
|
||||||
if (colProducer !== null) {
|
if (colProducer !== null) {
|
||||||
yield* Effect.tryPromise({
|
yield* Effect.tryPromise({
|
||||||
try: () => colProducer.close(),
|
try: () => colProducer.close(),
|
||||||
catch: (cause) => librarianServiceError("close-collection-producer", cause),
|
catch: (cause) => librarianServiceError("close-collection-producer", cause),
|
||||||
});
|
});
|
||||||
service.colProducer = null;
|
|
||||||
}
|
}
|
||||||
|
yield* updateHandles(service.state, {
|
||||||
|
libConsumer: null,
|
||||||
|
libProducer: null,
|
||||||
|
colConsumer: null,
|
||||||
|
colProducer: null,
|
||||||
|
});
|
||||||
yield* Effect.tryPromise({
|
yield* Effect.tryPromise({
|
||||||
try: () => baseStop(),
|
try: () => baseStop(),
|
||||||
catch: (cause) => librarianServiceError("stop", cause),
|
catch: (cause) => librarianServiceError("stop", cause),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue