@powerhousedao/reactor 6.2.2-dev.42 → 6.2.2-dev.44

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/index.js CHANGED
@@ -1,10 +1,10 @@
1
- import { A as OptimisticLockError, B as ExcessiveReshuffleError, C as DocumentMetaCache, D as APPEND_CONDITION_FAILED_PREFIX, E as CollectionMembershipCache, F as ModuleNotFoundError, H as matchesScope, I as AuthTimestampNotMonotonicError, L as AuthorizationDeniedError, M as DuplicateManifestError, N as DuplicateModuleError, O as AppendConditionFailedError, P as InvalidModuleError, R as DocumentDeletedError, S as KyselyOperationIndex, T as createEmptyConsistencyToken, U as parsePagingOptions, V as InvalidOperationTimestampError, W as throwIfAborted, _ as KyselyExecutionScope, a as createForwardingPoolInstrumentation, b as EventBus, c as KyselyKeyframeStore, d as DriveCollectionId, f as decideAtHead, g as authDecisionModel, h as buildDecisionModel, i as runMigrations, j as RevisionMismatchError, k as DuplicateOperationError, l as DocumentModelRegistry, m as documentDecisionModel, n as REACTOR_SCHEMA, o as instrumentPgPool, p as selectDecisionModel, r as getMigrationStatus, s as KyselyOperationStore, t as DEFAULT_DRIVE_CONTAINER_TYPES, u as SimpleJobExecutor, v as FLAG_PREREQUISITES, w as createConsistencyToken, x as KyselyWriteCache, y as validateFeatureFlags, z as DocumentNotFoundError } from "./drive-container-types-BJCKXJwH.js";
1
+ import { A as OptimisticLockError, B as ExcessiveReshuffleError, C as DocumentMetaCache, D as APPEND_CONDITION_FAILED_PREFIX, E as CollectionMembershipCache, F as ModuleNotFoundError, G as throwIfAborted, H as UpgradePreconditionFailedError, I as AuthTimestampNotMonotonicError, L as AuthorizationDeniedError, M as DuplicateManifestError, N as DuplicateModuleError, O as AppendConditionFailedError, P as InvalidModuleError, R as DocumentDeletedError, S as KyselyOperationIndex, T as createEmptyConsistencyToken, U as matchesScope, V as InvalidOperationTimestampError, W as parsePagingOptions, _ as KyselyExecutionScope, a as createForwardingPoolInstrumentation, b as EventBus, c as KyselyKeyframeStore, d as DriveCollectionId, f as decideAtHead, g as authDecisionModel, h as buildDecisionModel, i as runMigrations, j as RevisionMismatchError, k as DuplicateOperationError, l as DocumentModelRegistry, m as documentDecisionModel, n as REACTOR_SCHEMA, o as instrumentPgPool, p as selectDecisionModel, r as getMigrationStatus, s as KyselyOperationStore, t as DEFAULT_DRIVE_CONTAINER_TYPES, u as SimpleJobExecutor, v as FLAG_PREREQUISITES, w as createConsistencyToken, x as KyselyWriteCache, y as validateFeatureFlags, z as DocumentNotFoundError } from "./drive-container-types-h3M1AK3K.js";
2
2
  import { n as ReactorEventTypes, t as EventBusAggregateError } from "./types-DMKLa0Ok.js";
3
3
  import { i as WorkerInitFailedError, r as WorkerExitedError, t as WorkerAbortTimeoutError } from "./errors-D3S6Eysd.js";
4
- import { a as ReadModelCoordinator, i as KyselyDocumentView, n as ConsistencyTracker, o as BaseReadModel, r as makeConsistencyKey, t as KyselyDocumentIndexer } from "./document-indexer-C0GB0b8Q.js";
4
+ import { a as ReadModelCoordinator, i as KyselyDocumentView, n as ConsistencyTracker, o as BaseReadModel, r as makeConsistencyKey, t as KyselyDocumentIndexer } from "./document-indexer-BfdCgjG-.js";
5
5
  import { n as errorToInfo, r as sanitizeArg, t as createForwardingLogger } from "./forwarding-logger-BBkMSxuJ.js";
6
- import { t as workerEntryPath } from "./worker-jEJW6_j7.js";
7
- import { actions, actions as documentActions, createPresignedHeader, decide, garbageCollect, generateId, hashDocumentStateForScope, replayDocument, sortOperations } from "@powerhousedao/shared/document-model";
6
+ import { t as workerEntryPath } from "./worker-CbtYMIju.js";
7
+ import { DowngradeNotSupportedError, UnsupportedDocumentModelVersionError, actions, actions as documentActions, createPresignedHeader, decide, garbageCollect, generateId, groupDocumentType, groupMembershipActionTypes, hashDocumentStateForScope, normalizeDocumentModelVersion, sortOperations } from "@powerhousedao/shared/document-model";
8
8
  import { addFile, addFolder, copyNode, deleteNode, driveCreateDocument, generateNodesCopy, getDescendants, handleTargetNameCollisions, isFileNode, isFolderNode, moveNode, updateNode } from "@powerhousedao/shared/document-drive";
9
9
  import { v4 } from "uuid";
10
10
  import { ConsoleLogger, childLogger } from "document-model";
@@ -365,7 +365,7 @@ var DriveClient = class {
365
365
  documentId: document.header.id,
366
366
  model: document.header.documentType,
367
367
  fromVersion: 0,
368
- toVersion: 1,
368
+ toVersion: normalizeDocumentModelVersion(document.state.document?.version),
369
369
  initialState: document.state
370
370
  }),
371
371
  addRelationshipAction(driveIdentifier, documentId, "child")
@@ -470,8 +470,12 @@ var DriveClient = class {
470
470
  const node = drive.state.global.nodes.find((n) => n.id === entry.srcId);
471
471
  if (!node || !isFileNode(node)) continue;
472
472
  const srcDoc = await this.client.get(entry.srcId, void 0, signal);
473
- const module = await this.client.getDocumentModelModule(srcDoc.header.documentType);
474
- const duplicated = replayDocument(srcDoc.initialState, srcDoc.operations, module.reducer, createPresignedHeader(entry.targetId, srcDoc.header.documentType));
473
+ const duplicated = {
474
+ ...srcDoc,
475
+ header: createPresignedHeader(entry.targetId, srcDoc.header.documentType),
476
+ initialState: srcDoc.state,
477
+ operations: {}
478
+ };
475
479
  const resolvedName = resolvedNamesByTargetId.get(entry.targetId);
476
480
  if (resolvedName) duplicated.header.name = resolvedName;
477
481
  await this.addFile(driveIdentifier, duplicated, entry.targetParentFolder ?? void 0, signal);
@@ -769,9 +773,37 @@ var ReactorClient = class {
769
773
  * @returns The document model module
770
774
  */
771
775
  async getDocumentModelModule(documentType) {
772
- const module = (await this.reactor.getDocumentModels()).results.find((m) => m.documentModel.global.id === documentType);
773
- if (!module) throw new Error(`Document model module not found for type: ${documentType}`);
774
- return module;
776
+ const modules = await this.reactor.getDocumentModels();
777
+ let latestModule;
778
+ let latestVersion = -1;
779
+ for (const module of modules.results) {
780
+ if (module.documentModel.global.id !== documentType) continue;
781
+ const version = module.version ?? 1;
782
+ if (version > latestVersion) {
783
+ latestVersion = version;
784
+ latestModule = module;
785
+ }
786
+ }
787
+ if (!latestModule) throw new Error(`Document model module not found for type: ${documentType}`);
788
+ return latestModule;
789
+ }
790
+ /**
791
+ * Retrieves the document model module matching the version the document is
792
+ * stamped with, so not-yet-upgraded documents get the reducer their
793
+ * history was written with rather than the latest.
794
+ */
795
+ async getDocumentModelModuleForDocument(document) {
796
+ const documentType = document.header.documentType;
797
+ const version = normalizeDocumentModelVersion(document.state.document?.version);
798
+ const modules = await this.reactor.getDocumentModels();
799
+ const availableVersions = [];
800
+ for (const module of modules.results) {
801
+ if (module.documentModel.global.id !== documentType) continue;
802
+ const moduleVersion = normalizeDocumentModelVersion(module.version);
803
+ if (moduleVersion === version) return module;
804
+ availableVersions.push(moduleVersion);
805
+ }
806
+ throw new UnsupportedDocumentModelVersionError(documentType, version, availableVersions.sort((a, b) => a - b));
775
807
  }
776
808
  /**
777
809
  * Retrieves a specific PHDocument
@@ -925,7 +957,7 @@ var ReactorClient = class {
925
957
  documentId,
926
958
  model: document.header.documentType,
927
959
  fromVersion: 0,
928
- toVersion: document.state.document.version,
960
+ toVersion: normalizeDocumentModelVersion(document.state.document?.version),
929
961
  initialState: document.state
930
962
  })], this.signer, signal);
931
963
  const jobs = [{
@@ -960,20 +992,61 @@ var ReactorClient = class {
960
992
  const matchingModules = (await this.reactor.getDocumentModels(void 0, void 0, signal)).results.filter((m) => m.documentModel.global.id === documentModelType);
961
993
  let module;
962
994
  if (options?.documentModelVersion !== void 0) {
963
- module = matchingModules.find((m) => m.version === options.documentModelVersion);
995
+ const requestedVersion = normalizeDocumentModelVersion(options.documentModelVersion);
996
+ module = matchingModules.find((m) => normalizeDocumentModelVersion(m.version) === requestedVersion);
964
997
  if (!module) throw new Error(`Document model not found for type: ${documentModelType} with version: ${options.documentModelVersion}`);
965
998
  } else {
966
999
  module = matchingModules.reduce((latest, current) => {
967
1000
  if (latest === void 0) return current;
968
- return (current.version ?? 0) > (latest.version ?? 0) ? current : latest;
1001
+ return normalizeDocumentModelVersion(current.version) > normalizeDocumentModelVersion(latest.version) ? current : latest;
969
1002
  }, void 0);
970
1003
  if (!module) throw new Error(`Document model not found for type: ${documentModelType}`);
971
1004
  }
972
1005
  const document = module.utils.createDocument();
973
- document.state.document.version = module.version ?? 1;
1006
+ document.state.document.version = normalizeDocumentModelVersion(module.version);
974
1007
  return this.create(document, options?.parentIdentifier, signal);
975
1008
  }
976
1009
  /**
1010
+ * Upgrades a document to a newer document model version by dispatching an
1011
+ * UPGRADE_DOCUMENT action. When toVersion is omitted, upgrades to the
1012
+ * latest registered module version for the document's type. Returns the
1013
+ * document unchanged when it is already at the target version.
1014
+ *
1015
+ * The executor validates the action's version and revision snapshot against
1016
+ * the state the migration actually runs on. When a concurrent edit
1017
+ * invalidates the snapshot, the upgrade is rebuilt from a fresh read and
1018
+ * retried up to maxConflictRetries times before the conflict is surfaced.
1019
+ */
1020
+ async upgradeDocument(documentIdentifier, toVersion, options, signal) {
1021
+ this.logger.verbose("upgradeDocument(@documentIdentifier, @toVersion)", documentIdentifier, toVersion);
1022
+ const maxConflictRetries = options?.maxConflictRetries ?? 3;
1023
+ let lastConflictMessage = "";
1024
+ for (let attempt = 0; attempt <= maxConflictRetries; attempt++) {
1025
+ const document = await this.reactor.getByIdOrSlug(documentIdentifier, void 0, void 0, signal);
1026
+ const documentId = document.header.id;
1027
+ const documentType = document.header.documentType;
1028
+ const branch = document.header.branch || "main";
1029
+ const fromVersion = normalizeDocumentModelVersion(document.state.document?.version);
1030
+ let targetVersion = toVersion;
1031
+ if (targetVersion === void 0) targetVersion = normalizeDocumentModelVersion((await this.getDocumentModelModule(documentType)).version);
1032
+ if (targetVersion === fromVersion) return document;
1033
+ if (targetVersion < fromVersion) throw new DowngradeNotSupportedError(documentType, fromVersion, targetVersion);
1034
+ const signedActions = await signActions([upgradeDocumentAction({
1035
+ documentId,
1036
+ model: documentType,
1037
+ fromVersion,
1038
+ toVersion: targetVersion,
1039
+ revision: { ...document.header.revision }
1040
+ })], this.signer, signal);
1041
+ const jobInfo = await this.reactor.execute(documentId, branch, signedActions, signal);
1042
+ const completedJob = await this.waitForJob(jobInfo, signal);
1043
+ if (completedJob.status !== JobStatus.FAILED) return await this.reactor.getByIdOrSlug(documentId, { branch }, completedJob.consistencyToken, signal);
1044
+ if (completedJob.error?.name !== "UpgradePreconditionFailedError") throw new Error(completedJob.error?.message);
1045
+ lastConflictMessage = completedJob.error.message;
1046
+ }
1047
+ throw new Error(`Upgrade of document ${documentIdentifier} conflicted with concurrent edits after ${maxConflictRetries + 1} attempts: ${lastConflictMessage}`);
1048
+ }
1049
+ /**
977
1050
  * Creates an empty document in a drive as a single batched operation.
978
1051
  * Delegates to {@link IDriveClient.addFile}.
979
1052
  *
@@ -1274,7 +1347,7 @@ var JobResultHandler = class {
1274
1347
  callbacks.deferJob(handle.job.documentId, handle.job);
1275
1348
  return;
1276
1349
  }
1277
- if (result.error && (DocumentDeletedError.isError(result.error) || AuthorizationDeniedError.isError(result.error) || AuthTimestampNotMonotonicError.isError(result.error) || InvalidOperationTimestampError.isError(result.error) || ExcessiveReshuffleError.isError(result.error))) {
1350
+ if (result.error && (DocumentDeletedError.isError(result.error) || AuthorizationDeniedError.isError(result.error) || AuthTimestampNotMonotonicError.isError(result.error) || InvalidOperationTimestampError.isError(result.error) || ExcessiveReshuffleError.isError(result.error) || UpgradePreconditionFailedError.isError(result.error))) {
1278
1351
  const errorInfo = toErrorInfo(result.error);
1279
1352
  this.jobTracker.markFailed(handle.job.id, errorInfo, handle.job);
1280
1353
  this.eventBus.emit(ReactorEventTypes.JOB_FAILED, {
@@ -2668,9 +2741,11 @@ var InMemoryQueue = class {
2668
2741
  this.jobIndex.delete(jobId);
2669
2742
  this.completedJobs.add(jobId);
2670
2743
  await this.remove(jobId);
2744
+ const emittedError = new Error(error?.message ?? "Job failed");
2745
+ if (error?.name) emittedError.name = error.name;
2671
2746
  this.eventBus.emit(ReactorEventTypes.JOB_FAILED, {
2672
2747
  jobId,
2673
- error: new Error(error?.message ?? "Job failed"),
2748
+ error: emittedError,
2674
2749
  job
2675
2750
  }).catch(() => {});
2676
2751
  this.checkDrained();
@@ -3210,6 +3285,83 @@ var SubscriptionNotificationReadModel = class {
3210
3285
  }
3211
3286
  };
3212
3287
  //#endregion
3288
+ //#region src/core/group-reevaluation-trigger.ts
3289
+ /**
3290
+ * Watches committed writes for group membership changes and enqueues a
3291
+ * re-evaluation job for every document whose auth history references the
3292
+ * changed group, found through the reverse direction of the group-reference
3293
+ * relation. Each affected document is re-judged in its own job, so the work
3294
+ * runs under that document's execution slot rather than the group's.
3295
+ *
3296
+ * The job carries the earliest changed membership timestamp; the executor
3297
+ * skips the pass when everything the document holds sorts before it, which
3298
+ * keeps the common case (a membership write later than all history) free.
3299
+ */
3300
+ var GroupReevaluationTrigger = class {
3301
+ unsubscribe;
3302
+ constructor(logger, eventBus, queue, operationIndex) {
3303
+ this.logger = logger;
3304
+ this.eventBus = eventBus;
3305
+ this.queue = queue;
3306
+ this.operationIndex = operationIndex;
3307
+ }
3308
+ startup() {
3309
+ this.unsubscribe = this.eventBus.subscribe(ReactorEventTypes.JOB_WRITE_READY, async (_type, event) => this.onWriteReady(event));
3310
+ }
3311
+ shutdown() {
3312
+ this.unsubscribe?.();
3313
+ this.unsubscribe = void 0;
3314
+ }
3315
+ async onWriteReady(event) {
3316
+ const changed = /* @__PURE__ */ new Map();
3317
+ for (const { operation, context } of event.operations) {
3318
+ if (context.documentType !== groupDocumentType || context.scope !== "global" || !groupMembershipActionTypes.includes(operation.action.type)) continue;
3319
+ const existing = changed.get(context.documentId);
3320
+ if (existing === void 0 || Date.parse(operation.timestampUtcMs) < Date.parse(existing)) changed.set(context.documentId, operation.timestampUtcMs);
3321
+ }
3322
+ if (changed.size === 0) return;
3323
+ const affected = /* @__PURE__ */ new Map();
3324
+ for (const [groupId, timestamp] of changed) {
3325
+ let referencers;
3326
+ try {
3327
+ referencers = await this.operationIndex.getGroupReferencers(groupId);
3328
+ } catch (error) {
3329
+ this.logger.error("Failed to resolve referencers of group @groupId: @error", groupId, error);
3330
+ continue;
3331
+ }
3332
+ for (const documentId of referencers) {
3333
+ const existing = affected.get(documentId);
3334
+ if (existing === void 0 || Date.parse(timestamp) < Date.parse(existing)) affected.set(documentId, timestamp);
3335
+ }
3336
+ }
3337
+ for (const [documentId, timestamp] of affected) {
3338
+ const jobId = v4();
3339
+ const job = {
3340
+ id: jobId,
3341
+ kind: "reevaluation",
3342
+ documentId,
3343
+ scope: "global",
3344
+ branch: "main",
3345
+ actions: [],
3346
+ operations: [],
3347
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3348
+ queueHint: [],
3349
+ maxRetries: 3,
3350
+ errorHistory: [],
3351
+ meta: {
3352
+ ...buildSingleJobMeta(jobId),
3353
+ triggerTimestampUtcMs: timestamp
3354
+ }
3355
+ };
3356
+ try {
3357
+ await this.queue.enqueue(job);
3358
+ } catch (error) {
3359
+ this.logger.error("Failed to enqueue re-evaluation of @documentId: @error", documentId, error);
3360
+ }
3361
+ }
3362
+ }
3363
+ };
3364
+ //#endregion
3213
3365
  //#region src/sync/types.ts
3214
3366
  let ChannelScheme = /* @__PURE__ */ function(ChannelScheme) {
3215
3367
  ChannelScheme["CONNECT"] = "connect";
@@ -6385,7 +6537,7 @@ var Reactor = class {
6385
6537
  documentId: document.header.id,
6386
6538
  model: document.header.documentType,
6387
6539
  fromVersion: 0,
6388
- toVersion: document.state.document.version,
6540
+ toVersion: normalizeDocumentModelVersion(document.state.document?.version),
6389
6541
  initialState: document.state
6390
6542
  })];
6391
6543
  if (signer) actions = await signActions(actions, signer, signal);
@@ -7067,6 +7219,11 @@ var ReactorBuilder = class {
7067
7219
  syncModule = this.syncBuilder.buildModule(reactor, this.logger, operationIndex, eventBus, database, this.driveContainerTypes);
7068
7220
  await syncModule.syncManager.startup();
7069
7221
  }
7222
+ let groupReevaluationTrigger;
7223
+ if (this.executorConfig.featureFlags?.authGroups) {
7224
+ groupReevaluationTrigger = new GroupReevaluationTrigger(this.logger, eventBus, queue, operationIndex);
7225
+ groupReevaluationTrigger.startup();
7226
+ }
7070
7227
  const module = {
7071
7228
  eventBus,
7072
7229
  documentModelRegistry,
@@ -7088,6 +7245,7 @@ var ReactorBuilder = class {
7088
7245
  processorManagerConsistencyTracker,
7089
7246
  syncModule,
7090
7247
  reactor,
7248
+ groupReevaluationTrigger,
7091
7249
  pools: this.instrumentedPools
7092
7250
  };
7093
7251
  if (this.signalHandlersEnabled) this.attachSignalHandlers(module);
@@ -7162,7 +7320,7 @@ var ReactorBuilder = class {
7162
7320
  const [{ WorkerHandle }, { createThreadTransport }, { workerEntryPath }] = await Promise.all([
7163
7321
  import("./worker-handle-CrERzl8s.js"),
7164
7322
  import("./transport-ByGviWdZ.js"),
7165
- import("./worker-jEJW6_j7.js").then((n) => n.n)
7323
+ import("./worker-CbtYMIju.js").then((n) => n.n)
7166
7324
  ]);
7167
7325
  const poolConfig = {
7168
7326
  enabled: true,