@pegma/knowledge-base-application 0.0.0-stage → 0.1.0

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.
Files changed (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +140 -2
  3. package/dist/articles.d.ts +10 -0
  4. package/dist/articles.d.ts.map +1 -0
  5. package/dist/articles.js +454 -0
  6. package/dist/articles.js.map +1 -0
  7. package/dist/errors.d.ts +18 -0
  8. package/dist/errors.d.ts.map +1 -0
  9. package/dist/errors.js +34 -0
  10. package/dist/errors.js.map +1 -0
  11. package/dist/governance.d.ts +14 -0
  12. package/dist/governance.d.ts.map +1 -0
  13. package/dist/governance.js +364 -0
  14. package/dist/governance.js.map +1 -0
  15. package/dist/index.d.ts +13 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +46 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/lexical.d.ts +13 -0
  20. package/dist/lexical.d.ts.map +1 -0
  21. package/dist/lexical.js +105 -0
  22. package/dist/lexical.js.map +1 -0
  23. package/dist/publication.d.ts +42 -0
  24. package/dist/publication.d.ts.map +1 -0
  25. package/dist/publication.js +258 -0
  26. package/dist/publication.js.map +1 -0
  27. package/dist/reads.d.ts +5 -0
  28. package/dist/reads.d.ts.map +1 -0
  29. package/dist/reads.js +228 -0
  30. package/dist/reads.js.map +1 -0
  31. package/dist/records.d.ts +40 -0
  32. package/dist/records.d.ts.map +1 -0
  33. package/dist/records.js +122 -0
  34. package/dist/records.js.map +1 -0
  35. package/dist/repository.d.ts +74 -0
  36. package/dist/repository.d.ts.map +1 -0
  37. package/dist/repository.js +738 -0
  38. package/dist/repository.js.map +1 -0
  39. package/dist/retrieval-budget.d.ts +37 -0
  40. package/dist/retrieval-budget.d.ts.map +1 -0
  41. package/dist/retrieval-budget.js +111 -0
  42. package/dist/retrieval-budget.js.map +1 -0
  43. package/dist/retrieval-limits.d.ts +4 -0
  44. package/dist/retrieval-limits.d.ts.map +1 -0
  45. package/dist/retrieval-limits.js +21 -0
  46. package/dist/retrieval-limits.js.map +1 -0
  47. package/dist/retrieval-maintenance.d.ts +19 -0
  48. package/dist/retrieval-maintenance.d.ts.map +1 -0
  49. package/dist/retrieval-maintenance.js +359 -0
  50. package/dist/retrieval-maintenance.js.map +1 -0
  51. package/dist/retrieval-records.d.ts +55 -0
  52. package/dist/retrieval-records.d.ts.map +1 -0
  53. package/dist/retrieval-records.js +140 -0
  54. package/dist/retrieval-records.js.map +1 -0
  55. package/dist/retrieval-repository.d.ts +32 -0
  56. package/dist/retrieval-repository.d.ts.map +1 -0
  57. package/dist/retrieval-repository.js +750 -0
  58. package/dist/retrieval-repository.js.map +1 -0
  59. package/dist/retrieval-types.d.ts +185 -0
  60. package/dist/retrieval-types.d.ts.map +1 -0
  61. package/dist/retrieval-types.js +24 -0
  62. package/dist/retrieval-types.js.map +1 -0
  63. package/dist/retrieval.d.ts +12 -0
  64. package/dist/retrieval.d.ts.map +1 -0
  65. package/dist/retrieval.js +541 -0
  66. package/dist/retrieval.js.map +1 -0
  67. package/dist/storage-helpers.d.ts +23 -0
  68. package/dist/storage-helpers.d.ts.map +1 -0
  69. package/dist/storage-helpers.js +138 -0
  70. package/dist/storage-helpers.js.map +1 -0
  71. package/dist/types.d.ts +280 -0
  72. package/dist/types.d.ts.map +1 -0
  73. package/dist/types.js +2 -0
  74. package/dist/types.js.map +1 -0
  75. package/package.json +42 -4
@@ -0,0 +1,750 @@
1
+ import { encodeRetrievalRecord, retrievalRecords, requireRetrievalHint, requireRetrievalScope, } from "./retrieval-records.js";
2
+ import { resolveRetrievalLimits } from "./retrieval-limits.js";
3
+ import { canonicalJson, encodedBytes, fingerprint, RepositoryCapacityError, RepositoryConflictError, RepositoryIntegrityError, requireGeneration, requireIdentifier, snapshotData, } from "./storage-helpers.js";
4
+ const maximumEpochs = 32;
5
+ const maximumKnownScopes = 32;
6
+ const maximumAttempts = 3;
7
+ const maximumActions = 100;
8
+ const maximumTransactionBytes = 3 * 1024 * 1024;
9
+ /** Two complete posting rewrites fit even when both terms occupy sixteen pages. */
10
+ const termsPerBatch = 2;
11
+ const hashPattern = /^[a-f0-9]{64}$/u;
12
+ function exactObject(value, keys) {
13
+ if (value === null || typeof value !== "object" || Array.isArray(value))
14
+ throw new RepositoryIntegrityError();
15
+ const names = Object.keys(value);
16
+ if (names.length !== keys.length ||
17
+ names.some((name) => !keys.includes(name)))
18
+ throw new RepositoryIntegrityError();
19
+ return value;
20
+ }
21
+ function boundedArray(value, maximum, validate) {
22
+ if (!Array.isArray(value) || value.length > maximum)
23
+ throw new RepositoryIntegrityError();
24
+ for (const item of value)
25
+ validate(item);
26
+ return value;
27
+ }
28
+ function unique(values) {
29
+ if (new Set(values).size !== values.length)
30
+ throw new RepositoryIntegrityError();
31
+ }
32
+ function digest(value) {
33
+ if (typeof value !== "string" || !hashPattern.test(value))
34
+ throw new RepositoryIntegrityError();
35
+ }
36
+ function scopeIdentity(scope) {
37
+ return canonicalJson(scope);
38
+ }
39
+ function compare(a, b) {
40
+ return a < b ? -1 : a > b ? 1 : 0;
41
+ }
42
+ function sameScope(a, b) {
43
+ return scopeIdentity(a) === scopeIdentity(b);
44
+ }
45
+ function scopesUnion(scopes) {
46
+ const values = new Map(scopes.map((scope) => [scopeIdentity(scope), scope]));
47
+ return [...values.entries()]
48
+ .sort(([a], [b]) => compare(a, b))
49
+ .map(([, scope]) => scope);
50
+ }
51
+ function requireScopes(value, maximum = maximumKnownScopes) {
52
+ const scopes = boundedArray(value, maximum, requireRetrievalScope);
53
+ unique(scopes.map(scopeIdentity));
54
+ return scopes;
55
+ }
56
+ function requireTarget(target) {
57
+ exactObject(target, ["scope", "hint", "termHashes"]);
58
+ requireRetrievalScope(target.scope);
59
+ requireRetrievalHint(target.hint);
60
+ if (!sameScope(target.scope, {
61
+ collectionId: target.hint.collectionId,
62
+ policyVersion: target.hint.policyVersion,
63
+ use: target.hint.use,
64
+ }))
65
+ throw new RepositoryIntegrityError();
66
+ const terms = boundedArray(target.termHashes, 256, digest);
67
+ unique(terms);
68
+ }
69
+ function requireState(state) {
70
+ const keys = [
71
+ "articleId",
72
+ "generation",
73
+ "sourceGeneration",
74
+ "status",
75
+ "targets",
76
+ "pendingScopes",
77
+ "knownScopes",
78
+ ...(state.reason === undefined ? [] : ["reason"]),
79
+ ];
80
+ exactObject(state, keys);
81
+ requireIdentifier(state.articleId);
82
+ requireGeneration(state.generation, 1);
83
+ requireGeneration(state.sourceGeneration, 1);
84
+ if (!["pending", "ready", "blocked"].includes(state.status))
85
+ throw new RepositoryIntegrityError();
86
+ if (state.reason !== undefined &&
87
+ !["capacity", "source-unavailable", "conflict"].includes(state.reason))
88
+ throw new RepositoryIntegrityError();
89
+ const targets = boundedArray(state.targets, 2, requireTarget);
90
+ unique(targets.map((target) => scopeIdentity(target.scope)));
91
+ for (const target of targets)
92
+ if (target.hint.articleId !== state.articleId ||
93
+ target.hint.generation !== state.sourceGeneration)
94
+ throw new RepositoryIntegrityError();
95
+ const pending = requireScopes(state.pendingScopes);
96
+ const known = requireScopes(state.knownScopes);
97
+ if (pending.some((scope) => !known.some((item) => sameScope(scope, item))) ||
98
+ targets.some((target) => !known.some((scope) => sameScope(scope, target.scope))))
99
+ throw new RepositoryIntegrityError();
100
+ if (state.status === "ready" && pending.length !== 0)
101
+ throw new RepositoryIntegrityError();
102
+ }
103
+ function requireCatalog(data, collectionId) {
104
+ exactObject(data, ["entries", "epochs"]);
105
+ const entries = boundedArray(data.entries, 256, (entry) => {
106
+ exactObject(entry, ["articleId", "generation", "scopes"]);
107
+ requireIdentifier(entry.articleId);
108
+ requireGeneration(entry.generation, 1);
109
+ for (const scope of requireScopes(entry.scopes, 2))
110
+ if (scope.collectionId !== collectionId)
111
+ throw new RepositoryIntegrityError();
112
+ });
113
+ unique(entries.map((entry) => entry.articleId));
114
+ for (const scope of requireScopes(data.epochs, maximumEpochs))
115
+ if (scope.collectionId !== collectionId)
116
+ throw new RepositoryIntegrityError();
117
+ if (entries.some((entry) => entry.scopes.some((scope) => !data.epochs.some((epoch) => sameScope(scope, epoch)))))
118
+ throw new RepositoryIntegrityError();
119
+ }
120
+ function requireScopeHead(head) {
121
+ exactObject(head, ["generation", "articleIds", "rows", "bytes"]);
122
+ requireGeneration(head.generation, 1);
123
+ requireGeneration(head.rows, 1);
124
+ requireGeneration(head.bytes, 1);
125
+ const ids = boundedArray(head.articleIds, 256, requireIdentifier);
126
+ unique(ids);
127
+ }
128
+ function requireManifest(manifest, articleId, scope) {
129
+ exactObject(manifest, [
130
+ "articleId",
131
+ "generation",
132
+ "sourceGeneration",
133
+ "target",
134
+ "entries",
135
+ ]);
136
+ if (manifest.articleId !== articleId)
137
+ throw new RepositoryIntegrityError();
138
+ requireGeneration(manifest.generation, 1);
139
+ requireGeneration(manifest.sourceGeneration, 1);
140
+ if (manifest.target !== null) {
141
+ requireTarget(manifest.target);
142
+ if (!sameScope(manifest.target.scope, scope) ||
143
+ manifest.target.hint.articleId !== articleId ||
144
+ manifest.target.hint.generation !== manifest.sourceGeneration)
145
+ throw new RepositoryIntegrityError();
146
+ }
147
+ const entries = boundedArray(manifest.entries, 256, (entry) => {
148
+ exactObject(entry, ["termHash", "hint", "projectionGeneration"]);
149
+ digest(entry.termHash);
150
+ requireRetrievalHint(entry.hint);
151
+ requireGeneration(entry.projectionGeneration, 1);
152
+ if (entry.projectionGeneration > manifest.generation ||
153
+ entry.hint.articleId !== articleId ||
154
+ !sameScope(scope, {
155
+ collectionId: entry.hint.collectionId,
156
+ policyVersion: entry.hint.policyVersion,
157
+ use: entry.hint.use,
158
+ }))
159
+ throw new RepositoryIntegrityError();
160
+ });
161
+ unique(entries.map((entry) => entry.termHash));
162
+ }
163
+ export const retrievalTermHash = async (term) => {
164
+ if (typeof term !== "string" ||
165
+ term.length === 0 ||
166
+ term.length > 256 ||
167
+ !/^[\p{L}\p{N}]+$/u.test(term))
168
+ throw new TypeError("Expected a bounded normalized lexical term");
169
+ return fingerprint(term);
170
+ };
171
+ /** A repairable derived index; authoritative content always lives in KnowledgeRepository. */
172
+ export function createRetrievalRepository(store, overrides = {}) {
173
+ const limits = resolveRetrievalLimits(overrides);
174
+ const records = store.collection(retrievalRecords);
175
+ const row = (partition, id, kind, data) => ({ schemaVersion: 1, partition, id, kind, data });
176
+ const articlePartition = async (id) => "article:" + (await fingerprint(id));
177
+ const catalogPartition = async (id) => "catalog:" + (await fingerprint(id));
178
+ const scopePartition = async (scope) => "scope:" + (await fingerprint(scope));
179
+ async function get(partition, id, kind) {
180
+ const result = await records.getVersioned({ partition, id });
181
+ if (result !== null &&
182
+ (result.value.partition !== partition ||
183
+ result.value.id !== id ||
184
+ result.value.kind !== kind))
185
+ throw new RepositoryIntegrityError();
186
+ return result;
187
+ }
188
+ async function transact(partition, actions) {
189
+ if (actions.length > maximumActions)
190
+ throw new RepositoryCapacityError("index_transaction_actions");
191
+ let bytes = 0;
192
+ for (const action of actions)
193
+ bytes +=
194
+ action.action === "delete"
195
+ ? new TextEncoder().encode(canonicalJson(action.key)).length + 256
196
+ : encodedBytes(encodeRetrievalRecord(action.value)) + 256;
197
+ if (bytes > maximumTransactionBytes)
198
+ throw new RepositoryCapacityError("index_transaction_bytes");
199
+ return (await records.transact(partition, actions)).committed;
200
+ }
201
+ const write = (value, before) => before === null
202
+ ? { action: "insert", value }
203
+ : { action: "putIfUnchanged", value, version: before.version };
204
+ async function readState(articleId) {
205
+ requireIdentifier(articleId);
206
+ const before = await get(await articlePartition(articleId), "head", "article");
207
+ if (before === null)
208
+ return null;
209
+ try {
210
+ const state = before.value.data;
211
+ requireState(state);
212
+ if (state.articleId !== articleId)
213
+ throw new RepositoryIntegrityError();
214
+ return { state, row: before };
215
+ }
216
+ catch {
217
+ throw new RepositoryIntegrityError();
218
+ }
219
+ }
220
+ async function directory() {
221
+ const result = await get("directory", "head", "directory");
222
+ if (result === null)
223
+ return [];
224
+ try {
225
+ const data = exactObject(result.value.data, ["collectionIds"]);
226
+ const ids = boundedArray(data.collectionIds, 64, requireIdentifier);
227
+ unique(ids);
228
+ if (ids.length > limits.maxDirectoryCollections)
229
+ throw new RepositoryCapacityError("index_directory");
230
+ return ids;
231
+ }
232
+ catch (error) {
233
+ if (error instanceof RepositoryCapacityError)
234
+ throw error;
235
+ throw new RepositoryIntegrityError();
236
+ }
237
+ }
238
+ async function registerDirectory(collectionId) {
239
+ requireIdentifier(collectionId, "collectionId");
240
+ for (let attempt = 0; attempt < maximumAttempts; attempt += 1) {
241
+ const before = await get("directory", "head", "directory");
242
+ let ids = [];
243
+ if (before !== null) {
244
+ const data = exactObject(before.value.data, ["collectionIds"]);
245
+ ids = boundedArray(data.collectionIds, 64, requireIdentifier);
246
+ unique(ids);
247
+ }
248
+ if (ids.length > limits.maxDirectoryCollections)
249
+ throw new RepositoryCapacityError("index_directory");
250
+ if (ids.includes(collectionId))
251
+ return;
252
+ if (ids.length >= limits.maxDirectoryCollections)
253
+ throw new RepositoryCapacityError("index_directory");
254
+ if (await transact("directory", [
255
+ write(row("directory", "head", "directory", {
256
+ collectionIds: [...ids, collectionId].sort(),
257
+ }), before),
258
+ ]))
259
+ return;
260
+ }
261
+ throw new RepositoryConflictError();
262
+ }
263
+ async function loadCatalog(collectionId) {
264
+ requireIdentifier(collectionId);
265
+ const partition = await catalogPartition(collectionId);
266
+ const before = await get(partition, "head", "catalog");
267
+ const data = (before?.value.data ?? {
268
+ entries: [],
269
+ epochs: [],
270
+ });
271
+ try {
272
+ requireCatalog(data, collectionId);
273
+ }
274
+ catch {
275
+ throw new RepositoryIntegrityError();
276
+ }
277
+ return { data, row: before, partition };
278
+ }
279
+ async function registerCatalog(state, collectionId) {
280
+ await registerDirectory(collectionId);
281
+ for (let attempt = 0; attempt < maximumAttempts; attempt += 1) {
282
+ const loaded = await loadCatalog(collectionId);
283
+ const existing = loaded.data.entries.find((entry) => entry.articleId === state.articleId);
284
+ if (existing !== undefined && existing.generation > state.generation)
285
+ return;
286
+ const scopes = state.targets
287
+ .filter((target) => target.scope.collectionId === collectionId)
288
+ .map((target) => target.scope);
289
+ if (existing !== undefined && existing.generation === state.generation) {
290
+ if (canonicalJson(existing.scopes) !== canonicalJson(scopes))
291
+ throw new RepositoryConflictError();
292
+ return;
293
+ }
294
+ if (existing === undefined &&
295
+ loaded.data.entries.length >= limits.maxCatalogArticles)
296
+ throw new RepositoryCapacityError("index_catalog");
297
+ const epochs = scopesUnion([
298
+ ...loaded.data.epochs,
299
+ ...state.knownScopes.filter((scope) => scope.collectionId === collectionId),
300
+ ]);
301
+ if (epochs.length > maximumEpochs)
302
+ throw new RepositoryCapacityError("index_epochs");
303
+ const entries = [
304
+ ...loaded.data.entries.filter((entry) => entry.articleId !== state.articleId),
305
+ { articleId: state.articleId, generation: state.generation, scopes },
306
+ ].sort((a, b) => compare(a.articleId, b.articleId));
307
+ const value = row(loaded.partition, "head", "catalog", {
308
+ entries,
309
+ epochs,
310
+ });
311
+ if (await transact(loaded.partition, [write(value, loaded.row)]))
312
+ return;
313
+ }
314
+ throw new RepositoryConflictError();
315
+ }
316
+ async function loadPosting(partition, scope, termHash) {
317
+ const head = await get(partition, "posting:" + termHash, "posting");
318
+ if (head === null)
319
+ return { head, pages: [], entries: [] };
320
+ const data = head.value.data;
321
+ try {
322
+ exactObject(data, ["termHash", "pages"]);
323
+ if (data.termHash !== termHash)
324
+ throw new RepositoryIntegrityError();
325
+ requireGeneration(data.pages, 1);
326
+ if (data.pages > limits.maxPostingPagesPerTerm)
327
+ throw new RepositoryCapacityError("index_posting_pages");
328
+ }
329
+ catch (error) {
330
+ if (error instanceof RepositoryCapacityError)
331
+ throw error;
332
+ throw new RepositoryIntegrityError();
333
+ }
334
+ const pages = [];
335
+ const entries = [];
336
+ for (let page = 0; page < data.pages; page += 1) {
337
+ const loaded = await get(partition, "page:" + termHash + ":" + page, "page");
338
+ if (loaded === null)
339
+ throw new RepositoryConflictError();
340
+ try {
341
+ const value = loaded.value.data;
342
+ exactObject(value, ["termHash", "page", "entries"]);
343
+ if (value.termHash !== termHash || value.page !== page)
344
+ throw new RepositoryIntegrityError();
345
+ const members = boundedArray(value.entries, 16, (entry) => {
346
+ exactObject(entry, ["hint", "projectionGeneration"]);
347
+ requireRetrievalHint(entry.hint);
348
+ requireGeneration(entry.projectionGeneration, 1);
349
+ if (!sameScope(scope, {
350
+ collectionId: entry.hint.collectionId,
351
+ policyVersion: entry.hint.policyVersion,
352
+ use: entry.hint.use,
353
+ }))
354
+ throw new RepositoryIntegrityError();
355
+ });
356
+ if (members.length > limits.postingPageEntries)
357
+ throw new RepositoryCapacityError("index_posting_entries");
358
+ entries.push(...members);
359
+ }
360
+ catch (error) {
361
+ if (error instanceof RepositoryCapacityError)
362
+ throw error;
363
+ throw new RepositoryIntegrityError();
364
+ }
365
+ pages.push(loaded);
366
+ }
367
+ return { head, pages, entries };
368
+ }
369
+ async function applyScopeBatch(state, scope) {
370
+ const partition = await scopePartition(scope);
371
+ const manifestId = "manifest:" + (await fingerprint(state.articleId));
372
+ const desired = state.targets.find((target) => sameScope(target.scope, scope)) ?? null;
373
+ for (let attempt = 0; attempt < maximumAttempts; attempt += 1) {
374
+ const global = await readState(state.articleId);
375
+ if (global === null || global.state.generation !== state.generation)
376
+ throw new RepositoryConflictError();
377
+ const headRow = await get(partition, "head", "scope");
378
+ let head = null;
379
+ if (headRow !== null) {
380
+ try {
381
+ head = headRow.value.data;
382
+ requireScopeHead(head);
383
+ }
384
+ catch {
385
+ throw new RepositoryIntegrityError();
386
+ }
387
+ if (head.rows > limits.maxIndexPartitionRows ||
388
+ head.bytes > limits.maxIndexPartitionBytes)
389
+ throw new RepositoryCapacityError("index_partition");
390
+ }
391
+ const manifestRow = await get(partition, manifestId, "manifest");
392
+ let previous = null;
393
+ if (manifestRow !== null) {
394
+ try {
395
+ previous = manifestRow.value.data;
396
+ requireManifest(previous, state.articleId, scope);
397
+ }
398
+ catch {
399
+ throw new RepositoryIntegrityError();
400
+ }
401
+ if (previous.generation > state.generation)
402
+ return true;
403
+ if (previous.sourceGeneration > state.sourceGeneration)
404
+ throw new RepositoryConflictError();
405
+ if (previous.generation === state.generation &&
406
+ canonicalJson(previous.target) !== canonicalJson(desired))
407
+ throw new RepositoryConflictError();
408
+ }
409
+ if (head === null && manifestRow !== null)
410
+ throw new RepositoryConflictError();
411
+ const articleIds = head?.articleIds ?? [];
412
+ if (!articleIds.includes(state.articleId) &&
413
+ articleIds.length >= limits.maxCatalogArticles)
414
+ throw new RepositoryCapacityError("index_scope_articles");
415
+ const actual = new Map((previous?.entries ?? []).map((entry) => [entry.termHash, entry]));
416
+ const desiredTerms = new Set(desired?.termHashes ?? []);
417
+ const differs = (termHash) => {
418
+ const entry = actual.get(termHash);
419
+ return desiredTerms.has(termHash)
420
+ ? entry === undefined ||
421
+ entry.projectionGeneration !== state.generation ||
422
+ canonicalJson(entry.hint) !== canonicalJson(desired.hint)
423
+ : entry !== undefined;
424
+ };
425
+ // Remove obsolete terms before adding new ones so the actual manifest never exceeds 256.
426
+ const work = [...actual.keys()]
427
+ .filter((term) => !desiredTerms.has(term))
428
+ .sort()
429
+ .concat([...new Set([...actual.keys(), ...desiredTerms])]
430
+ .filter((term) => desiredTerms.has(term) && differs(term))
431
+ .sort())
432
+ .slice(0, termsPerBatch);
433
+ if (previous !== null &&
434
+ previous.generation === state.generation &&
435
+ work.length === 0)
436
+ return true;
437
+ const actions = [];
438
+ const changes = [];
439
+ let planningConflict = false;
440
+ for (const termHash of work) {
441
+ const loaded = await loadPosting(partition, scope, termHash);
442
+ const old = actual.get(termHash);
443
+ const found = loaded.entries.filter((entry) => entry.hint.articleId === state.articleId);
444
+ // Page reads are not a snapshot of a concurrent compaction. Retry the
445
+ // scope CAS plan rather than treating a mixed view as stored corruption.
446
+ if (found.length > 1) {
447
+ planningConflict = true;
448
+ break;
449
+ }
450
+ const current = found[0];
451
+ if (current !== undefined &&
452
+ current.projectionGeneration > state.generation) {
453
+ planningConflict = true;
454
+ break;
455
+ }
456
+ if (old !== undefined &&
457
+ (current === undefined ||
458
+ current.projectionGeneration !== old.projectionGeneration ||
459
+ canonicalJson(current.hint) !== canonicalJson(old.hint))) {
460
+ planningConflict = true;
461
+ break;
462
+ }
463
+ if (old === undefined && current !== undefined) {
464
+ planningConflict = true;
465
+ break;
466
+ }
467
+ const replacement = desiredTerms.has(termHash)
468
+ ? { hint: desired.hint, projectionGeneration: state.generation }
469
+ : null;
470
+ const members = loaded.entries.filter((entry) => entry.hint.articleId !== state.articleId);
471
+ if (replacement !== null)
472
+ members.push(replacement);
473
+ members.sort((a, b) => compare(a.hint.articleId, b.hint.articleId));
474
+ const count = Math.ceil(members.length / limits.postingPageEntries);
475
+ if (count > limits.maxPostingPagesPerTerm)
476
+ throw new RepositoryCapacityError("index_posting_pages");
477
+ const nextHead = count === 0
478
+ ? null
479
+ : row(partition, "posting:" + termHash, "posting", {
480
+ termHash,
481
+ pages: count,
482
+ });
483
+ if (nextHead === null) {
484
+ if (loaded.head !== null)
485
+ actions.push({
486
+ action: "delete",
487
+ key: { partition, id: loaded.head.value.id },
488
+ });
489
+ }
490
+ else
491
+ actions.push(write(nextHead, loaded.head));
492
+ if (loaded.head !== null || nextHead !== null)
493
+ changes.push({ before: loaded.head?.value ?? null, after: nextHead });
494
+ for (let page = 0; page < Math.max(count, loaded.pages.length); page += 1) {
495
+ const prior = loaded.pages[page] ?? null;
496
+ const next = page < count
497
+ ? row(partition, "page:" + termHash + ":" + page, "page", {
498
+ termHash,
499
+ page,
500
+ entries: members.slice(page * limits.postingPageEntries, (page + 1) * limits.postingPageEntries),
501
+ })
502
+ : null;
503
+ if (next === null) {
504
+ if (prior !== null)
505
+ actions.push({
506
+ action: "delete",
507
+ key: { partition, id: prior.value.id },
508
+ });
509
+ }
510
+ else
511
+ actions.push(write(next, prior));
512
+ changes.push({ before: prior?.value ?? null, after: next });
513
+ }
514
+ if (replacement === null)
515
+ actual.delete(termHash);
516
+ else
517
+ actual.set(termHash, { ...replacement, termHash });
518
+ }
519
+ if (planningConflict)
520
+ continue;
521
+ const manifest = {
522
+ articleId: state.articleId,
523
+ generation: state.generation,
524
+ sourceGeneration: state.sourceGeneration,
525
+ target: desired,
526
+ entries: [...actual.values()].sort((a, b) => compare(a.termHash, b.termHash)),
527
+ };
528
+ if (manifest.entries.length > 256)
529
+ throw new RepositoryCapacityError("index_article_terms");
530
+ const nextManifest = row(partition, manifestId, "manifest", manifest);
531
+ actions.push(write(nextManifest, manifestRow));
532
+ changes.push({ before: manifestRow?.value ?? null, after: nextManifest });
533
+ const nextRows = (head?.rows ?? 0) +
534
+ (headRow === null ? 1 : 0) +
535
+ changes.reduce((sum, change) => sum +
536
+ (change.after === null ? 0 : 1) -
537
+ (change.before === null ? 0 : 1), 0);
538
+ const baseBytes = (head?.bytes ?? 0) -
539
+ (headRow === null
540
+ ? 0
541
+ : encodedBytes(encodeRetrievalRecord(headRow.value))) +
542
+ changes.reduce((sum, change) => sum +
543
+ (change.after === null
544
+ ? 0
545
+ : encodedBytes(encodeRetrievalRecord(change.after))) -
546
+ (change.before === null
547
+ ? 0
548
+ : encodedBytes(encodeRetrievalRecord(change.before))), 0);
549
+ let nextHead = {
550
+ generation: (head?.generation ?? 0) + 1,
551
+ articleIds: [...new Set([...articleIds, state.articleId])].sort(),
552
+ rows: nextRows,
553
+ bytes: 0,
554
+ };
555
+ requireGeneration(nextHead.generation, 1);
556
+ let nextHeadRow = row(partition, "head", "scope", nextHead);
557
+ for (let pass = 0; pass < 12; pass += 1) {
558
+ const bytes = baseBytes + encodedBytes(encodeRetrievalRecord(nextHeadRow));
559
+ if (bytes === nextHead.bytes)
560
+ break;
561
+ nextHead = { ...nextHead, bytes };
562
+ nextHeadRow = row(partition, "head", "scope", nextHead);
563
+ }
564
+ if (nextRows > limits.maxIndexPartitionRows)
565
+ throw new RepositoryCapacityError("index_partition_rows");
566
+ if (nextHead.bytes > limits.maxIndexPartitionBytes)
567
+ throw new RepositoryCapacityError("index_partition_bytes");
568
+ actions.unshift(write(nextHeadRow, headRow));
569
+ // Every posting/manifest mutation changes this head. Its CAS also fences deletes.
570
+ if (!(await transact(partition, actions)))
571
+ continue;
572
+ return [...new Set([...actual.keys(), ...desiredTerms])].every((term) => !differs(term));
573
+ }
574
+ throw new RepositoryConflictError();
575
+ }
576
+ return {
577
+ registerCollection: registerDirectory,
578
+ readDirectory: directory,
579
+ async readCatalog(collectionId) {
580
+ const loaded = await loadCatalog(collectionId);
581
+ if (loaded.data.entries.length > limits.maxCatalogArticles)
582
+ throw new RepositoryCapacityError("index_catalog");
583
+ const active = loaded.data.entries.filter((entry) => entry.scopes.length !== 0);
584
+ return {
585
+ articleIds: active.map((entry) => entry.articleId),
586
+ scopes: scopesUnion(active.flatMap((entry) => entry.scopes)),
587
+ };
588
+ },
589
+ async readPostingPages(scope, term) {
590
+ requireRetrievalScope(scope);
591
+ const termHash = await retrievalTermHash(term);
592
+ const loaded = await loadPosting(await scopePartition(scope), scope, termHash);
593
+ return loaded.pages.map((page) => ({
594
+ entries: page.value.data.entries.map((entry) => snapshotData(entry.hint)),
595
+ }));
596
+ },
597
+ async readArticleProjection(articleId) {
598
+ return (await readState(articleId))?.state ?? null;
599
+ },
600
+ async beginArticleProjection(input) {
601
+ const request = snapshotData(input);
602
+ if (Object.keys(request).some((key) => ![
603
+ "articleId",
604
+ "generation",
605
+ "sourceGeneration",
606
+ "targets",
607
+ ].includes(key)))
608
+ throw new TypeError("Unexpected projection field");
609
+ requireIdentifier(request.articleId);
610
+ requireGeneration(request.generation, 1);
611
+ requireGeneration(request.sourceGeneration, 1);
612
+ if (!Array.isArray(request.targets) || request.targets.length > 2)
613
+ throw new TypeError("Expected bounded projection targets");
614
+ const targets = [];
615
+ for (const target of request.targets) {
616
+ exactObject(target, ["scope", "hint", "terms"]);
617
+ requireRetrievalScope(target.scope);
618
+ requireRetrievalHint(target.hint);
619
+ if (!Array.isArray(target.terms) ||
620
+ target.terms.length > limits.maxTermsPerArticle)
621
+ throw new RepositoryCapacityError("index_article_terms");
622
+ const termHashes = [
623
+ ...new Set(await Promise.all(target.terms.map(retrievalTermHash))),
624
+ ].sort();
625
+ const stored = { scope: target.scope, hint: target.hint, termHashes };
626
+ requireTarget(stored);
627
+ if (target.hint.articleId !== request.articleId ||
628
+ target.hint.generation !== request.sourceGeneration)
629
+ throw new TypeError("Projection hint does not match source");
630
+ targets.push(stored);
631
+ }
632
+ targets.sort((a, b) => compare(scopeIdentity(a.scope), scopeIdentity(b.scope)));
633
+ unique(targets.map((target) => scopeIdentity(target.scope)));
634
+ for (let attempt = 0; attempt < maximumAttempts; attempt += 1) {
635
+ const before = await readState(request.articleId);
636
+ const current = before?.state;
637
+ if (current !== undefined &&
638
+ request.sourceGeneration < current.sourceGeneration)
639
+ throw new RepositoryConflictError();
640
+ const same = current !== undefined &&
641
+ current.sourceGeneration === request.sourceGeneration &&
642
+ canonicalJson(current.targets) === canonicalJson(targets);
643
+ if (same && current.status !== "blocked")
644
+ return current;
645
+ if (!same && request.generation !== (current?.generation ?? 0) + 1)
646
+ throw new RepositoryConflictError();
647
+ const knownScopes = scopesUnion([
648
+ ...(current?.knownScopes ?? []),
649
+ ...targets.map((target) => target.scope),
650
+ ]);
651
+ if (knownScopes.length > maximumKnownScopes)
652
+ throw new RepositoryCapacityError("index_pending_scopes");
653
+ const state = same
654
+ ? { ...current, status: "pending" }
655
+ : {
656
+ articleId: request.articleId,
657
+ generation: request.generation,
658
+ sourceGeneration: request.sourceGeneration,
659
+ status: "pending",
660
+ targets,
661
+ knownScopes,
662
+ pendingScopes: knownScopes,
663
+ };
664
+ const { reason: _reason, ...clean } = state;
665
+ const partition = await articlePartition(request.articleId);
666
+ if (await transact(partition, [
667
+ write(row(partition, "head", "article", clean), before?.row ?? null),
668
+ ]))
669
+ return clean;
670
+ }
671
+ throw new RepositoryConflictError();
672
+ },
673
+ async advanceArticleProjection(articleId, expectedGeneration) {
674
+ requireGeneration(expectedGeneration, 1);
675
+ const before = await readState(articleId);
676
+ if (before === null || before.state.generation !== expectedGeneration)
677
+ throw new RepositoryConflictError();
678
+ const state = before.state;
679
+ if (state.status === "ready")
680
+ return { state, complete: true, changed: false };
681
+ if (state.status === "blocked")
682
+ return { state, complete: false, changed: false };
683
+ const scope = state.pendingScopes[0];
684
+ if (scope !== undefined) {
685
+ await registerCatalog(state, scope.collectionId);
686
+ if (!(await applyScopeBatch(state, scope)))
687
+ return { state, complete: false, changed: true };
688
+ }
689
+ for (let attempt = 0; attempt < maximumAttempts; attempt += 1) {
690
+ const latest = await readState(articleId);
691
+ if (latest === null || latest.state.generation !== expectedGeneration)
692
+ throw new RepositoryConflictError();
693
+ if (latest.state.status === "blocked")
694
+ return { state: latest.state, complete: false, changed: false };
695
+ const pendingScopes = scope === undefined
696
+ ? latest.state.pendingScopes
697
+ : latest.state.pendingScopes.filter((item) => !sameScope(item, scope));
698
+ const complete = pendingScopes.length === 0;
699
+ const next = {
700
+ ...latest.state,
701
+ pendingScopes,
702
+ knownScopes: complete
703
+ ? latest.state.targets.map((target) => target.scope)
704
+ : latest.state.knownScopes,
705
+ status: complete ? "ready" : "pending",
706
+ };
707
+ const partition = latest.row.value.partition;
708
+ if (await transact(partition, [
709
+ write(row(partition, "head", "article", next), latest.row),
710
+ ]))
711
+ return { state: next, complete, changed: true };
712
+ }
713
+ throw new RepositoryConflictError();
714
+ },
715
+ async markArticleProjectionBlocked(articleId, generation, reason, sourceGeneration) {
716
+ requireGeneration(generation, 1);
717
+ if (sourceGeneration !== undefined)
718
+ requireGeneration(sourceGeneration, 1);
719
+ if (!["capacity", "source-unavailable", "conflict"].includes(reason))
720
+ throw new TypeError("Expected a projection block reason");
721
+ for (let attempt = 0; attempt < maximumAttempts; attempt += 1) {
722
+ const before = await readState(articleId);
723
+ if (before === null &&
724
+ (generation !== 1 || sourceGeneration === undefined))
725
+ throw new RepositoryConflictError();
726
+ if (before !== null && before.state.generation !== generation)
727
+ throw new RepositoryConflictError();
728
+ const state = before === null
729
+ ? {
730
+ articleId,
731
+ generation,
732
+ sourceGeneration: sourceGeneration,
733
+ status: "blocked",
734
+ reason,
735
+ targets: [],
736
+ pendingScopes: [],
737
+ knownScopes: [],
738
+ }
739
+ : { ...before.state, status: "blocked", reason };
740
+ const partition = before?.row.value.partition ?? (await articlePartition(articleId));
741
+ if (await transact(partition, [
742
+ write(row(partition, "head", "article", state), before?.row ?? null),
743
+ ]))
744
+ return state;
745
+ }
746
+ throw new RepositoryConflictError();
747
+ },
748
+ };
749
+ }
750
+ //# sourceMappingURL=retrieval-repository.js.map