@powerhousedao/reactor 6.2.2-dev.61 → 6.2.2-dev.62
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-DBHkoWBR.js → build-worker-executor-DymEQNP9.js} +2 -2
- package/dist/{build-worker-executor-DBHkoWBR.js.map → build-worker-executor-DymEQNP9.js.map} +1 -1
- package/dist/{drive-container-types-bVQ_8YwX.js → drive-container-types-CruMdXue.js} +227 -40
- package/dist/drive-container-types-CruMdXue.js.map +1 -0
- package/dist/entry.js +2 -2
- package/dist/index.d.ts +131 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/projection-entry.js +2 -2
- package/dist/{worker-DXJpyHLW.js → worker-Dz9lit5F.js} +2 -2
- package/dist/{worker-DXJpyHLW.js.map → worker-Dz9lit5F.js.map} +1 -1
- package/package.json +6 -5
- package/dist/drive-container-types-bVQ_8YwX.js.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { _ as DocumentMetaCache, c as KyselyKeyframeStore, f as KyselyExecutionScope, g as KyselyOperationIndex, h as KyselyWriteCache, l as DocumentModelRegistry, m as EventBus, n as REACTOR_SCHEMA, s as KyselyOperationStore, t as DEFAULT_DRIVE_CONTAINER_TYPES, u as SimpleJobExecutor, v as CollectionMembershipCache } from "./drive-container-types-
|
|
1
|
+
import { _ as DocumentMetaCache, c as KyselyKeyframeStore, f as KyselyExecutionScope, g as KyselyOperationIndex, h as KyselyWriteCache, l as DocumentModelRegistry, m as EventBus, n as REACTOR_SCHEMA, s as KyselyOperationStore, t as DEFAULT_DRIVE_CONTAINER_TYPES, u as SimpleJobExecutor, v as CollectionMembershipCache } from "./drive-container-types-CruMdXue.js";
|
|
2
2
|
import { n as ReactorEventTypes } from "./types-DMKLa0Ok.js";
|
|
3
3
|
//#region src/executor/worker/build-worker-executor.ts
|
|
4
4
|
async function defaultLoadFactory(spec) {
|
|
@@ -80,4 +80,4 @@ async function buildWorkerExecutor(options) {
|
|
|
80
80
|
//#endregion
|
|
81
81
|
export { defaultLoadFactory as n, buildWorkerExecutor as t };
|
|
82
82
|
|
|
83
|
-
//# sourceMappingURL=build-worker-executor-
|
|
83
|
+
//# sourceMappingURL=build-worker-executor-DymEQNP9.js.map
|
package/dist/{build-worker-executor-DBHkoWBR.js.map → build-worker-executor-DymEQNP9.js.map}
RENAMED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build-worker-executor-
|
|
1
|
+
{"version":3,"file":"build-worker-executor-DymEQNP9.js","names":[],"sources":["../src/executor/worker/build-worker-executor.ts"],"sourcesContent":["import type {\n DocumentModelModule,\n OperationWithContext,\n} from \"@powerhousedao/shared/document-model\";\nimport type { ILogger } from \"document-model\";\nimport type { Kysely } from \"kysely\";\nimport { CollectionMembershipCache } from \"../../cache/collection-membership-cache.js\";\nimport { DocumentMetaCache } from \"../../cache/document-meta-cache.js\";\nimport { KyselyOperationIndex } from \"../../cache/kysely-operation-index.js\";\nimport { KyselyWriteCache } from \"../../cache/kysely-write-cache.js\";\nimport type { WriteCacheConfig } from \"../../cache/write-cache-types.js\";\nimport { DEFAULT_DRIVE_CONTAINER_TYPES } from \"../../core/drive-container-types.js\";\nimport type { Database } from \"../../core/types.js\";\nimport { EventBus } from \"../../events/event-bus.js\";\nimport {\n ReactorEventTypes,\n type JobWriteReadyEvent,\n} from \"../../events/types.js\";\nimport { DocumentModelRegistry } from \"../../registry/implementation.js\";\nimport type { JobMeta } from \"../../shared/types.js\";\nimport type { SignatureVerificationHandler } from \"../../signer/types.js\";\nimport { KyselyKeyframeStore } from \"../../storage/kysely/keyframe-store.js\";\nimport { KyselyOperationStore } from \"../../storage/kysely/store.js\";\nimport type { Database as StorageDatabase } from \"../../storage/kysely/types.js\";\nimport { REACTOR_SCHEMA } from \"../../storage/migrations/migrator.js\";\nimport { KyselyExecutionScope } from \"../execution-scope.js\";\nimport { SimpleJobExecutor } from \"../simple-job-executor.js\";\nimport type { JobExecutorConfig } from \"../types.js\";\nimport type {\n FactorySpec,\n InitMessage,\n ModelManifestEntry,\n} from \"./protocol.js\";\n\n/**\n * In-worker capture of the JOB_WRITE_READY event emitted by the executor.\n * The worker forwards `operations` and `jobMeta` back to the parent; the\n * parent re-enriches `collectionMemberships` at emit time.\n */\nexport type WorkerWriteReadyCapture = {\n operations: OperationWithContext[];\n jobMeta: JobMeta;\n};\n\nexport type WorkerExecutorStack = {\n executor: SimpleJobExecutor;\n registry: DocumentModelRegistry;\n /**\n * Synchronously pops the most-recent JOB_WRITE_READY captured on this\n * worker's local event bus and clears it. Returns null if the executor\n * did not produce one for this job.\n */\n takeLastWriteReady(): WorkerWriteReadyCapture | null;\n};\n\nexport type BuildWorkerExecutorOptions = {\n init: InitMessage;\n database: Kysely<Database>;\n logger: ILogger;\n executorConfig?: JobExecutorConfig;\n driveContainerTypes?: ReadonlySet<string>;\n /**\n * Override the module loader used to materialize factory specs. Tests\n * can inject a deterministic resolver instead of touching the real\n * Node module loader.\n */\n loadFactory?: (spec: FactorySpec) => Promise<unknown>;\n};\n\nexport async function defaultLoadFactory(spec: FactorySpec): Promise<unknown> {\n const ref = spec.module;\n const specifier =\n \"filePath\" in ref\n ? new URL(`file://${ref.filePath}`).href\n : ref.packageName;\n const mod = (await import(specifier)) as Record<string, unknown>;\n const exported = mod[ref.exportName];\n if (typeof exported === \"function\") {\n return (exported as (args: unknown) => unknown)(spec.initArgs);\n }\n return exported;\n}\n\nasync function loadModelManifest(\n entries: ModelManifestEntry[],\n loadFactory: (spec: FactorySpec) => Promise<unknown>,\n registry: DocumentModelRegistry,\n logger: ILogger,\n): Promise<void> {\n for (const entry of entries) {\n let module: DocumentModelModule;\n try {\n module = (await loadFactory(entry.spec)) as DocumentModelModule;\n } catch (error) {\n logger.error(\n \"worker failed to load document model: @entry @error\",\n entry,\n error,\n );\n throw error;\n }\n const [result] = registry.registerModules(module);\n if (result.status === \"error\") {\n logger.error(\n \"worker failed to register document model: @entry @error\",\n entry,\n result.error,\n );\n throw result.error;\n }\n }\n}\n\n/**\n * Assembles the in-worker storage stack plus a {@link SimpleJobExecutor}\n * bound to a pre-built Kysely instance. The parent owns the wire protocol\n * and routing; the worker owns everything below `SimpleJobExecutor`.\n *\n * The local event bus exists only to satisfy the executor's contract: its\n * JOB_WRITE_READY emissions are captured here and shipped to the parent\n * via {@link WorkerExecutorStack.takeLastWriteReady}.\n */\nexport async function buildWorkerExecutor(\n options: BuildWorkerExecutorOptions,\n): Promise<WorkerExecutorStack> {\n const { init, database: baseDatabase, logger } = options;\n const driveContainerTypes =\n options.driveContainerTypes ?? DEFAULT_DRIVE_CONTAINER_TYPES;\n const loadFactory = options.loadFactory ?? defaultLoadFactory;\n\n const registry = new DocumentModelRegistry();\n await loadModelManifest(init.models, loadFactory, registry, logger);\n\n let signatureVerifier: SignatureVerificationHandler | undefined;\n if (init.signatureVerifier) {\n try {\n signatureVerifier = (await loadFactory(\n init.signatureVerifier,\n )) as SignatureVerificationHandler;\n } catch (error) {\n logger.error(\n \"worker failed to load signature verifier: @spec @error\",\n init.signatureVerifier,\n error,\n );\n throw error;\n }\n }\n\n const database = baseDatabase.withSchema(REACTOR_SCHEMA);\n const operationStore = new KyselyOperationStore(\n database as unknown as Kysely<StorageDatabase>,\n );\n const keyframeStore = new KyselyKeyframeStore(\n database as unknown as Kysely<StorageDatabase>,\n );\n\n const cacheConfig: WriteCacheConfig = {\n maxDocuments: 100,\n ringBufferSize: 10,\n keyframeInterval: 10,\n };\n const writeCache = new KyselyWriteCache(\n keyframeStore,\n operationStore,\n registry,\n cacheConfig,\n );\n await writeCache.startup();\n\n const operationIndex = new KyselyOperationIndex(\n database as unknown as Kysely<StorageDatabase>,\n );\n\n const documentMetaCache = new DocumentMetaCache(operationStore, {\n maxDocuments: 1000,\n });\n await documentMetaCache.startup();\n\n const collectionMembershipCache = new CollectionMembershipCache(\n operationIndex,\n );\n\n const executionScope = new KyselyExecutionScope(\n database as unknown as Kysely<StorageDatabase>,\n operationStore,\n operationIndex,\n keyframeStore,\n writeCache,\n documentMetaCache,\n collectionMembershipCache,\n );\n\n const eventBus = new EventBus();\n let lastWriteReady: WorkerWriteReadyCapture | null = null;\n eventBus.subscribe(\n ReactorEventTypes.JOB_WRITE_READY,\n (_t: number, event: JobWriteReadyEvent) => {\n lastWriteReady = {\n operations: event.operations,\n jobMeta: event.jobMeta,\n };\n },\n );\n\n const executorConfig = options.executorConfig ?? {};\n const executor = new SimpleJobExecutor(\n logger,\n registry,\n operationStore,\n eventBus,\n writeCache,\n operationIndex,\n documentMetaCache,\n collectionMembershipCache,\n driveContainerTypes,\n executorConfig,\n signatureVerifier,\n executionScope,\n );\n\n return {\n executor,\n registry,\n takeLastWriteReady(): WorkerWriteReadyCapture | null {\n const captured = lastWriteReady;\n lastWriteReady = null;\n return captured;\n },\n };\n}\n"],"mappings":";;;AAqEA,eAAsB,mBAAmB,MAAqC;CAC5E,MAAM,MAAM,KAAK;CAMjB,MAAM,YADO,OAHX,cAAc,MAAA,OACV,IAAI,IAAI,UAAU,IAAI,WAAW,CAAC,QAAA,OAClC,IAAI,eAEW,IAAI;AACzB,KAAI,OAAO,aAAa,WACtB,QAAQ,SAAwC,KAAK,SAAS;AAEhE,QAAO;;AAGT,eAAe,kBACb,SACA,aACA,UACA,QACe;AACf,MAAK,MAAM,SAAS,SAAS;EAC3B,IAAI;AACJ,MAAI;AACF,YAAU,MAAM,YAAY,MAAM,KAAK;WAChC,OAAO;AACd,UAAO,MACL,uDACA,OACA,MACD;AACD,SAAM;;EAER,MAAM,CAAC,UAAU,SAAS,gBAAgB,OAAO;AACjD,MAAI,OAAO,WAAW,SAAS;AAC7B,UAAO,MACL,2DACA,OACA,OAAO,MACR;AACD,SAAM,OAAO;;;;;;;;;;;;;AAcnB,eAAsB,oBACpB,SAC8B;CAC9B,MAAM,EAAE,MAAM,UAAU,cAAc,WAAW;CACjD,MAAM,sBACJ,QAAQ,uBAAuB;CACjC,MAAM,cAAc,QAAQ,eAAe;CAE3C,MAAM,WAAW,IAAI,uBAAuB;AAC5C,OAAM,kBAAkB,KAAK,QAAQ,aAAa,UAAU,OAAO;CAEnE,IAAI;AACJ,KAAI,KAAK,kBACP,KAAI;AACF,sBAAqB,MAAM,YACzB,KAAK,kBACN;UACM,OAAO;AACd,SAAO,MACL,0DACA,KAAK,mBACL,MACD;AACD,QAAM;;CAIV,MAAM,WAAW,aAAa,WAAW,eAAe;CACxD,MAAM,iBAAiB,IAAI,qBACzB,SACD;CACD,MAAM,gBAAgB,IAAI,oBACxB,SACD;CAOD,MAAM,aAAa,IAAI,iBACrB,eACA,gBACA,UARoC;EACpC,cAAc;EACd,gBAAgB;EAChB,kBAAkB;EACnB,CAMA;AACD,OAAM,WAAW,SAAS;CAE1B,MAAM,iBAAiB,IAAI,qBACzB,SACD;CAED,MAAM,oBAAoB,IAAI,kBAAkB,gBAAgB,EAC9D,cAAc,KACf,CAAC;AACF,OAAM,kBAAkB,SAAS;CAEjC,MAAM,4BAA4B,IAAI,0BACpC,eACD;CAED,MAAM,iBAAiB,IAAI,qBACzB,UACA,gBACA,gBACA,eACA,YACA,mBACA,0BACD;CAED,MAAM,WAAW,IAAI,UAAU;CAC/B,IAAI,iBAAiD;AACrD,UAAS,UACP,kBAAkB,kBACjB,IAAY,UAA8B;AACzC,mBAAiB;GACf,YAAY,MAAM;GAClB,SAAS,MAAM;GAChB;GAEJ;AAkBD,QAAO;EACL,UAhBe,IAAI,kBACnB,QACA,UACA,gBACA,UACA,YACA,gBACA,mBACA,2BACA,qBAVqB,QAAQ,kBAAkB,EAAE,EAYjD,mBACA,eACD;EAIC;EACA,qBAAqD;GACnD,MAAM,WAAW;AACjB,oBAAiB;AACjB,UAAO;;EAEV"}
|
|
@@ -572,14 +572,16 @@ function documentDecisionModel(target) {
|
|
|
572
572
|
* append condition it returns is the read-set the store enforces at write time.
|
|
573
573
|
*
|
|
574
574
|
* With `conditions` supplied, the executing scope's state is read at the head
|
|
575
|
-
* for `doc.<scope>.*` paths
|
|
576
|
-
*
|
|
577
|
-
*
|
|
575
|
+
* for `doc.<scope>.*` paths, or taken from the run's carried document when the
|
|
576
|
+
* caller has already reduced earlier writes into it. That read carries no
|
|
577
|
+
* append-condition entry of its own: the written stream's expected-revision
|
|
578
|
+
* check already refuses a write whose scope grew between the read and the
|
|
579
|
+
* append.
|
|
578
580
|
*/
|
|
579
581
|
async function decideAtHead(model, cache, target, subject, request, signal, conditions) {
|
|
580
582
|
const built = await buildDecisionModel(cache, model, target, signal);
|
|
581
583
|
let scopeState;
|
|
582
|
-
if (conditions !== void 0) scopeState = (await cache.getState(target.documentId, request.scope, target.branch, void 0, signal)).state[request.scope];
|
|
584
|
+
if (conditions !== void 0) scopeState = (conditions.carriedDocument ?? await cache.getState(target.documentId, request.scope, target.branch, void 0, signal)).state[request.scope];
|
|
583
585
|
return {
|
|
584
586
|
evaluation: model(target).decide(built.model, subject, request, {
|
|
585
587
|
scopeState,
|
|
@@ -1740,6 +1742,17 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
1740
1742
|
putState(documentId, scope, branch, revision, document, position) {
|
|
1741
1743
|
this.store(documentId, scope, branch, revision, document, position);
|
|
1742
1744
|
}
|
|
1745
|
+
/**
|
|
1746
|
+
* Stores the run's head and mints a keyframe for every interval the run
|
|
1747
|
+
* crossed on its way there. Only the head enters the ring buffer; the
|
|
1748
|
+
* earlier revisions are keyframe candidates and nothing more.
|
|
1749
|
+
*/
|
|
1750
|
+
putRun(documentId, scope, branch, run) {
|
|
1751
|
+
if (run.length === 0) return;
|
|
1752
|
+
for (const entry of run.slice(0, -1)) this.persistKeyframe(documentId, scope, branch, entry.revision, entry.document);
|
|
1753
|
+
const head = run[run.length - 1];
|
|
1754
|
+
this.store(documentId, scope, branch, head.revision, head.document, SnapshotPosition.Head);
|
|
1755
|
+
}
|
|
1743
1756
|
store(documentId, scope, branch, revision, document, position) {
|
|
1744
1757
|
const streamKey = this.makeStreamKey(documentId, scope, branch);
|
|
1745
1758
|
const stream = this.getOrCreateStream(streamKey);
|
|
@@ -1753,7 +1766,12 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
1753
1766
|
position
|
|
1754
1767
|
};
|
|
1755
1768
|
stream.ringBuffer.push(snapshot);
|
|
1756
|
-
|
|
1769
|
+
this.persistKeyframe(documentId, scope, branch, revision, document);
|
|
1770
|
+
}
|
|
1771
|
+
/** Persists the snapshot if this revision is one the interval falls on. */
|
|
1772
|
+
persistKeyframe(documentId, scope, branch, revision, document) {
|
|
1773
|
+
if (!this.isKeyframeRevision(revision)) return;
|
|
1774
|
+
this.keyframeStore.putKeyframe(documentId, scope, branch, revision, {
|
|
1757
1775
|
...document,
|
|
1758
1776
|
operations: {},
|
|
1759
1777
|
clipboard: []
|
|
@@ -2327,14 +2345,15 @@ function reshuffleByTimestamp(startIndex, opsA, opsB) {
|
|
|
2327
2345
|
return [...opsA, ...opsB].sort((a, b) => {
|
|
2328
2346
|
const timestampDiff = new Date(a.timestampUtcMs).getTime() - new Date(b.timestampUtcMs).getTime();
|
|
2329
2347
|
if (timestampDiff !== 0) return timestampDiff;
|
|
2330
|
-
const
|
|
2331
|
-
const
|
|
2332
|
-
if (
|
|
2348
|
+
const rank = (op) => STRICT_ORDER_ACTION_TYPES.has(op.action?.type ?? "") ? 0 : 1;
|
|
2349
|
+
const rankDiff = rank(a) - rank(b);
|
|
2350
|
+
if (rankDiff !== 0) return rankDiff;
|
|
2351
|
+
if (rank(a) === 0) {
|
|
2352
|
+
const logicalIndexDiff = a.index - a.skip - (b.index - b.skip);
|
|
2333
2353
|
if (logicalIndexDiff !== 0) return logicalIndexDiff;
|
|
2334
2354
|
}
|
|
2335
2355
|
const actionIdDiff = (a.action?.id ?? "").localeCompare(b.action?.id ?? "");
|
|
2336
2356
|
if (actionIdDiff !== 0) return actionIdDiff;
|
|
2337
|
-
if (!shouldPrioritizeLogicalIndex && logicalIndexDiff !== 0) return logicalIndexDiff;
|
|
2338
2357
|
return a.id.localeCompare(b.id);
|
|
2339
2358
|
}).map((op, i) => ({
|
|
2340
2359
|
...op,
|
|
@@ -3155,9 +3174,6 @@ function isValidISOTimestamp(value) {
|
|
|
3155
3174
|
if (!ISO_TIMESTAMP_REGEX.test(value)) return false;
|
|
3156
3175
|
return !isNaN(new Date(value).getTime());
|
|
3157
3176
|
}
|
|
3158
|
-
/**
|
|
3159
|
-
* Simple job executor that processes a job by applying actions through document model reducers.
|
|
3160
|
-
*/
|
|
3161
3177
|
var SimpleJobExecutor = class {
|
|
3162
3178
|
config;
|
|
3163
3179
|
featureFlags;
|
|
@@ -3183,7 +3199,8 @@ var SimpleJobExecutor = class {
|
|
|
3183
3199
|
deferredJobTtlMs: config.deferredJobTtlMs ?? 3e4,
|
|
3184
3200
|
retryBaseDelayMs: config.retryBaseDelayMs ?? 100,
|
|
3185
3201
|
retryMaxDelayMs: config.retryMaxDelayMs ?? 5e3,
|
|
3186
|
-
yieldDeadlineMs: config.yieldDeadlineMs ?? 50
|
|
3202
|
+
yieldDeadlineMs: config.yieldDeadlineMs ?? 50,
|
|
3203
|
+
batchApplies: config.batchApplies ?? true
|
|
3187
3204
|
};
|
|
3188
3205
|
this.featureFlags = resolveFeatureFlags(config.featureFlags);
|
|
3189
3206
|
this.decisionModel = selectDecisionModel(this.featureFlags, registry);
|
|
@@ -3356,6 +3373,21 @@ var SimpleJobExecutor = class {
|
|
|
3356
3373
|
error: new InvalidOperationTimestampError(job.documentId, action.scope, action.timestampUtcMs, `action ${action.type} (id: ${action.id})`)
|
|
3357
3374
|
};
|
|
3358
3375
|
let lastYield = performance.now();
|
|
3376
|
+
if (this.config.batchApplies && this.canBatch(writes, executing)) {
|
|
3377
|
+
const batched = await this.executeRegularActionsBatched(writes, executing);
|
|
3378
|
+
const error = this.accumulateResultOrReturnError(batched, generatedOperations, operationsWithContext);
|
|
3379
|
+
if (error !== null) return {
|
|
3380
|
+
success: false,
|
|
3381
|
+
generatedOperations,
|
|
3382
|
+
operationsWithContext,
|
|
3383
|
+
error: error.error
|
|
3384
|
+
};
|
|
3385
|
+
return {
|
|
3386
|
+
success: true,
|
|
3387
|
+
generatedOperations,
|
|
3388
|
+
operationsWithContext
|
|
3389
|
+
};
|
|
3390
|
+
}
|
|
3359
3391
|
for (const write of writes) {
|
|
3360
3392
|
const result = DOCUMENT_SCOPE_ACTIONS.has(write.action.type) ? await this.documentActionHandler.execute(write, executing) : await this.executeRegularAction(write, executing);
|
|
3361
3393
|
const error = this.accumulateResultOrReturnError(result, generatedOperations, operationsWithContext);
|
|
@@ -3382,7 +3414,15 @@ var SimpleJobExecutor = class {
|
|
|
3382
3414
|
operationsWithContext
|
|
3383
3415
|
};
|
|
3384
3416
|
}
|
|
3385
|
-
|
|
3417
|
+
/**
|
|
3418
|
+
* Decides a write and reduces it, without persisting anything.
|
|
3419
|
+
*
|
|
3420
|
+
* Split from the commit so one write and a batch of them share this logic
|
|
3421
|
+
* rather than keeping two copies of it. `baseDocument` lets a batch thread
|
|
3422
|
+
* the previous action's result forward instead of reading its own write back
|
|
3423
|
+
* out of the cache, which is the only reason the reduce has to be sequential.
|
|
3424
|
+
*/
|
|
3425
|
+
async prepareRegularWrite(write, executing, baseDocument) {
|
|
3386
3426
|
const { action, skip, sourceOperation, sourceRemote, deniedReason } = write;
|
|
3387
3427
|
const { job, startTime, indexTxn, stores, signal } = executing;
|
|
3388
3428
|
let appendCondition;
|
|
@@ -3402,7 +3442,10 @@ var SimpleJobExecutor = class {
|
|
|
3402
3442
|
verb: "execute",
|
|
3403
3443
|
scope: action.scope,
|
|
3404
3444
|
operation: action.type
|
|
3405
|
-
}, signal, this.featureFlags.authConditions ? {
|
|
3445
|
+
}, signal, this.featureFlags.authConditions ? {
|
|
3446
|
+
actionInput: action.input,
|
|
3447
|
+
carriedDocument: baseDocument
|
|
3448
|
+
} : void 0);
|
|
3406
3449
|
} catch (error) {
|
|
3407
3450
|
return buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
|
|
3408
3451
|
}
|
|
@@ -3420,9 +3463,10 @@ var SimpleJobExecutor = class {
|
|
|
3420
3463
|
if (docMeta.state.isDeleted) return buildErrorResult(job, new DocumentDeletedError(job.documentId, docMeta.state.deletedAtUtcIso), startTime);
|
|
3421
3464
|
documentVersion = docMeta.state.version;
|
|
3422
3465
|
}
|
|
3423
|
-
if (isUndoRedo(action) || action.type === "PRUNE" ||
|
|
3466
|
+
if (isUndoRedo(action) || action.type === "PRUNE" || skip > 0) stores.writeCache.invalidate(job.documentId, job.scope, job.branch);
|
|
3424
3467
|
let document;
|
|
3425
|
-
|
|
3468
|
+
if (baseDocument !== void 0) document = baseDocument;
|
|
3469
|
+
else try {
|
|
3426
3470
|
document = await stores.writeCache.getState(job.documentId, job.scope, job.branch, void 0, signal);
|
|
3427
3471
|
} catch (error) {
|
|
3428
3472
|
return buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
|
|
@@ -3495,13 +3539,45 @@ var SimpleJobExecutor = class {
|
|
|
3495
3539
|
...updatedDocument.state,
|
|
3496
3540
|
header: updatedDocument.header
|
|
3497
3541
|
});
|
|
3542
|
+
return {
|
|
3543
|
+
action,
|
|
3544
|
+
sourceRemote,
|
|
3545
|
+
scope,
|
|
3546
|
+
document,
|
|
3547
|
+
updatedDocument,
|
|
3548
|
+
operation: newOperation,
|
|
3549
|
+
resultingState,
|
|
3550
|
+
appendCondition,
|
|
3551
|
+
denied: deniedReason !== void 0
|
|
3552
|
+
};
|
|
3553
|
+
}
|
|
3554
|
+
/**
|
|
3555
|
+
* Persists a run of prepared writes in one store transaction.
|
|
3556
|
+
*
|
|
3557
|
+
* The store has always accepted many operations per apply; the executor only
|
|
3558
|
+
* ever handed it one. Passing the whole run means one advisory lock over the
|
|
3559
|
+
* read set and one guarded insert for the batch, instead of one of each per
|
|
3560
|
+
* operation.
|
|
3561
|
+
*
|
|
3562
|
+
* The append condition is taken from the first write. Every write in a run
|
|
3563
|
+
* reads the same streams at the same revisions, because nothing outside the
|
|
3564
|
+
* run can change them mid-batch, and the caller has already refused to batch
|
|
3565
|
+
* the scopes where that does not hold.
|
|
3566
|
+
*/
|
|
3567
|
+
async commitPreparedWrites(prepared, executing) {
|
|
3568
|
+
const { job, startTime, indexTxn, stores, signal } = executing;
|
|
3569
|
+
const first = prepared[0];
|
|
3570
|
+
const last = prepared[prepared.length - 1];
|
|
3571
|
+
const scope = first.scope;
|
|
3572
|
+
const documentType = first.document.header.documentType;
|
|
3573
|
+
const operations = prepared.map((write) => write.operation);
|
|
3498
3574
|
let storedOperations;
|
|
3499
3575
|
try {
|
|
3500
|
-
storedOperations = await stores.operationStore.apply(job.documentId,
|
|
3501
|
-
txn.addOperations(
|
|
3502
|
-
}, signal, appendCondition);
|
|
3576
|
+
storedOperations = await stores.operationStore.apply(job.documentId, documentType, scope, job.branch, first.operation.index, (txn) => {
|
|
3577
|
+
txn.addOperations(...operations);
|
|
3578
|
+
}, signal, first.appendCondition);
|
|
3503
3579
|
} catch (error) {
|
|
3504
|
-
this.logger.error("Error writing @Operation to IOperationStore: @Error",
|
|
3580
|
+
this.logger.error("Error writing @Operation to IOperationStore: @Error", operations, error);
|
|
3505
3581
|
stores.writeCache.invalidate(job.documentId, scope, job.branch);
|
|
3506
3582
|
if (AppendConditionFailedError.isError(error)) for (const stream of error.condition.streams) stores.writeCache.invalidate(stream.documentId, stream.scope, stream.branch);
|
|
3507
3583
|
return {
|
|
@@ -3511,40 +3587,151 @@ var SimpleJobExecutor = class {
|
|
|
3511
3587
|
duration: Date.now() - startTime
|
|
3512
3588
|
};
|
|
3513
3589
|
}
|
|
3514
|
-
const
|
|
3515
|
-
updatedDocument.header.revision = {
|
|
3516
|
-
...updatedDocument.header.revision,
|
|
3517
|
-
[scope]:
|
|
3590
|
+
const head = storedOperations[storedOperations.length - 1];
|
|
3591
|
+
last.updatedDocument.header.revision = {
|
|
3592
|
+
...last.updatedDocument.header.revision,
|
|
3593
|
+
[scope]: head.index + 1
|
|
3518
3594
|
};
|
|
3519
|
-
stores.writeCache.
|
|
3520
|
-
|
|
3521
|
-
|
|
3595
|
+
stores.writeCache.putRun(job.documentId, scope, job.branch, storedOperations.map((operation, position) => ({
|
|
3596
|
+
revision: operation.index,
|
|
3597
|
+
document: prepared[position].updatedDocument
|
|
3598
|
+
})));
|
|
3599
|
+
indexTxn.write(storedOperations.map((operation, position) => ({
|
|
3600
|
+
...operation,
|
|
3522
3601
|
documentId: job.documentId,
|
|
3523
|
-
documentType
|
|
3602
|
+
documentType,
|
|
3524
3603
|
branch: job.branch,
|
|
3525
3604
|
scope,
|
|
3526
|
-
sourceRemote
|
|
3527
|
-
}
|
|
3528
|
-
if (scope === "auth") indexTxn.recordGroupReferences(job.documentId, mentionedGroupIds(action));
|
|
3605
|
+
sourceRemote: prepared[position].sourceRemote
|
|
3606
|
+
})));
|
|
3607
|
+
if (scope === "auth") for (const write of prepared) indexTxn.recordGroupReferences(job.documentId, mentionedGroupIds(write.action));
|
|
3529
3608
|
return {
|
|
3530
3609
|
job,
|
|
3531
3610
|
success: true,
|
|
3532
|
-
operations:
|
|
3533
|
-
operationsWithContext:
|
|
3534
|
-
operation
|
|
3611
|
+
operations: storedOperations,
|
|
3612
|
+
operationsWithContext: storedOperations.map((operation, position) => ({
|
|
3613
|
+
operation,
|
|
3535
3614
|
context: {
|
|
3536
3615
|
documentId: job.documentId,
|
|
3537
3616
|
scope,
|
|
3538
3617
|
branch: job.branch,
|
|
3539
|
-
documentType
|
|
3540
|
-
resultingState,
|
|
3618
|
+
documentType,
|
|
3619
|
+
resultingState: prepared[position].resultingState,
|
|
3541
3620
|
ordinal: 0
|
|
3542
3621
|
}
|
|
3543
|
-
}
|
|
3622
|
+
})),
|
|
3544
3623
|
duration: Date.now() - startTime
|
|
3545
3624
|
};
|
|
3546
3625
|
}
|
|
3547
3626
|
/**
|
|
3627
|
+
* Whether a job's writes may share one store transaction.
|
|
3628
|
+
*
|
|
3629
|
+
* Deliberately narrow. Batching changes only how many transactions the
|
|
3630
|
+
* operations arrive in, and every condition below is a case where that would
|
|
3631
|
+
* change something else as well:
|
|
3632
|
+
*
|
|
3633
|
+
* - A document-scope action goes through its own handler, which has its own
|
|
3634
|
+
* apply and its own reasons for it.
|
|
3635
|
+
* - A positional or replayed run carries skips and re-appended operations,
|
|
3636
|
+
* whose indices are not a simple ascending run from the head.
|
|
3637
|
+
* - UNDO, REDO, PRUNE and NOOP-with-skip each invalidate the write cache to
|
|
3638
|
+
* force a full-history rebuild, so they cannot be reduced against state
|
|
3639
|
+
* threaded from the write before them.
|
|
3640
|
+
* - The auth scope decides later writes against the policy earlier ones
|
|
3641
|
+
* install, so a batch would decide them all against the policy as it stood
|
|
3642
|
+
* before the batch.
|
|
3643
|
+
* - The document scope is read by every decision model, so writing it is
|
|
3644
|
+
* writing part of the read set; the per-write conditions would not agree.
|
|
3645
|
+
*
|
|
3646
|
+
* A run that fails any of these is executed one write at a time, unchanged.
|
|
3647
|
+
*/
|
|
3648
|
+
canBatch(writes, executing) {
|
|
3649
|
+
if (writes.length < 2) return false;
|
|
3650
|
+
if (executing.evaluatedByPosition || executing.replayingAcceptedHistory) return false;
|
|
3651
|
+
const scope = executing.job.scope;
|
|
3652
|
+
if (scope === "auth" || scope === "document") return false;
|
|
3653
|
+
return writes.every((write) => {
|
|
3654
|
+
const type = write.action.type;
|
|
3655
|
+
return write.skip === 0 && write.deniedReason === void 0 && write.sourceOperation === void 0 && !DOCUMENT_SCOPE_ACTIONS.has(type) && !isUndoRedo(write.action) && type !== "PRUNE" && type !== "NOOP";
|
|
3656
|
+
});
|
|
3657
|
+
}
|
|
3658
|
+
/**
|
|
3659
|
+
* Decides and reduces a run of writes, then persists them together.
|
|
3660
|
+
*
|
|
3661
|
+
* The reduce stays sequential - each action needs the state the one before it
|
|
3662
|
+
* produced - but the result is threaded in memory rather than read back from
|
|
3663
|
+
* the cache, and the whole run reaches the store in a single apply.
|
|
3664
|
+
*
|
|
3665
|
+
* A write that cannot be prepared, or that turns out to be denied, abandons
|
|
3666
|
+
* the batch and the caller replays the whole job one write at a time. That is
|
|
3667
|
+
* simpler than committing a partial run, and these are the paths where the
|
|
3668
|
+
* per-write behaviour is load-bearing.
|
|
3669
|
+
*/
|
|
3670
|
+
async executeRegularActionsBatched(writes, executing) {
|
|
3671
|
+
const prepared = [];
|
|
3672
|
+
let carried;
|
|
3673
|
+
let lastYield = performance.now();
|
|
3674
|
+
for (const write of writes) {
|
|
3675
|
+
const outcome = await this.prepareRegularWrite(write, executing, carried);
|
|
3676
|
+
if ("success" in outcome) return prepared.length === 0 ? outcome : this.executeRegularActionsSequentially(writes, executing);
|
|
3677
|
+
if (outcome.denied) return this.executeRegularActionsSequentially(writes, executing);
|
|
3678
|
+
prepared.push(outcome);
|
|
3679
|
+
carried = outcome.updatedDocument;
|
|
3680
|
+
if (performance.now() - lastYield > this.config.yieldDeadlineMs) {
|
|
3681
|
+
await yieldToMain();
|
|
3682
|
+
lastYield = performance.now();
|
|
3683
|
+
if (executing.signal?.aborted) return buildErrorResult(executing.job, /* @__PURE__ */ new Error("Aborted"), executing.startTime);
|
|
3684
|
+
}
|
|
3685
|
+
}
|
|
3686
|
+
if (!this.conditionsAgree(prepared)) return this.executeRegularActionsSequentially(writes, executing);
|
|
3687
|
+
return this.commitPreparedWrites(prepared, executing);
|
|
3688
|
+
}
|
|
3689
|
+
/** Whether every prepared write carries the same read-set condition. */
|
|
3690
|
+
conditionsAgree(prepared) {
|
|
3691
|
+
const shape = (write) => write.appendCondition === void 0 ? "none" : JSON.stringify([...write.appendCondition.streams].map((stream) => [
|
|
3692
|
+
stream.documentId,
|
|
3693
|
+
stream.scope,
|
|
3694
|
+
stream.branch,
|
|
3695
|
+
stream.revision
|
|
3696
|
+
]).sort());
|
|
3697
|
+
const first = shape(prepared[0]);
|
|
3698
|
+
return prepared.every((write) => shape(write) === first);
|
|
3699
|
+
}
|
|
3700
|
+
/**
|
|
3701
|
+
* The unbatched path, for a run that turned out not to qualify after its
|
|
3702
|
+
* writes were prepared. Nothing has been persisted at that point, so
|
|
3703
|
+
* replaying the whole run per write is safe.
|
|
3704
|
+
*/
|
|
3705
|
+
async executeRegularActionsSequentially(writes, executing) {
|
|
3706
|
+
const operations = [];
|
|
3707
|
+
const contexts = [];
|
|
3708
|
+
let lastYield = performance.now();
|
|
3709
|
+
for (const write of writes) {
|
|
3710
|
+
const result = await this.executeRegularAction(write, executing);
|
|
3711
|
+
if (!result.success) return result;
|
|
3712
|
+
operations.push(...result.operations ?? []);
|
|
3713
|
+
contexts.push(...result.operationsWithContext ?? []);
|
|
3714
|
+
if (performance.now() - lastYield > this.config.yieldDeadlineMs) {
|
|
3715
|
+
await yieldToMain();
|
|
3716
|
+
lastYield = performance.now();
|
|
3717
|
+
if (executing.signal?.aborted) return buildErrorResult(executing.job, /* @__PURE__ */ new Error("Aborted"), executing.startTime);
|
|
3718
|
+
}
|
|
3719
|
+
}
|
|
3720
|
+
return {
|
|
3721
|
+
job: executing.job,
|
|
3722
|
+
success: true,
|
|
3723
|
+
operations,
|
|
3724
|
+
operationsWithContext: contexts,
|
|
3725
|
+
duration: Date.now() - executing.startTime
|
|
3726
|
+
};
|
|
3727
|
+
}
|
|
3728
|
+
/** Decides, reduces and persists one write. */
|
|
3729
|
+
async executeRegularAction(write, executing) {
|
|
3730
|
+
const prepared = await this.prepareRegularWrite(write, executing);
|
|
3731
|
+
if ("success" in prepared) return prepared;
|
|
3732
|
+
return this.commitPreparedWrites([prepared], executing);
|
|
3733
|
+
}
|
|
3734
|
+
/**
|
|
3548
3735
|
* Orders a write by timestamp and decides it where it lands. The caller
|
|
3549
3736
|
* supplies the timestamp, so a write can belong before operations already
|
|
3550
3737
|
* stored; those are re-appended alongside it, the way a load reshuffles.
|
|
@@ -3926,7 +4113,7 @@ var SimpleJobExecutor = class {
|
|
|
3926
4113
|
...operation,
|
|
3927
4114
|
id: operation.id
|
|
3928
4115
|
})));
|
|
3929
|
-
for (const operation of reshuffledOperations) if (operation.action.type === "NOOP") operation.skip = 1;
|
|
4116
|
+
for (const operation of reshuffledOperations) if (operation.action.type === "NOOP" && operation.skip === 0) operation.skip = 1;
|
|
3930
4117
|
let deniedReasons;
|
|
3931
4118
|
if (this.featureFlags.documentDecisions) try {
|
|
3932
4119
|
deniedReasons = await evaluateByPosition(this.decisionModel, {
|
|
@@ -4947,4 +5134,4 @@ const DEFAULT_DRIVE_CONTAINER_TYPES = new Set(["powerhouse/document-drive", "pow
|
|
|
4947
5134
|
//#endregion
|
|
4948
5135
|
export { GATED_DOCUMENT_ACTIONS as A, AuthTimestampNotMonotonicError as B, DuplicateOperationError as C, DuplicateModuleError as D, DuplicateManifestError as E, selectDecisionModel as F, InvalidOperationTimestampError as G, DocumentDeletedError as H, documentDecisionModel as I, parsePagingOptions as J, UpgradePreconditionFailedError as K, authDecisionModel as L, createEmptyConsistencyToken as M, targetDocumentId as N, InvalidModuleError as O, decideAtHead as P, buildDecisionModel as R, AppendConditionFailedError as S, RevisionMismatchError as T, DocumentNotFoundError as U, AuthorizationDeniedError as V, ExcessiveReshuffleError as W, __exportAll as X, throwIfAborted as Y, DocumentMetaCache as _, createForwardingPoolInstrumentation as a, JobExecutorEventTypes as b, KyselyKeyframeStore as c, DriveCollectionId as d, KyselyExecutionScope as f, KyselyOperationIndex as g, KyselyWriteCache as h, runMigrations as i, createConsistencyToken as j, ModuleNotFoundError as k, DocumentModelRegistry as l, EventBus as m, REACTOR_SCHEMA as n, instrumentPgPool as o, resolveFeatureFlags as p, matchesScope as q, getMigrationStatus as r, KyselyOperationStore as s, DEFAULT_DRIVE_CONTAINER_TYPES as t, SimpleJobExecutor as u, CollectionMembershipCache as v, OptimisticLockError as w, APPEND_CONDITION_FAILED_PREFIX as x, DEFAULT_DEFERRED_JOB_TTL_MS as y, AuthEnforcementDisabledError as z };
|
|
4949
5136
|
|
|
4950
|
-
//# sourceMappingURL=drive-container-types-
|
|
5137
|
+
//# sourceMappingURL=drive-container-types-CruMdXue.js.map
|