@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.
@@ -219,93 +219,444 @@ var DocumentNotFoundError = class DocumentNotFoundError extends Error {
219
219
  }
220
220
  };
221
221
  //#endregion
222
- //#region src/registry/errors.ts
222
+ //#region src/decision/build-decision-model.ts
223
223
  /**
224
- * Error thrown when a document model module is not found in the registry.
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
- var ModuleNotFoundError = class extends Error {
227
- documentType;
228
- requestedVersion;
229
- constructor(documentType, version) {
230
- const versionSuffix = version !== void 0 ? ` version ${version}` : "";
231
- super(`Document model module not found for type: ${documentType}${versionSuffix}`);
232
- this.name = "ModuleNotFoundError";
233
- this.documentType = documentType;
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
- static isError(error) {
237
- return Error.isError(error) && error.name === "ModuleNotFoundError";
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
- * Error thrown when attempting to register a module that already exists.
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
- var DuplicateModuleError = class extends Error {
244
- constructor(documentType, version) {
245
- const versionSuffix = version !== void 0 ? ` (version ${version})` : "";
246
- super(`Document model module already registered for type: ${documentType}${versionSuffix}`);
247
- this.name = "DuplicateModuleError";
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
- * Error thrown when a module is invalid or malformed.
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
- var InvalidModuleError = class extends Error {
257
- constructor(message) {
258
- super(`Invalid document model module: ${message}`);
259
- this.name = "InvalidModuleError";
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
- * Error thrown when attempting to register an upgrade manifest that already exists.
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
- var DuplicateManifestError = class extends Error {
266
- constructor(documentType) {
267
- super(`Upgrade manifest already registered for type: ${documentType}`);
268
- this.name = "DuplicateManifestError";
269
- }
270
- static isError(error) {
271
- return Error.isError(error) && error.name === "DuplicateManifestError";
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
- * Error thrown when an upgrade manifest is not found.
276
- */
277
- var ManifestNotFoundError = class extends Error {
278
- constructor(documentType) {
279
- super(`Upgrade manifest not found for type: ${documentType}`);
280
- this.name = "ManifestNotFoundError";
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
- * Error thrown when a required upgrade transition is missing from the manifest.
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
- var MissingUpgradeTransitionError = class extends Error {
287
- constructor(documentType, fromVersion, toVersion) {
288
- super(`Missing upgrade transition for ${documentType}: v${fromVersion} to v${toVersion}`);
289
- this.name = "MissingUpgradeTransitionError";
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
- * Error thrown when getUpgradeReducer is called with a non-single-step version increment.
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
- var InvalidUpgradeStepError = class extends Error {
296
- constructor(documentType, fromVersion, toVersion) {
297
- super(`Invalid upgrade step for ${documentType}: must be single version increment, got v${fromVersion} to v${toVersion}`);
298
- this.name = "InvalidUpgradeStepError";
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
- //#endregion
302
- //#region src/storage/interfaces.ts
432
+ return reducer(document, operation.action);
433
+ }
303
434
  /**
304
- * Thrown when an operation with the same identity already exists in the store.
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
- var DuplicateOperationError = class extends Error {
307
- constructor(description) {
308
- super(`Duplicate operation: ${description}`);
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
- async function readStream(cache, query, reads, signal) {
2152
- const key = `${query.documentId}:${query.scope}:${query.branch}`;
2153
- const existing = reads.get(key);
2154
- if (existing) return existing;
2155
- const document = await cache.getState(query.documentId, query.scope, query.branch, void 0, signal);
2156
- const read = {
2157
- state: document.state[query.scope],
2158
- stream: {
2159
- documentId: query.documentId,
2160
- scope: query.scope,
2161
- branch: query.branch,
2162
- revision: observedRevision(document, query.scope)
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
- reads.set(key, read);
2166
- return read;
2167
- }
2156
+ if (errors.length > 0) throw new EventBusAggregateError(errors);
2157
+ }
2158
+ };
2159
+ //#endregion
2160
+ //#region src/core/feature-flags.ts
2168
2161
  /**
2169
- * The highest operation index the document reflects for the scope, or -1 if
2170
- * empty. `header.revision` is authoritative, not the rebuilt operation list.
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
- function observedRevision(document, scope) {
2173
- if (scope in document.header.revision) return document.header.revision[scope] - 1;
2174
- if (scope in document.operations) {
2175
- const operations = document.operations[scope];
2176
- if (operations.length > 0) return operations[operations.length - 1].index;
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 projections whose queries depend on folded state. A positional walk
2183
- * resolves their streams through `queryOverHistory`; a projection without one
2184
- * contributes no streams to a walk.
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 derivedReadSet(definition) {
2187
- const projections = [];
2188
- for (const [name, projection] of Object.entries(definition.projections)) {
2189
- if (typeof projection.query !== "function") continue;
2190
- projections.push({
2191
- name,
2192
- decidingActions: projection.decidingActions,
2193
- apply: projection.apply,
2194
- queryOverHistory: projection.queryOverHistory
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
- * The streams a model reads whose queries are known before it is built. A
2201
- * derived query needs the statically-queried projections first, so it is not
2202
- * included here.
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 staticReadSet(definition) {
2205
- const streams = [];
2206
- for (const [name, projection] of Object.entries(definition.projections)) {
2207
- if (typeof projection.query === "function") continue;
2208
- streams.push({
2209
- name,
2210
- query: projection.query,
2211
- decidingActions: projection.decidingActions,
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/decision/document-decision-model.ts
2219
- /**
2220
- * The simplest decision model: one projection over the document scope, which
2221
- * rejects on a deleted document.
2222
- */
2223
- function documentDecisionModel(target) {
2224
- return {
2225
- projections: { document: {
2226
- decidingActions: ["DELETE_DOCUMENT"],
2227
- apply: (document, operation) => operation.action.type === "DELETE_DOCUMENT" ? applyDeleteDocumentAction({
2228
- ...document,
2229
- state: { ...document.state }
2230
- }, operation.action) : document,
2231
- query: {
2232
- documentId: target.documentId,
2233
- branch: target.branch,
2234
- scope: "document"
2235
- }
2236
- } },
2237
- evaluatesScope() {
2238
- return true;
2239
- },
2240
- decide(model) {
2241
- return model.document.isDeleted ? {
2242
- decision: "deny",
2243
- reason: DOCUMENT_DELETED_REASON
2244
- } : { decision: "allow" };
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/decision/registered-model.ts
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
- * Builds the model at the stream heads and decides one request against it. The
2252
- * append condition it returns is the read-set the store enforces at write time.
2263
+ * Reshuffles operations by timestamp, then applies deterministic tie-breaking.
2264
+ * Used for merging concurrent operations from different branches.
2253
2265
  *
2254
- * With `conditions` supplied, the executing scope's state is read at the head
2255
- * for `doc.<scope>.*` paths. That read carries no append-condition entry of
2256
- * its own: the written stream's expected-revision check already refuses a
2257
- * write whose scope grew between the read and the append.
2258
- */
2259
- async function decideAtHead(model, cache, target, subject, request, signal, conditions) {
2260
- const built = await buildDecisionModel(cache, model, target, signal);
2261
- let scopeState;
2262
- if (conditions !== void 0) scopeState = (await cache.getState(target.documentId, request.scope, target.branch, void 0, signal)).state[request.scope];
2263
- return {
2264
- evaluation: model(target).decide(built.model, subject, request, {
2265
- scopeState,
2266
- actionInput: conditions?.actionInput
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 selectDecisionModel(flags, registry) {
2280
- if (flags.authConditions) return authConditionsDecisionModel(registry);
2281
- if (flags.authGroups) return authGroupsDecisionModel(registry);
2282
- return flags.authEnforcement ? authDecisionModel : documentDecisionModel;
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 { OptimisticLockError as A, ExcessiveReshuffleError as B, DocumentMetaCache as C, APPEND_CONDITION_FAILED_PREFIX as D, CollectionMembershipCache as E, ModuleNotFoundError as F, throwIfAborted as G, UpgradePreconditionFailedError as H, AuthTimestampNotMonotonicError as I, __exportAll as K, AuthorizationDeniedError as L, DuplicateManifestError as M, DuplicateModuleError as N, AppendConditionFailedError as O, InvalidModuleError as P, DocumentDeletedError as R, KyselyOperationIndex as S, createEmptyConsistencyToken as T, matchesScope as U, InvalidOperationTimestampError as V, parsePagingOptions as W, KyselyExecutionScope as _, createForwardingPoolInstrumentation as a, EventBus as b, KyselyKeyframeStore as c, DriveCollectionId as d, decideAtHead as f, authDecisionModel as g, buildDecisionModel as h, runMigrations as i, RevisionMismatchError as j, DuplicateOperationError as k, DocumentModelRegistry as l, documentDecisionModel as m, REACTOR_SCHEMA as n, instrumentPgPool as o, selectDecisionModel as p, getMigrationStatus as r, KyselyOperationStore as s, DEFAULT_DRIVE_CONTAINER_TYPES as t, SimpleJobExecutor as u, FLAG_PREREQUISITES as v, createConsistencyToken as w, KyselyWriteCache as x, validateFeatureFlags as y, DocumentNotFoundError as z };
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-h3M1AK3K.js.map
4826
+ //# sourceMappingURL=drive-container-types-BoY5t12r.js.map