@polpo-ai/core 0.15.47 → 0.15.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/brain-contracts.test.d.ts +2 -0
- package/dist/__tests__/brain-contracts.test.d.ts.map +1 -0
- package/dist/__tests__/brain-contracts.test.js +366 -0
- package/dist/__tests__/brain-contracts.test.js.map +1 -0
- package/dist/__tests__/brain-local.test.d.ts +2 -0
- package/dist/__tests__/brain-local.test.d.ts.map +1 -0
- package/dist/__tests__/brain-local.test.js +627 -0
- package/dist/__tests__/brain-local.test.js.map +1 -0
- package/dist/__tests__/brain-service.test.d.ts +2 -0
- package/dist/__tests__/brain-service.test.d.ts.map +1 -0
- package/dist/__tests__/brain-service.test.js +193 -0
- package/dist/__tests__/brain-service.test.js.map +1 -0
- package/dist/adapter.d.ts +5 -0
- package/dist/adapter.d.ts.map +1 -1
- package/dist/brain/chunking.d.ts +13 -0
- package/dist/brain/chunking.d.ts.map +1 -0
- package/dist/brain/chunking.js +99 -0
- package/dist/brain/chunking.js.map +1 -0
- package/dist/brain/contracts.d.ts +15 -0
- package/dist/brain/contracts.d.ts.map +1 -0
- package/dist/brain/contracts.js +361 -0
- package/dist/brain/contracts.js.map +1 -0
- package/dist/brain/errors.d.ts +7 -0
- package/dist/brain/errors.d.ts.map +1 -0
- package/dist/brain/errors.js +11 -0
- package/dist/brain/errors.js.map +1 -0
- package/dist/brain/in-memory-store.d.ts +59 -0
- package/dist/brain/in-memory-store.d.ts.map +1 -0
- package/dist/brain/in-memory-store.js +797 -0
- package/dist/brain/in-memory-store.js.map +1 -0
- package/dist/brain/index.d.ts +24 -0
- package/dist/brain/index.d.ts.map +1 -0
- package/dist/brain/index.js +14 -0
- package/dist/brain/index.js.map +1 -0
- package/dist/brain/ingestion.d.ts +23 -0
- package/dist/brain/ingestion.d.ts.map +1 -0
- package/dist/brain/ingestion.js +161 -0
- package/dist/brain/ingestion.js.map +1 -0
- package/dist/brain/lifecycle.d.ts +6 -0
- package/dist/brain/lifecycle.d.ts.map +1 -0
- package/dist/brain/lifecycle.js +50 -0
- package/dist/brain/lifecycle.js.map +1 -0
- package/dist/brain/parsers.d.ts +7 -0
- package/dist/brain/parsers.d.ts.map +1 -0
- package/dist/brain/parsers.js +47 -0
- package/dist/brain/parsers.js.map +1 -0
- package/dist/brain/ports.d.ts +18 -0
- package/dist/brain/ports.d.ts.map +1 -0
- package/dist/brain/ports.js +2 -0
- package/dist/brain/ports.js.map +1 -0
- package/dist/brain/reading.d.ts +25 -0
- package/dist/brain/reading.d.ts.map +1 -0
- package/dist/brain/reading.js +96 -0
- package/dist/brain/reading.js.map +1 -0
- package/dist/brain/retrieval.d.ts +19 -0
- package/dist/brain/retrieval.d.ts.map +1 -0
- package/dist/brain/retrieval.js +134 -0
- package/dist/brain/retrieval.js.map +1 -0
- package/dist/brain/scope.d.ts +5 -0
- package/dist/brain/scope.d.ts.map +1 -0
- package/dist/brain/scope.js +26 -0
- package/dist/brain/scope.js.map +1 -0
- package/dist/brain/service.d.ts +68 -0
- package/dist/brain/service.d.ts.map +1 -0
- package/dist/brain/service.js +2 -0
- package/dist/brain/service.js.map +1 -0
- package/dist/brain/store-errors.d.ts +17 -0
- package/dist/brain/store-errors.d.ts.map +1 -0
- package/dist/brain/store-errors.js +39 -0
- package/dist/brain/store-errors.js.map +1 -0
- package/dist/brain/stores.d.ts +44 -0
- package/dist/brain/stores.d.ts.map +1 -0
- package/dist/brain/stores.js +2 -0
- package/dist/brain/stores.js.map +1 -0
- package/dist/brain/types.d.ts +300 -0
- package/dist/brain/types.d.ts.map +1 -0
- package/dist/brain/types.js +43 -0
- package/dist/brain/types.js.map +1 -0
- package/dist/brain/validation.d.ts +12 -0
- package/dist/brain/validation.d.ts.map +1 -0
- package/dist/brain/validation.js +129 -0
- package/dist/brain/validation.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/package.json +5 -1
|
@@ -0,0 +1,797 @@
|
|
|
1
|
+
import { nanoid } from "nanoid";
|
|
2
|
+
import { createBrainIngestionJob, createBrainRetrievalResult, normalizeBrainChunk, normalizeBrainIngestionJob, normalizeBrainSource, normalizeBrainSourceVersion, } from "./contracts.js";
|
|
3
|
+
import { assertBrainIngestionJobStatusTransition, assertBrainSourceStatusTransition, assertBrainVersionStatusTransition, isBrainSourceRetrievable, } from "./lifecycle.js";
|
|
4
|
+
import { brainScopeKey, normalizeBrainScope } from "./scope.js";
|
|
5
|
+
import { BrainStoreConflictError, BrainStoreValidationError, } from "./store-errors.js";
|
|
6
|
+
function timestamp(value) {
|
|
7
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
8
|
+
if (!Number.isFinite(date.getTime())) {
|
|
9
|
+
throw new BrainStoreValidationError("Store time must be a valid timestamp");
|
|
10
|
+
}
|
|
11
|
+
return date.toISOString();
|
|
12
|
+
}
|
|
13
|
+
function text(value, path, max = 1_024) {
|
|
14
|
+
if (typeof value !== "string") {
|
|
15
|
+
throw new BrainStoreValidationError(`${path} must be a string`);
|
|
16
|
+
}
|
|
17
|
+
const normalized = value.trim();
|
|
18
|
+
if (!normalized || normalized.length > max) {
|
|
19
|
+
throw new BrainStoreValidationError(`${path} must contain between 1 and ${max} characters`);
|
|
20
|
+
}
|
|
21
|
+
return normalized;
|
|
22
|
+
}
|
|
23
|
+
function scopeKey(scope) {
|
|
24
|
+
return brainScopeKey(normalizeBrainScope(scope));
|
|
25
|
+
}
|
|
26
|
+
function sourceKey(scope, sourceId) {
|
|
27
|
+
return JSON.stringify([scopeKey(scope), text(sourceId, "sourceId", 512)]);
|
|
28
|
+
}
|
|
29
|
+
function versionKey(ref) {
|
|
30
|
+
return JSON.stringify([
|
|
31
|
+
sourceKey(ref.scope, ref.sourceId),
|
|
32
|
+
text(ref.version, "version", 512),
|
|
33
|
+
]);
|
|
34
|
+
}
|
|
35
|
+
function sourceKeyFromVersionKey(key) {
|
|
36
|
+
const parsed = JSON.parse(key);
|
|
37
|
+
if (!Array.isArray(parsed)
|
|
38
|
+
|| parsed.length !== 2
|
|
39
|
+
|| typeof parsed[0] !== "string") {
|
|
40
|
+
throw new BrainStoreValidationError("Malformed Brain version key");
|
|
41
|
+
}
|
|
42
|
+
return parsed[0];
|
|
43
|
+
}
|
|
44
|
+
function scopeFromSourceKey(key) {
|
|
45
|
+
const parsed = JSON.parse(key);
|
|
46
|
+
if (!Array.isArray(parsed)
|
|
47
|
+
|| parsed.length !== 2
|
|
48
|
+
|| typeof parsed[0] !== "string") {
|
|
49
|
+
throw new BrainStoreValidationError("Malformed Brain source key");
|
|
50
|
+
}
|
|
51
|
+
const parsedScope = JSON.parse(parsed[0]);
|
|
52
|
+
if (!Array.isArray(parsedScope)
|
|
53
|
+
|| parsedScope.length !== 2
|
|
54
|
+
|| typeof parsedScope[0] !== "string"
|
|
55
|
+
|| typeof parsedScope[1] !== "string") {
|
|
56
|
+
throw new BrainStoreValidationError("Malformed Brain scope key");
|
|
57
|
+
}
|
|
58
|
+
return normalizeBrainScope({
|
|
59
|
+
kind: parsedScope[0],
|
|
60
|
+
subjectId: parsedScope[1],
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
function scopeFromVersionKey(key) {
|
|
64
|
+
return scopeFromSourceKey(sourceKeyFromVersionKey(key));
|
|
65
|
+
}
|
|
66
|
+
function jobKey(scope, jobId) {
|
|
67
|
+
return JSON.stringify([scopeKey(scope), text(jobId, "jobId", 512)]);
|
|
68
|
+
}
|
|
69
|
+
function dedupeKey(job) {
|
|
70
|
+
return JSON.stringify([scopeKey(job.scope), job.dedupeKey]);
|
|
71
|
+
}
|
|
72
|
+
function cloneSource(value) {
|
|
73
|
+
return normalizeBrainSource(JSON.parse(JSON.stringify(value)));
|
|
74
|
+
}
|
|
75
|
+
function cloneVersion(value) {
|
|
76
|
+
return normalizeBrainSourceVersion(JSON.parse(JSON.stringify(value)));
|
|
77
|
+
}
|
|
78
|
+
function cloneChunk(value) {
|
|
79
|
+
return normalizeBrainChunk(JSON.parse(JSON.stringify(value)));
|
|
80
|
+
}
|
|
81
|
+
function cloneJob(value) {
|
|
82
|
+
return normalizeBrainIngestionJob(JSON.parse(JSON.stringify(value)));
|
|
83
|
+
}
|
|
84
|
+
function normalizeLimit(value, fallback = 100) {
|
|
85
|
+
if (value === undefined)
|
|
86
|
+
return fallback;
|
|
87
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > 1_000) {
|
|
88
|
+
throw new BrainStoreValidationError("limit must be an integer between 1 and 1000");
|
|
89
|
+
}
|
|
90
|
+
return value;
|
|
91
|
+
}
|
|
92
|
+
function normalizeScopes(scopes) {
|
|
93
|
+
if (!Array.isArray(scopes) || scopes.length === 0) {
|
|
94
|
+
throw new BrainStoreValidationError("At least one explicit Brain scope is required");
|
|
95
|
+
}
|
|
96
|
+
const unique = new Map();
|
|
97
|
+
for (const scope of scopes) {
|
|
98
|
+
const normalized = normalizeBrainScope(scope);
|
|
99
|
+
unique.set(scopeKey(normalized), normalized);
|
|
100
|
+
}
|
|
101
|
+
return [...unique.values()];
|
|
102
|
+
}
|
|
103
|
+
function normalizeSourceRefs(refs) {
|
|
104
|
+
if (!Array.isArray(refs)) {
|
|
105
|
+
throw new BrainStoreValidationError("sources must be an array");
|
|
106
|
+
}
|
|
107
|
+
const unique = new Map();
|
|
108
|
+
for (const ref of refs) {
|
|
109
|
+
let scope;
|
|
110
|
+
try {
|
|
111
|
+
scope = normalizeBrainScope(ref.scope);
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
throw new BrainStoreValidationError(`sources[].scope is invalid: ${error instanceof Error ? error.message : "validation failed"}`);
|
|
115
|
+
}
|
|
116
|
+
const sourceId = text(ref.sourceId, "sources[].sourceId", 512);
|
|
117
|
+
unique.set(sourceKey(scope, sourceId), Object.freeze({ scope, sourceId }));
|
|
118
|
+
}
|
|
119
|
+
return [...unique.values()];
|
|
120
|
+
}
|
|
121
|
+
function terms(value) {
|
|
122
|
+
return [...new Set(value
|
|
123
|
+
.normalize("NFKC")
|
|
124
|
+
.toLocaleLowerCase("en-US")
|
|
125
|
+
.match(/[\p{L}\p{N}_-]+/gu) ?? [])];
|
|
126
|
+
}
|
|
127
|
+
function lexicalScore(content, queryTerms) {
|
|
128
|
+
if (queryTerms.length === 0)
|
|
129
|
+
return 0;
|
|
130
|
+
const contentTerms = terms(content);
|
|
131
|
+
if (contentTerms.length === 0)
|
|
132
|
+
return 0;
|
|
133
|
+
const frequencies = new Map();
|
|
134
|
+
for (const term of contentTerms) {
|
|
135
|
+
frequencies.set(term, (frequencies.get(term) ?? 0) + 1);
|
|
136
|
+
}
|
|
137
|
+
let matched = 0;
|
|
138
|
+
let frequency = 0;
|
|
139
|
+
for (const term of queryTerms) {
|
|
140
|
+
const count = frequencies.get(term) ?? 0;
|
|
141
|
+
if (count > 0)
|
|
142
|
+
matched += 1;
|
|
143
|
+
frequency += Math.min(count, 4);
|
|
144
|
+
}
|
|
145
|
+
if (matched === 0)
|
|
146
|
+
return 0;
|
|
147
|
+
return matched / queryTerms.length + frequency / (queryTerms.length * 20);
|
|
148
|
+
}
|
|
149
|
+
function cosine(left, right) {
|
|
150
|
+
if (left.length === 0 || left.length !== right.length)
|
|
151
|
+
return 0;
|
|
152
|
+
let dot = 0;
|
|
153
|
+
let leftMagnitude = 0;
|
|
154
|
+
let rightMagnitude = 0;
|
|
155
|
+
for (let index = 0; index < left.length; index += 1) {
|
|
156
|
+
const l = left[index];
|
|
157
|
+
const r = right[index];
|
|
158
|
+
if (!Number.isFinite(l) || !Number.isFinite(r))
|
|
159
|
+
return 0;
|
|
160
|
+
dot += l * r;
|
|
161
|
+
leftMagnitude += l * l;
|
|
162
|
+
rightMagnitude += r * r;
|
|
163
|
+
}
|
|
164
|
+
if (leftMagnitude === 0 || rightMagnitude === 0)
|
|
165
|
+
return 0;
|
|
166
|
+
const score = dot / Math.sqrt(leftMagnitude * rightMagnitude);
|
|
167
|
+
return Number.isFinite(score) ? score : 0;
|
|
168
|
+
}
|
|
169
|
+
function validateVectors(vectors, expectedCount, expectedDimensions) {
|
|
170
|
+
return (vectors.length === expectedCount
|
|
171
|
+
&& expectedDimensions > 0
|
|
172
|
+
&& vectors.every((vector) => (vector.length === expectedDimensions
|
|
173
|
+
&& vector.every(Number.isFinite))));
|
|
174
|
+
}
|
|
175
|
+
export class InMemoryBrainStore {
|
|
176
|
+
sources = new Map();
|
|
177
|
+
versions = new Map();
|
|
178
|
+
chunks = new Map();
|
|
179
|
+
jobs = new Map();
|
|
180
|
+
jobsByDedupe = new Map();
|
|
181
|
+
vectors = new Map();
|
|
182
|
+
now;
|
|
183
|
+
createId;
|
|
184
|
+
embeddingProvider;
|
|
185
|
+
embeddingFailureMode;
|
|
186
|
+
constructor(options = {}) {
|
|
187
|
+
this.now = options.now ?? (() => new Date());
|
|
188
|
+
this.createId = options.createId ?? (() => nanoid(16));
|
|
189
|
+
this.embeddingProvider = options.embeddingProvider;
|
|
190
|
+
this.embeddingFailureMode = options.embeddingFailureMode ?? "fallback";
|
|
191
|
+
if (options.snapshot)
|
|
192
|
+
this.restore(options.snapshot);
|
|
193
|
+
}
|
|
194
|
+
at() {
|
|
195
|
+
return timestamp(this.now());
|
|
196
|
+
}
|
|
197
|
+
source(ref) {
|
|
198
|
+
return this.sources.get(sourceKey(ref.scope, ref.sourceId));
|
|
199
|
+
}
|
|
200
|
+
version(ref) {
|
|
201
|
+
return this.versions.get(versionKey(ref));
|
|
202
|
+
}
|
|
203
|
+
async createSource(value) {
|
|
204
|
+
const source = cloneSource(value);
|
|
205
|
+
const key = sourceKey(source.scope, source.id);
|
|
206
|
+
if (this.sources.has(key)) {
|
|
207
|
+
throw new BrainStoreConflictError(`Brain source "${source.id}" already exists in this scope`);
|
|
208
|
+
}
|
|
209
|
+
this.sources.set(key, source);
|
|
210
|
+
return cloneSource(source);
|
|
211
|
+
}
|
|
212
|
+
async getSource(ref) {
|
|
213
|
+
const source = this.source(ref);
|
|
214
|
+
return source ? cloneSource(source) : null;
|
|
215
|
+
}
|
|
216
|
+
async listSources(query) {
|
|
217
|
+
const scopes = normalizeScopes(query.scopes);
|
|
218
|
+
const allowedScopes = new Set(scopes.map(scopeKey));
|
|
219
|
+
const limit = normalizeLimit(query.limit);
|
|
220
|
+
const offset = query.cursor === undefined
|
|
221
|
+
? 0
|
|
222
|
+
: Number.parseInt(query.cursor, 10);
|
|
223
|
+
if (!Number.isSafeInteger(offset) || offset < 0) {
|
|
224
|
+
throw new BrainStoreValidationError("cursor must be a valid offset");
|
|
225
|
+
}
|
|
226
|
+
const statuses = query.statuses
|
|
227
|
+
? new Set(query.statuses)
|
|
228
|
+
: undefined;
|
|
229
|
+
const types = query.types ? new Set(query.types) : undefined;
|
|
230
|
+
const matches = [...this.sources.values()]
|
|
231
|
+
.filter((source) => allowedScopes.has(scopeKey(source.scope)))
|
|
232
|
+
.filter((source) => !statuses || statuses.has(source.status))
|
|
233
|
+
.filter((source) => !types || types.has(source.type))
|
|
234
|
+
.sort((left, right) => (left.createdAt.localeCompare(right.createdAt)
|
|
235
|
+
|| left.id.localeCompare(right.id)));
|
|
236
|
+
const selected = matches.slice(offset, offset + limit).map(cloneSource);
|
|
237
|
+
const cursor = offset + selected.length < matches.length
|
|
238
|
+
? String(offset + selected.length)
|
|
239
|
+
: undefined;
|
|
240
|
+
return Object.freeze({
|
|
241
|
+
sources: Object.freeze(selected),
|
|
242
|
+
...(cursor ? { cursor } : {}),
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
async updateSource(value, options = {}) {
|
|
246
|
+
const candidate = cloneSource(value);
|
|
247
|
+
const key = sourceKey(candidate.scope, candidate.id);
|
|
248
|
+
const current = this.sources.get(key);
|
|
249
|
+
if (!current) {
|
|
250
|
+
throw new BrainStoreConflictError(`Brain source "${candidate.id}" does not exist in this scope`);
|
|
251
|
+
}
|
|
252
|
+
if (options.expectedUpdatedAt !== undefined
|
|
253
|
+
&& current.updatedAt !== options.expectedUpdatedAt) {
|
|
254
|
+
throw new BrainStoreConflictError(`Brain source "${candidate.id}" changed during update`);
|
|
255
|
+
}
|
|
256
|
+
assertBrainSourceStatusTransition(current.status, candidate.status);
|
|
257
|
+
this.sources.set(key, candidate);
|
|
258
|
+
return cloneSource(candidate);
|
|
259
|
+
}
|
|
260
|
+
async publishVersion(input) {
|
|
261
|
+
const source = this.source(input);
|
|
262
|
+
const version = this.version(input);
|
|
263
|
+
const chunks = this.chunks.get(versionKey(input)) ?? [];
|
|
264
|
+
if (!source || !version) {
|
|
265
|
+
throw new BrainStoreConflictError("Brain source and source version must exist before publish");
|
|
266
|
+
}
|
|
267
|
+
if (version.status !== "indexing") {
|
|
268
|
+
throw new BrainStoreConflictError("Only an indexing Brain source version can be published");
|
|
269
|
+
}
|
|
270
|
+
if (chunks.length === 0) {
|
|
271
|
+
throw new BrainStoreValidationError("A Brain source version cannot be published without chunks");
|
|
272
|
+
}
|
|
273
|
+
if (Object.hasOwn(input, "expectedCurrentVersion")) {
|
|
274
|
+
const expected = input.expectedCurrentVersion ?? undefined;
|
|
275
|
+
if (source.currentVersion !== expected) {
|
|
276
|
+
throw new BrainStoreConflictError("Brain source current version changed before publish");
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
const at = this.at();
|
|
280
|
+
const indexed = normalizeBrainSourceVersion({
|
|
281
|
+
...version,
|
|
282
|
+
status: "indexed",
|
|
283
|
+
indexedAt: at,
|
|
284
|
+
updatedAt: at,
|
|
285
|
+
failure: undefined,
|
|
286
|
+
});
|
|
287
|
+
const published = normalizeBrainSource({
|
|
288
|
+
...source,
|
|
289
|
+
status: "indexed",
|
|
290
|
+
currentVersion: version.version,
|
|
291
|
+
failure: undefined,
|
|
292
|
+
updatedAt: at,
|
|
293
|
+
});
|
|
294
|
+
let priorVersion;
|
|
295
|
+
if (source.currentVersion && source.currentVersion !== version.version) {
|
|
296
|
+
const priorKey = versionKey({ ...input, version: source.currentVersion });
|
|
297
|
+
const prior = this.versions.get(priorKey);
|
|
298
|
+
if (prior?.status === "indexed") {
|
|
299
|
+
priorVersion = normalizeBrainSourceVersion({
|
|
300
|
+
...prior,
|
|
301
|
+
status: "superseded",
|
|
302
|
+
updatedAt: at,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
// All candidates are validated above; these synchronous writes form one
|
|
307
|
+
// observable publish boundary for the in-memory reference adapter.
|
|
308
|
+
this.versions.set(versionKey(input), indexed);
|
|
309
|
+
if (priorVersion) {
|
|
310
|
+
this.versions.set(versionKey({
|
|
311
|
+
...input,
|
|
312
|
+
version: priorVersion.version,
|
|
313
|
+
}), priorVersion);
|
|
314
|
+
}
|
|
315
|
+
this.sources.set(sourceKey(input.scope, input.sourceId), published);
|
|
316
|
+
return cloneSource(published);
|
|
317
|
+
}
|
|
318
|
+
async deleteSource(ref) {
|
|
319
|
+
const source = this.source(ref);
|
|
320
|
+
if (!source || source.status === "deleted")
|
|
321
|
+
return;
|
|
322
|
+
const deleted = normalizeBrainSource({
|
|
323
|
+
...source,
|
|
324
|
+
status: "deleted",
|
|
325
|
+
updatedAt: this.at(),
|
|
326
|
+
});
|
|
327
|
+
this.sources.set(sourceKey(ref.scope, ref.sourceId), deleted);
|
|
328
|
+
await this.deleteSourceChunks(ref);
|
|
329
|
+
}
|
|
330
|
+
async createVersion(scope, value) {
|
|
331
|
+
const normalizedScope = normalizeBrainScope(scope);
|
|
332
|
+
const version = cloneVersion(value);
|
|
333
|
+
if (!this.source({ scope: normalizedScope, sourceId: version.sourceId })) {
|
|
334
|
+
throw new BrainStoreConflictError(`Brain source "${version.sourceId}" does not exist in this scope`);
|
|
335
|
+
}
|
|
336
|
+
const key = versionKey({
|
|
337
|
+
scope: normalizedScope,
|
|
338
|
+
sourceId: version.sourceId,
|
|
339
|
+
version: version.version,
|
|
340
|
+
});
|
|
341
|
+
if (this.versions.has(key)) {
|
|
342
|
+
throw new BrainStoreConflictError(`Brain source version "${version.version}" already exists`);
|
|
343
|
+
}
|
|
344
|
+
this.versions.set(key, version);
|
|
345
|
+
return cloneVersion(version);
|
|
346
|
+
}
|
|
347
|
+
async getVersion(ref) {
|
|
348
|
+
const version = this.version(ref);
|
|
349
|
+
return version ? cloneVersion(version) : null;
|
|
350
|
+
}
|
|
351
|
+
async listVersions(ref) {
|
|
352
|
+
const prefix = sourceKey(ref.scope, ref.sourceId);
|
|
353
|
+
return Object.freeze([...this.versions.entries()]
|
|
354
|
+
.filter(([key]) => sourceKeyFromVersionKey(key) === prefix)
|
|
355
|
+
.map(([, version]) => cloneVersion(version))
|
|
356
|
+
.sort((left, right) => (left.createdAt.localeCompare(right.createdAt)
|
|
357
|
+
|| left.version.localeCompare(right.version))));
|
|
358
|
+
}
|
|
359
|
+
async updateVersion(scope, value, options = {}) {
|
|
360
|
+
const candidate = cloneVersion(value);
|
|
361
|
+
const ref = {
|
|
362
|
+
scope: normalizeBrainScope(scope),
|
|
363
|
+
sourceId: candidate.sourceId,
|
|
364
|
+
version: candidate.version,
|
|
365
|
+
};
|
|
366
|
+
const key = versionKey(ref);
|
|
367
|
+
const current = this.versions.get(key);
|
|
368
|
+
if (!current) {
|
|
369
|
+
throw new BrainStoreConflictError(`Brain source version "${candidate.version}" does not exist`);
|
|
370
|
+
}
|
|
371
|
+
if (options.expectedUpdatedAt !== undefined
|
|
372
|
+
&& current.updatedAt !== options.expectedUpdatedAt) {
|
|
373
|
+
throw new BrainStoreConflictError(`Brain source version "${candidate.version}" changed during update`);
|
|
374
|
+
}
|
|
375
|
+
assertBrainVersionStatusTransition(current.status, candidate.status);
|
|
376
|
+
this.versions.set(key, candidate);
|
|
377
|
+
return cloneVersion(candidate);
|
|
378
|
+
}
|
|
379
|
+
async deleteVersion(ref) {
|
|
380
|
+
const current = this.version(ref);
|
|
381
|
+
if (!current || current.status === "deleted")
|
|
382
|
+
return;
|
|
383
|
+
this.versions.set(versionKey(ref), normalizeBrainSourceVersion({
|
|
384
|
+
...current,
|
|
385
|
+
status: "deleted",
|
|
386
|
+
updatedAt: this.at(),
|
|
387
|
+
}));
|
|
388
|
+
await this.deleteVersionChunks(ref);
|
|
389
|
+
}
|
|
390
|
+
async replaceVersionChunks(input) {
|
|
391
|
+
const version = this.version(input);
|
|
392
|
+
if (!version) {
|
|
393
|
+
throw new BrainStoreConflictError("Brain source version does not exist");
|
|
394
|
+
}
|
|
395
|
+
if (version.status !== "pending" && version.status !== "indexing") {
|
|
396
|
+
throw new BrainStoreConflictError("Chunks may only be replaced for an unpublished version");
|
|
397
|
+
}
|
|
398
|
+
let chunks;
|
|
399
|
+
try {
|
|
400
|
+
chunks = input.chunks.map(cloneChunk);
|
|
401
|
+
}
|
|
402
|
+
catch (error) {
|
|
403
|
+
throw new BrainStoreValidationError(`Replacement chunks are invalid: ${error instanceof Error ? error.message : "validation failed"}`);
|
|
404
|
+
}
|
|
405
|
+
const ids = new Set();
|
|
406
|
+
const indexes = new Set();
|
|
407
|
+
for (const chunk of chunks) {
|
|
408
|
+
if (chunk.sourceId !== input.sourceId
|
|
409
|
+
|| chunk.version !== input.version) {
|
|
410
|
+
throw new BrainStoreValidationError("Every replacement chunk must match the source and version");
|
|
411
|
+
}
|
|
412
|
+
if (ids.has(chunk.id) || indexes.has(chunk.index)) {
|
|
413
|
+
throw new BrainStoreValidationError("Replacement chunks must have unique ids and indexes");
|
|
414
|
+
}
|
|
415
|
+
ids.add(chunk.id);
|
|
416
|
+
indexes.add(chunk.index);
|
|
417
|
+
}
|
|
418
|
+
if (chunks.some((chunk, index) => chunk.index !== index)) {
|
|
419
|
+
throw new BrainStoreValidationError("Replacement chunk indexes must be contiguous and ordered");
|
|
420
|
+
}
|
|
421
|
+
let embedded;
|
|
422
|
+
let model;
|
|
423
|
+
if (this.embeddingProvider && chunks.length > 0) {
|
|
424
|
+
try {
|
|
425
|
+
const result = await this.embeddingProvider.embed({
|
|
426
|
+
texts: chunks.map((chunk) => chunk.content),
|
|
427
|
+
});
|
|
428
|
+
if (!validateVectors(result.vectors, chunks.length, result.dimensions)) {
|
|
429
|
+
throw new BrainStoreValidationError("Embedding provider returned malformed vectors");
|
|
430
|
+
}
|
|
431
|
+
embedded = result.vectors;
|
|
432
|
+
model = result.model;
|
|
433
|
+
}
|
|
434
|
+
catch (error) {
|
|
435
|
+
if (this.embeddingFailureMode === "strict")
|
|
436
|
+
throw error;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
const key = versionKey(input);
|
|
440
|
+
this.chunks.set(key, Object.freeze(chunks));
|
|
441
|
+
for (const [storedKey] of this.vectors) {
|
|
442
|
+
if (storedKey.startsWith(`${key}:`))
|
|
443
|
+
this.vectors.delete(storedKey);
|
|
444
|
+
}
|
|
445
|
+
if (embedded && model) {
|
|
446
|
+
chunks.forEach((chunk, index) => {
|
|
447
|
+
this.vectors.set(`${key}:${chunk.id}`, {
|
|
448
|
+
values: Object.freeze([...embedded[index]]),
|
|
449
|
+
model: model,
|
|
450
|
+
});
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
async listVersionChunks(ref) {
|
|
455
|
+
return Object.freeze((this.chunks.get(versionKey(ref)) ?? []).map(cloneChunk));
|
|
456
|
+
}
|
|
457
|
+
async searchCandidates(query) {
|
|
458
|
+
const sources = normalizeSourceRefs(query.sources);
|
|
459
|
+
if (sources.length === 0) {
|
|
460
|
+
return Object.freeze([]);
|
|
461
|
+
}
|
|
462
|
+
const queryText = text(query.query, "query", 16_000);
|
|
463
|
+
const limit = normalizeLimit(query.limit, 10);
|
|
464
|
+
const queryTerms = terms(queryText);
|
|
465
|
+
let queryVector;
|
|
466
|
+
if (this.embeddingProvider) {
|
|
467
|
+
try {
|
|
468
|
+
const result = await this.embeddingProvider.embed({
|
|
469
|
+
texts: [queryText],
|
|
470
|
+
});
|
|
471
|
+
if (!validateVectors(result.vectors, 1, result.dimensions)) {
|
|
472
|
+
throw new BrainStoreValidationError("Embedding provider returned a malformed query vector");
|
|
473
|
+
}
|
|
474
|
+
queryVector = result.vectors[0];
|
|
475
|
+
}
|
|
476
|
+
catch (error) {
|
|
477
|
+
if (this.embeddingFailureMode === "strict")
|
|
478
|
+
throw error;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
const results = [];
|
|
482
|
+
for (const { scope, sourceId } of sources) {
|
|
483
|
+
const source = this.source({ scope, sourceId });
|
|
484
|
+
if (!source || !isBrainSourceRetrievable(source))
|
|
485
|
+
continue;
|
|
486
|
+
const currentVersion = source.currentVersion;
|
|
487
|
+
const ref = { scope, sourceId, version: currentVersion };
|
|
488
|
+
const chunks = this.chunks.get(versionKey(ref)) ?? [];
|
|
489
|
+
for (const chunk of chunks) {
|
|
490
|
+
const keyword = lexicalScore(chunk.content, queryTerms);
|
|
491
|
+
const stored = this.vectors.get(`${versionKey(ref)}:${chunk.id}`);
|
|
492
|
+
const semantic = queryVector && stored
|
|
493
|
+
? cosine(queryVector, stored.values)
|
|
494
|
+
: 0;
|
|
495
|
+
if (keyword <= 0 && semantic <= 0)
|
|
496
|
+
continue;
|
|
497
|
+
const score = keyword + Math.max(0, semantic);
|
|
498
|
+
results.push(createBrainRetrievalResult({
|
|
499
|
+
scope,
|
|
500
|
+
chunk,
|
|
501
|
+
score,
|
|
502
|
+
scores: {
|
|
503
|
+
...(keyword > 0 ? { keyword } : {}),
|
|
504
|
+
...(semantic > 0 ? { semantic } : {}),
|
|
505
|
+
},
|
|
506
|
+
trust: source.trust,
|
|
507
|
+
}));
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
return Object.freeze(results
|
|
511
|
+
.sort((left, right) => (right.score - left.score
|
|
512
|
+
|| left.chunk.sourceId.localeCompare(right.chunk.sourceId)
|
|
513
|
+
|| left.chunk.index - right.chunk.index
|
|
514
|
+
|| left.chunk.id.localeCompare(right.chunk.id)))
|
|
515
|
+
.slice(0, limit));
|
|
516
|
+
}
|
|
517
|
+
async deleteVersionChunks(ref) {
|
|
518
|
+
const key = versionKey(ref);
|
|
519
|
+
this.chunks.delete(key);
|
|
520
|
+
for (const [storedKey] of this.vectors) {
|
|
521
|
+
if (storedKey.startsWith(`${key}:`))
|
|
522
|
+
this.vectors.delete(storedKey);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
async deleteSourceChunks(ref) {
|
|
526
|
+
const prefix = sourceKey(ref.scope, ref.sourceId);
|
|
527
|
+
for (const key of [...this.chunks.keys()]) {
|
|
528
|
+
if (sourceKeyFromVersionKey(key) === prefix) {
|
|
529
|
+
this.chunks.delete(key);
|
|
530
|
+
for (const [storedKey] of this.vectors) {
|
|
531
|
+
if (storedKey.startsWith(`${key}:`))
|
|
532
|
+
this.vectors.delete(storedKey);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
async enqueueJob(value) {
|
|
538
|
+
const job = cloneJob(value);
|
|
539
|
+
const existingId = this.jobsByDedupe.get(dedupeKey(job));
|
|
540
|
+
if (existingId) {
|
|
541
|
+
const existing = this.jobs.get(jobKey(job.scope, existingId));
|
|
542
|
+
if (existing) {
|
|
543
|
+
return Object.freeze({ job: cloneJob(existing), created: false });
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
const key = jobKey(job.scope, job.id);
|
|
547
|
+
if (this.jobs.has(key)) {
|
|
548
|
+
throw new BrainStoreConflictError(`Brain ingestion job "${job.id}" already exists in this scope`);
|
|
549
|
+
}
|
|
550
|
+
this.jobs.set(key, job);
|
|
551
|
+
this.jobsByDedupe.set(dedupeKey(job), job.id);
|
|
552
|
+
return Object.freeze({ job: cloneJob(job), created: true });
|
|
553
|
+
}
|
|
554
|
+
async getJob(ref) {
|
|
555
|
+
const job = this.jobs.get(jobKey(ref.scope, ref.jobId));
|
|
556
|
+
return job ? cloneJob(job) : null;
|
|
557
|
+
}
|
|
558
|
+
async claimNextJob(input) {
|
|
559
|
+
const scope = normalizeBrainScope(input.scope);
|
|
560
|
+
const workerId = text(input.workerId, "workerId", 512);
|
|
561
|
+
const now = timestamp(input.now);
|
|
562
|
+
if (!Number.isSafeInteger(input.leaseMs)
|
|
563
|
+
|| input.leaseMs < 1
|
|
564
|
+
|| input.leaseMs > 86_400_000) {
|
|
565
|
+
throw new BrainStoreValidationError("leaseMs must be an integer between 1 and 86400000");
|
|
566
|
+
}
|
|
567
|
+
for (const [key, job] of this.jobs) {
|
|
568
|
+
if (scopeKey(job.scope) === scopeKey(scope)
|
|
569
|
+
&& job.status === "processing"
|
|
570
|
+
&& job.leaseExpiresAt
|
|
571
|
+
&& job.leaseExpiresAt <= now
|
|
572
|
+
&& job.attempt >= job.maxAttempts) {
|
|
573
|
+
this.jobs.set(key, createBrainIngestionJob({
|
|
574
|
+
...job,
|
|
575
|
+
status: "failed",
|
|
576
|
+
claimedBy: undefined,
|
|
577
|
+
claimToken: undefined,
|
|
578
|
+
leaseExpiresAt: undefined,
|
|
579
|
+
failure: {
|
|
580
|
+
code: "lease_exhausted",
|
|
581
|
+
message: "Ingestion lease expired after the final attempt",
|
|
582
|
+
retryable: false,
|
|
583
|
+
},
|
|
584
|
+
updatedAt: now,
|
|
585
|
+
}));
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
const matching = [...this.jobs.values()]
|
|
589
|
+
.filter((job) => scopeKey(job.scope) === scopeKey(scope))
|
|
590
|
+
.filter((job) => ((job.status === "pending" && job.availableAt <= now)
|
|
591
|
+
|| (job.status === "processing"
|
|
592
|
+
&& Boolean(job.leaseExpiresAt && job.leaseExpiresAt <= now)
|
|
593
|
+
&& job.attempt < job.maxAttempts)))
|
|
594
|
+
.sort((left, right) => (left.availableAt.localeCompare(right.availableAt)
|
|
595
|
+
|| left.createdAt.localeCompare(right.createdAt)
|
|
596
|
+
|| left.id.localeCompare(right.id)));
|
|
597
|
+
const current = matching[0];
|
|
598
|
+
if (!current)
|
|
599
|
+
return null;
|
|
600
|
+
assertBrainIngestionJobStatusTransition(current.status, "processing");
|
|
601
|
+
const claimed = createBrainIngestionJob({
|
|
602
|
+
...current,
|
|
603
|
+
status: "processing",
|
|
604
|
+
attempt: current.attempt + 1,
|
|
605
|
+
claimedBy: workerId,
|
|
606
|
+
claimToken: this.createId(),
|
|
607
|
+
leaseExpiresAt: new Date(Date.parse(now) + input.leaseMs).toISOString(),
|
|
608
|
+
failure: undefined,
|
|
609
|
+
updatedAt: now,
|
|
610
|
+
});
|
|
611
|
+
this.jobs.set(jobKey(scope, claimed.id), claimed);
|
|
612
|
+
return cloneJob(claimed);
|
|
613
|
+
}
|
|
614
|
+
claimedJob(input) {
|
|
615
|
+
const current = this.jobs.get(jobKey(input.scope, input.jobId));
|
|
616
|
+
if (!current
|
|
617
|
+
|| current.status !== "processing"
|
|
618
|
+
|| current.claimToken !== input.claimToken) {
|
|
619
|
+
throw new BrainStoreConflictError("Brain ingestion job claim is missing, stale, or already completed");
|
|
620
|
+
}
|
|
621
|
+
return current;
|
|
622
|
+
}
|
|
623
|
+
async completeJob(input) {
|
|
624
|
+
const current = this.claimedJob(input);
|
|
625
|
+
const now = timestamp(input.now);
|
|
626
|
+
assertBrainIngestionJobStatusTransition(current.status, "completed");
|
|
627
|
+
const completed = createBrainIngestionJob({
|
|
628
|
+
...current,
|
|
629
|
+
status: "completed",
|
|
630
|
+
claimedBy: undefined,
|
|
631
|
+
claimToken: undefined,
|
|
632
|
+
leaseExpiresAt: undefined,
|
|
633
|
+
failure: undefined,
|
|
634
|
+
updatedAt: now,
|
|
635
|
+
completedAt: now,
|
|
636
|
+
});
|
|
637
|
+
this.jobs.set(jobKey(input.scope, input.jobId), completed);
|
|
638
|
+
return cloneJob(completed);
|
|
639
|
+
}
|
|
640
|
+
async failJob(input) {
|
|
641
|
+
const current = this.claimedJob(input);
|
|
642
|
+
const now = timestamp(input.now);
|
|
643
|
+
const retry = (input.failure.retryable
|
|
644
|
+
&& current.attempt < current.maxAttempts
|
|
645
|
+
&& input.retryAt !== undefined);
|
|
646
|
+
const nextStatus = retry ? "pending" : "failed";
|
|
647
|
+
assertBrainIngestionJobStatusTransition(current.status, nextStatus);
|
|
648
|
+
const failed = createBrainIngestionJob({
|
|
649
|
+
...current,
|
|
650
|
+
status: nextStatus,
|
|
651
|
+
claimedBy: undefined,
|
|
652
|
+
claimToken: undefined,
|
|
653
|
+
leaseExpiresAt: undefined,
|
|
654
|
+
...(retry
|
|
655
|
+
? {
|
|
656
|
+
availableAt: timestamp(input.retryAt),
|
|
657
|
+
failure: undefined,
|
|
658
|
+
}
|
|
659
|
+
: { failure: input.failure }),
|
|
660
|
+
updatedAt: now,
|
|
661
|
+
completedAt: undefined,
|
|
662
|
+
});
|
|
663
|
+
this.jobs.set(jobKey(input.scope, input.jobId), failed);
|
|
664
|
+
return cloneJob(failed);
|
|
665
|
+
}
|
|
666
|
+
async cancelJob(input) {
|
|
667
|
+
const current = this.claimedJob(input);
|
|
668
|
+
const now = timestamp(input.now);
|
|
669
|
+
assertBrainIngestionJobStatusTransition(current.status, "cancelled");
|
|
670
|
+
const cancelled = createBrainIngestionJob({
|
|
671
|
+
...current,
|
|
672
|
+
status: "cancelled",
|
|
673
|
+
claimedBy: undefined,
|
|
674
|
+
claimToken: undefined,
|
|
675
|
+
leaseExpiresAt: undefined,
|
|
676
|
+
failure: undefined,
|
|
677
|
+
updatedAt: now,
|
|
678
|
+
completedAt: undefined,
|
|
679
|
+
});
|
|
680
|
+
this.jobs.set(jobKey(input.scope, input.jobId), cancelled);
|
|
681
|
+
return cloneJob(cancelled);
|
|
682
|
+
}
|
|
683
|
+
snapshot() {
|
|
684
|
+
const sources = [...this.sources.values()]
|
|
685
|
+
.map(cloneSource)
|
|
686
|
+
.sort((left, right) => (scopeKey(left.scope).localeCompare(scopeKey(right.scope))
|
|
687
|
+
|| left.id.localeCompare(right.id)));
|
|
688
|
+
const sourceVersions = [...this.versions.entries()]
|
|
689
|
+
.map(([key, value]) => Object.freeze({
|
|
690
|
+
scope: scopeFromVersionKey(key),
|
|
691
|
+
value: cloneVersion(value),
|
|
692
|
+
}))
|
|
693
|
+
.sort((left, right) => (scopeKey(left.scope).localeCompare(scopeKey(right.scope))
|
|
694
|
+
|| left.value.sourceId.localeCompare(right.value.sourceId)
|
|
695
|
+
|| left.value.version.localeCompare(right.value.version)));
|
|
696
|
+
const chunks = [...this.chunks.entries()].flatMap(([key, values]) => {
|
|
697
|
+
const scope = scopeFromVersionKey(key);
|
|
698
|
+
return values.map((value) => Object.freeze({
|
|
699
|
+
scope,
|
|
700
|
+
value: cloneChunk(value),
|
|
701
|
+
}));
|
|
702
|
+
});
|
|
703
|
+
const jobs = [...this.jobs.values()].map(cloneJob);
|
|
704
|
+
return Object.freeze({
|
|
705
|
+
version: 1,
|
|
706
|
+
sources: Object.freeze(sources),
|
|
707
|
+
sourceVersions: Object.freeze(sourceVersions),
|
|
708
|
+
chunks: Object.freeze(chunks),
|
|
709
|
+
jobs: Object.freeze(jobs),
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
restore(snapshot) {
|
|
713
|
+
if (!snapshot
|
|
714
|
+
|| typeof snapshot !== "object"
|
|
715
|
+
|| snapshot.version !== 1
|
|
716
|
+
|| !Array.isArray(snapshot.sources)
|
|
717
|
+
|| !Array.isArray(snapshot.sourceVersions)
|
|
718
|
+
|| !Array.isArray(snapshot.chunks)
|
|
719
|
+
|| !Array.isArray(snapshot.jobs)) {
|
|
720
|
+
throw new BrainStoreValidationError("Unsupported or malformed Brain store snapshot");
|
|
721
|
+
}
|
|
722
|
+
for (const value of snapshot.sources) {
|
|
723
|
+
const source = cloneSource(value);
|
|
724
|
+
const key = sourceKey(source.scope, source.id);
|
|
725
|
+
if (this.sources.has(key)) {
|
|
726
|
+
throw new BrainStoreValidationError("Snapshot contains duplicate sources");
|
|
727
|
+
}
|
|
728
|
+
this.sources.set(key, source);
|
|
729
|
+
}
|
|
730
|
+
for (const entry of snapshot.sourceVersions) {
|
|
731
|
+
const scope = normalizeBrainScope(entry.scope);
|
|
732
|
+
const value = cloneVersion(entry.value);
|
|
733
|
+
if (!this.source({ scope, sourceId: value.sourceId })) {
|
|
734
|
+
throw new BrainStoreValidationError("Snapshot source version has no matching source");
|
|
735
|
+
}
|
|
736
|
+
const key = versionKey({
|
|
737
|
+
scope,
|
|
738
|
+
sourceId: value.sourceId,
|
|
739
|
+
version: value.version,
|
|
740
|
+
});
|
|
741
|
+
if (this.versions.has(key)) {
|
|
742
|
+
throw new BrainStoreValidationError("Snapshot contains duplicate source versions");
|
|
743
|
+
}
|
|
744
|
+
this.versions.set(key, value);
|
|
745
|
+
}
|
|
746
|
+
for (const entry of snapshot.chunks) {
|
|
747
|
+
const scope = normalizeBrainScope(entry.scope);
|
|
748
|
+
const value = cloneChunk(entry.value);
|
|
749
|
+
const ref = {
|
|
750
|
+
scope,
|
|
751
|
+
sourceId: value.sourceId,
|
|
752
|
+
version: value.version,
|
|
753
|
+
};
|
|
754
|
+
if (!this.version(ref)) {
|
|
755
|
+
throw new BrainStoreValidationError("Snapshot chunk has no matching source version");
|
|
756
|
+
}
|
|
757
|
+
const key = versionKey(ref);
|
|
758
|
+
const current = [...(this.chunks.get(key) ?? []), value]
|
|
759
|
+
.sort((left, right) => left.index - right.index);
|
|
760
|
+
this.chunks.set(key, Object.freeze(current));
|
|
761
|
+
}
|
|
762
|
+
for (const values of this.chunks.values()) {
|
|
763
|
+
const ids = new Set();
|
|
764
|
+
for (const [index, chunk] of values.entries()) {
|
|
765
|
+
if (chunk.index !== index || ids.has(chunk.id)) {
|
|
766
|
+
throw new BrainStoreValidationError("Snapshot contains duplicate or non-contiguous Brain chunks");
|
|
767
|
+
}
|
|
768
|
+
ids.add(chunk.id);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
for (const source of this.sources.values()) {
|
|
772
|
+
if (!source.currentVersion)
|
|
773
|
+
continue;
|
|
774
|
+
const ref = {
|
|
775
|
+
scope: source.scope,
|
|
776
|
+
sourceId: source.id,
|
|
777
|
+
version: source.currentVersion,
|
|
778
|
+
};
|
|
779
|
+
const version = this.version(ref);
|
|
780
|
+
if (!version
|
|
781
|
+
|| version.status !== "indexed"
|
|
782
|
+
|| (this.chunks.get(versionKey(ref))?.length ?? 0) === 0) {
|
|
783
|
+
throw new BrainStoreValidationError("Snapshot current Brain version is missing indexed chunks");
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
for (const value of snapshot.jobs) {
|
|
787
|
+
const job = cloneJob(value);
|
|
788
|
+
const key = jobKey(job.scope, job.id);
|
|
789
|
+
if (this.jobs.has(key) || this.jobsByDedupe.has(dedupeKey(job))) {
|
|
790
|
+
throw new BrainStoreValidationError("Snapshot contains duplicate ingestion jobs");
|
|
791
|
+
}
|
|
792
|
+
this.jobs.set(key, job);
|
|
793
|
+
this.jobsByDedupe.set(dedupeKey(job), job.id);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
//# sourceMappingURL=in-memory-store.js.map
|