@powerhousedao/reactor 6.2.2-dev.49 → 6.2.2-dev.50
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/{build-worker-executor-CHkkh55C.js → build-worker-executor-CkuJGUlJ.js} +2 -2
- package/dist/{build-worker-executor-CHkkh55C.js.map → build-worker-executor-CkuJGUlJ.js.map} +1 -1
- package/dist/{document-indexer-BfdCgjG-.js → document-indexer-DlpJB8AK.js} +5 -4
- package/dist/document-indexer-DlpJB8AK.js.map +1 -0
- package/dist/{drive-container-types-h3M1AK3K.js → drive-container-types-BoY5t12r.js} +570 -561
- package/dist/drive-container-types-BoY5t12r.js.map +1 -0
- package/dist/entry.js +2 -2
- package/dist/index.d.ts +566 -385
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +381 -57
- package/dist/index.js.map +1 -1
- package/dist/projection-entry.js +3 -3
- package/dist/{worker-CbtYMIju.js → worker-BCMw-hBw.js} +2 -2
- package/dist/{worker-CbtYMIju.js.map → worker-BCMw-hBw.js.map} +1 -1
- package/package.json +4 -4
- package/dist/document-indexer-BfdCgjG-.js.map +0 -1
- package/dist/drive-container-types-h3M1AK3K.js.map +0 -1
|
@@ -219,93 +219,444 @@ var DocumentNotFoundError = class DocumentNotFoundError extends Error {
|
|
|
219
219
|
}
|
|
220
220
|
};
|
|
221
221
|
//#endregion
|
|
222
|
-
//#region src/
|
|
222
|
+
//#region src/decision/build-decision-model.ts
|
|
223
223
|
/**
|
|
224
|
-
*
|
|
224
|
+
* Reads each projection's stream through the supplied reader, recording the
|
|
225
|
+
* revision observed. Static projections resolve first; derived projections
|
|
226
|
+
* see only those and contribute a map from document id to state. Each
|
|
227
|
+
* distinct stream is read once and yields one append condition entry.
|
|
225
228
|
*/
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
this.requestedVersion = version;
|
|
229
|
+
async function buildDecisionModel(reader, definition, target, signal) {
|
|
230
|
+
const decisionModel = definition(target);
|
|
231
|
+
const projections = Object.entries(decisionModel.projections);
|
|
232
|
+
const reads = /* @__PURE__ */ new Map();
|
|
233
|
+
const model = {};
|
|
234
|
+
for (const [key, projection] of projections) {
|
|
235
|
+
if (typeof projection.query === "function") continue;
|
|
236
|
+
model[key] = (await readStream(reader, projection.query, reads, signal)).state;
|
|
235
237
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
+
const staticModel = { ...model };
|
|
239
|
+
for (const [key, projection] of projections) {
|
|
240
|
+
if (typeof projection.query !== "function") continue;
|
|
241
|
+
const queries = projection.query(staticModel);
|
|
242
|
+
const value = {};
|
|
243
|
+
for (const query of queries) {
|
|
244
|
+
let read;
|
|
245
|
+
try {
|
|
246
|
+
read = await readStream(reader, query, reads, signal);
|
|
247
|
+
} catch (error) {
|
|
248
|
+
if (error instanceof DocumentNotFoundError) {
|
|
249
|
+
recordEmptyStream(query, reads);
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
throw error;
|
|
253
|
+
}
|
|
254
|
+
value[query.documentId] = read.state;
|
|
255
|
+
}
|
|
256
|
+
model[key] = value;
|
|
238
257
|
}
|
|
239
|
-
|
|
258
|
+
return {
|
|
259
|
+
model,
|
|
260
|
+
appendCondition: { streams: [...reads.values()].map((read) => read.stream) }
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
/** Guards a stream that holds nothing yet: any operation appearing is growth. */
|
|
264
|
+
function recordEmptyStream(query, reads) {
|
|
265
|
+
const key = `${query.documentId}:${query.scope}:${query.branch}`;
|
|
266
|
+
if (reads.has(key)) return;
|
|
267
|
+
reads.set(key, {
|
|
268
|
+
state: void 0,
|
|
269
|
+
stream: {
|
|
270
|
+
documentId: query.documentId,
|
|
271
|
+
scope: query.scope,
|
|
272
|
+
branch: query.branch,
|
|
273
|
+
revision: -1
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
async function readStream(reader, query, reads, signal) {
|
|
278
|
+
const key = `${query.documentId}:${query.scope}:${query.branch}`;
|
|
279
|
+
const existing = reads.get(key);
|
|
280
|
+
if (existing) return existing;
|
|
281
|
+
const document = await reader.getState(query.documentId, query.scope, query.branch, void 0, signal);
|
|
282
|
+
const read = {
|
|
283
|
+
state: document.state[query.scope],
|
|
284
|
+
stream: {
|
|
285
|
+
documentId: query.documentId,
|
|
286
|
+
scope: query.scope,
|
|
287
|
+
branch: query.branch,
|
|
288
|
+
revision: observedRevision(document, query.scope)
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
reads.set(key, read);
|
|
292
|
+
return read;
|
|
293
|
+
}
|
|
240
294
|
/**
|
|
241
|
-
*
|
|
295
|
+
* The highest operation index the document reflects for the scope, or -1 if
|
|
296
|
+
* empty. `header.revision` is authoritative, not the rebuilt operation list.
|
|
242
297
|
*/
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
}
|
|
249
|
-
static isError(error) {
|
|
250
|
-
return Error.isError(error) && error.name === "DuplicateModuleError";
|
|
298
|
+
function observedRevision(document, scope) {
|
|
299
|
+
if (scope in document.header.revision) return document.header.revision[scope] - 1;
|
|
300
|
+
if (scope in document.operations) {
|
|
301
|
+
const operations = document.operations[scope];
|
|
302
|
+
if (operations.length > 0) return operations[operations.length - 1].index;
|
|
251
303
|
}
|
|
252
|
-
|
|
304
|
+
if (!(scope in document.header.revision)) return -1;
|
|
305
|
+
return document.header.revision[scope] - 1;
|
|
306
|
+
}
|
|
253
307
|
/**
|
|
254
|
-
*
|
|
308
|
+
* The projections whose queries depend on folded state. A positional walk
|
|
309
|
+
* resolves their streams through `queryOverHistory`; a projection without one
|
|
310
|
+
* contributes no streams to a walk.
|
|
255
311
|
*/
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
312
|
+
function derivedReadSet(definition) {
|
|
313
|
+
const projections = [];
|
|
314
|
+
for (const [name, projection] of Object.entries(definition.projections)) {
|
|
315
|
+
if (typeof projection.query !== "function") continue;
|
|
316
|
+
projections.push({
|
|
317
|
+
name,
|
|
318
|
+
decidingActions: projection.decidingActions,
|
|
319
|
+
apply: projection.apply,
|
|
320
|
+
queryOverHistory: projection.queryOverHistory
|
|
321
|
+
});
|
|
260
322
|
}
|
|
261
|
-
|
|
323
|
+
return projections;
|
|
324
|
+
}
|
|
262
325
|
/**
|
|
263
|
-
*
|
|
326
|
+
* The streams a model reads whose queries are known before it is built. A
|
|
327
|
+
* derived query needs the statically-queried projections first, so it is not
|
|
328
|
+
* included here.
|
|
264
329
|
*/
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
330
|
+
function staticReadSet(definition) {
|
|
331
|
+
const streams = [];
|
|
332
|
+
for (const [name, projection] of Object.entries(definition.projections)) {
|
|
333
|
+
if (typeof projection.query === "function") continue;
|
|
334
|
+
streams.push({
|
|
335
|
+
name,
|
|
336
|
+
query: projection.query,
|
|
337
|
+
decidingActions: projection.decidingActions,
|
|
338
|
+
apply: projection.apply
|
|
339
|
+
});
|
|
272
340
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
341
|
+
return streams;
|
|
342
|
+
}
|
|
343
|
+
//#endregion
|
|
344
|
+
//#region src/decision/auth-decision-model.ts
|
|
345
|
+
function refusalReason(refusal) {
|
|
346
|
+
switch (refusal) {
|
|
347
|
+
case "version-unsupported": return AUTH_VERSION_UNSUPPORTED_REASON;
|
|
348
|
+
case "denied-by-grant": return AUTH_DENIED_BY_GRANT_REASON;
|
|
349
|
+
case "no-applicable-grant": return AUTH_NO_GRANT_REASON;
|
|
281
350
|
}
|
|
282
|
-
}
|
|
351
|
+
}
|
|
352
|
+
function decideAuthModel(model, subject, request, groups, conditions) {
|
|
353
|
+
if (request.verb === "execute" && model.document.isDeleted) return {
|
|
354
|
+
decision: "deny",
|
|
355
|
+
reason: DOCUMENT_DELETED_REASON
|
|
356
|
+
};
|
|
357
|
+
const evaluation = evaluate(model.auth, subject, request, groups, conditions);
|
|
358
|
+
if (evaluation.decision === "allow") return { decision: "allow" };
|
|
359
|
+
return {
|
|
360
|
+
decision: "deny",
|
|
361
|
+
reason: refusalReason(evaluation.refusal)
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
function documentProjection(target) {
|
|
365
|
+
return {
|
|
366
|
+
decidingActions: ["DELETE_DOCUMENT"],
|
|
367
|
+
apply: (document, operation) => operation.action.type === "DELETE_DOCUMENT" ? applyDeleteDocumentAction({
|
|
368
|
+
...document,
|
|
369
|
+
state: { ...document.state }
|
|
370
|
+
}, operation.action) : document,
|
|
371
|
+
query: {
|
|
372
|
+
documentId: target.documentId,
|
|
373
|
+
branch: target.branch,
|
|
374
|
+
scope: "document"
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
function authProjection(target) {
|
|
379
|
+
return {
|
|
380
|
+
decidingActions: [...AUTH_ACTION_TYPES],
|
|
381
|
+
apply: (document, operation) => applyAuthAction(document, operation.action),
|
|
382
|
+
query: {
|
|
383
|
+
documentId: target.documentId,
|
|
384
|
+
branch: target.branch,
|
|
385
|
+
scope: "auth"
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
/** This decision model uses both the document and the auth streams. */
|
|
390
|
+
function authDecisionModel(target) {
|
|
391
|
+
return {
|
|
392
|
+
projections: {
|
|
393
|
+
document: documentProjection(target),
|
|
394
|
+
auth: authProjection(target)
|
|
395
|
+
},
|
|
396
|
+
evaluatesScope() {
|
|
397
|
+
return true;
|
|
398
|
+
},
|
|
399
|
+
decide(model, subject, request) {
|
|
400
|
+
return decideAuthModel(model, subject, request);
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
}
|
|
283
404
|
/**
|
|
284
|
-
*
|
|
405
|
+
* Folds one group-stream operation with the registered group model's reducer.
|
|
406
|
+
* A reactor without the module registered folds nothing, so the member list
|
|
407
|
+
* stays as read and a missing reducer never widens access.
|
|
285
408
|
*/
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
409
|
+
function applyGroupOperation(registry, document, operation) {
|
|
410
|
+
let reducer;
|
|
411
|
+
try {
|
|
412
|
+
reducer = registry.getModule(groupDocumentType).reducer;
|
|
413
|
+
} catch {
|
|
414
|
+
return document;
|
|
290
415
|
}
|
|
291
|
-
|
|
416
|
+
return reducer(document, operation.action);
|
|
417
|
+
}
|
|
292
418
|
/**
|
|
293
|
-
*
|
|
419
|
+
* Folds one evaluated-scope operation with the reducer registered for the
|
|
420
|
+
* document's own type, at the document's stamped version. A reactor without
|
|
421
|
+
* that module folds nothing, so conditions read the base state and an
|
|
422
|
+
* unresolvable reducer never widens access.
|
|
294
423
|
*/
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
424
|
+
function applyModelOperation(registry, document, operation) {
|
|
425
|
+
let reducer;
|
|
426
|
+
try {
|
|
427
|
+
const version = normalizeDocumentModelVersion(document.state.document?.version);
|
|
428
|
+
reducer = registry.getModule(document.header.documentType, version).reducer;
|
|
429
|
+
} catch {
|
|
430
|
+
return document;
|
|
299
431
|
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
//#region src/storage/interfaces.ts
|
|
432
|
+
return reducer(document, operation.action);
|
|
433
|
+
}
|
|
303
434
|
/**
|
|
304
|
-
*
|
|
435
|
+
* The auth model extended with a derived groups projection: the streams it
|
|
436
|
+
* reads are the group documents the folded grant list names, so adding a
|
|
437
|
+
* grant that names a new group pulls that group's stream into the read-set.
|
|
438
|
+
* Group queries pin the main branch, because a group's member list lives on
|
|
439
|
+
* its main branch no matter which branch the referencing document is on.
|
|
305
440
|
*/
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
441
|
+
function groupsProjection(registry) {
|
|
442
|
+
return {
|
|
443
|
+
decidingActions: [...groupMembershipActionTypes],
|
|
444
|
+
apply: (document, operation) => applyGroupOperation(registry, document, operation),
|
|
445
|
+
query: (model) => referencedGroupIds(model.auth?.grants ?? []).map((id) => ({
|
|
446
|
+
documentId: id,
|
|
447
|
+
branch: "main",
|
|
448
|
+
scope: "global"
|
|
449
|
+
})),
|
|
450
|
+
queryOverHistory: (reads) => {
|
|
451
|
+
const ids = [];
|
|
452
|
+
for (const read of reads) {
|
|
453
|
+
if (read.name !== "auth") continue;
|
|
454
|
+
for (const operation of read.operations) for (const id of mentionedGroupIds(operation.action)) if (!ids.includes(id)) ids.push(id);
|
|
455
|
+
}
|
|
456
|
+
return ids.map((id) => ({
|
|
457
|
+
documentId: id,
|
|
458
|
+
branch: "main",
|
|
459
|
+
scope: "global"
|
|
460
|
+
}));
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
function authGroupsDecisionModel(registry) {
|
|
465
|
+
return (target) => ({
|
|
466
|
+
projections: {
|
|
467
|
+
document: documentProjection(target),
|
|
468
|
+
auth: authProjection(target),
|
|
469
|
+
groups: groupsProjection(registry)
|
|
470
|
+
},
|
|
471
|
+
evaluatesScope() {
|
|
472
|
+
return true;
|
|
473
|
+
},
|
|
474
|
+
decide(model, subject, request) {
|
|
475
|
+
return decideAuthModel(model, subject, request, model.groups);
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* The groups model with conditions live: decide hands the executing scope's
|
|
481
|
+
* state and the action input through to the evaluator, so `where` clauses
|
|
482
|
+
* and { match } principals apply. The model folds the evaluated scope during
|
|
483
|
+
* a positional walk, so a condition reads the state as it stood at each
|
|
484
|
+
* operation's position.
|
|
485
|
+
*/
|
|
486
|
+
function authConditionsDecisionModel(registry) {
|
|
487
|
+
return (target) => ({
|
|
488
|
+
projections: {
|
|
489
|
+
document: documentProjection(target),
|
|
490
|
+
auth: authProjection(target),
|
|
491
|
+
groups: groupsProjection(registry)
|
|
492
|
+
},
|
|
493
|
+
foldEvaluatedScope: (document, operation) => applyModelOperation(registry, document, operation),
|
|
494
|
+
evaluatesScope() {
|
|
495
|
+
return true;
|
|
496
|
+
},
|
|
497
|
+
decide(model, subject, request, ctx) {
|
|
498
|
+
return decideAuthModel(model, subject, request, model.groups, {
|
|
499
|
+
scopeState: ctx.scopeState,
|
|
500
|
+
actionInput: ctx.actionInput
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
//#endregion
|
|
506
|
+
//#region src/decision/document-decision-model.ts
|
|
507
|
+
/**
|
|
508
|
+
* The simplest decision model: one projection over the document scope, which
|
|
509
|
+
* rejects on a deleted document.
|
|
510
|
+
*/
|
|
511
|
+
function documentDecisionModel(target) {
|
|
512
|
+
return {
|
|
513
|
+
projections: { document: {
|
|
514
|
+
decidingActions: ["DELETE_DOCUMENT"],
|
|
515
|
+
apply: (document, operation) => operation.action.type === "DELETE_DOCUMENT" ? applyDeleteDocumentAction({
|
|
516
|
+
...document,
|
|
517
|
+
state: { ...document.state }
|
|
518
|
+
}, operation.action) : document,
|
|
519
|
+
query: {
|
|
520
|
+
documentId: target.documentId,
|
|
521
|
+
branch: target.branch,
|
|
522
|
+
scope: "document"
|
|
523
|
+
}
|
|
524
|
+
} },
|
|
525
|
+
evaluatesScope() {
|
|
526
|
+
return true;
|
|
527
|
+
},
|
|
528
|
+
decide(model, subject, request) {
|
|
529
|
+
return request.verb === "execute" && model.document.isDeleted ? {
|
|
530
|
+
decision: "deny",
|
|
531
|
+
reason: DOCUMENT_DELETED_REASON
|
|
532
|
+
} : { decision: "allow" };
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
//#endregion
|
|
537
|
+
//#region src/decision/registered-model.ts
|
|
538
|
+
/**
|
|
539
|
+
* Builds the model at the stream heads and decides one request against it. The
|
|
540
|
+
* append condition it returns is the read-set the store enforces at write time.
|
|
541
|
+
*
|
|
542
|
+
* With `conditions` supplied, the executing scope's state is read at the head
|
|
543
|
+
* for `doc.<scope>.*` paths. That read carries no append-condition entry of
|
|
544
|
+
* its own: the written stream's expected-revision check already refuses a
|
|
545
|
+
* write whose scope grew between the read and the append.
|
|
546
|
+
*/
|
|
547
|
+
async function decideAtHead(model, cache, target, subject, request, signal, conditions) {
|
|
548
|
+
const built = await buildDecisionModel(cache, model, target, signal);
|
|
549
|
+
let scopeState;
|
|
550
|
+
if (conditions !== void 0) scopeState = (await cache.getState(target.documentId, request.scope, target.branch, void 0, signal)).state[request.scope];
|
|
551
|
+
return {
|
|
552
|
+
evaluation: model(target).decide(built.model, subject, request, {
|
|
553
|
+
scopeState,
|
|
554
|
+
actionInput: conditions?.actionInput
|
|
555
|
+
}),
|
|
556
|
+
appendCondition: built.appendCondition,
|
|
557
|
+
documentVersion: built.model.document.version,
|
|
558
|
+
deletedAtUtcIso: built.model.document.deletedAtUtcIso ?? null
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* The model this reactor enforces. With `authEnforcement` off the auth scope is
|
|
563
|
+
* absent from every append condition and no load walks it; with `authGroups`
|
|
564
|
+
* on, the group documents the grant list names join the read-set and the
|
|
565
|
+
* registry supplies the reducer that folds them.
|
|
566
|
+
*/
|
|
567
|
+
function selectDecisionModel(flags, registry) {
|
|
568
|
+
if (flags.authConditions) return authConditionsDecisionModel(registry);
|
|
569
|
+
if (flags.authGroups) return authGroupsDecisionModel(registry);
|
|
570
|
+
return flags.authEnforcement ? authDecisionModel : documentDecisionModel;
|
|
571
|
+
}
|
|
572
|
+
//#endregion
|
|
573
|
+
//#region src/registry/errors.ts
|
|
574
|
+
/**
|
|
575
|
+
* Error thrown when a document model module is not found in the registry.
|
|
576
|
+
*/
|
|
577
|
+
var ModuleNotFoundError = class extends Error {
|
|
578
|
+
documentType;
|
|
579
|
+
requestedVersion;
|
|
580
|
+
constructor(documentType, version) {
|
|
581
|
+
const versionSuffix = version !== void 0 ? ` version ${version}` : "";
|
|
582
|
+
super(`Document model module not found for type: ${documentType}${versionSuffix}`);
|
|
583
|
+
this.name = "ModuleNotFoundError";
|
|
584
|
+
this.documentType = documentType;
|
|
585
|
+
this.requestedVersion = version;
|
|
586
|
+
}
|
|
587
|
+
static isError(error) {
|
|
588
|
+
return Error.isError(error) && error.name === "ModuleNotFoundError";
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
/**
|
|
592
|
+
* Error thrown when attempting to register a module that already exists.
|
|
593
|
+
*/
|
|
594
|
+
var DuplicateModuleError = class extends Error {
|
|
595
|
+
constructor(documentType, version) {
|
|
596
|
+
const versionSuffix = version !== void 0 ? ` (version ${version})` : "";
|
|
597
|
+
super(`Document model module already registered for type: ${documentType}${versionSuffix}`);
|
|
598
|
+
this.name = "DuplicateModuleError";
|
|
599
|
+
}
|
|
600
|
+
static isError(error) {
|
|
601
|
+
return Error.isError(error) && error.name === "DuplicateModuleError";
|
|
602
|
+
}
|
|
603
|
+
};
|
|
604
|
+
/**
|
|
605
|
+
* Error thrown when a module is invalid or malformed.
|
|
606
|
+
*/
|
|
607
|
+
var InvalidModuleError = class extends Error {
|
|
608
|
+
constructor(message) {
|
|
609
|
+
super(`Invalid document model module: ${message}`);
|
|
610
|
+
this.name = "InvalidModuleError";
|
|
611
|
+
}
|
|
612
|
+
};
|
|
613
|
+
/**
|
|
614
|
+
* Error thrown when attempting to register an upgrade manifest that already exists.
|
|
615
|
+
*/
|
|
616
|
+
var DuplicateManifestError = class extends Error {
|
|
617
|
+
constructor(documentType) {
|
|
618
|
+
super(`Upgrade manifest already registered for type: ${documentType}`);
|
|
619
|
+
this.name = "DuplicateManifestError";
|
|
620
|
+
}
|
|
621
|
+
static isError(error) {
|
|
622
|
+
return Error.isError(error) && error.name === "DuplicateManifestError";
|
|
623
|
+
}
|
|
624
|
+
};
|
|
625
|
+
/**
|
|
626
|
+
* Error thrown when an upgrade manifest is not found.
|
|
627
|
+
*/
|
|
628
|
+
var ManifestNotFoundError = class extends Error {
|
|
629
|
+
constructor(documentType) {
|
|
630
|
+
super(`Upgrade manifest not found for type: ${documentType}`);
|
|
631
|
+
this.name = "ManifestNotFoundError";
|
|
632
|
+
}
|
|
633
|
+
};
|
|
634
|
+
/**
|
|
635
|
+
* Error thrown when a required upgrade transition is missing from the manifest.
|
|
636
|
+
*/
|
|
637
|
+
var MissingUpgradeTransitionError = class extends Error {
|
|
638
|
+
constructor(documentType, fromVersion, toVersion) {
|
|
639
|
+
super(`Missing upgrade transition for ${documentType}: v${fromVersion} to v${toVersion}`);
|
|
640
|
+
this.name = "MissingUpgradeTransitionError";
|
|
641
|
+
}
|
|
642
|
+
};
|
|
643
|
+
/**
|
|
644
|
+
* Error thrown when getUpgradeReducer is called with a non-single-step version increment.
|
|
645
|
+
*/
|
|
646
|
+
var InvalidUpgradeStepError = class extends Error {
|
|
647
|
+
constructor(documentType, fromVersion, toVersion) {
|
|
648
|
+
super(`Invalid upgrade step for ${documentType}: must be single version increment, got v${fromVersion} to v${toVersion}`);
|
|
649
|
+
this.name = "InvalidUpgradeStepError";
|
|
650
|
+
}
|
|
651
|
+
};
|
|
652
|
+
//#endregion
|
|
653
|
+
//#region src/storage/interfaces.ts
|
|
654
|
+
/**
|
|
655
|
+
* Thrown when an operation with the same identity already exists in the store.
|
|
656
|
+
*/
|
|
657
|
+
var DuplicateOperationError = class extends Error {
|
|
658
|
+
constructor(description) {
|
|
659
|
+
super(`Duplicate operation: ${description}`);
|
|
309
660
|
this.name = "DuplicateOperationError";
|
|
310
661
|
}
|
|
311
662
|
};
|
|
@@ -1777,509 +2128,173 @@ var EventBus = class {
|
|
|
1777
2128
|
subscribe(type, subscriber) {
|
|
1778
2129
|
let list = this.eventTypeToSubscribers.get(type);
|
|
1779
2130
|
if (!list) {
|
|
1780
|
-
list = [];
|
|
1781
|
-
this.eventTypeToSubscribers.set(type, list);
|
|
1782
|
-
}
|
|
1783
|
-
list.push(subscriber);
|
|
1784
|
-
let done = false;
|
|
1785
|
-
return () => {
|
|
1786
|
-
if (done) return;
|
|
1787
|
-
done = true;
|
|
1788
|
-
const arr = this.eventTypeToSubscribers.get(type);
|
|
1789
|
-
if (!arr) return;
|
|
1790
|
-
const idx = arr.indexOf(subscriber);
|
|
1791
|
-
if (idx !== -1) arr.splice(idx, 1);
|
|
1792
|
-
if (arr.length === 0) this.eventTypeToSubscribers.delete(type);
|
|
1793
|
-
};
|
|
1794
|
-
}
|
|
1795
|
-
async emit(type, data) {
|
|
1796
|
-
const list = this.eventTypeToSubscribers.get(type);
|
|
1797
|
-
if (!list || list.length === 0) return;
|
|
1798
|
-
const snapshot = list.slice();
|
|
1799
|
-
const errors = [];
|
|
1800
|
-
for (const fn of snapshot) try {
|
|
1801
|
-
await Promise.resolve(fn(type, data));
|
|
1802
|
-
} catch (err) {
|
|
1803
|
-
errors.push(err);
|
|
1804
|
-
}
|
|
1805
|
-
if (errors.length > 0) throw new EventBusAggregateError(errors);
|
|
1806
|
-
}
|
|
1807
|
-
};
|
|
1808
|
-
//#endregion
|
|
1809
|
-
//#region src/core/feature-flags.ts
|
|
1810
|
-
/**
|
|
1811
|
-
* Every flag this reactor knows, with the flags it requires. A stage adds its
|
|
1812
|
-
* flag here when it ships, so asking an older reactor for a later stage's flag
|
|
1813
|
-
* is an unrecognized name rather than a flag that quietly does nothing.
|
|
1814
|
-
*/
|
|
1815
|
-
const FLAG_PREREQUISITES = {
|
|
1816
|
-
documentDecisions: [],
|
|
1817
|
-
authEnforcement: ["documentDecisions"],
|
|
1818
|
-
authGroups: ["authEnforcement"],
|
|
1819
|
-
authConditions: ["authGroups"]
|
|
1820
|
-
};
|
|
1821
|
-
/**
|
|
1822
|
-
* Throws when the flags ask for enforcement the reactor cannot deliver. Either
|
|
1823
|
-
* failure would otherwise read as enforcement being on while the reactor
|
|
1824
|
-
* applies less than the caller asked for.
|
|
1825
|
-
*/
|
|
1826
|
-
function validateFeatureFlags(flags, prerequisites) {
|
|
1827
|
-
const known = Object.keys(prerequisites);
|
|
1828
|
-
const unrecognized = Object.keys(flags).filter((name) => !known.includes(name));
|
|
1829
|
-
if (unrecognized.length > 0) throw new Error(`Unrecognized reactor feature flag: ${unrecognized.join(", ")}. This reactor knows: ${known.join(", ")}.`);
|
|
1830
|
-
for (const name of known) {
|
|
1831
|
-
if (flags[name] !== true) continue;
|
|
1832
|
-
const missing = prerequisites[name].filter((required) => flags[required] !== true);
|
|
1833
|
-
if (missing.length > 0) throw new Error(`Reactor feature flag ${name} requires ${missing.join(", ")}.`);
|
|
1834
|
-
}
|
|
1835
|
-
}
|
|
1836
|
-
//#endregion
|
|
1837
|
-
//#region src/executor/execution-scope.ts
|
|
1838
|
-
var DefaultExecutionScope = class {
|
|
1839
|
-
constructor(operationStore, operationIndex, writeCache, documentMetaCache, collectionMembershipCache) {
|
|
1840
|
-
this.operationStore = operationStore;
|
|
1841
|
-
this.operationIndex = operationIndex;
|
|
1842
|
-
this.writeCache = writeCache;
|
|
1843
|
-
this.documentMetaCache = documentMetaCache;
|
|
1844
|
-
this.collectionMembershipCache = collectionMembershipCache;
|
|
1845
|
-
}
|
|
1846
|
-
async run(fn, signal) {
|
|
1847
|
-
signal?.throwIfAborted();
|
|
1848
|
-
return fn({
|
|
1849
|
-
operationStore: this.operationStore,
|
|
1850
|
-
operationIndex: this.operationIndex,
|
|
1851
|
-
writeCache: this.writeCache,
|
|
1852
|
-
documentMetaCache: this.documentMetaCache,
|
|
1853
|
-
collectionMembershipCache: this.collectionMembershipCache
|
|
1854
|
-
});
|
|
1855
|
-
}
|
|
1856
|
-
};
|
|
1857
|
-
var KyselyExecutionScope = class {
|
|
1858
|
-
constructor(db, operationStore, operationIndex, keyframeStore, writeCache, documentMetaCache, collectionMembershipCache) {
|
|
1859
|
-
this.db = db;
|
|
1860
|
-
this.operationStore = operationStore;
|
|
1861
|
-
this.operationIndex = operationIndex;
|
|
1862
|
-
this.keyframeStore = keyframeStore;
|
|
1863
|
-
this.writeCache = writeCache;
|
|
1864
|
-
this.documentMetaCache = documentMetaCache;
|
|
1865
|
-
this.collectionMembershipCache = collectionMembershipCache;
|
|
1866
|
-
}
|
|
1867
|
-
async run(fn, signal) {
|
|
1868
|
-
signal?.throwIfAborted();
|
|
1869
|
-
return this.db.transaction().execute(async (trx) => {
|
|
1870
|
-
const scopedOperationStore = this.operationStore.withTransaction(trx);
|
|
1871
|
-
const scopedOperationIndex = this.operationIndex.withTransaction(trx);
|
|
1872
|
-
const scopedKeyframeStore = this.keyframeStore.withTransaction(trx);
|
|
1873
|
-
return fn({
|
|
1874
|
-
operationStore: scopedOperationStore,
|
|
1875
|
-
operationIndex: scopedOperationIndex,
|
|
1876
|
-
writeCache: this.writeCache.withScopedStores(scopedOperationStore, scopedKeyframeStore),
|
|
1877
|
-
documentMetaCache: this.documentMetaCache.withScopedStore(scopedOperationStore),
|
|
1878
|
-
collectionMembershipCache: this.collectionMembershipCache.withScopedIndex(scopedOperationIndex)
|
|
1879
|
-
});
|
|
1880
|
-
});
|
|
1881
|
-
}
|
|
1882
|
-
};
|
|
1883
|
-
//#endregion
|
|
1884
|
-
//#region src/utils/reshuffle.ts
|
|
1885
|
-
const STRICT_ORDER_ACTION_TYPES = new Set([
|
|
1886
|
-
"CREATE_DOCUMENT",
|
|
1887
|
-
"DELETE_DOCUMENT",
|
|
1888
|
-
"UPGRADE_DOCUMENT",
|
|
1889
|
-
"ADD_RELATIONSHIP",
|
|
1890
|
-
"REMOVE_RELATIONSHIP",
|
|
1891
|
-
"UPDATE_RELATIONSHIP",
|
|
1892
|
-
"ADD_FOLDER",
|
|
1893
|
-
"UPDATE_FOLDER",
|
|
1894
|
-
"REMOVE_FOLDER"
|
|
1895
|
-
]);
|
|
1896
|
-
/**
|
|
1897
|
-
* Reshuffles operations by timestamp, then applies deterministic tie-breaking.
|
|
1898
|
-
* Used for merging concurrent operations from different branches.
|
|
1899
|
-
*
|
|
1900
|
-
* For strict document-structure actions (e.g., CREATE_DOCUMENT/UPGRADE_DOCUMENT),
|
|
1901
|
-
* logical index (index - skip) is prioritized to preserve causal replay order.
|
|
1902
|
-
*
|
|
1903
|
-
* For other actions, action ID is prioritized to ensure a canonical cross-reactor order
|
|
1904
|
-
* for concurrent operations that may have diverged local indices due to prior reshuffles.
|
|
1905
|
-
* Logical index and operation ID are then used as deterministic tie-breakers.
|
|
1906
|
-
*
|
|
1907
|
-
* Example:
|
|
1908
|
-
* [0:0, 1:0, 2:0, A3:0, A4:0, A5:0] + [0:0, 1:0, 2:0, B3:0, B4:2, B5:0]
|
|
1909
|
-
* GC => [0:0, 1:0, 2:0, A3:0, A4:0, A5:0] + [0:0, 1:0, B4:2, B5:0]
|
|
1910
|
-
* Split => [0:0, 1:0] + [2:0, A3:0, A4:0, A5:0] + [B4:2, B5:0]
|
|
1911
|
-
* Reshuffle(6:4) => [6:4, 7:0, 8:0, 9:0, 10:0, 11:0]
|
|
1912
|
-
* merge => [0:0, 1:0, 6:4, 7:0, 8:0, 9:0, 10:0, 11:0]
|
|
1913
|
-
*/
|
|
1914
|
-
function reshuffleByTimestamp(startIndex, opsA, opsB) {
|
|
1915
|
-
return [...opsA, ...opsB].sort((a, b) => {
|
|
1916
|
-
const timestampDiff = new Date(a.timestampUtcMs).getTime() - new Date(b.timestampUtcMs).getTime();
|
|
1917
|
-
if (timestampDiff !== 0) return timestampDiff;
|
|
1918
|
-
const shouldPrioritizeLogicalIndex = STRICT_ORDER_ACTION_TYPES.has(a.action?.type ?? "") || STRICT_ORDER_ACTION_TYPES.has(b.action?.type ?? "");
|
|
1919
|
-
const logicalIndexDiff = a.index - a.skip - (b.index - b.skip);
|
|
1920
|
-
if (shouldPrioritizeLogicalIndex) {
|
|
1921
|
-
if (logicalIndexDiff !== 0) return logicalIndexDiff;
|
|
1922
|
-
}
|
|
1923
|
-
const actionIdDiff = (a.action?.id ?? "").localeCompare(b.action?.id ?? "");
|
|
1924
|
-
if (actionIdDiff !== 0) return actionIdDiff;
|
|
1925
|
-
if (!shouldPrioritizeLogicalIndex && logicalIndexDiff !== 0) return logicalIndexDiff;
|
|
1926
|
-
return a.id.localeCompare(b.id);
|
|
1927
|
-
}).map((op, i) => ({
|
|
1928
|
-
...op,
|
|
1929
|
-
index: startIndex.index + i,
|
|
1930
|
-
skip: i === 0 ? startIndex.skip : 0
|
|
1931
|
-
}));
|
|
1932
|
-
}
|
|
1933
|
-
//#endregion
|
|
1934
|
-
//#region src/decision/auth-decision-model.ts
|
|
1935
|
-
function refusalReason(refusal) {
|
|
1936
|
-
switch (refusal) {
|
|
1937
|
-
case "version-unsupported": return AUTH_VERSION_UNSUPPORTED_REASON;
|
|
1938
|
-
case "denied-by-grant": return AUTH_DENIED_BY_GRANT_REASON;
|
|
1939
|
-
case "no-applicable-grant": return AUTH_NO_GRANT_REASON;
|
|
1940
|
-
}
|
|
1941
|
-
}
|
|
1942
|
-
function decideAuthModel(model, subject, request, groups, conditions) {
|
|
1943
|
-
if (request.verb === "execute" && model.document.isDeleted) return {
|
|
1944
|
-
decision: "deny",
|
|
1945
|
-
reason: DOCUMENT_DELETED_REASON
|
|
1946
|
-
};
|
|
1947
|
-
const evaluation = evaluate(model.auth, subject, request, groups, conditions);
|
|
1948
|
-
if (evaluation.decision === "allow") return { decision: "allow" };
|
|
1949
|
-
return {
|
|
1950
|
-
decision: "deny",
|
|
1951
|
-
reason: refusalReason(evaluation.refusal)
|
|
1952
|
-
};
|
|
1953
|
-
}
|
|
1954
|
-
function documentProjection(target) {
|
|
1955
|
-
return {
|
|
1956
|
-
decidingActions: ["DELETE_DOCUMENT"],
|
|
1957
|
-
apply: (document, operation) => operation.action.type === "DELETE_DOCUMENT" ? applyDeleteDocumentAction({
|
|
1958
|
-
...document,
|
|
1959
|
-
state: { ...document.state }
|
|
1960
|
-
}, operation.action) : document,
|
|
1961
|
-
query: {
|
|
1962
|
-
documentId: target.documentId,
|
|
1963
|
-
branch: target.branch,
|
|
1964
|
-
scope: "document"
|
|
1965
|
-
}
|
|
1966
|
-
};
|
|
1967
|
-
}
|
|
1968
|
-
function authProjection(target) {
|
|
1969
|
-
return {
|
|
1970
|
-
decidingActions: [...AUTH_ACTION_TYPES],
|
|
1971
|
-
apply: (document, operation) => applyAuthAction(document, operation.action),
|
|
1972
|
-
query: {
|
|
1973
|
-
documentId: target.documentId,
|
|
1974
|
-
branch: target.branch,
|
|
1975
|
-
scope: "auth"
|
|
1976
|
-
}
|
|
1977
|
-
};
|
|
1978
|
-
}
|
|
1979
|
-
/** This decision model uses both the document and the auth streams. */
|
|
1980
|
-
function authDecisionModel(target) {
|
|
1981
|
-
return {
|
|
1982
|
-
projections: {
|
|
1983
|
-
document: documentProjection(target),
|
|
1984
|
-
auth: authProjection(target)
|
|
1985
|
-
},
|
|
1986
|
-
evaluatesScope() {
|
|
1987
|
-
return true;
|
|
1988
|
-
},
|
|
1989
|
-
decide(model, subject, request) {
|
|
1990
|
-
return decideAuthModel(model, subject, request);
|
|
1991
|
-
}
|
|
1992
|
-
};
|
|
1993
|
-
}
|
|
1994
|
-
/**
|
|
1995
|
-
* Folds one group-stream operation with the registered group model's reducer.
|
|
1996
|
-
* A reactor without the module registered folds nothing, so the member list
|
|
1997
|
-
* stays as read and a missing reducer never widens access.
|
|
1998
|
-
*/
|
|
1999
|
-
function applyGroupOperation(registry, document, operation) {
|
|
2000
|
-
let reducer;
|
|
2001
|
-
try {
|
|
2002
|
-
reducer = registry.getModule(groupDocumentType).reducer;
|
|
2003
|
-
} catch {
|
|
2004
|
-
return document;
|
|
2005
|
-
}
|
|
2006
|
-
return reducer(document, operation.action);
|
|
2007
|
-
}
|
|
2008
|
-
/**
|
|
2009
|
-
* Folds one evaluated-scope operation with the reducer registered for the
|
|
2010
|
-
* document's own type, at the document's stamped version. A reactor without
|
|
2011
|
-
* that module folds nothing, so conditions read the base state and an
|
|
2012
|
-
* unresolvable reducer never widens access.
|
|
2013
|
-
*/
|
|
2014
|
-
function applyModelOperation(registry, document, operation) {
|
|
2015
|
-
let reducer;
|
|
2016
|
-
try {
|
|
2017
|
-
const version = normalizeDocumentModelVersion(document.state.document?.version);
|
|
2018
|
-
reducer = registry.getModule(document.header.documentType, version).reducer;
|
|
2019
|
-
} catch {
|
|
2020
|
-
return document;
|
|
2021
|
-
}
|
|
2022
|
-
return reducer(document, operation.action);
|
|
2023
|
-
}
|
|
2024
|
-
/**
|
|
2025
|
-
* The auth model extended with a derived groups projection: the streams it
|
|
2026
|
-
* reads are the group documents the folded grant list names, so adding a
|
|
2027
|
-
* grant that names a new group pulls that group's stream into the read-set.
|
|
2028
|
-
* Group queries pin the main branch, because a group's member list lives on
|
|
2029
|
-
* its main branch no matter which branch the referencing document is on.
|
|
2030
|
-
*/
|
|
2031
|
-
function groupsProjection(registry) {
|
|
2032
|
-
return {
|
|
2033
|
-
decidingActions: [...groupMembershipActionTypes],
|
|
2034
|
-
apply: (document, operation) => applyGroupOperation(registry, document, operation),
|
|
2035
|
-
query: (model) => referencedGroupIds(model.auth?.grants ?? []).map((id) => ({
|
|
2036
|
-
documentId: id,
|
|
2037
|
-
branch: "main",
|
|
2038
|
-
scope: "global"
|
|
2039
|
-
})),
|
|
2040
|
-
queryOverHistory: (reads) => {
|
|
2041
|
-
const ids = [];
|
|
2042
|
-
for (const read of reads) {
|
|
2043
|
-
if (read.name !== "auth") continue;
|
|
2044
|
-
for (const operation of read.operations) for (const id of mentionedGroupIds(operation.action)) if (!ids.includes(id)) ids.push(id);
|
|
2045
|
-
}
|
|
2046
|
-
return ids.map((id) => ({
|
|
2047
|
-
documentId: id,
|
|
2048
|
-
branch: "main",
|
|
2049
|
-
scope: "global"
|
|
2050
|
-
}));
|
|
2051
|
-
}
|
|
2052
|
-
};
|
|
2053
|
-
}
|
|
2054
|
-
function authGroupsDecisionModel(registry) {
|
|
2055
|
-
return (target) => ({
|
|
2056
|
-
projections: {
|
|
2057
|
-
document: documentProjection(target),
|
|
2058
|
-
auth: authProjection(target),
|
|
2059
|
-
groups: groupsProjection(registry)
|
|
2060
|
-
},
|
|
2061
|
-
evaluatesScope() {
|
|
2062
|
-
return true;
|
|
2063
|
-
},
|
|
2064
|
-
decide(model, subject, request) {
|
|
2065
|
-
return decideAuthModel(model, subject, request, model.groups);
|
|
2066
|
-
}
|
|
2067
|
-
});
|
|
2068
|
-
}
|
|
2069
|
-
/**
|
|
2070
|
-
* The groups model with conditions live: decide hands the executing scope's
|
|
2071
|
-
* state and the action input through to the evaluator, so `where` clauses
|
|
2072
|
-
* and { match } principals apply. The model folds the evaluated scope during
|
|
2073
|
-
* a positional walk, so a condition reads the state as it stood at each
|
|
2074
|
-
* operation's position.
|
|
2075
|
-
*/
|
|
2076
|
-
function authConditionsDecisionModel(registry) {
|
|
2077
|
-
return (target) => ({
|
|
2078
|
-
projections: {
|
|
2079
|
-
document: documentProjection(target),
|
|
2080
|
-
auth: authProjection(target),
|
|
2081
|
-
groups: groupsProjection(registry)
|
|
2082
|
-
},
|
|
2083
|
-
foldEvaluatedScope: (document, operation) => applyModelOperation(registry, document, operation),
|
|
2084
|
-
evaluatesScope() {
|
|
2085
|
-
return true;
|
|
2086
|
-
},
|
|
2087
|
-
decide(model, subject, request, ctx) {
|
|
2088
|
-
return decideAuthModel(model, subject, request, model.groups, {
|
|
2089
|
-
scopeState: ctx.scopeState,
|
|
2090
|
-
actionInput: ctx.actionInput
|
|
2091
|
-
});
|
|
2092
|
-
}
|
|
2093
|
-
});
|
|
2094
|
-
}
|
|
2095
|
-
//#endregion
|
|
2096
|
-
//#region src/decision/build-decision-model.ts
|
|
2097
|
-
/**
|
|
2098
|
-
* Reads each projection's stream through the write cache, recording the
|
|
2099
|
-
* revision observed. Static projections resolve first; derived projections
|
|
2100
|
-
* see only those and contribute a map from document id to state. Each
|
|
2101
|
-
* distinct stream is read once and yields one append condition entry.
|
|
2102
|
-
*/
|
|
2103
|
-
async function buildDecisionModel(cache, definition, target, signal) {
|
|
2104
|
-
const decisionModel = definition(target);
|
|
2105
|
-
const projections = Object.entries(decisionModel.projections);
|
|
2106
|
-
const reads = /* @__PURE__ */ new Map();
|
|
2107
|
-
const model = {};
|
|
2108
|
-
for (const [key, projection] of projections) {
|
|
2109
|
-
if (typeof projection.query === "function") continue;
|
|
2110
|
-
model[key] = (await readStream(cache, projection.query, reads, signal)).state;
|
|
2111
|
-
}
|
|
2112
|
-
const staticModel = { ...model };
|
|
2113
|
-
for (const [key, projection] of projections) {
|
|
2114
|
-
if (typeof projection.query !== "function") continue;
|
|
2115
|
-
const queries = projection.query(staticModel);
|
|
2116
|
-
const value = {};
|
|
2117
|
-
for (const query of queries) {
|
|
2118
|
-
let read;
|
|
2119
|
-
try {
|
|
2120
|
-
read = await readStream(cache, query, reads, signal);
|
|
2121
|
-
} catch (error) {
|
|
2122
|
-
if (error instanceof DocumentNotFoundError) {
|
|
2123
|
-
recordEmptyStream(query, reads);
|
|
2124
|
-
continue;
|
|
2125
|
-
}
|
|
2126
|
-
throw error;
|
|
2127
|
-
}
|
|
2128
|
-
value[query.documentId] = read.state;
|
|
2129
|
-
}
|
|
2130
|
-
model[key] = value;
|
|
2131
|
-
}
|
|
2132
|
-
return {
|
|
2133
|
-
model,
|
|
2134
|
-
appendCondition: { streams: [...reads.values()].map((read) => read.stream) }
|
|
2135
|
-
};
|
|
2136
|
-
}
|
|
2137
|
-
/** Guards a stream that holds nothing yet: any operation appearing is growth. */
|
|
2138
|
-
function recordEmptyStream(query, reads) {
|
|
2139
|
-
const key = `${query.documentId}:${query.scope}:${query.branch}`;
|
|
2140
|
-
if (reads.has(key)) return;
|
|
2141
|
-
reads.set(key, {
|
|
2142
|
-
state: void 0,
|
|
2143
|
-
stream: {
|
|
2144
|
-
documentId: query.documentId,
|
|
2145
|
-
scope: query.scope,
|
|
2146
|
-
branch: query.branch,
|
|
2147
|
-
revision: -1
|
|
2131
|
+
list = [];
|
|
2132
|
+
this.eventTypeToSubscribers.set(type, list);
|
|
2148
2133
|
}
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2134
|
+
list.push(subscriber);
|
|
2135
|
+
let done = false;
|
|
2136
|
+
return () => {
|
|
2137
|
+
if (done) return;
|
|
2138
|
+
done = true;
|
|
2139
|
+
const arr = this.eventTypeToSubscribers.get(type);
|
|
2140
|
+
if (!arr) return;
|
|
2141
|
+
const idx = arr.indexOf(subscriber);
|
|
2142
|
+
if (idx !== -1) arr.splice(idx, 1);
|
|
2143
|
+
if (arr.length === 0) this.eventTypeToSubscribers.delete(type);
|
|
2144
|
+
};
|
|
2145
|
+
}
|
|
2146
|
+
async emit(type, data) {
|
|
2147
|
+
const list = this.eventTypeToSubscribers.get(type);
|
|
2148
|
+
if (!list || list.length === 0) return;
|
|
2149
|
+
const snapshot = list.slice();
|
|
2150
|
+
const errors = [];
|
|
2151
|
+
for (const fn of snapshot) try {
|
|
2152
|
+
await Promise.resolve(fn(type, data));
|
|
2153
|
+
} catch (err) {
|
|
2154
|
+
errors.push(err);
|
|
2163
2155
|
}
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2156
|
+
if (errors.length > 0) throw new EventBusAggregateError(errors);
|
|
2157
|
+
}
|
|
2158
|
+
};
|
|
2159
|
+
//#endregion
|
|
2160
|
+
//#region src/core/feature-flags.ts
|
|
2168
2161
|
/**
|
|
2169
|
-
*
|
|
2170
|
-
*
|
|
2162
|
+
* Every flag this reactor knows, with the flags it requires. A stage adds its
|
|
2163
|
+
* flag here when it ships, so asking an older reactor for a later stage's flag
|
|
2164
|
+
* is an unrecognized name rather than a flag that quietly does nothing.
|
|
2171
2165
|
*/
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
if (!(scope in document.header.revision)) return -1;
|
|
2179
|
-
return document.header.revision[scope] - 1;
|
|
2180
|
-
}
|
|
2166
|
+
const FLAG_PREREQUISITES = {
|
|
2167
|
+
documentDecisions: [],
|
|
2168
|
+
authEnforcement: ["documentDecisions"],
|
|
2169
|
+
authGroups: ["authEnforcement"],
|
|
2170
|
+
authConditions: ["authGroups"]
|
|
2171
|
+
};
|
|
2181
2172
|
/**
|
|
2182
|
-
* The
|
|
2183
|
-
*
|
|
2184
|
-
*
|
|
2173
|
+
* The flags as plain booleans, with anything unset off, validated. Callers hold
|
|
2174
|
+
* a partial set, because that is what crosses to a pooled worker, and every
|
|
2175
|
+
* consumer needs the same resolution of it.
|
|
2185
2176
|
*/
|
|
2186
|
-
function
|
|
2187
|
-
const
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
});
|
|
2196
|
-
}
|
|
2197
|
-
return projections;
|
|
2177
|
+
function resolveFeatureFlags(flags = {}) {
|
|
2178
|
+
const resolved = {
|
|
2179
|
+
documentDecisions: flags.documentDecisions ?? false,
|
|
2180
|
+
authEnforcement: flags.authEnforcement ?? false,
|
|
2181
|
+
authGroups: flags.authGroups ?? false,
|
|
2182
|
+
authConditions: flags.authConditions ?? false
|
|
2183
|
+
};
|
|
2184
|
+
validateFeatureFlags(flags, FLAG_PREREQUISITES);
|
|
2185
|
+
return resolved;
|
|
2198
2186
|
}
|
|
2199
2187
|
/**
|
|
2200
|
-
*
|
|
2201
|
-
*
|
|
2202
|
-
*
|
|
2188
|
+
* Throws when the flags ask for enforcement the reactor cannot deliver. Either
|
|
2189
|
+
* failure would otherwise read as enforcement being on while the reactor
|
|
2190
|
+
* applies less than the caller asked for.
|
|
2203
2191
|
*/
|
|
2204
|
-
function
|
|
2205
|
-
const
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
apply: projection.apply
|
|
2213
|
-
});
|
|
2192
|
+
function validateFeatureFlags(flags, prerequisites) {
|
|
2193
|
+
const known = Object.keys(prerequisites);
|
|
2194
|
+
const unrecognized = Object.keys(flags).filter((name) => !known.includes(name));
|
|
2195
|
+
if (unrecognized.length > 0) throw new Error(`Unrecognized reactor feature flag: ${unrecognized.join(", ")}. This reactor knows: ${known.join(", ")}.`);
|
|
2196
|
+
for (const name of known) {
|
|
2197
|
+
if (flags[name] !== true) continue;
|
|
2198
|
+
const missing = prerequisites[name].filter((required) => flags[required] !== true);
|
|
2199
|
+
if (missing.length > 0) throw new Error(`Reactor feature flag ${name} requires ${missing.join(", ")}.`);
|
|
2214
2200
|
}
|
|
2215
|
-
return streams;
|
|
2216
2201
|
}
|
|
2217
2202
|
//#endregion
|
|
2218
|
-
//#region src/
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
}
|
|
2203
|
+
//#region src/executor/execution-scope.ts
|
|
2204
|
+
var DefaultExecutionScope = class {
|
|
2205
|
+
constructor(operationStore, operationIndex, writeCache, documentMetaCache, collectionMembershipCache) {
|
|
2206
|
+
this.operationStore = operationStore;
|
|
2207
|
+
this.operationIndex = operationIndex;
|
|
2208
|
+
this.writeCache = writeCache;
|
|
2209
|
+
this.documentMetaCache = documentMetaCache;
|
|
2210
|
+
this.collectionMembershipCache = collectionMembershipCache;
|
|
2211
|
+
}
|
|
2212
|
+
async run(fn, signal) {
|
|
2213
|
+
signal?.throwIfAborted();
|
|
2214
|
+
return fn({
|
|
2215
|
+
operationStore: this.operationStore,
|
|
2216
|
+
operationIndex: this.operationIndex,
|
|
2217
|
+
writeCache: this.writeCache,
|
|
2218
|
+
documentMetaCache: this.documentMetaCache,
|
|
2219
|
+
collectionMembershipCache: this.collectionMembershipCache
|
|
2220
|
+
});
|
|
2221
|
+
}
|
|
2222
|
+
};
|
|
2223
|
+
var KyselyExecutionScope = class {
|
|
2224
|
+
constructor(db, operationStore, operationIndex, keyframeStore, writeCache, documentMetaCache, collectionMembershipCache) {
|
|
2225
|
+
this.db = db;
|
|
2226
|
+
this.operationStore = operationStore;
|
|
2227
|
+
this.operationIndex = operationIndex;
|
|
2228
|
+
this.keyframeStore = keyframeStore;
|
|
2229
|
+
this.writeCache = writeCache;
|
|
2230
|
+
this.documentMetaCache = documentMetaCache;
|
|
2231
|
+
this.collectionMembershipCache = collectionMembershipCache;
|
|
2232
|
+
}
|
|
2233
|
+
async run(fn, signal) {
|
|
2234
|
+
signal?.throwIfAborted();
|
|
2235
|
+
return this.db.transaction().execute(async (trx) => {
|
|
2236
|
+
const scopedOperationStore = this.operationStore.withTransaction(trx);
|
|
2237
|
+
const scopedOperationIndex = this.operationIndex.withTransaction(trx);
|
|
2238
|
+
const scopedKeyframeStore = this.keyframeStore.withTransaction(trx);
|
|
2239
|
+
return fn({
|
|
2240
|
+
operationStore: scopedOperationStore,
|
|
2241
|
+
operationIndex: scopedOperationIndex,
|
|
2242
|
+
writeCache: this.writeCache.withScopedStores(scopedOperationStore, scopedKeyframeStore),
|
|
2243
|
+
documentMetaCache: this.documentMetaCache.withScopedStore(scopedOperationStore),
|
|
2244
|
+
collectionMembershipCache: this.collectionMembershipCache.withScopedIndex(scopedOperationIndex)
|
|
2245
|
+
});
|
|
2246
|
+
});
|
|
2247
|
+
}
|
|
2248
|
+
};
|
|
2248
2249
|
//#endregion
|
|
2249
|
-
//#region src/
|
|
2250
|
+
//#region src/utils/reshuffle.ts
|
|
2251
|
+
const STRICT_ORDER_ACTION_TYPES = new Set([
|
|
2252
|
+
"CREATE_DOCUMENT",
|
|
2253
|
+
"DELETE_DOCUMENT",
|
|
2254
|
+
"UPGRADE_DOCUMENT",
|
|
2255
|
+
"ADD_RELATIONSHIP",
|
|
2256
|
+
"REMOVE_RELATIONSHIP",
|
|
2257
|
+
"UPDATE_RELATIONSHIP",
|
|
2258
|
+
"ADD_FOLDER",
|
|
2259
|
+
"UPDATE_FOLDER",
|
|
2260
|
+
"REMOVE_FOLDER"
|
|
2261
|
+
]);
|
|
2250
2262
|
/**
|
|
2251
|
-
*
|
|
2252
|
-
*
|
|
2263
|
+
* Reshuffles operations by timestamp, then applies deterministic tie-breaking.
|
|
2264
|
+
* Used for merging concurrent operations from different branches.
|
|
2253
2265
|
*
|
|
2254
|
-
*
|
|
2255
|
-
*
|
|
2256
|
-
*
|
|
2257
|
-
*
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
}),
|
|
2268
|
-
appendCondition: built.appendCondition,
|
|
2269
|
-
documentVersion: built.model.document.version,
|
|
2270
|
-
deletedAtUtcIso: built.model.document.deletedAtUtcIso ?? null
|
|
2271
|
-
};
|
|
2272
|
-
}
|
|
2273
|
-
/**
|
|
2274
|
-
* The model this reactor enforces. With `authEnforcement` off the auth scope is
|
|
2275
|
-
* absent from every append condition and no load walks it; with `authGroups`
|
|
2276
|
-
* on, the group documents the grant list names join the read-set and the
|
|
2277
|
-
* registry supplies the reducer that folds them.
|
|
2266
|
+
* For strict document-structure actions (e.g., CREATE_DOCUMENT/UPGRADE_DOCUMENT),
|
|
2267
|
+
* logical index (index - skip) is prioritized to preserve causal replay order.
|
|
2268
|
+
*
|
|
2269
|
+
* For other actions, action ID is prioritized to ensure a canonical cross-reactor order
|
|
2270
|
+
* for concurrent operations that may have diverged local indices due to prior reshuffles.
|
|
2271
|
+
* Logical index and operation ID are then used as deterministic tie-breakers.
|
|
2272
|
+
*
|
|
2273
|
+
* Example:
|
|
2274
|
+
* [0:0, 1:0, 2:0, A3:0, A4:0, A5:0] + [0:0, 1:0, 2:0, B3:0, B4:2, B5:0]
|
|
2275
|
+
* GC => [0:0, 1:0, 2:0, A3:0, A4:0, A5:0] + [0:0, 1:0, B4:2, B5:0]
|
|
2276
|
+
* Split => [0:0, 1:0] + [2:0, A3:0, A4:0, A5:0] + [B4:2, B5:0]
|
|
2277
|
+
* Reshuffle(6:4) => [6:4, 7:0, 8:0, 9:0, 10:0, 11:0]
|
|
2278
|
+
* merge => [0:0, 1:0, 6:4, 7:0, 8:0, 9:0, 10:0, 11:0]
|
|
2278
2279
|
*/
|
|
2279
|
-
function
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2280
|
+
function reshuffleByTimestamp(startIndex, opsA, opsB) {
|
|
2281
|
+
return [...opsA, ...opsB].sort((a, b) => {
|
|
2282
|
+
const timestampDiff = new Date(a.timestampUtcMs).getTime() - new Date(b.timestampUtcMs).getTime();
|
|
2283
|
+
if (timestampDiff !== 0) return timestampDiff;
|
|
2284
|
+
const shouldPrioritizeLogicalIndex = STRICT_ORDER_ACTION_TYPES.has(a.action?.type ?? "") || STRICT_ORDER_ACTION_TYPES.has(b.action?.type ?? "");
|
|
2285
|
+
const logicalIndexDiff = a.index - a.skip - (b.index - b.skip);
|
|
2286
|
+
if (shouldPrioritizeLogicalIndex) {
|
|
2287
|
+
if (logicalIndexDiff !== 0) return logicalIndexDiff;
|
|
2288
|
+
}
|
|
2289
|
+
const actionIdDiff = (a.action?.id ?? "").localeCompare(b.action?.id ?? "");
|
|
2290
|
+
if (actionIdDiff !== 0) return actionIdDiff;
|
|
2291
|
+
if (!shouldPrioritizeLogicalIndex && logicalIndexDiff !== 0) return logicalIndexDiff;
|
|
2292
|
+
return a.id.localeCompare(b.id);
|
|
2293
|
+
}).map((op, i) => ({
|
|
2294
|
+
...op,
|
|
2295
|
+
index: startIndex.index + i,
|
|
2296
|
+
skip: i === 0 ? startIndex.skip : 0
|
|
2297
|
+
}));
|
|
2283
2298
|
}
|
|
2284
2299
|
//#endregion
|
|
2285
2300
|
//#region src/decision/merged-order.ts
|
|
@@ -3122,13 +3137,7 @@ var SimpleJobExecutor = class {
|
|
|
3122
3137
|
retryMaxDelayMs: config.retryMaxDelayMs ?? 5e3,
|
|
3123
3138
|
yieldDeadlineMs: config.yieldDeadlineMs ?? 50
|
|
3124
3139
|
};
|
|
3125
|
-
this.featureFlags =
|
|
3126
|
-
documentDecisions: config.featureFlags?.documentDecisions ?? false,
|
|
3127
|
-
authEnforcement: config.featureFlags?.authEnforcement ?? false,
|
|
3128
|
-
authGroups: config.featureFlags?.authGroups ?? false,
|
|
3129
|
-
authConditions: config.featureFlags?.authConditions ?? false
|
|
3130
|
-
};
|
|
3131
|
-
validateFeatureFlags(this.featureFlags, FLAG_PREREQUISITES);
|
|
3140
|
+
this.featureFlags = resolveFeatureFlags(config.featureFlags);
|
|
3132
3141
|
this.decisionModel = selectDecisionModel(this.featureFlags, registry);
|
|
3133
3142
|
this.signatureVerifierModule = new SignatureVerifier(signatureVerifier);
|
|
3134
3143
|
this.documentActionHandler = new DocumentActionHandler(registry, logger, driveContainerTypes, this.featureFlags, this.decisionModel);
|
|
@@ -4812,6 +4821,6 @@ async function getMigrationStatus(db, schema = REACTOR_SCHEMA) {
|
|
|
4812
4821
|
//#region src/core/drive-container-types.ts
|
|
4813
4822
|
const DEFAULT_DRIVE_CONTAINER_TYPES = new Set(["powerhouse/document-drive", "powerhouse/reactor-drive"]);
|
|
4814
4823
|
//#endregion
|
|
4815
|
-
export {
|
|
4824
|
+
export { decideAtHead as A, InvalidOperationTimestampError as B, DuplicateOperationError as C, DuplicateModuleError as D, DuplicateManifestError as E, AuthTimestampNotMonotonicError as F, __exportAll as G, matchesScope as H, AuthorizationDeniedError as I, DocumentDeletedError as L, documentDecisionModel as M, authDecisionModel as N, InvalidModuleError as O, buildDecisionModel as P, DocumentNotFoundError as R, AppendConditionFailedError as S, RevisionMismatchError as T, parsePagingOptions as U, UpgradePreconditionFailedError as V, throwIfAborted as W, DocumentMetaCache as _, createForwardingPoolInstrumentation as a, CollectionMembershipCache as b, KyselyKeyframeStore as c, DriveCollectionId as d, KyselyExecutionScope as f, KyselyOperationIndex as g, KyselyWriteCache as h, runMigrations as i, selectDecisionModel as j, ModuleNotFoundError as k, DocumentModelRegistry as l, EventBus as m, REACTOR_SCHEMA as n, instrumentPgPool as o, resolveFeatureFlags as p, getMigrationStatus as r, KyselyOperationStore as s, DEFAULT_DRIVE_CONTAINER_TYPES as t, SimpleJobExecutor as u, createConsistencyToken as v, OptimisticLockError as w, APPEND_CONDITION_FAILED_PREFIX as x, createEmptyConsistencyToken as y, ExcessiveReshuffleError as z };
|
|
4816
4825
|
|
|
4817
|
-
//# sourceMappingURL=drive-container-types-
|
|
4826
|
+
//# sourceMappingURL=drive-container-types-BoY5t12r.js.map
|