@powerhousedao/reactor 6.2.3-dev.3 → 6.2.3-dev.31
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-DnU3ZU4H.js → build-worker-executor-Bvoba-AV.js} +24 -8
- package/dist/build-worker-executor-Bvoba-AV.js.map +1 -0
- package/dist/{document-indexer-C5Gsa1B3.js → document-indexer-BVTGbEB0.js} +190 -40
- package/dist/document-indexer-BVTGbEB0.js.map +1 -0
- package/dist/entry.d.ts +2 -1
- package/dist/entry.js +10 -6
- package/dist/entry.js.map +1 -1
- package/dist/{errors-D3S6Eysd.js → errors-BM_ULVWd.js} +9 -1
- package/dist/{errors-D3S6Eysd.js.map → errors-BM_ULVWd.js.map} +1 -1
- package/dist/{drive-container-types-yZrksiJR.js → forwarding-logger-CnuVDVze.js} +1297 -241
- package/dist/forwarding-logger-CnuVDVze.js.map +1 -0
- package/dist/index.d.ts +2707 -2040
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1708 -648
- package/dist/index.js.map +1 -1
- package/dist/projection-entry.d.ts +2 -1
- package/dist/projection-entry.js +11 -17
- package/dist/projection-entry.js.map +1 -1
- package/dist/{projection-shard-manager-CPQc5XHL.js → projection-shard-manager-BpcqDtnQ.js} +4 -4
- package/dist/projection-shard-manager-BpcqDtnQ.js.map +1 -0
- package/dist/{projection-worker-wI4PwcV2.js → projection-worker-D8uNFdwX.js} +1 -1
- package/dist/{projection-worker-wI4PwcV2.js.map → projection-worker-D8uNFdwX.js.map} +1 -1
- package/dist/{transport-CuogVKN_.js → transport-B3G50KVd.js} +1 -1
- package/dist/{transport-CuogVKN_.js.map → transport-B3G50KVd.js.map} +1 -1
- package/dist/{transport-ByGviWdZ.js → transport-CZDHzfvy.js} +1 -1
- package/dist/{transport-ByGviWdZ.js.map → transport-CZDHzfvy.js.map} +1 -1
- package/dist/{worker-handle-CrERzl8s.js → worker-handle-C2rLUHwv.js} +22 -16
- package/dist/worker-handle-C2rLUHwv.js.map +1 -0
- package/package.json +19 -10
- package/dist/build-worker-executor-DnU3ZU4H.js.map +0 -1
- package/dist/document-indexer-C5Gsa1B3.js.map +0 -1
- package/dist/drive-container-types-yZrksiJR.js.map +0 -1
- package/dist/error-info-Cpu4OY3o.js +0 -62
- package/dist/error-info-Cpu4OY3o.js.map +0 -1
- package/dist/forwarding-logger-BBkMSxuJ.js +0 -85
- package/dist/forwarding-logger-BBkMSxuJ.js.map +0 -1
- package/dist/projection-shard-manager-CPQc5XHL.js.map +0 -1
- package/dist/types-DMKLa0Ok.js +0 -33
- package/dist/types-DMKLa0Ok.js.map +0 -1
- package/dist/worker-HPysBfhx.js +0 -22
- package/dist/worker-HPysBfhx.js.map +0 -1
- package/dist/worker-handle-CrERzl8s.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
import { A as
|
|
2
|
-
import {
|
|
3
|
-
import { i as
|
|
4
|
-
import {
|
|
5
|
-
import { n as errorToInfo, r as sanitizeArg, t as createForwardingLogger } from "./forwarding-logger-BBkMSxuJ.js";
|
|
6
|
-
import { t as workerEntryPath } from "./worker-HPysBfhx.js";
|
|
7
|
-
import { DowngradeNotSupportedError, MAX_AUTH_GRANTS, UnsupportedDocumentModelVersionError, actions, actions as documentActions, createPresignedHeader, decide, deserializeSignature, garbageCollect, generateId, groupDocumentType, groupMembershipActionTypes, hashDocumentStateForScope, normalizeDocumentModelVersion, sortOperations, toTransportAction } from "@powerhousedao/shared/document-model";
|
|
1
|
+
import { $ as DocumentNotFoundError, A as ModuleNotFoundError, B as authDecisionModel, C as DocumentMetaCache, D as DuplicateManifestError, E as JobExecutorEventTypes, F as summarizeSubmittedActions, G as DocumentExistence, H as APPEND_CONDITION_FAILED_PREFIX, I as targetDocumentId, J as RevisionMismatchError, K as DuplicateOperationError, L as decideAtHead, M as createConsistencyToken, N as createEmptyConsistencyToken, O as DuplicateModuleError, P as submittedActionIds, Q as DocumentDeletedError, R as selectDecisionModel, S as KyselyOperationIndex, T as DEFAULT_DEFERRED_JOB_TTL_MS, U as AppendConditionFailedError, V as buildDecisionModel, W as DocumentAlreadyExistsError, X as AuthTimestampNotMonotonicError, Y as AuthEnforcementDisabledError, Z as AuthorizationDeniedError, _ as DriveCollectionId, a as DEFAULT_DRIVE_CONTAINER_TYPES, at as EventBusAggregateError, b as EventBus, c as runMigrations, ct as parsePagingOptions, d as KyselyOperationStore, dt as __exportAll, et as ExcessiveReshuffleError, f as KyselyKeyframeStore, g as PassthroughSigner, h as verifyActionSignature, it as UpgradePreconditionFailedError, j as GATED_DOCUMENT_ACTIONS, k as InvalidModuleError, l as createForwardingPoolInstrumentation, lt as throwIfAborted, m as SimpleJobExecutor, n as errorToInfo, nt as InvalidSignatureError, o as REACTOR_SCHEMA, ot as ReactorEventTypes, p as DocumentModelRegistry, q as OptimisticLockError, r as sanitizeArg, rt as RelationshipNotFoundError, s as getMigrationStatus, st as matchesScope, t as createForwardingLogger, tt as InvalidOperationTimestampError, u as instrumentPgPool, v as KyselyExecutionScope, w as CollectionMembershipCache, x as KyselyWriteCache, y as resolveFeatureFlags, z as documentDecisionModel } from "./forwarding-logger-CnuVDVze.js";
|
|
2
|
+
import { i as WorkerInitFailedError, r as WorkerExitedError, t as WorkerAbortTimeoutError } from "./errors-BM_ULVWd.js";
|
|
3
|
+
import { a as KyselyDocumentView, c as ReadModelCoordinator, d as DEFAULT_READ_MODEL_YIELD_DEADLINE_MS, f as defaultReadModelIndexingConfig, i as DeletedDocumentRead, l as BaseReadModel, n as ConsistencyTracker, o as DOCUMENT_INDEXER_READ_MODEL, p as unchunkedReadModelIndexingConfig, r as makeConsistencyKey, s as DOCUMENT_VIEW_READ_MODEL, t as KyselyDocumentIndexer, u as DEFAULT_COMMIT_CHUNK_SIZE } from "./document-indexer-BVTGbEB0.js";
|
|
4
|
+
import { DEFAULT_SIGNATURE_POLICY, DowngradeNotSupportedError, MAX_AUTH_GRANTS, UnsupportedDocumentModelVersionError, actionSignerIdentity, actionSigningTarget, actions, actions as documentActions, createCopyHeader, decide, deserializeSignature, garbageCollect, generateId, groupDocumentType, groupMembershipActionTypes, hashDocumentStateForScope, normalizeDocumentModelVersion, requestedSignaturePolicy, sortOperations, toTransportAction, withSignaturePolicy } from "@powerhousedao/shared/document-model";
|
|
8
5
|
import { addFile, addFolder, copyNode, deleteNode, driveCreateDocument, generateNodesCopy, getDescendants, handleTargetNameCollisions, isFileNode, isFolderNode, moveNode, updateNode } from "@powerhousedao/shared/document-drive";
|
|
9
6
|
import { v4 } from "uuid";
|
|
10
7
|
import { ConsoleLogger, childLogger } from "document-model";
|
|
@@ -237,37 +234,31 @@ function getSharedActionScope(actions) {
|
|
|
237
234
|
return baseScope;
|
|
238
235
|
}
|
|
239
236
|
/**
|
|
240
|
-
* Signs an action
|
|
241
|
-
*
|
|
237
|
+
* Signs an action for `target`, the log it is written to. An action already
|
|
238
|
+
* signed under a key is returned unchanged; one whose signer or last tuple has
|
|
239
|
+
* no key is unsigned, and is signed like any other.
|
|
242
240
|
*/
|
|
243
|
-
const signAction = async (action, signer, signal) => {
|
|
244
|
-
const
|
|
245
|
-
if (
|
|
246
|
-
const signature = await signer.signAction(action, signal);
|
|
241
|
+
const signAction = async (action, signer, target, signal) => {
|
|
242
|
+
const existing = action.context?.signer;
|
|
243
|
+
if (existing?.app?.key && existing.signatures.at(-1)?.[1]) return action;
|
|
244
|
+
const signature = await signer.signAction(action, target, signal);
|
|
247
245
|
return {
|
|
248
246
|
...action,
|
|
249
247
|
context: {
|
|
250
248
|
...action.context,
|
|
251
249
|
signer: {
|
|
252
|
-
|
|
253
|
-
address: signer.user?.address || "",
|
|
254
|
-
networkId: signer.user?.networkId || "",
|
|
255
|
-
chainId: signer.user?.chainId || 0
|
|
256
|
-
},
|
|
257
|
-
app: {
|
|
258
|
-
name: signer.app?.name || "",
|
|
259
|
-
key: signer.app?.key || ""
|
|
260
|
-
},
|
|
250
|
+
...actionSignerIdentity(signer),
|
|
261
251
|
signatures: [signature]
|
|
262
252
|
}
|
|
263
253
|
}
|
|
264
254
|
};
|
|
265
255
|
};
|
|
266
256
|
/**
|
|
267
|
-
* Signs
|
|
257
|
+
* Signs the actions of a job on `job.documentId`, each for the log it is
|
|
258
|
+
* written to: a relationship action lands in its source document.
|
|
268
259
|
*/
|
|
269
|
-
const signActions = async (actions, signer, signal) => {
|
|
270
|
-
return Promise.all(actions.map((action) => signAction(action, signer, signal)));
|
|
260
|
+
const signActions = async (actions, signer, job, signal) => {
|
|
261
|
+
return Promise.all(actions.map((action) => signAction(action, signer, actionSigningTarget(action, job.documentId, job.branch), signal)));
|
|
271
262
|
};
|
|
272
263
|
function buildSingleJobMeta(jobId, callerMeta) {
|
|
273
264
|
return {
|
|
@@ -321,6 +312,10 @@ let JobStatus = /* @__PURE__ */ function(JobStatus) {
|
|
|
321
312
|
* during construction, so the partial-`this` hazard does not apply.
|
|
322
313
|
*/
|
|
323
314
|
var DriveClient = class {
|
|
315
|
+
client;
|
|
316
|
+
logger;
|
|
317
|
+
reactor;
|
|
318
|
+
signer;
|
|
324
319
|
constructor(client, logger, reactor, signer) {
|
|
325
320
|
this.client = client;
|
|
326
321
|
this.logger = logger;
|
|
@@ -329,11 +324,11 @@ var DriveClient = class {
|
|
|
329
324
|
}
|
|
330
325
|
async create(input, signal) {
|
|
331
326
|
this.logger.verbose("drives.create(@input)", input);
|
|
332
|
-
const driveDoc = driveCreateDocument({ global: {
|
|
327
|
+
const driveDoc = withSignaturePolicy(driveCreateDocument({ global: {
|
|
333
328
|
name: input.global.name || "",
|
|
334
329
|
icon: input.global.icon ?? null,
|
|
335
330
|
nodes: []
|
|
336
|
-
} });
|
|
331
|
+
} }), requestedSignaturePolicy(input, await this.client.getCreateSignaturePolicy()), { protocolVersions: input.protocolVersions });
|
|
337
332
|
if (input.preferredEditor) driveDoc.header.meta = {
|
|
338
333
|
...driveDoc.header.meta,
|
|
339
334
|
preferredEditor: input.preferredEditor
|
|
@@ -343,24 +338,25 @@ var DriveClient = class {
|
|
|
343
338
|
async addFile(driveIdentifier, document, parentFolder, signal) {
|
|
344
339
|
this.logger.verbose("drives.addFile(@driveIdentifier, @document, @parentFolder)", driveIdentifier, document.header.id, parentFolder);
|
|
345
340
|
const documentId = document.header.id;
|
|
341
|
+
const createInput = {
|
|
342
|
+
model: document.header.documentType,
|
|
343
|
+
version: 0,
|
|
344
|
+
documentId: document.header.id,
|
|
345
|
+
signing: {
|
|
346
|
+
signature: document.header.id,
|
|
347
|
+
publicKey: document.header.sig.publicKey,
|
|
348
|
+
nonce: document.header.sig.nonce,
|
|
349
|
+
createdAtUtcIso: document.header.createdAtUtcIso,
|
|
350
|
+
documentType: document.header.documentType
|
|
351
|
+
},
|
|
352
|
+
slug: document.header.slug,
|
|
353
|
+
name: document.header.name,
|
|
354
|
+
branch: document.header.branch,
|
|
355
|
+
meta: document.header.meta,
|
|
356
|
+
protocolVersions: document.header.protocolVersions ?? { "base-reducer": 2 }
|
|
357
|
+
};
|
|
346
358
|
const documentActions = await signActions([
|
|
347
|
-
createDocumentAction(
|
|
348
|
-
model: document.header.documentType,
|
|
349
|
-
version: 0,
|
|
350
|
-
documentId: document.header.id,
|
|
351
|
-
signing: {
|
|
352
|
-
signature: document.header.id,
|
|
353
|
-
publicKey: document.header.sig.publicKey,
|
|
354
|
-
nonce: document.header.sig.nonce,
|
|
355
|
-
createdAtUtcIso: document.header.createdAtUtcIso,
|
|
356
|
-
documentType: document.header.documentType
|
|
357
|
-
},
|
|
358
|
-
slug: document.header.slug,
|
|
359
|
-
name: document.header.name,
|
|
360
|
-
branch: document.header.branch,
|
|
361
|
-
meta: document.header.meta,
|
|
362
|
-
protocolVersions: document.header.protocolVersions ?? { "base-reducer": 2 }
|
|
363
|
-
}),
|
|
359
|
+
createDocumentAction(createInput),
|
|
364
360
|
upgradeDocumentAction({
|
|
365
361
|
documentId: document.header.id,
|
|
366
362
|
model: document.header.documentType,
|
|
@@ -369,13 +365,19 @@ var DriveClient = class {
|
|
|
369
365
|
initialState: document.state
|
|
370
366
|
}),
|
|
371
367
|
addRelationshipAction(driveIdentifier, documentId, "child")
|
|
372
|
-
], this.signer,
|
|
368
|
+
], this.signer, {
|
|
369
|
+
documentId,
|
|
370
|
+
branch: "main"
|
|
371
|
+
}, signal);
|
|
373
372
|
const driveActions = await signActions([addFile({
|
|
374
373
|
id: documentId,
|
|
375
374
|
name: document.header.name || documentId,
|
|
376
375
|
documentType: document.header.documentType,
|
|
377
376
|
parentFolder
|
|
378
|
-
})], this.signer,
|
|
377
|
+
})], this.signer, {
|
|
378
|
+
documentId: driveIdentifier,
|
|
379
|
+
branch: "main"
|
|
380
|
+
}, signal);
|
|
379
381
|
await this.runJobs([{
|
|
380
382
|
key: "document",
|
|
381
383
|
documentId,
|
|
@@ -465,18 +467,20 @@ var DriveClient = class {
|
|
|
465
467
|
});
|
|
466
468
|
resolvedNamesByTargetId.set(entry.targetId, resolved);
|
|
467
469
|
}
|
|
470
|
+
const policy = await this.client.getCreateSignaturePolicy();
|
|
468
471
|
for (const entry of copyPlan) {
|
|
469
472
|
const node = drive.state.global.nodes.find((n) => n.id === entry.srcId);
|
|
470
473
|
if (!node || !isFileNode(node)) continue;
|
|
471
474
|
const srcDoc = await this.client.get(entry.srcId, void 0, signal);
|
|
472
475
|
const duplicated = {
|
|
473
476
|
...srcDoc,
|
|
474
|
-
header:
|
|
477
|
+
header: createCopyHeader(srcDoc.header, entry.targetId, policy),
|
|
475
478
|
initialState: srcDoc.state,
|
|
476
479
|
operations: {}
|
|
477
480
|
};
|
|
478
481
|
const resolvedName = resolvedNamesByTargetId.get(entry.targetId);
|
|
479
482
|
if (resolvedName) duplicated.header.name = resolvedName;
|
|
483
|
+
entry.targetId = duplicated.header.id;
|
|
480
484
|
await this.addFile(driveIdentifier, duplicated, entry.targetParentFolder ?? void 0, signal);
|
|
481
485
|
}
|
|
482
486
|
return this.client.execute(driveIdentifier, "main", copyPlan.map((entry) => copyNode(entry)), signal);
|
|
@@ -500,8 +504,7 @@ var DriveClient = class {
|
|
|
500
504
|
return {
|
|
501
505
|
results: filtered.slice(startIndex, endIndex),
|
|
502
506
|
options: effective,
|
|
503
|
-
...endIndex < filtered.length ? { nextCursor: String(endIndex) } : {}
|
|
504
|
-
totalCount: filtered.length
|
|
507
|
+
...endIndex < filtered.length ? { nextCursor: String(endIndex) } : {}
|
|
505
508
|
};
|
|
506
509
|
}
|
|
507
510
|
/**
|
|
@@ -512,7 +515,7 @@ var DriveClient = class {
|
|
|
512
515
|
async runJobs(jobs, signal) {
|
|
513
516
|
const batchResult = await this.reactor.executeBatch({ jobs }, signal);
|
|
514
517
|
const completedJobs = await Promise.all(Object.values(batchResult.jobs).map((job) => this.client.waitForJob(job, signal)));
|
|
515
|
-
for (const job of completedJobs) if (job.status ===
|
|
518
|
+
for (const job of completedJobs) if (job.status === "FAILED") throw this.toError(job);
|
|
516
519
|
return completedJobs;
|
|
517
520
|
}
|
|
518
521
|
toError(job) {
|
|
@@ -529,8 +532,14 @@ var DriveClient = class {
|
|
|
529
532
|
}
|
|
530
533
|
}
|
|
531
534
|
async removeFileNode(driveId, fileId, signal) {
|
|
532
|
-
const relationshipActions = await signActions([removeRelationshipAction(driveId, fileId, "child")], this.signer,
|
|
533
|
-
|
|
535
|
+
const relationshipActions = await signActions([removeRelationshipAction(driveId, fileId, "child")], this.signer, {
|
|
536
|
+
documentId: driveId,
|
|
537
|
+
branch: "main"
|
|
538
|
+
}, signal);
|
|
539
|
+
const driveActions = await signActions([deleteNode({ id: fileId })], this.signer, {
|
|
540
|
+
documentId: driveId,
|
|
541
|
+
branch: "main"
|
|
542
|
+
}, signal);
|
|
534
543
|
const batchResult = await this.reactor.executeBatch({ jobs: [{
|
|
535
544
|
key: "relationship",
|
|
536
545
|
documentId: driveId,
|
|
@@ -547,10 +556,10 @@ var DriveClient = class {
|
|
|
547
556
|
dependsOn: ["relationship"]
|
|
548
557
|
}] }, signal);
|
|
549
558
|
const completedJobs = await Promise.all(Object.values(batchResult.jobs).map((job) => this.client.waitForJob(job, signal)));
|
|
550
|
-
for (const job of completedJobs) if (job.status ===
|
|
559
|
+
for (const job of completedJobs) if (job.status === "FAILED") throw new Error(job.error?.message);
|
|
551
560
|
const deleteJob = await this.reactor.deleteDocument(fileId, this.signer, signal);
|
|
552
561
|
const deleteCompleted = await this.client.waitForJob(deleteJob, signal);
|
|
553
|
-
if (deleteCompleted.status ===
|
|
562
|
+
if (deleteCompleted.status === "FAILED") throw new Error(deleteCompleted.error?.message);
|
|
554
563
|
}
|
|
555
564
|
};
|
|
556
565
|
//#endregion
|
|
@@ -561,13 +570,15 @@ var DriveClient = class {
|
|
|
561
570
|
* Only READ_READY and FAILED are truly terminal.
|
|
562
571
|
*/
|
|
563
572
|
function isTerminalStatus(status) {
|
|
564
|
-
return status ===
|
|
573
|
+
return status === "READ_READY" || status === "FAILED";
|
|
565
574
|
}
|
|
566
575
|
/**
|
|
567
576
|
* Event-driven implementation of IJobAwaiter.
|
|
568
577
|
* Subscribes to operation events to detect job completion without polling.
|
|
569
578
|
*/
|
|
570
579
|
var JobAwaiter = class {
|
|
580
|
+
eventBus;
|
|
581
|
+
getJobStatus;
|
|
571
582
|
pendingJobs = /* @__PURE__ */ new Map();
|
|
572
583
|
unsubscribers = [];
|
|
573
584
|
constructor(eventBus, getJobStatus) {
|
|
@@ -709,7 +720,54 @@ let DocumentChangeType = /* @__PURE__ */ function(DocumentChangeType) {
|
|
|
709
720
|
* would diverge permanently. The document scope carries the metadata the same
|
|
710
721
|
* argument covers. Grants gate domain-scope reads only.
|
|
711
722
|
*/
|
|
712
|
-
const ALWAYS_READABLE_SCOPES = new Set(["auth", "document"]);
|
|
723
|
+
const ALWAYS_READABLE_SCOPES = /* @__PURE__ */ new Set(["auth", "document"]);
|
|
724
|
+
const DECLARED_DOMAIN_SCOPES = ["global", "local"];
|
|
725
|
+
/**
|
|
726
|
+
* The scopes grants gate: the declared ones, and any other the document holds.
|
|
727
|
+
* A document fetched for a narrowed view holds only that view's scopes.
|
|
728
|
+
*/
|
|
729
|
+
function domainScopesOf(document) {
|
|
730
|
+
const state = document.state;
|
|
731
|
+
const held = Object.keys(state ?? {}).filter((scope) => !ALWAYS_READABLE_SCOPES.has(scope));
|
|
732
|
+
return [.../* @__PURE__ */ new Set([...DECLARED_DOMAIN_SCOPES, ...held])];
|
|
733
|
+
}
|
|
734
|
+
function allowsSomeDomainScope(document, readable) {
|
|
735
|
+
return domainScopesOf(document).some(readable);
|
|
736
|
+
}
|
|
737
|
+
/** A document with no state at all is not policied, so nothing withholds it. */
|
|
738
|
+
function refusesEveryDomainScope(document, readable) {
|
|
739
|
+
return document.state !== void 0 && !allowsSomeDomainScope(document, readable);
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* The domain scopes the document does not hold whose read decision reads their
|
|
743
|
+
* own state. A condition's `doc.<scope>` path resolves only for that scope, so
|
|
744
|
+
* every other scope is decided on its name and the policy alone.
|
|
745
|
+
*/
|
|
746
|
+
function unheldScopesReadOnState(document) {
|
|
747
|
+
const grants = authOf(document)?.grants ?? [];
|
|
748
|
+
const onState = /* @__PURE__ */ new Set();
|
|
749
|
+
for (const grant of grants) {
|
|
750
|
+
collectStateScopes(grant.where, onState);
|
|
751
|
+
collectStateScopes(grant.principal, onState);
|
|
752
|
+
}
|
|
753
|
+
if (onState.size === 0) return [];
|
|
754
|
+
const held = document.state ?? {};
|
|
755
|
+
return domainScopesOf(document).filter((scope) => onState.has(scope) && !(scope in held));
|
|
756
|
+
}
|
|
757
|
+
function collectStateScopes(node, into) {
|
|
758
|
+
if (Array.isArray(node)) {
|
|
759
|
+
for (const child of node) collectStateScopes(child, into);
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
if (typeof node !== "object" || node === null) return;
|
|
763
|
+
const { attr } = node;
|
|
764
|
+
if (typeof attr === "string") {
|
|
765
|
+
const [root, scope] = attr.split(".");
|
|
766
|
+
if (root === "doc" && scope) into.add(scope);
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
for (const child of Object.values(node)) collectStateScopes(child, into);
|
|
770
|
+
}
|
|
713
771
|
/** What a gate does with an uninitialized policy when told nothing. */
|
|
714
772
|
const OPEN_BY_DEFAULT = { withholdUninitialized: false };
|
|
715
773
|
/**
|
|
@@ -772,8 +830,26 @@ var BareReadGate = class {
|
|
|
772
830
|
scope
|
|
773
831
|
}) === "allow");
|
|
774
832
|
}
|
|
833
|
+
prepare(document) {
|
|
834
|
+
return Promise.resolve((subject) => this.scopePredicate(document, subject));
|
|
835
|
+
}
|
|
775
836
|
};
|
|
776
837
|
/**
|
|
838
|
+
* Rethrows a failed read unless the document is confirmed not live. The read
|
|
839
|
+
* side reports absence as a plain Error, so absence is checked rather than
|
|
840
|
+
* inferred from the message, and a failed check surfaces the read's own error.
|
|
841
|
+
*/
|
|
842
|
+
async function assertAbsent(documentView, documentId, error, signal) {
|
|
843
|
+
if (DocumentNotFoundError.isError(error)) return;
|
|
844
|
+
let exists;
|
|
845
|
+
try {
|
|
846
|
+
exists = await documentView.exists([documentId], "LiveOnly", void 0, signal);
|
|
847
|
+
} catch {
|
|
848
|
+
throw error;
|
|
849
|
+
}
|
|
850
|
+
if (exists[0]) throw error;
|
|
851
|
+
}
|
|
852
|
+
/**
|
|
777
853
|
* Answers a stream read from the document already fetched, and anything else
|
|
778
854
|
* through the read side.
|
|
779
855
|
*
|
|
@@ -783,6 +859,9 @@ var BareReadGate = class {
|
|
|
783
859
|
* list names is fetched.
|
|
784
860
|
*/
|
|
785
861
|
var SeededStateReader = class {
|
|
862
|
+
documentView;
|
|
863
|
+
seed;
|
|
864
|
+
branch;
|
|
786
865
|
constructor(documentView, seed, branch) {
|
|
787
866
|
this.documentView = documentView;
|
|
788
867
|
this.seed = seed;
|
|
@@ -804,20 +883,10 @@ var SeededStateReader = class {
|
|
|
804
883
|
scopes: [scope]
|
|
805
884
|
}, void 0, signal);
|
|
806
885
|
} catch (error) {
|
|
807
|
-
await this.
|
|
886
|
+
await assertAbsent(this.documentView, documentId, error, signal);
|
|
808
887
|
throw new DocumentNotFoundError(documentId);
|
|
809
888
|
}
|
|
810
889
|
}
|
|
811
|
-
async assertAbsent(documentId, error, signal) {
|
|
812
|
-
if (DocumentNotFoundError.isError(error)) return;
|
|
813
|
-
let exists;
|
|
814
|
-
try {
|
|
815
|
-
exists = await this.documentView.exists([documentId], DocumentExistence.LiveOnly, void 0, signal);
|
|
816
|
-
} catch {
|
|
817
|
-
throw error;
|
|
818
|
-
}
|
|
819
|
-
if (exists[0]) throw error;
|
|
820
|
-
}
|
|
821
890
|
};
|
|
822
891
|
/**
|
|
823
892
|
* Evaluates a read against the registered decision model, built at the stream
|
|
@@ -833,6 +902,12 @@ var SeededStateReader = class {
|
|
|
833
902
|
* from state no commit ever invalidates.
|
|
834
903
|
*/
|
|
835
904
|
var ModelReadGate = class {
|
|
905
|
+
model;
|
|
906
|
+
documentView;
|
|
907
|
+
servesGroups;
|
|
908
|
+
operationIndex;
|
|
909
|
+
logger;
|
|
910
|
+
options;
|
|
836
911
|
constructor(model, documentView, servesGroups, operationIndex, logger, options = OPEN_BY_DEFAULT) {
|
|
837
912
|
this.model = model;
|
|
838
913
|
this.documentView = documentView;
|
|
@@ -847,10 +922,18 @@ var ModelReadGate = class {
|
|
|
847
922
|
* group's other scopes are its own business and stay behind its own grants.
|
|
848
923
|
*/
|
|
849
924
|
async scopePredicate(document, subject, branch, signal) {
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
925
|
+
return (await this.prepare(document, branch, signal))(subject);
|
|
926
|
+
}
|
|
927
|
+
async prepare(document, branch, signal) {
|
|
928
|
+
const policy = await this.ownPolicy(document, branch, signal);
|
|
929
|
+
if (!this.servesGroup(document)) return (subject) => Promise.resolve(policy(subject));
|
|
930
|
+
const referencers = this.referencerReads(document.header.id, signal);
|
|
931
|
+
return async (subject) => {
|
|
932
|
+
const own = policy(subject);
|
|
933
|
+
if (own(GROUP_MEMBERSHIP_SCOPE)) return own;
|
|
934
|
+
const audience = await this.servesGroupTo(subject, referencers);
|
|
935
|
+
return (scope) => own(scope) || audience && scope === GROUP_MEMBERSHIP_SCOPE;
|
|
936
|
+
};
|
|
854
937
|
}
|
|
855
938
|
/**
|
|
856
939
|
* Whether the subject is served this group because a policy names it.
|
|
@@ -884,11 +967,8 @@ var ModelReadGate = class {
|
|
|
884
967
|
* running with nobody awaiting it, which is where unhandled rejections come
|
|
885
968
|
* from.
|
|
886
969
|
*/
|
|
887
|
-
async servesGroupTo(
|
|
888
|
-
|
|
889
|
-
const referencers = await this.operationIndex.getGroupReferencers(groupId, signal);
|
|
890
|
-
const examined = referencers.slice(0, MAX_EXAMINED_REFERENCERS);
|
|
891
|
-
if (examined.length < referencers.length) this.logger?.warn(`Group ${groupId} is referenced by ${referencers.length} documents; only the first ${MAX_EXAMINED_REFERENCERS} decide whether it is served`);
|
|
970
|
+
async servesGroupTo(subject, referencers) {
|
|
971
|
+
const examined = await referencers.examined();
|
|
892
972
|
const walk = {
|
|
893
973
|
next: 0,
|
|
894
974
|
served: false,
|
|
@@ -899,7 +979,7 @@ var ModelReadGate = class {
|
|
|
899
979
|
const referencerId = examined[walk.next++];
|
|
900
980
|
let serves = false;
|
|
901
981
|
try {
|
|
902
|
-
serves = await
|
|
982
|
+
serves = (await referencers.serves(referencerId))(subject);
|
|
903
983
|
} catch (error) {
|
|
904
984
|
walk.failure ??= Error.isError(error) ? error : /* @__PURE__ */ new Error(`Probing referencer ${referencerId} failed`);
|
|
905
985
|
}
|
|
@@ -910,21 +990,44 @@ var ModelReadGate = class {
|
|
|
910
990
|
if (!walk.served && walk.failure) throw walk.failure;
|
|
911
991
|
return walk.served;
|
|
912
992
|
}
|
|
993
|
+
/** Each read made once, however many subjects the walk runs for. */
|
|
994
|
+
referencerReads(groupId, signal) {
|
|
995
|
+
let examined;
|
|
996
|
+
const serves = /* @__PURE__ */ new Map();
|
|
997
|
+
return {
|
|
998
|
+
examined: () => examined ??= this.examinedReferencers(groupId, signal),
|
|
999
|
+
serves: (referencerId) => {
|
|
1000
|
+
let read = serves.get(referencerId);
|
|
1001
|
+
if (!read) {
|
|
1002
|
+
read = this.servesThrough(referencerId, signal);
|
|
1003
|
+
serves.set(referencerId, read);
|
|
1004
|
+
}
|
|
1005
|
+
return read;
|
|
1006
|
+
}
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
1009
|
+
async examinedReferencers(groupId, signal) {
|
|
1010
|
+
if (!this.operationIndex) return [];
|
|
1011
|
+
const referencers = await this.operationIndex.getGroupReferencers(groupId, signal);
|
|
1012
|
+
const examined = referencers.slice(0, MAX_EXAMINED_REFERENCERS);
|
|
1013
|
+
if (examined.length < referencers.length) this.logger?.warn(`Group ${groupId} is referenced by ${referencers.length} documents; only the first ${MAX_EXAMINED_REFERENCERS} decide whether it is served`);
|
|
1014
|
+
return examined;
|
|
1015
|
+
}
|
|
913
1016
|
/**
|
|
914
|
-
* Whether one referencing document serves
|
|
1017
|
+
* Whether one referencing document serves a subject any domain scope. A
|
|
915
1018
|
* referencer this replica does not hold serves nothing, which fails closed
|
|
916
1019
|
* the same way a group it does not hold does.
|
|
917
1020
|
*/
|
|
918
|
-
async servesThrough(referencerId,
|
|
1021
|
+
async servesThrough(referencerId, signal) {
|
|
919
1022
|
let referencer;
|
|
920
1023
|
try {
|
|
921
1024
|
referencer = await this.documentView.get(referencerId, { branch: GROUP_BRANCH }, void 0, signal);
|
|
922
1025
|
} catch {
|
|
923
|
-
return false;
|
|
1026
|
+
return () => false;
|
|
924
1027
|
}
|
|
925
|
-
if (this.servesGroup(referencer)) return false;
|
|
926
|
-
const
|
|
927
|
-
return
|
|
1028
|
+
if (this.servesGroup(referencer)) return () => false;
|
|
1029
|
+
const policy = await this.ownPolicy(referencer, GROUP_BRANCH, signal);
|
|
1030
|
+
return (subject) => allowsSomeDomainScope(referencer, policy(subject));
|
|
928
1031
|
}
|
|
929
1032
|
servesGroup(document) {
|
|
930
1033
|
return this.servesGroups && this.operationIndex !== void 0 && document.header.documentType === groupDocumentType;
|
|
@@ -942,9 +1045,12 @@ var ModelReadGate = class {
|
|
|
942
1045
|
* publish an unpoliced group it names: the referencer walk asks this question
|
|
943
1046
|
* of the referencing document, whose real policy answers it.
|
|
944
1047
|
*/
|
|
945
|
-
async
|
|
1048
|
+
async ownPolicy(document, branch, signal) {
|
|
946
1049
|
const auth = authOf(document);
|
|
947
|
-
if (!auth || !auth.version)
|
|
1050
|
+
if (!auth || !auth.version) {
|
|
1051
|
+
const readable = this.options.withholdUninitialized ? (scope) => ALWAYS_READABLE_SCOPES.has(scope) : () => true;
|
|
1052
|
+
return () => readable;
|
|
1053
|
+
}
|
|
948
1054
|
const target = {
|
|
949
1055
|
documentId: document.header.id,
|
|
950
1056
|
branch
|
|
@@ -952,7 +1058,7 @@ var ModelReadGate = class {
|
|
|
952
1058
|
const built = await buildDecisionModel(new SeededStateReader(this.documentView, document, branch), this.model, target, signal);
|
|
953
1059
|
const definition = this.model(target);
|
|
954
1060
|
const scopeStates = document.state ?? {};
|
|
955
|
-
return (scope) => ALWAYS_READABLE_SCOPES.has(scope) || definition.decide(built.model, subject, {
|
|
1061
|
+
return (subject) => (scope) => ALWAYS_READABLE_SCOPES.has(scope) || definition.decide(built.model, subject, {
|
|
956
1062
|
verb: "read",
|
|
957
1063
|
scope
|
|
958
1064
|
}, {
|
|
@@ -962,6 +1068,78 @@ var ModelReadGate = class {
|
|
|
962
1068
|
}
|
|
963
1069
|
};
|
|
964
1070
|
//#endregion
|
|
1071
|
+
//#region src/client/event-reads.ts
|
|
1072
|
+
/** One change event's reads, shared by every subscription it reaches. */
|
|
1073
|
+
var EventReads = class {
|
|
1074
|
+
reactor;
|
|
1075
|
+
documentView;
|
|
1076
|
+
readGate;
|
|
1077
|
+
documents = /* @__PURE__ */ new Map();
|
|
1078
|
+
existence = /* @__PURE__ */ new Map();
|
|
1079
|
+
prepared = /* @__PURE__ */ new WeakMap();
|
|
1080
|
+
constructor(reactor, documentView, readGate) {
|
|
1081
|
+
this.reactor = reactor;
|
|
1082
|
+
this.documentView = documentView;
|
|
1083
|
+
this.readGate = readGate;
|
|
1084
|
+
}
|
|
1085
|
+
get(id, view) {
|
|
1086
|
+
const key = JSON.stringify([
|
|
1087
|
+
id,
|
|
1088
|
+
view?.branch,
|
|
1089
|
+
view?.scopes
|
|
1090
|
+
]);
|
|
1091
|
+
let read = this.documents.get(key);
|
|
1092
|
+
if (!read) {
|
|
1093
|
+
read = this.reactor.get(id, view);
|
|
1094
|
+
this.documents.set(key, read);
|
|
1095
|
+
}
|
|
1096
|
+
return read;
|
|
1097
|
+
}
|
|
1098
|
+
exists(ids, existence) {
|
|
1099
|
+
const key = JSON.stringify([ids, existence]);
|
|
1100
|
+
let read = this.existence.get(key);
|
|
1101
|
+
if (!read) {
|
|
1102
|
+
read = this.documentView.exists(ids, existence);
|
|
1103
|
+
this.existence.set(key, read);
|
|
1104
|
+
}
|
|
1105
|
+
return read;
|
|
1106
|
+
}
|
|
1107
|
+
async scopePredicate(document, subject, branch) {
|
|
1108
|
+
if (!this.readGate.prepare) return this.readGate.scopePredicate(document, subject, branch);
|
|
1109
|
+
let byBranch = this.prepared.get(document);
|
|
1110
|
+
if (!byBranch) {
|
|
1111
|
+
byBranch = /* @__PURE__ */ new Map();
|
|
1112
|
+
this.prepared.set(document, byBranch);
|
|
1113
|
+
}
|
|
1114
|
+
let prepared = byBranch.get(branch);
|
|
1115
|
+
if (!prepared) {
|
|
1116
|
+
prepared = this.readGate.prepare(document, branch);
|
|
1117
|
+
byBranch.set(branch, prepared);
|
|
1118
|
+
}
|
|
1119
|
+
return (await prepared)(subject);
|
|
1120
|
+
}
|
|
1121
|
+
};
|
|
1122
|
+
var EventReadsSource = class {
|
|
1123
|
+
reactor;
|
|
1124
|
+
documentView;
|
|
1125
|
+
readGate;
|
|
1126
|
+
current;
|
|
1127
|
+
constructor(reactor, documentView, readGate) {
|
|
1128
|
+
this.reactor = reactor;
|
|
1129
|
+
this.documentView = documentView;
|
|
1130
|
+
this.readGate = readGate;
|
|
1131
|
+
}
|
|
1132
|
+
forEvent() {
|
|
1133
|
+
if (!this.current) {
|
|
1134
|
+
this.current = new EventReads(this.reactor, this.documentView, this.readGate);
|
|
1135
|
+
queueMicrotask(() => {
|
|
1136
|
+
this.current = void 0;
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
return this.current;
|
|
1140
|
+
}
|
|
1141
|
+
};
|
|
1142
|
+
//#endregion
|
|
965
1143
|
//#region src/client/util.ts
|
|
966
1144
|
function authSubjectFromSigner(signer) {
|
|
967
1145
|
return {
|
|
@@ -972,10 +1150,23 @@ function authSubjectFromSigner(signer) {
|
|
|
972
1150
|
function withAuthScope(view) {
|
|
973
1151
|
if (view?.scopes && view.scopes.length > 0) return {
|
|
974
1152
|
...view,
|
|
975
|
-
scopes: [
|
|
1153
|
+
scopes: [.../* @__PURE__ */ new Set([...view.scopes, "auth"])]
|
|
976
1154
|
};
|
|
977
1155
|
return view;
|
|
978
1156
|
}
|
|
1157
|
+
function withAllScopes(view) {
|
|
1158
|
+
if (!view?.scopes) return view;
|
|
1159
|
+
const { scopes: _narrowed, ...whole } = view;
|
|
1160
|
+
return whole;
|
|
1161
|
+
}
|
|
1162
|
+
function narrowedScopes(view) {
|
|
1163
|
+
if (!view?.scopes || view.scopes.length === 0) return;
|
|
1164
|
+
return /* @__PURE__ */ new Set([
|
|
1165
|
+
"document",
|
|
1166
|
+
"auth",
|
|
1167
|
+
...view.scopes
|
|
1168
|
+
]);
|
|
1169
|
+
}
|
|
979
1170
|
/**
|
|
980
1171
|
* Drops the scopes the predicate refuses. The predicate is resolved by the read
|
|
981
1172
|
* gate, which decides against the whole policy once per document; this only
|
|
@@ -1048,9 +1239,11 @@ var ReactorClient = class {
|
|
|
1048
1239
|
documentIndexer;
|
|
1049
1240
|
documentView;
|
|
1050
1241
|
readGate;
|
|
1242
|
+
eventReads;
|
|
1051
1243
|
actionEvaluation;
|
|
1244
|
+
createSignaturePolicy;
|
|
1052
1245
|
drives;
|
|
1053
|
-
constructor(logger, reactor, signer, subscriptionManager, jobAwaiter, documentIndexer, documentView, readGate = new BareReadGate(), actionEvaluation) {
|
|
1246
|
+
constructor(logger, reactor, signer, subscriptionManager, jobAwaiter, documentIndexer, documentView, readGate = new BareReadGate(), actionEvaluation, createSignaturePolicy = DEFAULT_SIGNATURE_POLICY) {
|
|
1054
1247
|
this.logger = logger;
|
|
1055
1248
|
this.reactor = reactor;
|
|
1056
1249
|
this.signer = signer;
|
|
@@ -1059,32 +1252,12 @@ var ReactorClient = class {
|
|
|
1059
1252
|
this.documentIndexer = documentIndexer;
|
|
1060
1253
|
this.documentView = documentView;
|
|
1061
1254
|
this.readGate = readGate;
|
|
1255
|
+
this.eventReads = new EventReadsSource(reactor, documentView, readGate);
|
|
1062
1256
|
this.actionEvaluation = actionEvaluation;
|
|
1257
|
+
this.createSignaturePolicy = createSignaturePolicy;
|
|
1063
1258
|
this.drives = new DriveClient(this, logger, reactor, signer);
|
|
1064
1259
|
this.logger.verbose("ReactorClient initialized");
|
|
1065
1260
|
}
|
|
1066
|
-
readSubject(subject) {
|
|
1067
|
-
return subject ?? authSubjectFromSigner(this.signer);
|
|
1068
|
-
}
|
|
1069
|
-
/**
|
|
1070
|
-
* Which scopes of one document the subject may read. Resolved once per
|
|
1071
|
-
* document, so the gate builds its model once however many scopes are then
|
|
1072
|
-
* tested, and the filtering itself stays synchronous.
|
|
1073
|
-
*/
|
|
1074
|
-
readableScopes(document, view, signal) {
|
|
1075
|
-
return this.readGate.scopePredicate(document, this.readSubject(view?.subject), view?.branch ?? "main", signal);
|
|
1076
|
-
}
|
|
1077
|
-
/**
|
|
1078
|
-
* One document, filtered to the scopes the subject may read. Every method
|
|
1079
|
-
* that hands a document back goes through here, including the ones that
|
|
1080
|
-
* follow a write: a document returned from a mutation is a read like any
|
|
1081
|
-
* other, and returning it whole served scopes the same subject would be
|
|
1082
|
-
* refused by `get`. Its author still sees what it wrote, because an allow on
|
|
1083
|
-
* execute confers read of that scope.
|
|
1084
|
-
*/
|
|
1085
|
-
async gateDocument(document, view, signal) {
|
|
1086
|
-
return filterReadableScopes(document, await this.readableScopes(document, view, signal));
|
|
1087
|
-
}
|
|
1088
1261
|
/**
|
|
1089
1262
|
* Retrieves a list of document model modules.
|
|
1090
1263
|
*/
|
|
@@ -1150,7 +1323,7 @@ var ReactorClient = class {
|
|
|
1150
1323
|
/** True when the id is taken; a soft-deleted document's id is still taken. */
|
|
1151
1324
|
async isDocumentIdTaken(documentId, signal) {
|
|
1152
1325
|
this.logger.verbose("isDocumentIdTaken(@documentId)", documentId);
|
|
1153
|
-
return (await this.documentView.exists([documentId],
|
|
1326
|
+
return (await this.documentView.exists([documentId], "IncludingDeleted", void 0, signal))[0];
|
|
1154
1327
|
}
|
|
1155
1328
|
/**
|
|
1156
1329
|
* Retrieves operations for a document
|
|
@@ -1190,31 +1363,6 @@ var ReactorClient = class {
|
|
|
1190
1363
|
nextCursor: Object.keys(activeCursors).length > 0 ? encodeCompositeCursor(activeCursors) : void 0
|
|
1191
1364
|
};
|
|
1192
1365
|
}
|
|
1193
|
-
async getOperationsWithCompositeCursor(documentId, view, filter, paging, signal, canRead) {
|
|
1194
|
-
const scopeCursors = decodeCompositeCursor(paging.cursor);
|
|
1195
|
-
const allOperations = [];
|
|
1196
|
-
const activeCursors = {};
|
|
1197
|
-
for (const [scopeName, cursor] of Object.entries(scopeCursors)) {
|
|
1198
|
-
if (!canRead(scopeName)) continue;
|
|
1199
|
-
const scopeView = {
|
|
1200
|
-
...view,
|
|
1201
|
-
scopes: [scopeName]
|
|
1202
|
-
};
|
|
1203
|
-
const scopePaging = {
|
|
1204
|
-
cursor,
|
|
1205
|
-
limit: paging.limit
|
|
1206
|
-
};
|
|
1207
|
-
const scopeResult = (await this.reactor.getOperations(documentId, scopeView, filter, scopePaging, void 0, signal))[scopeName];
|
|
1208
|
-
allOperations.push(...scopeResult.results);
|
|
1209
|
-
if (scopeResult.nextCursor) activeCursors[scopeName] = scopeResult.nextCursor;
|
|
1210
|
-
}
|
|
1211
|
-
allOperations.sort((a, b) => a.index - b.index);
|
|
1212
|
-
return {
|
|
1213
|
-
results: allOperations,
|
|
1214
|
-
options: paging,
|
|
1215
|
-
nextCursor: Object.keys(activeCursors).length > 0 ? encodeCompositeCursor(activeCursors) : void 0
|
|
1216
|
-
};
|
|
1217
|
-
}
|
|
1218
1366
|
/**
|
|
1219
1367
|
* Retrieves outgoing relationships of a given type from a source document.
|
|
1220
1368
|
*/
|
|
@@ -1248,15 +1396,44 @@ var ReactorClient = class {
|
|
|
1248
1396
|
return this.find({ ids: sourceIds }, view, paging, signal);
|
|
1249
1397
|
}
|
|
1250
1398
|
/**
|
|
1399
|
+
* Retrieves the outgoing relationship edges of a source document, carrying the
|
|
1400
|
+
* metadata and timestamps the far-end documents do not.
|
|
1401
|
+
*/
|
|
1402
|
+
async getOutgoingRelationshipEdges(sourceIdentifier, relationshipType, view, paging, signal) {
|
|
1403
|
+
this.logger.verbose("getOutgoingRelationshipEdges(@sourceIdentifier, @relationshipType, @view, @paging)", sourceIdentifier, relationshipType, view, paging);
|
|
1404
|
+
const sourceId = await this.documentView.resolveIdOrSlug(sourceIdentifier, view, void 0, signal);
|
|
1405
|
+
const edges = await this.reactor.getOutgoingRelationshipEdges(sourceId, relationshipType, paging, void 0, signal);
|
|
1406
|
+
return this.gateEdges(edges, "targetId", view, signal);
|
|
1407
|
+
}
|
|
1408
|
+
/**
|
|
1409
|
+
* Retrieves the incoming relationship edges of a target document, carrying the
|
|
1410
|
+
* metadata and timestamps the far-end documents do not.
|
|
1411
|
+
*/
|
|
1412
|
+
async getIncomingRelationshipEdges(targetIdentifier, relationshipType, view, paging, signal) {
|
|
1413
|
+
this.logger.verbose("getIncomingRelationshipEdges(@targetIdentifier, @relationshipType, @view, @paging)", targetIdentifier, relationshipType, view, paging);
|
|
1414
|
+
const targetId = await this.documentView.resolveIdOrSlug(targetIdentifier, view, void 0, signal);
|
|
1415
|
+
const edges = await this.reactor.getIncomingRelationshipEdges(targetId, relationshipType, paging, void 0, signal);
|
|
1416
|
+
return this.gateEdges(edges, "sourceId", view, signal);
|
|
1417
|
+
}
|
|
1418
|
+
/**
|
|
1251
1419
|
* Filters documents by criteria and returns a list of them
|
|
1252
1420
|
*/
|
|
1253
1421
|
async find(search, view, paging, signal) {
|
|
1254
1422
|
this.logger.verbose("find(@search, @view, @paging)", search, view, paging);
|
|
1255
1423
|
const results = await this.reactor.find(search, withAuthScope(view), paging, void 0, signal);
|
|
1256
|
-
return
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1424
|
+
return this.gateListing(results, view, signal);
|
|
1425
|
+
}
|
|
1426
|
+
/** Whether `find` would serve the document; false when it is absent. */
|
|
1427
|
+
async isServed(identifier, view, signal) {
|
|
1428
|
+
this.logger.verbose("isServed(@identifier, @view)", identifier, view);
|
|
1429
|
+
let document;
|
|
1430
|
+
try {
|
|
1431
|
+
document = await this.reactor.getByIdOrSlug(identifier, withAuthScope(view), void 0, signal);
|
|
1432
|
+
} catch (error) {
|
|
1433
|
+
await assertAbsent(this.documentView, identifier, error, signal);
|
|
1434
|
+
return false;
|
|
1435
|
+
}
|
|
1436
|
+
return await this.gateListed(document, view, signal) !== void 0;
|
|
1260
1437
|
}
|
|
1261
1438
|
/**
|
|
1262
1439
|
* Predicts the admission verdict for each candidate. See
|
|
@@ -1297,38 +1474,8 @@ var ReactorClient = class {
|
|
|
1297
1474
|
anyDenied: allowed < candidates.length
|
|
1298
1475
|
};
|
|
1299
1476
|
}
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
*
|
|
1303
|
-
* The document is fetched unfiltered, because the policy is what decides:
|
|
1304
|
-
* reading it through the read gate would withhold the very scopes the
|
|
1305
|
-
* decision is about. A deleted document is served at its deletion boundary,
|
|
1306
|
-
* which is what lets the model refuse an execute against it -- authEnforcement
|
|
1307
|
-
* requires documentDecisions, so that read is available whenever this runs.
|
|
1308
|
-
*
|
|
1309
|
-
* Reading past the gate discloses nothing a submit does not. The `auth` and
|
|
1310
|
-
* `document` scopes are readable by every holder, so a verdict resting on the
|
|
1311
|
-
* policy alone is one the caller could compute unaided; and a verdict resting
|
|
1312
|
-
* on a conditional grant reads the executing scope's state exactly as
|
|
1313
|
-
* admission reads it, so the answer here is what submitting and being refused
|
|
1314
|
-
* would have revealed anyway.
|
|
1315
|
-
*
|
|
1316
|
-
* The append condition the build records is dropped. It guards a write, and
|
|
1317
|
-
* this makes none; reproducing it is also what the preflight cannot do, which
|
|
1318
|
-
* is why the answer is a prediction.
|
|
1319
|
-
*/
|
|
1320
|
-
async buildEvaluationTarget(config, documentId, branch, signal) {
|
|
1321
|
-
const document = await this.documentView.get(documentId, { branch }, void 0, signal);
|
|
1322
|
-
const target = {
|
|
1323
|
-
documentId,
|
|
1324
|
-
branch
|
|
1325
|
-
};
|
|
1326
|
-
const built = await buildDecisionModel(new SeededStateReader(this.documentView, document, branch), config.model, target, signal);
|
|
1327
|
-
return {
|
|
1328
|
-
definition: config.model(target),
|
|
1329
|
-
model: built.model,
|
|
1330
|
-
scopeStates: document.state ?? {}
|
|
1331
|
-
};
|
|
1477
|
+
getCreateSignaturePolicy() {
|
|
1478
|
+
return Promise.resolve(this.createSignaturePolicy);
|
|
1332
1479
|
}
|
|
1333
1480
|
/**
|
|
1334
1481
|
* Creates a document and waits for completion
|
|
@@ -1336,7 +1483,8 @@ var ReactorClient = class {
|
|
|
1336
1483
|
async create(document, parentIdentifier, signal) {
|
|
1337
1484
|
this.logger.verbose("create(@id, @parentIdentifier)", document.header.id, parentIdentifier);
|
|
1338
1485
|
const documentId = document.header.id;
|
|
1339
|
-
const
|
|
1486
|
+
const branch = document.header.branch || "main";
|
|
1487
|
+
const createInput = {
|
|
1340
1488
|
model: document.header.documentType,
|
|
1341
1489
|
version: 0,
|
|
1342
1490
|
documentId,
|
|
@@ -1352,23 +1500,30 @@ var ReactorClient = class {
|
|
|
1352
1500
|
branch: document.header.branch,
|
|
1353
1501
|
meta: document.header.meta,
|
|
1354
1502
|
protocolVersions: document.header.protocolVersions ?? { "base-reducer": 2 }
|
|
1355
|
-
}
|
|
1503
|
+
};
|
|
1504
|
+
const createActions = await signActions([createDocumentAction(createInput), upgradeDocumentAction({
|
|
1356
1505
|
documentId,
|
|
1357
1506
|
model: document.header.documentType,
|
|
1358
1507
|
fromVersion: 0,
|
|
1359
1508
|
toVersion: normalizeDocumentModelVersion(document.state.document?.version),
|
|
1360
1509
|
initialState: document.state
|
|
1361
|
-
})], this.signer,
|
|
1510
|
+
})], this.signer, {
|
|
1511
|
+
documentId,
|
|
1512
|
+
branch
|
|
1513
|
+
}, signal);
|
|
1362
1514
|
const jobs = [{
|
|
1363
1515
|
key: "create",
|
|
1364
1516
|
documentId,
|
|
1365
1517
|
scope: getSharedActionScope(createActions),
|
|
1366
|
-
branch
|
|
1518
|
+
branch,
|
|
1367
1519
|
actions: createActions,
|
|
1368
1520
|
dependsOn: []
|
|
1369
1521
|
}];
|
|
1370
1522
|
if (parentIdentifier) {
|
|
1371
|
-
const parentActions = await signActions([addRelationshipAction(parentIdentifier, documentId, "child")], this.signer,
|
|
1523
|
+
const parentActions = await signActions([addRelationshipAction(parentIdentifier, documentId, "child")], this.signer, {
|
|
1524
|
+
documentId: parentIdentifier,
|
|
1525
|
+
branch: "main"
|
|
1526
|
+
}, signal);
|
|
1372
1527
|
jobs.push({
|
|
1373
1528
|
key: "parent",
|
|
1374
1529
|
documentId: parentIdentifier,
|
|
@@ -1380,7 +1535,7 @@ var ReactorClient = class {
|
|
|
1380
1535
|
}
|
|
1381
1536
|
const batchResult = await this.reactor.executeBatch({ jobs }, signal);
|
|
1382
1537
|
const completedJobs = await Promise.all(Object.values(batchResult.jobs).map((job) => this.waitForJob(job, signal)));
|
|
1383
|
-
for (const job of completedJobs) if (job.status ===
|
|
1538
|
+
for (const job of completedJobs) if (job.status === "FAILED") throw new Error(job.error?.message);
|
|
1384
1539
|
const created = await this.reactor.get(documentId);
|
|
1385
1540
|
return this.gateDocument(created, void 0, signal);
|
|
1386
1541
|
}
|
|
@@ -1402,7 +1557,7 @@ var ReactorClient = class {
|
|
|
1402
1557
|
}, void 0);
|
|
1403
1558
|
if (!module) throw new Error(`Document model not found for type: ${documentModelType}`);
|
|
1404
1559
|
}
|
|
1405
|
-
const document = module.utils.createDocument();
|
|
1560
|
+
const document = withSignaturePolicy(module.utils.createDocument(), requestedSignaturePolicy(options, this.createSignaturePolicy), { protocolVersions: options?.protocolVersions });
|
|
1406
1561
|
document.state.document.version = normalizeDocumentModelVersion(module.version);
|
|
1407
1562
|
return this.create(document, options?.parentIdentifier, signal);
|
|
1408
1563
|
}
|
|
@@ -1428,19 +1583,26 @@ var ReactorClient = class {
|
|
|
1428
1583
|
const branch = document.header.branch || "main";
|
|
1429
1584
|
const fromVersion = normalizeDocumentModelVersion(document.state.document?.version);
|
|
1430
1585
|
let targetVersion = toVersion;
|
|
1431
|
-
if (targetVersion === void 0)
|
|
1586
|
+
if (targetVersion === void 0) {
|
|
1587
|
+
const module = await this.getDocumentModelModule(documentType);
|
|
1588
|
+
targetVersion = normalizeDocumentModelVersion(module.version);
|
|
1589
|
+
}
|
|
1432
1590
|
if (targetVersion === fromVersion) return this.gateDocument(document, { branch }, signal);
|
|
1433
1591
|
if (targetVersion < fromVersion) throw new DowngradeNotSupportedError(documentType, fromVersion, targetVersion);
|
|
1434
|
-
const
|
|
1592
|
+
const action = upgradeDocumentAction({
|
|
1435
1593
|
documentId,
|
|
1436
1594
|
model: documentType,
|
|
1437
1595
|
fromVersion,
|
|
1438
1596
|
toVersion: targetVersion,
|
|
1439
1597
|
revision: { ...document.header.revision }
|
|
1440
|
-
})
|
|
1598
|
+
});
|
|
1599
|
+
const signedActions = await signActions([action], this.signer, {
|
|
1600
|
+
documentId,
|
|
1601
|
+
branch
|
|
1602
|
+
}, signal);
|
|
1441
1603
|
const jobInfo = await this.reactor.execute(documentId, branch, signedActions, signal);
|
|
1442
1604
|
const completedJob = await this.waitForJob(jobInfo, signal);
|
|
1443
|
-
if (completedJob.status !==
|
|
1605
|
+
if (completedJob.status !== "FAILED") {
|
|
1444
1606
|
const upgraded = await this.reactor.getByIdOrSlug(documentId, { branch }, completedJob.consistencyToken, signal);
|
|
1445
1607
|
return this.gateDocument(upgraded, { branch }, signal);
|
|
1446
1608
|
}
|
|
@@ -1467,10 +1629,14 @@ var ReactorClient = class {
|
|
|
1467
1629
|
*/
|
|
1468
1630
|
async execute(documentIdentifier, branch, actions, signal, subject) {
|
|
1469
1631
|
this.logger.verbose("execute(@documentIdentifier, @branch, @count actions)", documentIdentifier, branch, actions.length);
|
|
1470
|
-
const
|
|
1471
|
-
const
|
|
1632
|
+
const documentId = await this.resolveWriteTarget(documentIdentifier, branch, actions, signal);
|
|
1633
|
+
const signedActions = await signActions(actions, this.signer, {
|
|
1634
|
+
documentId,
|
|
1635
|
+
branch
|
|
1636
|
+
}, signal);
|
|
1637
|
+
const jobInfo = await this.reactor.execute(documentId, branch, signedActions, signal);
|
|
1472
1638
|
const completedJob = await this.waitForJob(jobInfo, signal);
|
|
1473
|
-
if (completedJob.status ===
|
|
1639
|
+
if (completedJob.status === "FAILED") throw new Error(completedJob.error?.message);
|
|
1474
1640
|
const view = {
|
|
1475
1641
|
branch,
|
|
1476
1642
|
subject
|
|
@@ -1483,18 +1649,29 @@ var ReactorClient = class {
|
|
|
1483
1649
|
*/
|
|
1484
1650
|
async executeAsync(documentIdentifier, branch, actions, signal) {
|
|
1485
1651
|
this.logger.verbose("executeAsync(@documentIdentifier, @branch, @count actions)", documentIdentifier, branch, actions.length);
|
|
1486
|
-
const
|
|
1487
|
-
|
|
1652
|
+
const documentId = await this.resolveWriteTarget(documentIdentifier, branch, actions, signal);
|
|
1653
|
+
const signedActions = await signActions(actions, this.signer, {
|
|
1654
|
+
documentId,
|
|
1655
|
+
branch
|
|
1656
|
+
}, signal);
|
|
1657
|
+
return this.reactor.execute(documentId, branch, signedActions, signal);
|
|
1488
1658
|
}
|
|
1489
1659
|
async executeBatch(request, signal) {
|
|
1490
1660
|
this.logger.verbose("executeBatch(@count jobs)", request.jobs.length);
|
|
1491
|
-
const signedJobs = await Promise.all(request.jobs.map(async (job) =>
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1661
|
+
const signedJobs = await Promise.all(request.jobs.map(async (job) => {
|
|
1662
|
+
const documentId = await this.resolveWriteTarget(job.documentId, job.branch, job.actions, signal);
|
|
1663
|
+
return {
|
|
1664
|
+
...job,
|
|
1665
|
+
documentId,
|
|
1666
|
+
actions: await signActions(job.actions, this.signer, {
|
|
1667
|
+
documentId,
|
|
1668
|
+
branch: job.branch
|
|
1669
|
+
}, signal)
|
|
1670
|
+
};
|
|
1671
|
+
}));
|
|
1495
1672
|
const batchResult = await this.reactor.executeBatch({ jobs: signedJobs }, signal);
|
|
1496
1673
|
const completedJobs = await Promise.all(Object.values(batchResult.jobs).map((job) => this.waitForJob(job, signal)));
|
|
1497
|
-
for (const job of completedJobs) if (job.status ===
|
|
1674
|
+
for (const job of completedJobs) if (job.status === "FAILED") throw new Error(job.error?.message);
|
|
1498
1675
|
return batchResult;
|
|
1499
1676
|
}
|
|
1500
1677
|
/**
|
|
@@ -1515,11 +1692,23 @@ var ReactorClient = class {
|
|
|
1515
1692
|
/**
|
|
1516
1693
|
* Adds multiple documents as children to another and waits for completion
|
|
1517
1694
|
*/
|
|
1518
|
-
async addRelationship(sourceIdentifier, targetIdentifier, relationshipType, branch = "main", signal) {
|
|
1519
|
-
this.logger.verbose("addRelationship(@sourceIdentifier, @targetIdentifier, @relationshipType, @branch)", sourceIdentifier, targetIdentifier, relationshipType, branch);
|
|
1520
|
-
const jobInfo = await this.reactor.addRelationship(sourceIdentifier, targetIdentifier, relationshipType, branch, this.signer, signal);
|
|
1695
|
+
async addRelationship(sourceIdentifier, targetIdentifier, relationshipType, metadata, branch = "main", signal) {
|
|
1696
|
+
this.logger.verbose("addRelationship(@sourceIdentifier, @targetIdentifier, @relationshipType, @metadata, @branch)", sourceIdentifier, targetIdentifier, relationshipType, metadata, branch);
|
|
1697
|
+
const jobInfo = await this.reactor.addRelationship(sourceIdentifier, targetIdentifier, relationshipType, metadata, branch, this.signer, signal);
|
|
1521
1698
|
const completedJob = await this.waitForJob(jobInfo, signal);
|
|
1522
|
-
if (completedJob.status ===
|
|
1699
|
+
if (completedJob.status === "FAILED") throw new Error(completedJob.error?.message);
|
|
1700
|
+
const result = await this.reactor.getByIdOrSlug(sourceIdentifier, { branch }, completedJob.consistencyToken, signal);
|
|
1701
|
+
return this.gateDocument(result, { branch }, signal);
|
|
1702
|
+
}
|
|
1703
|
+
/**
|
|
1704
|
+
* Replaces the metadata of an existing relationship and waits for completion.
|
|
1705
|
+
*/
|
|
1706
|
+
async updateRelationship(sourceIdentifier, targetIdentifier, relationshipType, metadata, branch = "main", signal) {
|
|
1707
|
+
this.logger.verbose("updateRelationship(@sourceIdentifier, @targetIdentifier, @relationshipType, @metadata, @branch)", sourceIdentifier, targetIdentifier, relationshipType, metadata, branch);
|
|
1708
|
+
if (!await this.readRelationshipEdge(sourceIdentifier, targetIdentifier, relationshipType, { branch }, signal)) throw new RelationshipNotFoundError(sourceIdentifier, targetIdentifier, relationshipType);
|
|
1709
|
+
const jobInfo = await this.reactor.updateRelationship(sourceIdentifier, targetIdentifier, relationshipType, metadata, branch, this.signer, signal);
|
|
1710
|
+
const completedJob = await this.waitForJob(jobInfo, signal);
|
|
1711
|
+
if (completedJob.status === "FAILED") throw new Error(completedJob.error?.message);
|
|
1523
1712
|
const result = await this.reactor.getByIdOrSlug(sourceIdentifier, { branch }, completedJob.consistencyToken, signal);
|
|
1524
1713
|
return this.gateDocument(result, { branch }, signal);
|
|
1525
1714
|
}
|
|
@@ -1530,7 +1719,7 @@ var ReactorClient = class {
|
|
|
1530
1719
|
this.logger.verbose("removeRelationship(@sourceIdentifier, @targetIdentifier, @relationshipType, @branch)", sourceIdentifier, targetIdentifier, relationshipType, branch);
|
|
1531
1720
|
const jobInfo = await this.reactor.removeRelationship(sourceIdentifier, targetIdentifier, relationshipType, branch, this.signer, signal);
|
|
1532
1721
|
const completedJob = await this.waitForJob(jobInfo, signal);
|
|
1533
|
-
if (completedJob.status ===
|
|
1722
|
+
if (completedJob.status === "FAILED") throw new Error(completedJob.error?.message);
|
|
1534
1723
|
const result = await this.reactor.getByIdOrSlug(sourceIdentifier, { branch }, completedJob.consistencyToken, signal);
|
|
1535
1724
|
return this.gateDocument(result, { branch }, signal);
|
|
1536
1725
|
}
|
|
@@ -1539,12 +1728,13 @@ var ReactorClient = class {
|
|
|
1539
1728
|
*/
|
|
1540
1729
|
async moveRelationship(sourceParentIdentifier, targetParentIdentifier, targetIdentifier, relationshipType, branch = "main", signal) {
|
|
1541
1730
|
this.logger.verbose("moveRelationship(@sourceParentIdentifier, @targetParentIdentifier, @targetIdentifier, @relationshipType, @branch)", sourceParentIdentifier, targetParentIdentifier, targetIdentifier, relationshipType, branch);
|
|
1731
|
+
const metadata = (await this.readRelationshipEdge(sourceParentIdentifier, targetIdentifier, relationshipType, { branch }, signal))?.metadata;
|
|
1542
1732
|
const removeJobInfo = await this.reactor.removeRelationship(sourceParentIdentifier, targetIdentifier, relationshipType, branch, this.signer, signal);
|
|
1543
1733
|
const removeCompletedJob = await this.waitForJob(removeJobInfo, signal);
|
|
1544
|
-
if (removeCompletedJob.status ===
|
|
1545
|
-
const addJobInfo = await this.reactor.addRelationship(targetParentIdentifier, targetIdentifier, relationshipType, branch, this.signer, signal);
|
|
1734
|
+
if (removeCompletedJob.status === "FAILED") throw new Error(removeCompletedJob.error?.message);
|
|
1735
|
+
const addJobInfo = await this.reactor.addRelationship(targetParentIdentifier, targetIdentifier, relationshipType, metadata, branch, this.signer, signal);
|
|
1546
1736
|
const addCompletedJob = await this.waitForJob(addJobInfo, signal);
|
|
1547
|
-
if (addCompletedJob.status ===
|
|
1737
|
+
if (addCompletedJob.status === "FAILED") throw new Error(addCompletedJob.error?.message);
|
|
1548
1738
|
const sourceResult = await this.reactor.getByIdOrSlug(sourceParentIdentifier, { branch }, removeCompletedJob.consistencyToken, signal);
|
|
1549
1739
|
const targetResult = await this.reactor.getByIdOrSlug(targetParentIdentifier, { branch }, addCompletedJob.consistencyToken, signal);
|
|
1550
1740
|
return {
|
|
@@ -1558,7 +1748,7 @@ var ReactorClient = class {
|
|
|
1558
1748
|
const completedJobs = await Promise.all(Object.entries(result.jobs).map(async ([key, jobInfo]) => {
|
|
1559
1749
|
return [key, await this.waitForJob(jobInfo, signal)];
|
|
1560
1750
|
}));
|
|
1561
|
-
for (const [, completedJob] of completedJobs) if (completedJob.status ===
|
|
1751
|
+
for (const [, completedJob] of completedJobs) if (completedJob.status === "FAILED") throw new Error(completedJob.error?.message);
|
|
1562
1752
|
return { jobs: Object.fromEntries(completedJobs) };
|
|
1563
1753
|
}
|
|
1564
1754
|
/**
|
|
@@ -1567,8 +1757,8 @@ var ReactorClient = class {
|
|
|
1567
1757
|
async deleteDocument(identifier, propagate, signal) {
|
|
1568
1758
|
this.logger.verbose("deleteDocument(@identifier, @propagate)", identifier, propagate);
|
|
1569
1759
|
const jobs = [];
|
|
1570
|
-
if (propagate ===
|
|
1571
|
-
const toDelete = new Set([identifier]);
|
|
1760
|
+
if (propagate === "cascade") {
|
|
1761
|
+
const toDelete = /* @__PURE__ */ new Set([identifier]);
|
|
1572
1762
|
let changed = true;
|
|
1573
1763
|
while (changed) {
|
|
1574
1764
|
if (signal?.aborted) throw new Error("Operation aborted");
|
|
@@ -1592,7 +1782,7 @@ var ReactorClient = class {
|
|
|
1592
1782
|
const jobInfo = await this.reactor.deleteDocument(identifier, this.signer, signal);
|
|
1593
1783
|
jobs.push(jobInfo);
|
|
1594
1784
|
const completedJobs = await Promise.all(jobs.map((job) => this.waitForJob(job, signal)));
|
|
1595
|
-
for (const completedJob of completedJobs) if (completedJob.status ===
|
|
1785
|
+
for (const completedJob of completedJobs) if (completedJob.status === "FAILED") throw new Error(completedJob.error?.message);
|
|
1596
1786
|
}
|
|
1597
1787
|
/**
|
|
1598
1788
|
* Deletes documents and waits for completion
|
|
@@ -1622,7 +1812,13 @@ var ReactorClient = class {
|
|
|
1622
1812
|
*/
|
|
1623
1813
|
subscribe(search, callback, view) {
|
|
1624
1814
|
this.logger.verbose("subscribe(@search, @view)", search, view);
|
|
1625
|
-
const
|
|
1815
|
+
const served = async (type, documents, reads) => {
|
|
1816
|
+
const documentsServed = await this.gateServedAll(documents, view, void 0, reads);
|
|
1817
|
+
return documentsServed.length > 0 ? {
|
|
1818
|
+
type,
|
|
1819
|
+
documents: documentsServed
|
|
1820
|
+
} : void 0;
|
|
1821
|
+
};
|
|
1626
1822
|
let disposed = false;
|
|
1627
1823
|
let delivering = Promise.resolve();
|
|
1628
1824
|
/**
|
|
@@ -1636,49 +1832,46 @@ var ReactorClient = class {
|
|
|
1636
1832
|
* gated is withheld, because serving it unfiltered would leak the scopes
|
|
1637
1833
|
* the gate did not clear -- but it is logged rather than swallowed, since
|
|
1638
1834
|
* the gate rethrows a transient failure precisely so it is not read as a
|
|
1639
|
-
* denial.
|
|
1835
|
+
* denial. An event that resolves to undefined was withheld.
|
|
1640
1836
|
*/
|
|
1641
1837
|
const deliver = (event) => {
|
|
1642
1838
|
Promise.resolve(event).catch(() => void 0);
|
|
1643
1839
|
delivering = delivering.then(async () => {
|
|
1644
1840
|
const built = await event;
|
|
1645
|
-
if (disposed) return;
|
|
1841
|
+
if (disposed || !built) return;
|
|
1646
1842
|
callback(built);
|
|
1647
1843
|
}).catch((error) => {
|
|
1648
1844
|
this.logger.error("Subscription delivery failed for @search: @Error", { search }, error);
|
|
1649
1845
|
});
|
|
1650
1846
|
};
|
|
1651
1847
|
const unsubscribeCreated = this.subscriptionManager.onDocumentCreated((result) => {
|
|
1848
|
+
const reads = this.eventReads.forEvent();
|
|
1652
1849
|
deliver((async () => {
|
|
1653
|
-
const documents = await Promise.all(result.results.map((id) =>
|
|
1654
|
-
return
|
|
1655
|
-
type: DocumentChangeType.Created,
|
|
1656
|
-
documents: await Promise.all(documents.map(readable))
|
|
1657
|
-
};
|
|
1850
|
+
const documents = await Promise.all(result.results.map((id) => reads.get(id, withAllScopes(view))));
|
|
1851
|
+
return served("created", documents, reads);
|
|
1658
1852
|
})());
|
|
1659
1853
|
}, search);
|
|
1660
1854
|
const unsubscribeDeleted = this.subscriptionManager.onDocumentDeleted((documentIds) => {
|
|
1661
|
-
|
|
1662
|
-
|
|
1855
|
+
const reads = this.eventReads.forEvent();
|
|
1856
|
+
for (const childId of documentIds) deliver((async () => await this.servesEvery([childId], view, reads) ? {
|
|
1857
|
+
type: "deleted",
|
|
1663
1858
|
documents: [],
|
|
1664
|
-
context: { childId
|
|
1665
|
-
});
|
|
1859
|
+
context: { childId }
|
|
1860
|
+
} : void 0)());
|
|
1666
1861
|
}, search);
|
|
1667
1862
|
const unsubscribeUpdated = this.subscriptionManager.onDocumentStateUpdated((result) => {
|
|
1668
|
-
deliver((
|
|
1669
|
-
type: DocumentChangeType.Updated,
|
|
1670
|
-
documents: await Promise.all(result.results.map(readable))
|
|
1671
|
-
}))());
|
|
1863
|
+
deliver(served("updated", result.results, this.eventReads.forEvent()));
|
|
1672
1864
|
}, search, view);
|
|
1673
1865
|
const unsubscribeRelationship = this.subscriptionManager.onRelationshipChanged((parentId, childId, changeType) => {
|
|
1674
|
-
|
|
1675
|
-
|
|
1866
|
+
const reads = this.eventReads.forEvent();
|
|
1867
|
+
deliver((async () => await this.servesEvery([parentId, childId], view, reads) ? {
|
|
1868
|
+
type: changeType === "added" ? "child_added" : "child_removed",
|
|
1676
1869
|
documents: [],
|
|
1677
1870
|
context: {
|
|
1678
1871
|
parentId,
|
|
1679
1872
|
childId
|
|
1680
1873
|
}
|
|
1681
|
-
});
|
|
1874
|
+
} : void 0)());
|
|
1682
1875
|
}, search);
|
|
1683
1876
|
return () => {
|
|
1684
1877
|
disposed = true;
|
|
@@ -1688,6 +1881,225 @@ var ReactorClient = class {
|
|
|
1688
1881
|
unsubscribeRelationship();
|
|
1689
1882
|
};
|
|
1690
1883
|
}
|
|
1884
|
+
readSubject(subject) {
|
|
1885
|
+
return subject ?? authSubjectFromSigner(this.signer);
|
|
1886
|
+
}
|
|
1887
|
+
/**
|
|
1888
|
+
* Which scopes of one document the subject may read. Resolved once per
|
|
1889
|
+
* document, so the gate builds its model once however many scopes are then
|
|
1890
|
+
* tested, and the filtering itself stays synchronous.
|
|
1891
|
+
*/
|
|
1892
|
+
readableScopes(document, view, signal, reads) {
|
|
1893
|
+
const subject = this.readSubject(view?.subject);
|
|
1894
|
+
const branch = view?.branch ?? "main";
|
|
1895
|
+
return reads ? reads.scopePredicate(document, subject, branch) : this.readGate.scopePredicate(document, subject, branch, signal);
|
|
1896
|
+
}
|
|
1897
|
+
/**
|
|
1898
|
+
* One document, filtered to the scopes the subject may read. Every method
|
|
1899
|
+
* that hands a document back goes through here, including the ones that
|
|
1900
|
+
* follow a write: a document returned from a mutation is a read like any
|
|
1901
|
+
* other, and returning it whole served scopes the same subject would be
|
|
1902
|
+
* refused by `get`. Its author still sees what it wrote, because an allow on
|
|
1903
|
+
* execute confers read of that scope.
|
|
1904
|
+
*/
|
|
1905
|
+
async gateDocument(document, view, signal) {
|
|
1906
|
+
return filterReadableScopes(document, await this.readableScopes(document, view, signal));
|
|
1907
|
+
}
|
|
1908
|
+
/**
|
|
1909
|
+
* One document as a listing or a feed serves it, or undefined when withheld.
|
|
1910
|
+
* The document must hold every scope, or withholding is decided on a subset;
|
|
1911
|
+
* a narrowing view is applied here instead.
|
|
1912
|
+
*/
|
|
1913
|
+
async gateServed(document, view, signal, reads) {
|
|
1914
|
+
const readable = await this.readableScopes(document, view, signal, reads);
|
|
1915
|
+
if (refusesEveryDomainScope(document, readable)) return;
|
|
1916
|
+
const narrowed = narrowedScopes(view);
|
|
1917
|
+
return filterReadableScopes(document, narrowed ? (scope) => readable(scope) && narrowed.has(scope) : readable);
|
|
1918
|
+
}
|
|
1919
|
+
async gateServedAll(documents, view, signal, reads) {
|
|
1920
|
+
return (await Promise.all(documents.map((document) => this.gateServed(document, view, signal, reads)))).filter((document) => document !== void 0);
|
|
1921
|
+
}
|
|
1922
|
+
/**
|
|
1923
|
+
* Gates a page and every page after it. `nextCursor` is left as the stream
|
|
1924
|
+
* reported it.
|
|
1925
|
+
*/
|
|
1926
|
+
async gateListing(page, view, signal) {
|
|
1927
|
+
const results = (await Promise.all(page.results.map((document) => this.gateListed(document, view, signal)))).filter((document) => document !== void 0);
|
|
1928
|
+
const next = page.next;
|
|
1929
|
+
return {
|
|
1930
|
+
...page,
|
|
1931
|
+
results,
|
|
1932
|
+
next: next ? async () => this.gateListing(await next(), view, signal) : void 0
|
|
1933
|
+
};
|
|
1934
|
+
}
|
|
1935
|
+
/**
|
|
1936
|
+
* One document as a listing serves it, or undefined when withheld. It holds
|
|
1937
|
+
* only the view's scopes; a domain scope it lacks is decided on its name, or
|
|
1938
|
+
* on its state read now when a condition needs that.
|
|
1939
|
+
*/
|
|
1940
|
+
async gateListed(document, view, signal) {
|
|
1941
|
+
const readable = await this.readableScopes(document, view, signal);
|
|
1942
|
+
const unheld = unheldScopesReadOnState(document);
|
|
1943
|
+
if (refusesEveryDomainScope(document, unheld.length === 0 ? readable : (scope) => !unheld.includes(scope) && readable(scope)) && !await this.servesUnheld(document, unheld, view, signal)) return;
|
|
1944
|
+
return filterReadableScopes(document, readable);
|
|
1945
|
+
}
|
|
1946
|
+
async servesUnheld(document, scopes, view, signal) {
|
|
1947
|
+
if (scopes.length === 0) return false;
|
|
1948
|
+
const id = document.header.id;
|
|
1949
|
+
let held;
|
|
1950
|
+
try {
|
|
1951
|
+
held = await this.reactor.get(id, withAuthScope({
|
|
1952
|
+
...view,
|
|
1953
|
+
scopes
|
|
1954
|
+
}), void 0, signal);
|
|
1955
|
+
} catch (error) {
|
|
1956
|
+
await assertAbsent(this.documentView, id, error, signal);
|
|
1957
|
+
return false;
|
|
1958
|
+
}
|
|
1959
|
+
const readable = await this.readableScopes(held, view, signal);
|
|
1960
|
+
return scopes.some(readable);
|
|
1961
|
+
}
|
|
1962
|
+
/**
|
|
1963
|
+
* Whether an event naming only these ids may reach the subject. An id with no
|
|
1964
|
+
* live document behind it has no content left to protect, so it does not
|
|
1965
|
+
* withhold; a deleted document is read at its deletion boundary when
|
|
1966
|
+
* deletion is positional, and gated like any other.
|
|
1967
|
+
*/
|
|
1968
|
+
async servesEvery(ids, view, reads) {
|
|
1969
|
+
return (await Promise.all(ids.map((id) => this.servesId(id, view, reads)))).every(Boolean);
|
|
1970
|
+
}
|
|
1971
|
+
async servesId(id, view, reads) {
|
|
1972
|
+
let document;
|
|
1973
|
+
try {
|
|
1974
|
+
document = await reads.get(id, withAllScopes(view));
|
|
1975
|
+
} catch (error) {
|
|
1976
|
+
await assertAbsent(reads, id, error);
|
|
1977
|
+
return true;
|
|
1978
|
+
}
|
|
1979
|
+
return !refusesEveryDomainScope(document, await this.readableScopes(document, view, void 0, reads));
|
|
1980
|
+
}
|
|
1981
|
+
async getOperationsWithCompositeCursor(documentId, view, filter, paging, signal, canRead) {
|
|
1982
|
+
const scopeCursors = decodeCompositeCursor(paging.cursor);
|
|
1983
|
+
const allOperations = [];
|
|
1984
|
+
const activeCursors = {};
|
|
1985
|
+
for (const [scopeName, cursor] of Object.entries(scopeCursors)) {
|
|
1986
|
+
if (!canRead(scopeName)) continue;
|
|
1987
|
+
const scopeView = {
|
|
1988
|
+
...view,
|
|
1989
|
+
scopes: [scopeName]
|
|
1990
|
+
};
|
|
1991
|
+
const scopePaging = {
|
|
1992
|
+
cursor,
|
|
1993
|
+
limit: paging.limit
|
|
1994
|
+
};
|
|
1995
|
+
const scopeResult = (await this.reactor.getOperations(documentId, scopeView, filter, scopePaging, void 0, signal))[scopeName];
|
|
1996
|
+
allOperations.push(...scopeResult.results);
|
|
1997
|
+
if (scopeResult.nextCursor) activeCursors[scopeName] = scopeResult.nextCursor;
|
|
1998
|
+
}
|
|
1999
|
+
allOperations.sort((a, b) => a.index - b.index);
|
|
2000
|
+
return {
|
|
2001
|
+
results: allOperations,
|
|
2002
|
+
options: paging,
|
|
2003
|
+
nextCursor: Object.keys(activeCursors).length > 0 ? encodeCompositeCursor(activeCursors) : void 0
|
|
2004
|
+
};
|
|
2005
|
+
}
|
|
2006
|
+
/**
|
|
2007
|
+
* Drops the edges whose far-end document the subject may read no domain scope
|
|
2008
|
+
* of. An edge is withheld whole rather than stripped of its metadata: the
|
|
2009
|
+
* document-shaped relationship reads already answer with the far end stripped
|
|
2010
|
+
* to the scopes the gate allows, so the far end's existence is disclosed
|
|
2011
|
+
* either way, but an edge's metadata is content about the pair that the far
|
|
2012
|
+
* end's own reads would refuse. An edge to a far end stripped to nothing
|
|
2013
|
+
* therefore carries content past a refusal, and there is no useful shell to
|
|
2014
|
+
* hand back in its place.
|
|
2015
|
+
*
|
|
2016
|
+
* `nextCursor` and `options` are left as the underlying stream reported them,
|
|
2017
|
+
* because a caller must feed them back to resume from the right position. A
|
|
2018
|
+
* gated page can therefore be shorter than the limit it asked for.
|
|
2019
|
+
*/
|
|
2020
|
+
async gateEdges(page, farEnd, view, signal) {
|
|
2021
|
+
const ids = [...new Set(page.results.map((edge) => edge[farEnd]))];
|
|
2022
|
+
if (ids.length === 0) return page;
|
|
2023
|
+
const farEndView = {
|
|
2024
|
+
subject: view?.subject,
|
|
2025
|
+
branch: view?.branch
|
|
2026
|
+
};
|
|
2027
|
+
const documents = await this.reactor.find({ ids }, farEndView, {
|
|
2028
|
+
cursor: "0",
|
|
2029
|
+
limit: ids.length
|
|
2030
|
+
}, void 0, signal);
|
|
2031
|
+
const readable = new Map(await Promise.all(documents.results.map(async (doc) => {
|
|
2032
|
+
const allows = await this.readableScopes(doc, view, signal);
|
|
2033
|
+
return [doc.header.id, !refusesEveryDomainScope(doc, allows)];
|
|
2034
|
+
})));
|
|
2035
|
+
const results = page.results.filter((edge) => readable.get(edge[farEnd]) !== false);
|
|
2036
|
+
if (results.length === page.results.length) return page;
|
|
2037
|
+
const nextPage = page.next;
|
|
2038
|
+
return {
|
|
2039
|
+
...page,
|
|
2040
|
+
results,
|
|
2041
|
+
next: nextPage ? async () => this.gateEdges(await nextPage(), farEnd, view, signal) : void 0
|
|
2042
|
+
};
|
|
2043
|
+
}
|
|
2044
|
+
/**
|
|
2045
|
+
* One relationship edge, or undefined when it does not exist. A point lookup:
|
|
2046
|
+
* the pair is filtered in SQL rather than scanned out of the source's edge
|
|
2047
|
+
* list, which on a drive with thousands of children is the difference between
|
|
2048
|
+
* one query and dozens.
|
|
2049
|
+
*/
|
|
2050
|
+
async readRelationshipEdge(sourceIdentifier, targetIdentifier, relationshipType, view, signal) {
|
|
2051
|
+
const sourceId = await this.documentView.resolveIdOrSlug(sourceIdentifier, view, void 0, signal);
|
|
2052
|
+
const targetId = await this.documentView.resolveIdOrSlug(targetIdentifier, view, void 0, signal);
|
|
2053
|
+
return (await this.documentIndexer.getDirectedRelationships(sourceId, targetId, [relationshipType], {
|
|
2054
|
+
cursor: "0",
|
|
2055
|
+
limit: 1
|
|
2056
|
+
}, void 0, signal)).results[0];
|
|
2057
|
+
}
|
|
2058
|
+
/**
|
|
2059
|
+
* The id a write on `identifier` is stored under, which its signatures bind.
|
|
2060
|
+
* A create names its own id, and an id no slug maps to is taken as given, so
|
|
2061
|
+
* a document still in flight resolves to itself.
|
|
2062
|
+
*/
|
|
2063
|
+
async resolveWriteTarget(identifier, branch, actions, signal) {
|
|
2064
|
+
if (actions.some((action) => action.type === "CREATE_DOCUMENT")) return identifier;
|
|
2065
|
+
const view = { branch };
|
|
2066
|
+
const bySlug = await this.documentView.resolveSlug(identifier, view, void 0, signal);
|
|
2067
|
+
if (bySlug === void 0 || bySlug === identifier) return identifier;
|
|
2068
|
+
return this.documentView.resolveIdOrSlug(identifier, view, void 0, signal);
|
|
2069
|
+
}
|
|
2070
|
+
/**
|
|
2071
|
+
* The decision model for one target document, built at its stream heads.
|
|
2072
|
+
*
|
|
2073
|
+
* The document is fetched unfiltered, because the policy is what decides:
|
|
2074
|
+
* reading it through the read gate would withhold the very scopes the
|
|
2075
|
+
* decision is about. A deleted document is served at its deletion boundary,
|
|
2076
|
+
* which is what lets the model refuse an execute against it -- authEnforcement
|
|
2077
|
+
* requires documentDecisions, so that read is available whenever this runs.
|
|
2078
|
+
*
|
|
2079
|
+
* Reading past the gate discloses nothing a submit does not. The `auth` and
|
|
2080
|
+
* `document` scopes are readable by every holder, so a verdict resting on the
|
|
2081
|
+
* policy alone is one the caller could compute unaided; and a verdict resting
|
|
2082
|
+
* on a conditional grant reads the executing scope's state exactly as
|
|
2083
|
+
* admission reads it, so the answer here is what submitting and being refused
|
|
2084
|
+
* would have revealed anyway.
|
|
2085
|
+
*
|
|
2086
|
+
* The append condition the build records is dropped. It guards a write, and
|
|
2087
|
+
* this makes none; reproducing it is also what the preflight cannot do, which
|
|
2088
|
+
* is why the answer is a prediction.
|
|
2089
|
+
*/
|
|
2090
|
+
async buildEvaluationTarget(config, documentId, branch, signal) {
|
|
2091
|
+
const document = await this.documentView.get(documentId, { branch }, void 0, signal);
|
|
2092
|
+
const target = {
|
|
2093
|
+
documentId,
|
|
2094
|
+
branch
|
|
2095
|
+
};
|
|
2096
|
+
const built = await buildDecisionModel(new SeededStateReader(this.documentView, document, branch), config.model, target, signal);
|
|
2097
|
+
return {
|
|
2098
|
+
definition: config.model(target),
|
|
2099
|
+
model: built.model,
|
|
2100
|
+
scopeStates: document.state ?? {}
|
|
2101
|
+
};
|
|
2102
|
+
}
|
|
1691
2103
|
async removeAllIncomingRelationships(documentId, signal) {
|
|
1692
2104
|
const incoming = await this.documentIndexer.getIncoming(documentId, void 0, void 0, void 0, signal);
|
|
1693
2105
|
const jobs = [];
|
|
@@ -1701,18 +2113,6 @@ var ReactorClient = class {
|
|
|
1701
2113
|
//#endregion
|
|
1702
2114
|
//#region src/queue/types.ts
|
|
1703
2115
|
/**
|
|
1704
|
-
* State of a job in the queue
|
|
1705
|
-
*/
|
|
1706
|
-
let JobQueueState = /* @__PURE__ */ function(JobQueueState) {
|
|
1707
|
-
JobQueueState[JobQueueState["UNKNOWN"] = -1] = "UNKNOWN";
|
|
1708
|
-
JobQueueState[JobQueueState["PREPROCESSING"] = 0] = "PREPROCESSING";
|
|
1709
|
-
JobQueueState[JobQueueState["PENDING"] = 1] = "PENDING";
|
|
1710
|
-
JobQueueState[JobQueueState["READY"] = 2] = "READY";
|
|
1711
|
-
JobQueueState[JobQueueState["RUNNING"] = 3] = "RUNNING";
|
|
1712
|
-
JobQueueState[JobQueueState["RESOLVED"] = 4] = "RESOLVED";
|
|
1713
|
-
return JobQueueState;
|
|
1714
|
-
}({});
|
|
1715
|
-
/**
|
|
1716
2116
|
* How a retry is accounted against the job's retry limit.
|
|
1717
2117
|
* - `CountAgainstLimit` (default): a fault; the job eventually exhausts its
|
|
1718
2118
|
* retries and fails terminally.
|
|
@@ -1745,6 +2145,11 @@ function toErrorInfo(error) {
|
|
|
1745
2145
|
};
|
|
1746
2146
|
}
|
|
1747
2147
|
var JobResultHandler = class {
|
|
2148
|
+
queue;
|
|
2149
|
+
jobTracker;
|
|
2150
|
+
eventBus;
|
|
2151
|
+
resolver;
|
|
2152
|
+
logger;
|
|
1748
2153
|
constructor(queue, jobTracker, eventBus, resolver, logger) {
|
|
1749
2154
|
this.queue = queue;
|
|
1750
2155
|
this.jobTracker = jobTracker;
|
|
@@ -1775,7 +2180,7 @@ var JobResultHandler = class {
|
|
|
1775
2180
|
if (result.error && AppendConditionFailedError.isError(result.error) && this.countConflicts(handle.job) < MAX_EXEMPT_CONFLICT_RETRIES) {
|
|
1776
2181
|
const errorInfo = toErrorInfo(result.error);
|
|
1777
2182
|
try {
|
|
1778
|
-
await this.queue.retryJob(handle.job.id, errorInfo,
|
|
2183
|
+
await this.queue.retryJob(handle.job.id, errorInfo, "exempt-from-limit");
|
|
1779
2184
|
return;
|
|
1780
2185
|
} catch {}
|
|
1781
2186
|
}
|
|
@@ -1784,7 +2189,7 @@ var JobResultHandler = class {
|
|
|
1784
2189
|
callbacks.deferJob(result.error.documentId, handle.job);
|
|
1785
2190
|
return;
|
|
1786
2191
|
}
|
|
1787
|
-
if (result.error && (DocumentDeletedError.isError(result.error) || DocumentAlreadyExistsError.isError(result.error) || DocumentNotFoundError.isError(result.error) || AuthorizationDeniedError.isError(result.error) || AuthTimestampNotMonotonicError.isError(result.error) || InvalidOperationTimestampError.isError(result.error) || ExcessiveReshuffleError.isError(result.error) || UpgradePreconditionFailedError.isError(result.error))) {
|
|
2192
|
+
if (result.error && (DocumentDeletedError.isError(result.error) || DocumentAlreadyExistsError.isError(result.error) || DocumentNotFoundError.isError(result.error) || AuthorizationDeniedError.isError(result.error) || AuthTimestampNotMonotonicError.isError(result.error) || InvalidOperationTimestampError.isError(result.error) || ExcessiveReshuffleError.isError(result.error) || InvalidSignatureError.isError(result.error) || UpgradePreconditionFailedError.isError(result.error))) {
|
|
1788
2193
|
const errorInfo = toErrorInfo(result.error);
|
|
1789
2194
|
this.jobTracker.markFailed(handle.job.id, errorInfo, handle.job);
|
|
1790
2195
|
this.eventBus.emit(ReactorEventTypes.JOB_FAILED, {
|
|
@@ -1873,6 +2278,12 @@ var JobResultHandler = class {
|
|
|
1873
2278
|
* is stranded at PENDING for as long as the deferral lasts.
|
|
1874
2279
|
*/
|
|
1875
2280
|
var DeferredJobs = class {
|
|
2281
|
+
queue;
|
|
2282
|
+
jobTracker;
|
|
2283
|
+
eventBus;
|
|
2284
|
+
logger;
|
|
2285
|
+
ttlMs;
|
|
2286
|
+
onReleased;
|
|
1876
2287
|
#byDocumentId = /* @__PURE__ */ new Map();
|
|
1877
2288
|
#timers = /* @__PURE__ */ new Map();
|
|
1878
2289
|
constructor(queue, jobTracker, eventBus, logger, ttlMs = DEFAULT_DEFERRED_JOB_TTL_MS, onReleased = () => Promise.resolve()) {
|
|
@@ -2034,6 +2445,13 @@ function bucketFor(documentId, numWorkers) {
|
|
|
2034
2445
|
* (Powerhouse board wiki id: d400d711-f07e-4389-a226-4e9fdd4fa8ba)
|
|
2035
2446
|
*/
|
|
2036
2447
|
var WorkerPoolJobExecutorManager = class {
|
|
2448
|
+
workerFactory;
|
|
2449
|
+
eventBus;
|
|
2450
|
+
queue;
|
|
2451
|
+
jobTracker;
|
|
2452
|
+
logger;
|
|
2453
|
+
resolver;
|
|
2454
|
+
operationIndex;
|
|
2037
2455
|
workers = [];
|
|
2038
2456
|
isRunning = false;
|
|
2039
2457
|
activeJobs = 0;
|
|
@@ -2172,6 +2590,7 @@ var WorkerPoolJobExecutorManager = class {
|
|
|
2172
2590
|
await this.tryDispatchFor(worker);
|
|
2173
2591
|
return;
|
|
2174
2592
|
}
|
|
2593
|
+
for (const refusal of outcome.signatureRefusals ?? []) this.eventBus.emit(ReactorEventTypes.SIGNATURE_REFUSED, refusal).catch(() => {});
|
|
2175
2594
|
if (outcome.result.success) {
|
|
2176
2595
|
this.totalJobsProcessed++;
|
|
2177
2596
|
const completedEvent = {
|
|
@@ -2212,6 +2631,7 @@ var WorkerPoolJobExecutorManager = class {
|
|
|
2212
2631
|
jobId: job.id,
|
|
2213
2632
|
operations: payload.operations,
|
|
2214
2633
|
jobMeta: payload.jobMeta,
|
|
2634
|
+
submittedActionIds: submittedActionIds(job),
|
|
2215
2635
|
collectionMemberships
|
|
2216
2636
|
};
|
|
2217
2637
|
try {
|
|
@@ -2296,6 +2716,12 @@ function fillMissingMemberships(documentIds, found) {
|
|
|
2296
2716
|
* Listens for job available events and dispatches jobs to executors.
|
|
2297
2717
|
*/
|
|
2298
2718
|
var SimpleJobExecutorManager = class {
|
|
2719
|
+
executorFactory;
|
|
2720
|
+
eventBus;
|
|
2721
|
+
queue;
|
|
2722
|
+
jobTracker;
|
|
2723
|
+
logger;
|
|
2724
|
+
resolver;
|
|
2299
2725
|
executors = [];
|
|
2300
2726
|
isRunning = false;
|
|
2301
2727
|
activeJobs = 0;
|
|
@@ -2470,6 +2896,7 @@ var SimpleJobExecutorManager = class {
|
|
|
2470
2896
|
* Subscribes to operation events to update job states.
|
|
2471
2897
|
*/
|
|
2472
2898
|
var InMemoryJobTracker = class {
|
|
2899
|
+
eventBus;
|
|
2473
2900
|
jobs = /* @__PURE__ */ new Map();
|
|
2474
2901
|
unsubscribers = [];
|
|
2475
2902
|
constructor(eventBus) {
|
|
@@ -2490,21 +2917,22 @@ var InMemoryJobTracker = class {
|
|
|
2490
2917
|
handleWriteReady(event) {
|
|
2491
2918
|
const jobId = event.jobId;
|
|
2492
2919
|
const job = this.jobs.get(jobId);
|
|
2493
|
-
if (job && job.status ===
|
|
2920
|
+
if (job && job.status === "RUNNING") {
|
|
2494
2921
|
const consistencyToken = createConsistencyToken(event.operations);
|
|
2495
2922
|
this.jobs.set(jobId, {
|
|
2496
2923
|
...job,
|
|
2497
|
-
status:
|
|
2498
|
-
consistencyToken
|
|
2924
|
+
status: "WRITE_READY",
|
|
2925
|
+
consistencyToken,
|
|
2926
|
+
result: summarizeSubmittedActions(event.operations, event.submittedActionIds)
|
|
2499
2927
|
});
|
|
2500
2928
|
}
|
|
2501
2929
|
}
|
|
2502
2930
|
handleReadReady(event) {
|
|
2503
2931
|
const jobId = event.jobId;
|
|
2504
2932
|
const job = this.jobs.get(jobId);
|
|
2505
|
-
if (job && job.status ===
|
|
2933
|
+
if (job && job.status === "WRITE_READY") this.jobs.set(jobId, {
|
|
2506
2934
|
...job,
|
|
2507
|
-
status:
|
|
2935
|
+
status: "READ_READY"
|
|
2508
2936
|
});
|
|
2509
2937
|
}
|
|
2510
2938
|
handleJobFailed(event) {
|
|
@@ -2527,7 +2955,7 @@ var InMemoryJobTracker = class {
|
|
|
2527
2955
|
this.jobs.set(jobId, {
|
|
2528
2956
|
id: jobId,
|
|
2529
2957
|
documentId: "",
|
|
2530
|
-
status:
|
|
2958
|
+
status: "RUNNING",
|
|
2531
2959
|
createdAtUtcIso: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2532
2960
|
consistencyToken: createEmptyConsistencyToken(),
|
|
2533
2961
|
meta: {
|
|
@@ -2539,7 +2967,7 @@ var InMemoryJobTracker = class {
|
|
|
2539
2967
|
}
|
|
2540
2968
|
this.jobs.set(jobId, {
|
|
2541
2969
|
...job,
|
|
2542
|
-
status:
|
|
2970
|
+
status: "RUNNING"
|
|
2543
2971
|
});
|
|
2544
2972
|
}
|
|
2545
2973
|
markFailed(jobId, error, job) {
|
|
@@ -2548,7 +2976,7 @@ var InMemoryJobTracker = class {
|
|
|
2548
2976
|
this.jobs.set(jobId, {
|
|
2549
2977
|
id: jobId,
|
|
2550
2978
|
documentId: job?.documentId ?? "",
|
|
2551
|
-
status:
|
|
2979
|
+
status: "FAILED",
|
|
2552
2980
|
createdAtUtcIso: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2553
2981
|
completedAtUtcIso: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2554
2982
|
error,
|
|
@@ -2563,7 +2991,7 @@ var InMemoryJobTracker = class {
|
|
|
2563
2991
|
}
|
|
2564
2992
|
this.jobs.set(jobId, {
|
|
2565
2993
|
...existing,
|
|
2566
|
-
status:
|
|
2994
|
+
status: "FAILED",
|
|
2567
2995
|
completedAtUtcIso: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2568
2996
|
error,
|
|
2569
2997
|
job,
|
|
@@ -2610,99 +3038,336 @@ function matchesFilter(op, filter) {
|
|
|
2610
3038
|
if (filter.scope && filter.scope.length > 0) {
|
|
2611
3039
|
if (!filter.scope.includes(op.context.scope)) return false;
|
|
2612
3040
|
}
|
|
2613
|
-
if (filter.branch && filter.branch.length > 0) {
|
|
2614
|
-
if (!filter.branch.includes(op.context.branch)) return false;
|
|
3041
|
+
if (filter.branch && filter.branch.length > 0) {
|
|
3042
|
+
if (!filter.branch.includes(op.context.branch)) return false;
|
|
3043
|
+
}
|
|
3044
|
+
if (filter.documentId && filter.documentId.length > 0) {
|
|
3045
|
+
if (!filter.documentId.includes("*") && !filter.documentId.includes(op.context.documentId)) return false;
|
|
3046
|
+
}
|
|
3047
|
+
return true;
|
|
3048
|
+
}
|
|
3049
|
+
function nonEmpty(value) {
|
|
3050
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
3051
|
+
}
|
|
3052
|
+
function namespaceOf(processor) {
|
|
3053
|
+
return nonEmpty(processor.namespace);
|
|
3054
|
+
}
|
|
3055
|
+
function classNameOf(processor) {
|
|
3056
|
+
const name = processor.constructor.name;
|
|
3057
|
+
return name && name !== "Object" ? name : void 0;
|
|
3058
|
+
}
|
|
3059
|
+
function resolveProcessorSlots(records, legacy) {
|
|
3060
|
+
if (legacy) return records.map((_, i) => String(i));
|
|
3061
|
+
const seen = /* @__PURE__ */ new Map();
|
|
3062
|
+
return records.map((record, i) => {
|
|
3063
|
+
const base = nonEmpty(record.id) ?? namespaceOf(record.processor) ?? classNameOf(record.processor) ?? String(i);
|
|
3064
|
+
const count = seen.get(base) ?? 0;
|
|
3065
|
+
seen.set(base, count + 1);
|
|
3066
|
+
return count === 0 ? base : `${base}#${count}`;
|
|
3067
|
+
});
|
|
3068
|
+
}
|
|
3069
|
+
//#endregion
|
|
3070
|
+
//#region src/processors/processor-queue.ts
|
|
3071
|
+
function isDelivery(task) {
|
|
3072
|
+
return task?.kind === "live" || task?.kind === "advance";
|
|
3073
|
+
}
|
|
3074
|
+
function errorMessage(error) {
|
|
3075
|
+
return error instanceof Error ? error.message : String(error);
|
|
3076
|
+
}
|
|
3077
|
+
function lowestOf(ops) {
|
|
3078
|
+
let lowest = ops[0].context.ordinal;
|
|
3079
|
+
for (const op of ops) lowest = Math.min(lowest, op.context.ordinal);
|
|
3080
|
+
return lowest;
|
|
3081
|
+
}
|
|
3082
|
+
function highestOf(ops) {
|
|
3083
|
+
let highest = 0;
|
|
3084
|
+
for (const op of ops) highest = Math.max(highest, op.context.ordinal);
|
|
3085
|
+
return highest;
|
|
3086
|
+
}
|
|
3087
|
+
var ProcessorQueue = class {
|
|
3088
|
+
options;
|
|
3089
|
+
tasks = [];
|
|
3090
|
+
running = false;
|
|
3091
|
+
closed = false;
|
|
3092
|
+
replaysAhead = 0;
|
|
3093
|
+
unrouted = /* @__PURE__ */ new Set();
|
|
3094
|
+
constructor(options) {
|
|
3095
|
+
this.options = options;
|
|
3096
|
+
}
|
|
3097
|
+
get isClosed() {
|
|
3098
|
+
return this.closed;
|
|
3099
|
+
}
|
|
3100
|
+
/** Resolves at once behind a replay, so a pass never waits out a backfill. */
|
|
3101
|
+
live(ops) {
|
|
3102
|
+
const delivered = this.push((done) => ({
|
|
3103
|
+
kind: "live",
|
|
3104
|
+
ops,
|
|
3105
|
+
done
|
|
3106
|
+
}));
|
|
3107
|
+
return this.replaysAhead > 0 ? Promise.resolve() : delivered;
|
|
3108
|
+
}
|
|
3109
|
+
/** Raises the cursor past a batch with nothing for this processor. */
|
|
3110
|
+
advance(through) {
|
|
3111
|
+
const last = this.tasks.at(-1);
|
|
3112
|
+
if (last?.kind === "advance") {
|
|
3113
|
+
last.through = Math.max(last.through, through);
|
|
3114
|
+
return Promise.resolve();
|
|
3115
|
+
}
|
|
3116
|
+
return this.push((done) => ({
|
|
3117
|
+
kind: "advance",
|
|
3118
|
+
through,
|
|
3119
|
+
done
|
|
3120
|
+
}));
|
|
3121
|
+
}
|
|
3122
|
+
/** Replays from the cursor as it stands when the task runs. */
|
|
3123
|
+
backfill() {
|
|
3124
|
+
return this.replay("backfill", () => this.runBackfill());
|
|
3125
|
+
}
|
|
3126
|
+
/** Clears an error and replays from the cursor. No-op when active. */
|
|
3127
|
+
retry() {
|
|
3128
|
+
return this.replay("retry", async () => {
|
|
3129
|
+
const { cursor } = this.options;
|
|
3130
|
+
if (cursor.status !== "errored") return;
|
|
3131
|
+
cursor.status = "active";
|
|
3132
|
+
cursor.lastError = void 0;
|
|
3133
|
+
cursor.lastErrorTimestamp = void 0;
|
|
3134
|
+
await this.persist();
|
|
3135
|
+
await this.runBackfill();
|
|
3136
|
+
});
|
|
3137
|
+
}
|
|
3138
|
+
/** Stops cursor writes now; queued tasks still run before the disconnect. */
|
|
3139
|
+
close() {
|
|
3140
|
+
if (this.closed) return Promise.resolve();
|
|
3141
|
+
const disconnect = this.enqueue("disconnect", async () => {
|
|
3142
|
+
try {
|
|
3143
|
+
await this.options.processor.onDisconnect();
|
|
3144
|
+
} catch (error) {
|
|
3145
|
+
this.options.logger.error("Error disconnecting processor '@ProcessorId': @Error", this.options.processorId, error);
|
|
3146
|
+
}
|
|
3147
|
+
});
|
|
3148
|
+
this.closed = true;
|
|
3149
|
+
return disconnect;
|
|
3150
|
+
}
|
|
3151
|
+
replay(kind, run) {
|
|
3152
|
+
if (this.closed) return Promise.resolve();
|
|
3153
|
+
this.replaysAhead++;
|
|
3154
|
+
return this.enqueue(kind, async () => {
|
|
3155
|
+
try {
|
|
3156
|
+
await run();
|
|
3157
|
+
} finally {
|
|
3158
|
+
this.replaysAhead--;
|
|
3159
|
+
}
|
|
3160
|
+
});
|
|
3161
|
+
}
|
|
3162
|
+
enqueue(kind, run) {
|
|
3163
|
+
return this.push((done) => ({
|
|
3164
|
+
kind,
|
|
3165
|
+
run,
|
|
3166
|
+
done
|
|
3167
|
+
}));
|
|
3168
|
+
}
|
|
3169
|
+
push(task) {
|
|
3170
|
+
if (this.closed) return Promise.resolve();
|
|
3171
|
+
return new Promise((resolve) => {
|
|
3172
|
+
this.tasks.push(task(resolve));
|
|
3173
|
+
if (this.running) return;
|
|
3174
|
+
this.running = true;
|
|
3175
|
+
queueMicrotask(() => void this.drain());
|
|
3176
|
+
});
|
|
3177
|
+
}
|
|
3178
|
+
async drain() {
|
|
3179
|
+
while (this.tasks.length > 0) {
|
|
3180
|
+
const task = this.tasks.shift();
|
|
3181
|
+
const batch = [];
|
|
3182
|
+
if (isDelivery(task)) {
|
|
3183
|
+
batch.push(task);
|
|
3184
|
+
let size = task.kind === "live" ? task.ops.length : 0;
|
|
3185
|
+
let next = this.tasks[0];
|
|
3186
|
+
while (isDelivery(next) && size < 500) {
|
|
3187
|
+
if (next.kind === "live") size += next.ops.length;
|
|
3188
|
+
batch.push(next);
|
|
3189
|
+
this.tasks.shift();
|
|
3190
|
+
next = this.tasks[0];
|
|
3191
|
+
}
|
|
3192
|
+
}
|
|
3193
|
+
try {
|
|
3194
|
+
await (isDelivery(task) ? this.deliverLive(batch) : task.run());
|
|
3195
|
+
} catch (error) {
|
|
3196
|
+
this.options.logger.error("Processor '@ProcessorId' task '@Kind' failed: @Error", this.options.processorId, task.kind, error);
|
|
3197
|
+
} finally {
|
|
3198
|
+
task.done();
|
|
3199
|
+
for (const merged of batch.slice(1)) merged.done();
|
|
3200
|
+
}
|
|
3201
|
+
}
|
|
3202
|
+
this.running = false;
|
|
3203
|
+
}
|
|
3204
|
+
async deliverLive(batch) {
|
|
3205
|
+
const { cursor, floor } = this.options;
|
|
3206
|
+
const ops = [];
|
|
3207
|
+
let through = 0;
|
|
3208
|
+
for (const task of batch) if (task.kind === "live") ops.push(...task.ops);
|
|
3209
|
+
else through = Math.max(through, task.through);
|
|
3210
|
+
const fresh = ops.filter((op) => {
|
|
3211
|
+
const ordinal = op.context.ordinal;
|
|
3212
|
+
if (ordinal <= floor) return false;
|
|
3213
|
+
return !this.unrouted.delete(ordinal);
|
|
3214
|
+
});
|
|
3215
|
+
if (fresh.length === 0) {
|
|
3216
|
+
await this.raiseCursor(through);
|
|
3217
|
+
return;
|
|
3218
|
+
}
|
|
3219
|
+
if (cursor.status !== "active") {
|
|
3220
|
+
await this.parkBelow(fresh);
|
|
3221
|
+
return;
|
|
3222
|
+
}
|
|
3223
|
+
if (!await this.deliver(fresh)) {
|
|
3224
|
+
await this.parkBelow(fresh);
|
|
3225
|
+
return;
|
|
3226
|
+
}
|
|
3227
|
+
await this.raiseCursor(Math.max(highestOf(fresh), through));
|
|
3228
|
+
}
|
|
3229
|
+
async runBackfill() {
|
|
3230
|
+
const { cursor, filter, floor } = this.options;
|
|
3231
|
+
if (cursor.status !== "active") return;
|
|
3232
|
+
let page;
|
|
3233
|
+
try {
|
|
3234
|
+
page = await this.options.readSince(cursor.lastOrdinal);
|
|
3235
|
+
} catch (error) {
|
|
3236
|
+
await this.fail(error, "reading backfill");
|
|
3237
|
+
return;
|
|
3238
|
+
}
|
|
3239
|
+
while (page.results.length > 0) {
|
|
3240
|
+
if (this.closed) return;
|
|
3241
|
+
const matching = page.results.filter((op) => op.context.ordinal > floor && matchesFilter(op, filter));
|
|
3242
|
+
if (matching.length > 0) {
|
|
3243
|
+
if (!await this.deliver(matching)) {
|
|
3244
|
+
await this.persist();
|
|
3245
|
+
return;
|
|
3246
|
+
}
|
|
3247
|
+
this.dedupeQueued(matching);
|
|
3248
|
+
}
|
|
3249
|
+
await this.raiseCursor(highestOf(page.results));
|
|
3250
|
+
if (!page.next) break;
|
|
3251
|
+
try {
|
|
3252
|
+
page = await page.next();
|
|
3253
|
+
} catch (error) {
|
|
3254
|
+
await this.fail(error, "reading backfill");
|
|
3255
|
+
return;
|
|
3256
|
+
}
|
|
3257
|
+
}
|
|
3258
|
+
}
|
|
3259
|
+
dedupeQueued(delivered) {
|
|
3260
|
+
const routed = this.options.routedThrough();
|
|
3261
|
+
const queued = /* @__PURE__ */ new Set();
|
|
3262
|
+
for (const op of delivered) {
|
|
3263
|
+
const ordinal = op.context.ordinal;
|
|
3264
|
+
if (ordinal > routed) this.unrouted.add(ordinal);
|
|
3265
|
+
else queued.add(ordinal);
|
|
3266
|
+
}
|
|
3267
|
+
if (queued.size === 0) return;
|
|
3268
|
+
for (const task of this.tasks) {
|
|
3269
|
+
if (task.kind !== "live") continue;
|
|
3270
|
+
task.ops = task.ops.filter((op) => !queued.has(op.context.ordinal));
|
|
3271
|
+
}
|
|
3272
|
+
}
|
|
3273
|
+
async deliver(ops) {
|
|
3274
|
+
try {
|
|
3275
|
+
await this.options.processor.onOperations(ops);
|
|
3276
|
+
return true;
|
|
3277
|
+
} catch (error) {
|
|
3278
|
+
this.markErrored(error);
|
|
3279
|
+
this.options.logger.error("Processor '@ProcessorId' failed at ordinal @Ordinal: @Error", this.options.processorId, lowestOf(ops), error);
|
|
3280
|
+
return false;
|
|
3281
|
+
}
|
|
2615
3282
|
}
|
|
2616
|
-
|
|
2617
|
-
|
|
3283
|
+
async fail(error, during) {
|
|
3284
|
+
this.markErrored(error);
|
|
3285
|
+
await this.persist();
|
|
3286
|
+
this.options.logger.error("Processor '@ProcessorId' failed @During: @Error", this.options.processorId, during, error);
|
|
3287
|
+
}
|
|
3288
|
+
markErrored(error) {
|
|
3289
|
+
const { cursor } = this.options;
|
|
3290
|
+
cursor.status = "errored";
|
|
3291
|
+
cursor.lastError = errorMessage(error);
|
|
3292
|
+
cursor.lastErrorTimestamp = /* @__PURE__ */ new Date();
|
|
3293
|
+
}
|
|
3294
|
+
async raiseCursor(through) {
|
|
3295
|
+
const { cursor } = this.options;
|
|
3296
|
+
if (cursor.status !== "active") return;
|
|
3297
|
+
if (through <= cursor.lastOrdinal) return;
|
|
3298
|
+
cursor.lastOrdinal = through;
|
|
3299
|
+
await this.persist();
|
|
3300
|
+
}
|
|
3301
|
+
async parkBelow(missed) {
|
|
3302
|
+
const { cursor } = this.options;
|
|
3303
|
+
cursor.lastOrdinal = Math.min(cursor.lastOrdinal, lowestOf(missed) - 1);
|
|
3304
|
+
await this.persist();
|
|
3305
|
+
}
|
|
3306
|
+
async persist() {
|
|
3307
|
+
if (this.closed) return;
|
|
3308
|
+
const { cursor } = this.options;
|
|
3309
|
+
try {
|
|
3310
|
+
await this.options.persist({
|
|
3311
|
+
lastOrdinal: cursor.lastOrdinal,
|
|
3312
|
+
status: cursor.status,
|
|
3313
|
+
lastError: cursor.lastError,
|
|
3314
|
+
lastErrorTimestamp: cursor.lastErrorTimestamp
|
|
3315
|
+
});
|
|
3316
|
+
} catch (error) {
|
|
3317
|
+
this.options.logger.error("Failed to persist cursor for '@ProcessorId': @Error", this.options.processorId, error);
|
|
3318
|
+
}
|
|
2618
3319
|
}
|
|
2619
|
-
|
|
2620
|
-
}
|
|
2621
|
-
function nonEmpty(value) {
|
|
2622
|
-
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
2623
|
-
}
|
|
2624
|
-
function namespaceOf(processor) {
|
|
2625
|
-
return nonEmpty(processor.namespace);
|
|
2626
|
-
}
|
|
2627
|
-
function classNameOf(processor) {
|
|
2628
|
-
const name = processor.constructor.name;
|
|
2629
|
-
return name && name !== "Object" ? name : void 0;
|
|
2630
|
-
}
|
|
2631
|
-
function resolveProcessorSlots(records, legacy) {
|
|
2632
|
-
if (legacy) return records.map((_, i) => String(i));
|
|
2633
|
-
const seen = /* @__PURE__ */ new Map();
|
|
2634
|
-
return records.map((record, i) => {
|
|
2635
|
-
const base = nonEmpty(record.id) ?? namespaceOf(record.processor) ?? classNameOf(record.processor) ?? String(i);
|
|
2636
|
-
const count = seen.get(base) ?? 0;
|
|
2637
|
-
seen.set(base, count + 1);
|
|
2638
|
-
return count === 0 ? base : `${base}#${count}`;
|
|
2639
|
-
});
|
|
2640
|
-
}
|
|
3320
|
+
};
|
|
2641
3321
|
//#endregion
|
|
2642
3322
|
//#region src/processors/processor-manager.ts
|
|
2643
|
-
/**
|
|
2644
|
-
* Manages processor lifecycle based on operations.
|
|
2645
|
-
* Extends BaseReadModel to receive operations from ReadModelCoordinator.
|
|
2646
|
-
*
|
|
2647
|
-
* Responsibilities:
|
|
2648
|
-
* 1. Detect drive creation from CREATE_DOCUMENT operations
|
|
2649
|
-
* 2. Create processors for each drive using registered factories
|
|
2650
|
-
* 3. Route operations to matching processors based on filters
|
|
2651
|
-
* 4. Clean up processors when drives are deleted or factories are unregistered
|
|
2652
|
-
* 5. Track per-processor cursors for failure recovery and backfill
|
|
2653
|
-
*/
|
|
2654
3323
|
var ProcessorManager = class extends BaseReadModel {
|
|
2655
3324
|
factoryRegistry = /* @__PURE__ */ new Map();
|
|
2656
3325
|
processorsByDrive = /* @__PURE__ */ new Map();
|
|
2657
|
-
|
|
3326
|
+
pendingSlots = /* @__PURE__ */ new Set();
|
|
2658
3327
|
knownDrives = /* @__PURE__ */ new Map();
|
|
3328
|
+
highestRoutedOrdinal = 0;
|
|
2659
3329
|
cursorCache = /* @__PURE__ */ new Map();
|
|
3330
|
+
cursorWrites = /* @__PURE__ */ new Map();
|
|
3331
|
+
draining = /* @__PURE__ */ new Map();
|
|
2660
3332
|
logger;
|
|
2661
3333
|
driveContainerTypes;
|
|
2662
3334
|
legacyProcessorIds;
|
|
2663
3335
|
constructor(db, operationIndex, writeCache, consistencyTracker, logger, driveContainerTypes, options = {}) {
|
|
2664
3336
|
super(db, operationIndex, writeCache, consistencyTracker, {
|
|
2665
3337
|
readModelId: "processor-manager",
|
|
2666
|
-
rebuildStateOnInit: true
|
|
3338
|
+
rebuildStateOnInit: true,
|
|
3339
|
+
indexing: unchunkedReadModelIndexingConfig
|
|
2667
3340
|
});
|
|
2668
3341
|
this.logger = logger;
|
|
2669
3342
|
this.driveContainerTypes = driveContainerTypes;
|
|
2670
3343
|
this.legacyProcessorIds = options.legacyProcessorIds ?? true;
|
|
2671
3344
|
}
|
|
2672
3345
|
async init() {
|
|
2673
|
-
await super.init();
|
|
2674
3346
|
await this.loadAllCursors();
|
|
3347
|
+
await super.init();
|
|
2675
3348
|
await this.discoverExistingDrives();
|
|
2676
3349
|
}
|
|
2677
3350
|
async commitOperations(items) {
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
3351
|
+
const { runs, reserved } = this.detectNewDrives(items);
|
|
3352
|
+
const disconnects = this.detectDeletedDrives(items);
|
|
3353
|
+
const deliveries = this.enqueueRouted(items, reserved);
|
|
3354
|
+
await Promise.all([
|
|
3355
|
+
...runs.map((run) => run()),
|
|
3356
|
+
...disconnects,
|
|
3357
|
+
...deliveries
|
|
3358
|
+
]);
|
|
2681
3359
|
}
|
|
2682
3360
|
async registerFactory(identifier, factory) {
|
|
2683
|
-
|
|
3361
|
+
const removals = this.removeFactory(identifier);
|
|
2684
3362
|
this.factoryRegistry.set(identifier, factory);
|
|
2685
|
-
this.
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
3363
|
+
const previous = this.draining.get(identifier);
|
|
3364
|
+
const creationOrdinal = this.highWater() + 1;
|
|
3365
|
+
const runs = [];
|
|
3366
|
+
for (const [driveId, documentType] of this.knownDrives) runs.push(this.reserveSlot(identifier, factory, driveId, createMinimalDriveHeader(driveId, documentType), creationOrdinal, void 0, false, previous).run);
|
|
3367
|
+
await Promise.all([...removals, ...runs.map((run) => run())]);
|
|
2690
3368
|
}
|
|
2691
3369
|
async unregisterFactory(identifier) {
|
|
2692
|
-
|
|
2693
|
-
if (!factoryProcessors) return;
|
|
2694
|
-
for (const [driveId, tracked] of factoryProcessors) {
|
|
2695
|
-
for (const t of tracked) await this.safeDisconnect(t.record.processor);
|
|
2696
|
-
const driveProcessors = this.processorsByDrive.get(driveId);
|
|
2697
|
-
if (driveProcessors) {
|
|
2698
|
-
const remaining = driveProcessors.filter((p) => !tracked.includes(p));
|
|
2699
|
-
if (remaining.length > 0) this.processorsByDrive.set(driveId, remaining);
|
|
2700
|
-
else this.processorsByDrive.delete(driveId);
|
|
2701
|
-
}
|
|
2702
|
-
}
|
|
2703
|
-
await this.deleteProcessorCursors({ factoryId: identifier });
|
|
2704
|
-
this.factoryToProcessors.delete(identifier);
|
|
2705
|
-
this.factoryRegistry.delete(identifier);
|
|
3370
|
+
await Promise.all(this.removeFactory(identifier));
|
|
2706
3371
|
}
|
|
2707
3372
|
get(processorId) {
|
|
2708
3373
|
for (const tracked of this.allTrackedProcessors()) if (tracked.processorId === processorId) return tracked;
|
|
@@ -2710,208 +3375,283 @@ var ProcessorManager = class extends BaseReadModel {
|
|
|
2710
3375
|
getAll() {
|
|
2711
3376
|
return Array.from(this.allTrackedProcessors());
|
|
2712
3377
|
}
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
for (const op of
|
|
3378
|
+
/** Synchronous: reserves a slot per registered factory for each new drive. */
|
|
3379
|
+
detectNewDrives(items) {
|
|
3380
|
+
const runs = [];
|
|
3381
|
+
const reserved = /* @__PURE__ */ new Set();
|
|
3382
|
+
for (const op of items) {
|
|
2718
3383
|
if (!this.isDriveCreation(op)) continue;
|
|
2719
3384
|
const driveId = op.context.documentId;
|
|
2720
3385
|
if (this.knownDrives.has(driveId)) continue;
|
|
2721
3386
|
this.knownDrives.set(driveId, op.context.documentType);
|
|
2722
3387
|
const driveHeader = extractDriveHeader(op);
|
|
2723
3388
|
if (!driveHeader) continue;
|
|
2724
|
-
for (const [identifier, factory] of this.factoryRegistry)
|
|
3389
|
+
for (const [identifier, factory] of this.factoryRegistry) {
|
|
3390
|
+
const { slot, run } = this.reserveSlot(identifier, factory, driveId, driveHeader, op.context.ordinal, items, true);
|
|
3391
|
+
reserved.add(slot);
|
|
3392
|
+
runs.push(run);
|
|
3393
|
+
}
|
|
2725
3394
|
}
|
|
3395
|
+
return {
|
|
3396
|
+
runs,
|
|
3397
|
+
reserved
|
|
3398
|
+
};
|
|
2726
3399
|
}
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
for (const op of operations) {
|
|
3400
|
+
/** Synchronous: drops a deleted drive's processors from the tables. */
|
|
3401
|
+
detectDeletedDrives(items) {
|
|
3402
|
+
const pending = [];
|
|
3403
|
+
for (const op of items) {
|
|
2732
3404
|
if (!isDriveDeletion(op)) continue;
|
|
2733
3405
|
const driveId = extractDeletedDocumentId(op);
|
|
2734
3406
|
if (!driveId || !this.knownDrives.has(driveId)) continue;
|
|
2735
|
-
if (!this.isDeletedDocumentADrive(driveId)) continue;
|
|
2736
|
-
await this.cleanupDriveProcessors(driveId);
|
|
2737
3407
|
this.knownDrives.delete(driveId);
|
|
3408
|
+
for (const slot of this.pendingSlots) if (slot.driveId === driveId) this.pendingSlots.delete(slot);
|
|
3409
|
+
for (const { queue } of this.processorsByDrive.get(driveId) ?? []) queue.close();
|
|
3410
|
+
this.processorsByDrive.delete(driveId);
|
|
3411
|
+
pending.push(...this.deleteCursors((row) => row.driveId === driveId));
|
|
3412
|
+
}
|
|
3413
|
+
return pending;
|
|
3414
|
+
}
|
|
3415
|
+
/** Synchronous: puts each processor's share of the batch on its queue. */
|
|
3416
|
+
enqueueRouted(items, reserved) {
|
|
3417
|
+
if (items.length === 0) return [];
|
|
3418
|
+
let lowest = items[0].context.ordinal;
|
|
3419
|
+
let highest = 0;
|
|
3420
|
+
for (const item of items) {
|
|
3421
|
+
lowest = Math.min(lowest, item.context.ordinal);
|
|
3422
|
+
highest = Math.max(highest, item.context.ordinal);
|
|
3423
|
+
}
|
|
3424
|
+
this.highestRoutedOrdinal = Math.max(this.highestRoutedOrdinal, highest);
|
|
3425
|
+
for (const slot of this.pendingSlots) {
|
|
3426
|
+
if (reserved.has(slot)) continue;
|
|
3427
|
+
slot.lowestRoutedOrdinal = Math.min(slot.lowestRoutedOrdinal ?? lowest, lowest);
|
|
3428
|
+
}
|
|
3429
|
+
const deliveries = [];
|
|
3430
|
+
for (const { tracked, queue } of this.allBound()) {
|
|
3431
|
+
const matching = items.filter((op) => matchesFilter(op, tracked.record.filter));
|
|
3432
|
+
if (matching.length > 0) deliveries.push(queue.live(matching));
|
|
3433
|
+
else queue.advance(highest);
|
|
3434
|
+
}
|
|
3435
|
+
return deliveries;
|
|
3436
|
+
}
|
|
3437
|
+
/** Synchronous: binds records, or discards them if the slot was cancelled. */
|
|
3438
|
+
bind(slot, records, creationOrdinal, creationItems) {
|
|
3439
|
+
const released = this.pendingSlots.delete(slot);
|
|
3440
|
+
if (!records || records.length === 0) {
|
|
3441
|
+
const settled = Promise.resolve();
|
|
3442
|
+
return {
|
|
3443
|
+
persisted: settled,
|
|
3444
|
+
delivered: settled
|
|
3445
|
+
};
|
|
3446
|
+
}
|
|
3447
|
+
if (!released) {
|
|
3448
|
+
const discarded = records.map((record) => this.discard(slot, record.processor.onDisconnect.bind(record.processor)));
|
|
3449
|
+
const settled = Promise.all(discarded).then(() => void 0);
|
|
3450
|
+
return {
|
|
3451
|
+
persisted: settled,
|
|
3452
|
+
delivered: settled
|
|
3453
|
+
};
|
|
2738
3454
|
}
|
|
3455
|
+
const { factoryId, driveId, lowestRoutedOrdinal } = slot;
|
|
3456
|
+
const ids = resolveProcessorSlots(records, this.legacyProcessorIds);
|
|
3457
|
+
const bound = records.map((record, i) => this.track(slot, record, i, `${factoryId}:${driveId}:${ids[i]}`, creationOrdinal, lowestRoutedOrdinal));
|
|
3458
|
+
const drive = this.processorsByDrive.get(driveId) ?? [];
|
|
3459
|
+
this.processorsByDrive.set(driveId, [...drive, ...bound]);
|
|
3460
|
+
const liveIds = new Set(bound.map((b) => b.tracked.processorId));
|
|
3461
|
+
const writes = this.deleteCursors((row) => row.factoryId === factoryId && row.driveId === driveId && !liveIds.has(row.processorId));
|
|
3462
|
+
for (const { tracked } of bound) writes.push(this.writeCursor(tracked, tracked));
|
|
3463
|
+
const deliveries = [];
|
|
3464
|
+
for (const { tracked, queue } of bound) {
|
|
3465
|
+
const missed = tracked.lastOrdinal < slot.reservedAt || lowestRoutedOrdinal !== void 0;
|
|
3466
|
+
if (tracked.status === "active" && missed) deliveries.push(queue.backfill());
|
|
3467
|
+
const matching = creationItems?.filter((op) => matchesFilter(op, tracked.record.filter));
|
|
3468
|
+
if (matching && matching.length > 0) deliveries.push(queue.live(matching));
|
|
3469
|
+
}
|
|
3470
|
+
return {
|
|
3471
|
+
persisted: Promise.all(writes).then(() => void 0),
|
|
3472
|
+
delivered: Promise.all(deliveries).then(() => void 0)
|
|
3473
|
+
};
|
|
2739
3474
|
}
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
3475
|
+
/** Synchronous: removes a factory's slots, processors and cursor rows. */
|
|
3476
|
+
removeFactory(identifier) {
|
|
3477
|
+
if (!this.factoryRegistry.delete(identifier)) return [];
|
|
3478
|
+
const closing = [];
|
|
3479
|
+
for (const slot of this.pendingSlots) {
|
|
3480
|
+
if (slot.factoryId !== identifier) continue;
|
|
3481
|
+
this.pendingSlots.delete(slot);
|
|
3482
|
+
if (slot.settled) closing.push(slot.settled);
|
|
3483
|
+
}
|
|
3484
|
+
for (const [driveId, drive] of this.processorsByDrive) {
|
|
3485
|
+
const remaining = [];
|
|
3486
|
+
for (const b of drive) if (b.tracked.factoryId === identifier) closing.push(b.queue.close());
|
|
3487
|
+
else remaining.push(b);
|
|
3488
|
+
if (remaining.length > 0) this.processorsByDrive.set(driveId, remaining);
|
|
3489
|
+
else this.processorsByDrive.delete(driveId);
|
|
3490
|
+
}
|
|
3491
|
+
if (closing.length > 0) {
|
|
3492
|
+
const drained = Promise.all([this.draining.get(identifier), ...closing]).then(() => void 0);
|
|
3493
|
+
this.draining.set(identifier, drained);
|
|
3494
|
+
drained.then(() => {
|
|
3495
|
+
if (this.draining.get(identifier) === drained) this.draining.delete(identifier);
|
|
3496
|
+
});
|
|
3497
|
+
}
|
|
3498
|
+
return this.deleteCursors((row) => row.factoryId === identifier);
|
|
2743
3499
|
}
|
|
2744
|
-
|
|
2745
|
-
|
|
3500
|
+
reserveSlot(factoryId, factory, driveId, driveHeader, creationOrdinal, creationItems, awaitDelivery, previous) {
|
|
3501
|
+
const slot = {
|
|
3502
|
+
factoryId,
|
|
3503
|
+
driveId,
|
|
3504
|
+
reservedAt: this.highWater(),
|
|
3505
|
+
lowestRoutedOrdinal: void 0,
|
|
3506
|
+
settled: void 0
|
|
3507
|
+
};
|
|
3508
|
+
this.pendingSlots.add(slot);
|
|
3509
|
+
const run = async () => {
|
|
3510
|
+
const bound = (async () => {
|
|
3511
|
+
await previous;
|
|
3512
|
+
const records = await this.runFactory(slot, factory, driveHeader);
|
|
3513
|
+
return this.bind(slot, records, creationOrdinal, creationItems);
|
|
3514
|
+
})();
|
|
3515
|
+
slot.settled = bound.then(({ persisted }) => persisted);
|
|
3516
|
+
const { persisted, delivered } = await bound;
|
|
3517
|
+
await (awaitDelivery ? Promise.all([persisted, delivered]) : persisted);
|
|
3518
|
+
};
|
|
3519
|
+
return {
|
|
3520
|
+
slot,
|
|
3521
|
+
run
|
|
3522
|
+
};
|
|
2746
3523
|
}
|
|
2747
|
-
async
|
|
2748
|
-
let records;
|
|
3524
|
+
async runFactory(slot, factory, driveHeader) {
|
|
2749
3525
|
try {
|
|
2750
|
-
|
|
3526
|
+
return await factory(driveHeader);
|
|
2751
3527
|
} catch (error) {
|
|
2752
|
-
this.logger.error("Factory '@FactoryId' failed for drive '@DriveId': @Error",
|
|
3528
|
+
this.logger.error("Factory '@FactoryId' failed for drive '@DriveId': @Error", slot.factoryId, slot.driveId, error);
|
|
2753
3529
|
return;
|
|
2754
3530
|
}
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
const
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
status = "active";
|
|
2774
|
-
lastError = void 0;
|
|
2775
|
-
lastErrorTimestamp = void 0;
|
|
2776
|
-
}
|
|
2777
|
-
const tracked = {
|
|
2778
|
-
processorId,
|
|
2779
|
-
factoryId: identifier,
|
|
2780
|
-
driveId,
|
|
2781
|
-
processorIndex: i,
|
|
2782
|
-
record,
|
|
2783
|
-
lastOrdinal,
|
|
2784
|
-
status,
|
|
2785
|
-
lastError,
|
|
2786
|
-
lastErrorTimestamp,
|
|
2787
|
-
retry: () => this.retryProcessor(tracked)
|
|
3531
|
+
}
|
|
3532
|
+
track(slot, record, processorIndex, processorId, creationOrdinal, lowestRoutedOrdinal) {
|
|
3533
|
+
const cached = this.cursorCache.get(processorId);
|
|
3534
|
+
let floor = 0;
|
|
3535
|
+
let cursor;
|
|
3536
|
+
if (cached) cursor = {
|
|
3537
|
+
lastOrdinal: cached.lastOrdinal,
|
|
3538
|
+
status: cached.status,
|
|
3539
|
+
lastError: cached.lastError ?? void 0,
|
|
3540
|
+
lastErrorTimestamp: cached.lastErrorTimestamp ?? void 0
|
|
3541
|
+
};
|
|
3542
|
+
else {
|
|
3543
|
+
if (record.startFrom === "current") floor = creationOrdinal - 1;
|
|
3544
|
+
cursor = {
|
|
3545
|
+
lastOrdinal: floor,
|
|
3546
|
+
status: "active",
|
|
3547
|
+
lastError: void 0,
|
|
3548
|
+
lastErrorTimestamp: void 0
|
|
2788
3549
|
};
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
if (!page.next) break;
|
|
2818
|
-
page = await page.next();
|
|
2819
|
-
}
|
|
2820
|
-
}
|
|
2821
|
-
async retryProcessor(tracked) {
|
|
2822
|
-
if (tracked.status !== "errored") return;
|
|
2823
|
-
tracked.status = "active";
|
|
2824
|
-
tracked.lastError = void 0;
|
|
2825
|
-
tracked.lastErrorTimestamp = void 0;
|
|
2826
|
-
await this.saveProcessorCursor(tracked);
|
|
2827
|
-
await this.backfillProcessor(tracked);
|
|
2828
|
-
}
|
|
2829
|
-
async cleanupDriveProcessors(driveId) {
|
|
2830
|
-
const processors = this.processorsByDrive.get(driveId);
|
|
2831
|
-
if (!processors) return;
|
|
2832
|
-
for (const tracked of processors) await this.safeDisconnect(tracked.record.processor);
|
|
2833
|
-
this.processorsByDrive.delete(driveId);
|
|
2834
|
-
for (const factoryProcessors of this.factoryToProcessors.values()) factoryProcessors.delete(driveId);
|
|
2835
|
-
await this.deleteProcessorCursors({ driveId });
|
|
2836
|
-
}
|
|
2837
|
-
async safeDisconnect(processor) {
|
|
3550
|
+
}
|
|
3551
|
+
if (lowestRoutedOrdinal !== void 0) cursor.lastOrdinal = Math.max(floor, Math.min(cursor.lastOrdinal, lowestRoutedOrdinal - 1));
|
|
3552
|
+
const tracked = {
|
|
3553
|
+
processorId,
|
|
3554
|
+
factoryId: slot.factoryId,
|
|
3555
|
+
driveId: slot.driveId,
|
|
3556
|
+
processorIndex,
|
|
3557
|
+
record,
|
|
3558
|
+
...cursor,
|
|
3559
|
+
retry: () => queue.retry()
|
|
3560
|
+
};
|
|
3561
|
+
const queue = new ProcessorQueue({
|
|
3562
|
+
processorId,
|
|
3563
|
+
processor: record.processor,
|
|
3564
|
+
filter: record.filter,
|
|
3565
|
+
cursor: tracked,
|
|
3566
|
+
floor,
|
|
3567
|
+
readSince: (ordinal) => this.operationIndex.getSinceOrdinal(ordinal),
|
|
3568
|
+
routedThrough: () => this.highWater(),
|
|
3569
|
+
persist: (state) => this.writeCursor(tracked, state),
|
|
3570
|
+
logger: this.logger
|
|
3571
|
+
});
|
|
3572
|
+
return {
|
|
3573
|
+
tracked,
|
|
3574
|
+
queue
|
|
3575
|
+
};
|
|
3576
|
+
}
|
|
3577
|
+
async discard(slot, disconnect) {
|
|
2838
3578
|
try {
|
|
2839
|
-
await
|
|
3579
|
+
await disconnect();
|
|
2840
3580
|
} catch (error) {
|
|
2841
|
-
this.logger.error("Error disconnecting processor: @Error", error);
|
|
3581
|
+
this.logger.error("Error disconnecting discarded processor for '@FactoryId' on '@DriveId': @Error", slot.factoryId, slot.driveId, error);
|
|
2842
3582
|
}
|
|
2843
3583
|
}
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
return;
|
|
2860
|
-
}
|
|
2861
|
-
tracked.lastOrdinal = maxOrdinal;
|
|
2862
|
-
await this.safeSaveProcessorCursor(tracked);
|
|
2863
|
-
}));
|
|
3584
|
+
highWater() {
|
|
3585
|
+
return Math.max(this.lastOrdinal, this.highestRoutedOrdinal);
|
|
3586
|
+
}
|
|
3587
|
+
*allBound() {
|
|
3588
|
+
for (const drive of this.processorsByDrive.values()) yield* drive;
|
|
3589
|
+
}
|
|
3590
|
+
*allTrackedProcessors() {
|
|
3591
|
+
for (const { tracked } of this.allBound()) yield tracked;
|
|
3592
|
+
}
|
|
3593
|
+
isDriveCreation(op) {
|
|
3594
|
+
return op.operation.action.type === "CREATE_DOCUMENT" && this.driveContainerTypes.has(op.context.documentType);
|
|
3595
|
+
}
|
|
3596
|
+
async discoverExistingDrives() {
|
|
3597
|
+
const drives = await this.db.selectFrom("DocumentSnapshot").select(["documentId", "documentType"]).where("documentType", "in", [...this.driveContainerTypes]).where("isDeleted", "=", false).execute();
|
|
3598
|
+
for (const drive of drives) this.knownDrives.set(drive.documentId, drive.documentType);
|
|
2864
3599
|
}
|
|
2865
3600
|
async loadAllCursors() {
|
|
2866
3601
|
const rows = await this.db.selectFrom("ProcessorCursor").selectAll().execute();
|
|
2867
3602
|
for (const row of rows) this.cursorCache.set(row.processorId, row);
|
|
2868
3603
|
}
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
}
|
|
2873
|
-
|
|
2874
|
-
|
|
3604
|
+
lane(processorId, write) {
|
|
3605
|
+
const next = (this.cursorWrites.get(processorId) ?? Promise.resolve()).then(write).then(() => void 0, (error) => {
|
|
3606
|
+
this.logger.error("Failed to write cursor for '@ProcessorId': @Error", processorId, error);
|
|
3607
|
+
});
|
|
3608
|
+
this.cursorWrites.set(processorId, next);
|
|
3609
|
+
next.then(() => {
|
|
3610
|
+
if (this.cursorWrites.get(processorId) === next) this.cursorWrites.delete(processorId);
|
|
3611
|
+
});
|
|
3612
|
+
return next;
|
|
2875
3613
|
}
|
|
2876
|
-
|
|
2877
|
-
|
|
3614
|
+
writeCursor(tracked, state) {
|
|
3615
|
+
const now = /* @__PURE__ */ new Date();
|
|
3616
|
+
const row = {
|
|
2878
3617
|
processorId: tracked.processorId,
|
|
2879
3618
|
factoryId: tracked.factoryId,
|
|
2880
3619
|
driveId: tracked.driveId,
|
|
2881
3620
|
processorIndex: tracked.processorIndex,
|
|
2882
|
-
lastOrdinal:
|
|
2883
|
-
status:
|
|
2884
|
-
lastError:
|
|
2885
|
-
lastErrorTimestamp:
|
|
2886
|
-
|
|
3621
|
+
lastOrdinal: state.lastOrdinal,
|
|
3622
|
+
status: state.status,
|
|
3623
|
+
lastError: state.lastError ?? null,
|
|
3624
|
+
lastErrorTimestamp: state.lastErrorTimestamp ?? null,
|
|
3625
|
+
createdAt: now,
|
|
3626
|
+
updatedAt: now
|
|
3627
|
+
};
|
|
3628
|
+
this.cursorCache.set(row.processorId, row);
|
|
3629
|
+
return this.lane(row.processorId, () => this.db.insertInto("ProcessorCursor").values({
|
|
3630
|
+
processorId: row.processorId,
|
|
3631
|
+
factoryId: row.factoryId,
|
|
3632
|
+
driveId: row.driveId,
|
|
3633
|
+
processorIndex: row.processorIndex,
|
|
3634
|
+
lastOrdinal: row.lastOrdinal,
|
|
3635
|
+
status: row.status,
|
|
3636
|
+
lastError: row.lastError,
|
|
3637
|
+
lastErrorTimestamp: row.lastErrorTimestamp,
|
|
3638
|
+
updatedAt: now
|
|
2887
3639
|
}).onConflict((oc) => oc.column("processorId").doUpdateSet({
|
|
2888
|
-
lastOrdinal:
|
|
2889
|
-
status:
|
|
2890
|
-
lastError:
|
|
2891
|
-
lastErrorTimestamp:
|
|
2892
|
-
updatedAt:
|
|
2893
|
-
})).execute();
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
createdAt: /* @__PURE__ */ new Date(),
|
|
2904
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
2905
|
-
});
|
|
2906
|
-
}
|
|
2907
|
-
async deleteProcessorCursors(filter) {
|
|
2908
|
-
if ("factoryId" in filter) {
|
|
2909
|
-
await this.db.deleteFrom("ProcessorCursor").where("factoryId", "=", filter.factoryId).execute();
|
|
2910
|
-
for (const [id, row] of this.cursorCache) if (row.factoryId === filter.factoryId) this.cursorCache.delete(id);
|
|
2911
|
-
} else {
|
|
2912
|
-
await this.db.deleteFrom("ProcessorCursor").where("driveId", "=", filter.driveId).execute();
|
|
2913
|
-
for (const [id, row] of this.cursorCache) if (row.driveId === filter.driveId) this.cursorCache.delete(id);
|
|
2914
|
-
}
|
|
3640
|
+
lastOrdinal: row.lastOrdinal,
|
|
3641
|
+
status: row.status,
|
|
3642
|
+
lastError: row.lastError,
|
|
3643
|
+
lastErrorTimestamp: row.lastErrorTimestamp,
|
|
3644
|
+
updatedAt: now
|
|
3645
|
+
})).execute());
|
|
3646
|
+
}
|
|
3647
|
+
deleteCursors(matches) {
|
|
3648
|
+
const deletes = [];
|
|
3649
|
+
for (const [processorId, row] of this.cursorCache) {
|
|
3650
|
+
if (!matches(row)) continue;
|
|
3651
|
+
this.cursorCache.delete(processorId);
|
|
3652
|
+
deletes.push(this.lane(processorId, () => this.db.deleteFrom("ProcessorCursor").where("processorId", "=", processorId).execute()));
|
|
3653
|
+
}
|
|
3654
|
+
return deletes;
|
|
2915
3655
|
}
|
|
2916
3656
|
};
|
|
2917
3657
|
//#endregion
|
|
@@ -2928,11 +3668,11 @@ var JobExecutionHandle = class {
|
|
|
2928
3668
|
onDefer;
|
|
2929
3669
|
getStateName(state) {
|
|
2930
3670
|
switch (state) {
|
|
2931
|
-
case
|
|
2932
|
-
case
|
|
2933
|
-
case
|
|
2934
|
-
case
|
|
2935
|
-
case
|
|
3671
|
+
case 0: return "PREPROCESSING";
|
|
3672
|
+
case 1: return "PENDING";
|
|
3673
|
+
case 2: return "READY";
|
|
3674
|
+
case 3: return "RUNNING";
|
|
3675
|
+
case 4: return "RESOLVED";
|
|
2936
3676
|
default: return `UNKNOWN`;
|
|
2937
3677
|
}
|
|
2938
3678
|
}
|
|
@@ -2951,23 +3691,23 @@ var JobExecutionHandle = class {
|
|
|
2951
3691
|
return this._state;
|
|
2952
3692
|
}
|
|
2953
3693
|
start() {
|
|
2954
|
-
if (this._state !==
|
|
2955
|
-
this._state =
|
|
3694
|
+
if (this._state !== 2) throw new Error(`Cannot start job in state ${this.getStateName(this._state)}`);
|
|
3695
|
+
this._state = 3;
|
|
2956
3696
|
this.onStart?.();
|
|
2957
3697
|
}
|
|
2958
3698
|
complete() {
|
|
2959
|
-
if (this._state !==
|
|
2960
|
-
this._state =
|
|
3699
|
+
if (this._state !== 3) throw new Error(`Cannot complete job in state ${this.getStateName(this._state)}`);
|
|
3700
|
+
this._state = 4;
|
|
2961
3701
|
this.onComplete?.();
|
|
2962
3702
|
}
|
|
2963
3703
|
fail(error) {
|
|
2964
|
-
if (this._state !==
|
|
2965
|
-
this._state =
|
|
3704
|
+
if (this._state !== 3) throw new Error(`Cannot fail job in state ${this.getStateName(this._state)}`);
|
|
3705
|
+
this._state = 4;
|
|
2966
3706
|
this.onFail?.(error);
|
|
2967
3707
|
}
|
|
2968
3708
|
defer() {
|
|
2969
|
-
if (this._state !==
|
|
2970
|
-
this._state =
|
|
3709
|
+
if (this._state !== 3) throw new Error(`Cannot defer job in state ${this.getStateName(this._state)}`);
|
|
3710
|
+
this._state = 4;
|
|
2971
3711
|
this.onDefer?.();
|
|
2972
3712
|
}
|
|
2973
3713
|
};
|
|
@@ -2980,6 +3720,8 @@ var JobExecutionHandle = class {
|
|
|
2980
3720
|
* Implements dependency management through queue hints.
|
|
2981
3721
|
*/
|
|
2982
3722
|
var InMemoryQueue = class {
|
|
3723
|
+
eventBus;
|
|
3724
|
+
resolver;
|
|
2983
3725
|
queues = /* @__PURE__ */ new Map();
|
|
2984
3726
|
jobIdToQueueKey = /* @__PURE__ */ new Map();
|
|
2985
3727
|
docIdToJobId = /* @__PURE__ */ new Map();
|
|
@@ -3114,7 +3856,7 @@ var InMemoryQueue = class {
|
|
|
3114
3856
|
this.jobIdToQueueKey.delete(job.id);
|
|
3115
3857
|
this.markJobExecuting(job);
|
|
3116
3858
|
if (queue.length === 0) this.queues.delete(queueKey);
|
|
3117
|
-
const handle = new JobExecutionHandle(job,
|
|
3859
|
+
const handle = new JobExecutionHandle(job, 2, {
|
|
3118
3860
|
onStart: () => {},
|
|
3119
3861
|
onComplete: () => {
|
|
3120
3862
|
this.completeJob(job.id);
|
|
@@ -3140,7 +3882,7 @@ var InMemoryQueue = class {
|
|
|
3140
3882
|
this.jobIdToQueueKey.delete(job.id);
|
|
3141
3883
|
this.markJobExecuting(job);
|
|
3142
3884
|
if (queue.length === 0) this.queues.delete(queueKey);
|
|
3143
|
-
const handle = new JobExecutionHandle(job,
|
|
3885
|
+
const handle = new JobExecutionHandle(job, 2, {
|
|
3144
3886
|
onStart: () => {},
|
|
3145
3887
|
onComplete: () => {
|
|
3146
3888
|
this.completeJob(job.id);
|
|
@@ -3174,7 +3916,7 @@ var InMemoryQueue = class {
|
|
|
3174
3916
|
this.jobIdToQueueKey.delete(job.id);
|
|
3175
3917
|
this.markJobExecuting(job);
|
|
3176
3918
|
if (queue.length === 0) this.queues.delete(queueKey);
|
|
3177
|
-
const handle = new JobExecutionHandle(job,
|
|
3919
|
+
const handle = new JobExecutionHandle(job, 2, {
|
|
3178
3920
|
onStart: () => {},
|
|
3179
3921
|
onComplete: () => {
|
|
3180
3922
|
this.completeJob(job.id);
|
|
@@ -3276,7 +4018,7 @@ var InMemoryQueue = class {
|
|
|
3276
4018
|
if (documentId) this.markJobComplete(jobId, documentId);
|
|
3277
4019
|
this.jobIndex.delete(jobId);
|
|
3278
4020
|
}
|
|
3279
|
-
async retryJob(jobId, error, accounting =
|
|
4021
|
+
async retryJob(jobId, error, accounting = "count-against-limit") {
|
|
3280
4022
|
const job = this.jobIndex.get(jobId);
|
|
3281
4023
|
if (!job) return;
|
|
3282
4024
|
job.lastError = error;
|
|
@@ -3288,7 +4030,7 @@ var InMemoryQueue = class {
|
|
|
3288
4030
|
const retryCount = job.retryCount || 0;
|
|
3289
4031
|
const updatedJob = {
|
|
3290
4032
|
...job,
|
|
3291
|
-
retryCount: accounting ===
|
|
4033
|
+
retryCount: accounting === "count-against-limit" ? retryCount + 1 : retryCount,
|
|
3292
4034
|
lastError: error
|
|
3293
4035
|
};
|
|
3294
4036
|
await this.enqueue(updatedJob);
|
|
@@ -3489,6 +4231,8 @@ function describeSource(source) {
|
|
|
3489
4231
|
* and failure-caching state.
|
|
3490
4232
|
*/
|
|
3491
4233
|
var DocumentModelResolver = class {
|
|
4234
|
+
registry;
|
|
4235
|
+
loader;
|
|
3492
4236
|
loadingModels = /* @__PURE__ */ new Map();
|
|
3493
4237
|
failedModelTypes = /* @__PURE__ */ new Set();
|
|
3494
4238
|
broadcastHook = null;
|
|
@@ -3559,6 +4303,7 @@ var DocumentModelResolver = class {
|
|
|
3559
4303
|
* Since there is no loader, missing models cannot be recovered.
|
|
3560
4304
|
*/
|
|
3561
4305
|
var NullDocumentModelResolver = class {
|
|
4306
|
+
registry;
|
|
3562
4307
|
constructor(registry) {
|
|
3563
4308
|
this.registry = registry;
|
|
3564
4309
|
}
|
|
@@ -3782,6 +4527,8 @@ var ReactorSubscriptionManager = class {
|
|
|
3782
4527
|
* is emitted, so that reactor.get() returns fresh data when callbacks fire.
|
|
3783
4528
|
*/
|
|
3784
4529
|
var SubscriptionNotificationReadModel = class {
|
|
4530
|
+
subscriptionManager;
|
|
4531
|
+
documentView;
|
|
3785
4532
|
name = "subscription-notification";
|
|
3786
4533
|
constructor(subscriptionManager, documentView) {
|
|
3787
4534
|
this.subscriptionManager = subscriptionManager;
|
|
@@ -3804,10 +4551,10 @@ var SubscriptionNotificationReadModel = class {
|
|
|
3804
4551
|
deleted.push(deletedId);
|
|
3805
4552
|
} else if (actionType === "ADD_RELATIONSHIP") {
|
|
3806
4553
|
const input = operation.action.input;
|
|
3807
|
-
this.subscriptionManager.notifyRelationshipChanged(input.sourceId, input.targetId,
|
|
4554
|
+
this.subscriptionManager.notifyRelationshipChanged(input.sourceId, input.targetId, "added", input.childType);
|
|
3808
4555
|
} else if (actionType === "REMOVE_RELATIONSHIP") {
|
|
3809
4556
|
const input = operation.action.input;
|
|
3810
|
-
this.subscriptionManager.notifyRelationshipChanged(input.sourceId, input.targetId,
|
|
4557
|
+
this.subscriptionManager.notifyRelationshipChanged(input.sourceId, input.targetId, "removed", input.childType);
|
|
3811
4558
|
} else if (!created.includes(context.documentId)) updatedIds.add(context.documentId);
|
|
3812
4559
|
}
|
|
3813
4560
|
if (created.length > 0) this.subscriptionManager.notifyDocumentsCreated(created, documentTypes, parentIds);
|
|
@@ -3832,6 +4579,10 @@ var SubscriptionNotificationReadModel = class {
|
|
|
3832
4579
|
* keeps the common case (a membership write later than all history) free.
|
|
3833
4580
|
*/
|
|
3834
4581
|
var GroupReevaluationTrigger = class {
|
|
4582
|
+
logger;
|
|
4583
|
+
eventBus;
|
|
4584
|
+
queue;
|
|
4585
|
+
operationIndex;
|
|
3835
4586
|
unsubscribe;
|
|
3836
4587
|
constructor(logger, eventBus, queue, operationIndex) {
|
|
3837
4588
|
this.logger = logger;
|
|
@@ -3983,7 +4734,7 @@ var Mailbox = class {
|
|
|
3983
4734
|
this.itemsMap.set(item.id, item);
|
|
3984
4735
|
for (const op of item.operations) this._latestOrdinal = Math.max(this._latestOrdinal, op.context.ordinal);
|
|
3985
4736
|
item.on((syncOp, _, next) => {
|
|
3986
|
-
if (next ===
|
|
4737
|
+
if (next === 2) for (const op of syncOp.operations) this._ack = Math.max(this._ack, op.context.ordinal);
|
|
3987
4738
|
});
|
|
3988
4739
|
}
|
|
3989
4740
|
if (this.paused) {
|
|
@@ -4097,7 +4848,7 @@ var BufferedMailbox = class {
|
|
|
4097
4848
|
this.itemsMap.set(item.id, item);
|
|
4098
4849
|
for (const op of item.operations) this._latestOrdinal = Math.max(this._latestOrdinal, op.context.ordinal);
|
|
4099
4850
|
item.on((syncOp, _, next) => {
|
|
4100
|
-
if (next ===
|
|
4851
|
+
if (next === 2) for (const op of syncOp.operations) this._ack = Math.max(this._ack, op.context.ordinal);
|
|
4101
4852
|
});
|
|
4102
4853
|
}
|
|
4103
4854
|
this.addedBuffer.push(...items);
|
|
@@ -4211,7 +4962,14 @@ var GraphQLRequestError = class extends Error {
|
|
|
4211
4962
|
* difference between a channel that recovers and one that is dead for the
|
|
4212
4963
|
* process lifetime.
|
|
4213
4964
|
*/
|
|
4214
|
-
const RECOVERABLE_GRAPHQL_ERROR_CODES = {
|
|
4965
|
+
const RECOVERABLE_GRAPHQL_ERROR_CODES = {
|
|
4966
|
+
/**
|
|
4967
|
+
* A stored operation cannot be represented in the schema - an action with no
|
|
4968
|
+
* id, say. The document holding it needs repairing, but the channel serves
|
|
4969
|
+
* every other document, and a peer that stopped polling would stop receiving
|
|
4970
|
+
* those too.
|
|
4971
|
+
*/
|
|
4972
|
+
malformedStoredOperation: "MALFORMED_STORED_OPERATION" };
|
|
4215
4973
|
const RECOVERABLE_CODES = new Set(Object.values(RECOVERABLE_GRAPHQL_ERROR_CODES));
|
|
4216
4974
|
/**
|
|
4217
4975
|
* True when every error the response carried named a recoverable code.
|
|
@@ -4229,9 +4987,35 @@ const DRIVE_AUTH_ERROR_MESSAGES = {
|
|
|
4229
4987
|
forbidden: "Forbidden: insufficient permissions",
|
|
4230
4988
|
authenticationRequired: "Forbidden: authentication required"
|
|
4231
4989
|
};
|
|
4990
|
+
/**
|
|
4991
|
+
* A non-GraphQL HTTP failure against a drive endpoint.
|
|
4992
|
+
*
|
|
4993
|
+
* Drive discovery (`GET <base>/d/:drive`) is REST, not GraphQL, so its
|
|
4994
|
+
* failures cannot be a `GraphQLRequestError` without the name lying about
|
|
4995
|
+
* what was called. It carries the status for the same reason that one does:
|
|
4996
|
+
* `isDriveAuthError` is what decides whether a failure prompts a login, and a
|
|
4997
|
+
* bare `Error` tells it nothing.
|
|
4998
|
+
*/
|
|
4999
|
+
var DriveRequestError = class extends Error {
|
|
5000
|
+
statusCode;
|
|
5001
|
+
constructor(message, statusCode) {
|
|
5002
|
+
super(message);
|
|
5003
|
+
this.name = "DriveRequestError";
|
|
5004
|
+
this.statusCode = statusCode;
|
|
5005
|
+
}
|
|
5006
|
+
};
|
|
4232
5007
|
/** True when the remote rejected the caller as unauthenticated/unauthorized:
|
|
4233
|
-
* an HTTP 401/403, or a Forbidden/Unauthorized GraphQL error.
|
|
5008
|
+
* an HTTP 401/403, or a Forbidden/Unauthorized GraphQL error.
|
|
5009
|
+
*
|
|
5010
|
+
* 403 and 401 only — NOT 404. The drive info endpoint answers a drive the
|
|
5011
|
+
* caller may not read with the same 404 it gives a drive that does not exist,
|
|
5012
|
+
* so that an unauthorized caller cannot enumerate drives by probing slugs.
|
|
5013
|
+
* That is deliberate, and it costs exactly this: a protected drive is
|
|
5014
|
+
* indistinguishable from a typo, and prompting for a login on every 404 would
|
|
5015
|
+
* fire on every mistyped URL. See the `WWW-Authenticate` note on the endpoint
|
|
5016
|
+
* for the signal that would let a client tell the two apart. */
|
|
4234
5017
|
function isDriveAuthError(error) {
|
|
5018
|
+
if (error instanceof DriveRequestError) return error.statusCode === 401 || error.statusCode === 403;
|
|
4235
5019
|
if (!(error instanceof GraphQLRequestError)) return false;
|
|
4236
5020
|
if (error.category === "http") return error.statusCode === 401 || error.statusCode === 403;
|
|
4237
5021
|
if (error.category === "graphql") return Object.values(DRIVE_AUTH_ERROR_MESSAGES).some((m) => error.message.includes(m));
|
|
@@ -4294,23 +5078,23 @@ var SyncOperation = class {
|
|
|
4294
5078
|
this.scopes = scopes;
|
|
4295
5079
|
this.branch = branch;
|
|
4296
5080
|
this.operations = operations;
|
|
4297
|
-
this.status =
|
|
5081
|
+
this.status = -1;
|
|
4298
5082
|
}
|
|
4299
5083
|
on(callback) {
|
|
4300
5084
|
this.callbacks.push(callback);
|
|
4301
5085
|
}
|
|
4302
5086
|
started() {
|
|
4303
|
-
this.transition(
|
|
5087
|
+
this.transition(0);
|
|
4304
5088
|
}
|
|
4305
5089
|
transported() {
|
|
4306
|
-
this.transition(
|
|
5090
|
+
this.transition(1);
|
|
4307
5091
|
}
|
|
4308
5092
|
executed() {
|
|
4309
|
-
this.transition(
|
|
5093
|
+
this.transition(2);
|
|
4310
5094
|
}
|
|
4311
5095
|
failed(error) {
|
|
4312
5096
|
this.error = error;
|
|
4313
|
-
this.transition(
|
|
5097
|
+
this.transition(3);
|
|
4314
5098
|
}
|
|
4315
5099
|
transition(next) {
|
|
4316
5100
|
const prev = this.status;
|
|
@@ -4520,9 +5304,11 @@ function consolidateSyncOperations(syncOps) {
|
|
|
4520
5304
|
}
|
|
4521
5305
|
const first = group.ops[0];
|
|
4522
5306
|
const merged = new SyncOperation(first.id, first.jobId, remappedDeps, first.remoteName, first.documentId, first.scopes, first.branch, allOperations);
|
|
4523
|
-
if (first.status >
|
|
4524
|
-
|
|
4525
|
-
|
|
5307
|
+
if (first.status > 0) {
|
|
5308
|
+
if (first.status >= 3) merged.executed();
|
|
5309
|
+
else if (first.status >= 2) merged.transported();
|
|
5310
|
+
else merged.started();
|
|
5311
|
+
}
|
|
4526
5312
|
result.push(merged);
|
|
4527
5313
|
}
|
|
4528
5314
|
return result;
|
|
@@ -4653,7 +5439,7 @@ function syncOperationErrorType(error) {
|
|
|
4653
5439
|
return error?.errorType ?? classifyJobFailure(error?.error?.name ?? "Error");
|
|
4654
5440
|
}
|
|
4655
5441
|
/** Dead-letter types that must not stop the document syncing. */
|
|
4656
|
-
const NON_QUARANTINING_ERROR_TYPES = new Set(["AUTH_TIMESTAMP_NOT_MONOTONIC"]);
|
|
5442
|
+
const NON_QUARANTINING_ERROR_TYPES = /* @__PURE__ */ new Set(["AUTH_TIMESTAMP_NOT_MONOTONIC"]);
|
|
4657
5443
|
/**
|
|
4658
5444
|
* A held auth operation must not quarantine: reconciling the two policies needs
|
|
4659
5445
|
* the traffic a quarantine would stop.
|
|
@@ -4879,7 +5665,7 @@ function envelopesToSyncOperations(envelope, remoteName) {
|
|
|
4879
5665
|
}
|
|
4880
5666
|
const getLatestAppliedOrdinal = (syncOps) => {
|
|
4881
5667
|
let maxOrdinal = 0;
|
|
4882
|
-
for (const syncOp of syncOps) if (syncOp.status ===
|
|
5668
|
+
for (const syncOp of syncOps) if (syncOp.status === 2) for (const op of syncOp.operations) maxOrdinal = Math.max(maxOrdinal, op.context.ordinal);
|
|
4883
5669
|
return maxOrdinal;
|
|
4884
5670
|
};
|
|
4885
5671
|
//#endregion
|
|
@@ -4894,6 +5680,7 @@ const DECISION_FIELDS = ["deniedReason", "errorType"];
|
|
|
4894
5680
|
* GraphQL-based synchronization channel for network communication between reactors.
|
|
4895
5681
|
*/
|
|
4896
5682
|
var GqlRequestChannel = class {
|
|
5683
|
+
logger;
|
|
4897
5684
|
inbox;
|
|
4898
5685
|
outbox;
|
|
4899
5686
|
deadLetter;
|
|
@@ -5028,6 +5815,10 @@ var GqlRequestChannel = class {
|
|
|
5028
5815
|
if (this.isShutdown) return;
|
|
5029
5816
|
this.pollTimer.triggerNow();
|
|
5030
5817
|
}
|
|
5818
|
+
/** This channel polls a remote itself; it has no holder to hear from. */
|
|
5819
|
+
notePoll() {}
|
|
5820
|
+
/** No holder, so nothing this channel reports may strand one. */
|
|
5821
|
+
lastHolderPollUtcMs() {}
|
|
5031
5822
|
/**
|
|
5032
5823
|
* Initializes the channel by registering it on the remote server and starting polling.
|
|
5033
5824
|
*/
|
|
@@ -5097,7 +5888,7 @@ var GqlRequestChannel = class {
|
|
|
5097
5888
|
const syncOps = [];
|
|
5098
5889
|
for (const dl of deadLetters) {
|
|
5099
5890
|
const syncOp = new SyncOperation(crypto.randomUUID(), dl.jobId, [], this.remoteName, dl.documentId, dl.scopes, dl.branch, []);
|
|
5100
|
-
syncOp.failed(new ChannelError(
|
|
5891
|
+
syncOp.failed(new ChannelError("outbox", new Error(dl.error), dl.errorType ?? void 0));
|
|
5101
5892
|
syncOps.push(syncOp);
|
|
5102
5893
|
}
|
|
5103
5894
|
this.deadLetter.add(...syncOps);
|
|
@@ -5117,7 +5908,7 @@ var GqlRequestChannel = class {
|
|
|
5117
5908
|
const classification = this.classifyError(err);
|
|
5118
5909
|
this.failureCount++;
|
|
5119
5910
|
this.lastFailureUtcMs = Date.now();
|
|
5120
|
-
const channelError = new ChannelError(
|
|
5911
|
+
const channelError = new ChannelError("inbox", err);
|
|
5121
5912
|
this.logger.error("GqlChannel poll error (@FailureCount, @Classification): @Error", this.failureCount, classification, channelError);
|
|
5122
5913
|
if (classification === "unrecoverable") {
|
|
5123
5914
|
this.pollTimer.stop();
|
|
@@ -5361,7 +6152,7 @@ var GqlRequestChannel = class {
|
|
|
5361
6152
|
this.transitionConnectionState("reconnecting");
|
|
5362
6153
|
this.schedulePushRetry();
|
|
5363
6154
|
} else {
|
|
5364
|
-
const channelError = new ChannelError(
|
|
6155
|
+
const channelError = new ChannelError("outbox", err);
|
|
5365
6156
|
for (const syncOp of syncOps) syncOp.failed(channelError);
|
|
5366
6157
|
this.deadLetter.add(...syncOps);
|
|
5367
6158
|
this.outbox.remove(...syncOps);
|
|
@@ -5583,7 +6374,7 @@ var GqlRequestChannelFactory = class {
|
|
|
5583
6374
|
...retryMaxDelayMs !== void 0 && { retryMaxDelayMs },
|
|
5584
6375
|
...maxQueueDepth !== void 0 && { maxQueueDepth },
|
|
5585
6376
|
...backpressureCheckIntervalMs !== void 0 && { backpressureCheckIntervalMs },
|
|
5586
|
-
startPaused: options?.pollBehavior ===
|
|
6377
|
+
startPaused: options?.pollBehavior === "manual"
|
|
5587
6378
|
});
|
|
5588
6379
|
return new GqlRequestChannel(this.logger, remoteId, remoteName, cursorStorage, gqlConfig, operationIndex, pollTimer);
|
|
5589
6380
|
}
|
|
@@ -5600,6 +6391,7 @@ function firstOrdinalOf$1(syncOp) {
|
|
|
5600
6391
|
* In general, the resolvers are responsible for updating mailboxes.
|
|
5601
6392
|
*/
|
|
5602
6393
|
var GqlResponseChannel = class {
|
|
6394
|
+
logger;
|
|
5603
6395
|
inbox;
|
|
5604
6396
|
outbox;
|
|
5605
6397
|
deadLetter;
|
|
@@ -5611,6 +6403,7 @@ var GqlResponseChannel = class {
|
|
|
5611
6403
|
lastPersistedOutboxOrdinal = 0;
|
|
5612
6404
|
evictedOutboxFloor = Number.POSITIVE_INFINITY;
|
|
5613
6405
|
appliedOutboxOrdinal = 0;
|
|
6406
|
+
lastPollUtcMs = Date.now();
|
|
5614
6407
|
connectionState = "connecting";
|
|
5615
6408
|
connectionStateCallbacks = /* @__PURE__ */ new Set();
|
|
5616
6409
|
constructor(logger, channelId, remoteName, cursorStorage) {
|
|
@@ -5653,7 +6446,7 @@ var GqlResponseChannel = class {
|
|
|
5653
6446
|
return {
|
|
5654
6447
|
state: this.connectionState,
|
|
5655
6448
|
failureCount: 0,
|
|
5656
|
-
lastSuccessUtcMs:
|
|
6449
|
+
lastSuccessUtcMs: this.lastPollUtcMs,
|
|
5657
6450
|
lastFailureUtcMs: 0,
|
|
5658
6451
|
pushBlocked: false,
|
|
5659
6452
|
pushFailureCount: 0,
|
|
@@ -5669,6 +6462,13 @@ var GqlResponseChannel = class {
|
|
|
5669
6462
|
}
|
|
5670
6463
|
/** Response channels are push-driven; resolvers populate mailboxes directly. */
|
|
5671
6464
|
triggerPull() {}
|
|
6465
|
+
notePoll() {
|
|
6466
|
+
this.lastPollUtcMs = Date.now();
|
|
6467
|
+
}
|
|
6468
|
+
/** This channel is served: its holder's polls are the liveness it reports. */
|
|
6469
|
+
lastHolderPollUtcMs() {
|
|
6470
|
+
return this.lastPollUtcMs;
|
|
6471
|
+
}
|
|
5672
6472
|
async init() {
|
|
5673
6473
|
const cursors = await this.cursorStorage.list(this.remoteName);
|
|
5674
6474
|
const inboxOrdinal = cursors.find((c) => c.cursorType === "inbox")?.cursorOrdinal ?? 0;
|
|
@@ -5702,7 +6502,7 @@ var GqlResponseChannel = class {
|
|
|
5702
6502
|
*/
|
|
5703
6503
|
rememberUnserved(syncOps) {
|
|
5704
6504
|
for (const syncOp of syncOps) {
|
|
5705
|
-
if (syncOp.status ===
|
|
6505
|
+
if (syncOp.status === 2) continue;
|
|
5706
6506
|
const first = firstOrdinalOf$1(syncOp);
|
|
5707
6507
|
if (first > 0 && first < this.evictedOutboxFloor) this.evictedOutboxFloor = first;
|
|
5708
6508
|
}
|
|
@@ -5747,7 +6547,7 @@ var GqlResponseChannel = class {
|
|
|
5747
6547
|
unservedFloor() {
|
|
5748
6548
|
let floor = this.evictedOutboxFloor;
|
|
5749
6549
|
for (const syncOp of this.outbox.items) {
|
|
5750
|
-
if (syncOp.status ===
|
|
6550
|
+
if (syncOp.status === 2) continue;
|
|
5751
6551
|
const first = firstOrdinalOf$1(syncOp);
|
|
5752
6552
|
if (first > 0 && first < floor) floor = first;
|
|
5753
6553
|
}
|
|
@@ -5787,6 +6587,7 @@ function remoteCursorToRow(cursor) {
|
|
|
5787
6587
|
};
|
|
5788
6588
|
}
|
|
5789
6589
|
var KyselySyncCursorStorage = class {
|
|
6590
|
+
db;
|
|
5790
6591
|
constructor(db) {
|
|
5791
6592
|
this.db = db;
|
|
5792
6593
|
}
|
|
@@ -5862,6 +6663,7 @@ function deadLetterRecordToRow(record) {
|
|
|
5862
6663
|
* PGlite/Kysely-backed implementation of {@link ISyncDeadLetterStorage}.
|
|
5863
6664
|
*/
|
|
5864
6665
|
var KyselySyncDeadLetterStorage = class {
|
|
6666
|
+
db;
|
|
5865
6667
|
constructor(db) {
|
|
5866
6668
|
this.db = db;
|
|
5867
6669
|
}
|
|
@@ -5983,6 +6785,7 @@ function remoteRecordToRow(remote) {
|
|
|
5983
6785
|
};
|
|
5984
6786
|
}
|
|
5985
6787
|
var KyselySyncRemoteStorage = class {
|
|
6788
|
+
db;
|
|
5986
6789
|
constructor(db) {
|
|
5987
6790
|
this.db = db;
|
|
5988
6791
|
}
|
|
@@ -6133,6 +6936,7 @@ var BatchAggregator = class {
|
|
|
6133
6936
|
* to provide a fast path for jobs that have already synced.
|
|
6134
6937
|
*/
|
|
6135
6938
|
var SyncAwaiter = class {
|
|
6939
|
+
eventBus;
|
|
6136
6940
|
completedResults = /* @__PURE__ */ new Map();
|
|
6137
6941
|
pendingWaiters = /* @__PURE__ */ new Map();
|
|
6138
6942
|
unsubscribers = [];
|
|
@@ -6332,25 +7136,30 @@ var SyncStatusTracker = class {
|
|
|
6332
7136
|
}
|
|
6333
7137
|
};
|
|
6334
7138
|
function deriveStatus(inboxCount, outboxCount, errorCount) {
|
|
6335
|
-
if (errorCount > 0) return
|
|
6336
|
-
if (inboxCount > 0 && outboxCount > 0) return
|
|
6337
|
-
if (inboxCount > 0) return
|
|
6338
|
-
if (outboxCount > 0) return
|
|
6339
|
-
return
|
|
7139
|
+
if (errorCount > 0) return "ERROR";
|
|
7140
|
+
if (inboxCount > 0 && outboxCount > 0) return "OUTGOING_AND_INCOMING";
|
|
7141
|
+
if (inboxCount > 0) return "INCOMING";
|
|
7142
|
+
if (outboxCount > 0) return "OUTGOING";
|
|
7143
|
+
return "SYNCED";
|
|
6340
7144
|
}
|
|
6341
7145
|
//#endregion
|
|
6342
7146
|
//#region src/sync/sync-manager.ts
|
|
6343
|
-
var OutboxMode = /* @__PURE__ */ function(OutboxMode) {
|
|
6344
|
-
OutboxMode["Backfill"] = "backfill";
|
|
6345
|
-
OutboxMode["BatchTriggered"] = "batch-triggered";
|
|
6346
|
-
return OutboxMode;
|
|
6347
|
-
}(OutboxMode || {});
|
|
6348
7147
|
const defaultSyncManagerConfig = {
|
|
6349
7148
|
maxDeadLettersPerRemote: 100,
|
|
6350
7149
|
maxInboxBatchSize: 32,
|
|
6351
|
-
maxHeldOperationsPerRemote: 1e4
|
|
7150
|
+
maxHeldOperationsPerRemote: 1e4,
|
|
7151
|
+
staleRemotePollWindowMs: 3e5
|
|
6352
7152
|
};
|
|
6353
7153
|
const PLAN_KEY_TO_JOB_UUID_CAP = 1e4;
|
|
7154
|
+
/**
|
|
7155
|
+
* Whether a channel failure says the caller could not authenticate or could not
|
|
7156
|
+
* reach the remote, rather than that the remote itself is misconfigured. The
|
|
7157
|
+
* remote record stays on disk for these so a retry after sign-in can re-add it.
|
|
7158
|
+
*/
|
|
7159
|
+
function isCredentialOrNetworkError(error) {
|
|
7160
|
+
if (isDriveAuthError(error)) return true;
|
|
7161
|
+
return error instanceof GraphQLRequestError && error.category === "network";
|
|
7162
|
+
}
|
|
6354
7163
|
/** Where a sync operation's run of ordinals begins. */
|
|
6355
7164
|
function firstOrdinalOf(syncOp) {
|
|
6356
7165
|
return syncOp.operations.length > 0 ? syncOp.operations[0].context.ordinal : 0;
|
|
@@ -6379,6 +7188,11 @@ var SyncManager = class {
|
|
|
6379
7188
|
backfillAbortControllers = /* @__PURE__ */ new Map();
|
|
6380
7189
|
planKeyToJobUuid = /* @__PURE__ */ new Map();
|
|
6381
7190
|
evictedOutboxFloors = /* @__PURE__ */ new Map();
|
|
7191
|
+
prunePending = /* @__PURE__ */ new Set();
|
|
7192
|
+
pruneChain = Promise.resolve();
|
|
7193
|
+
derivingOutboxes = 0;
|
|
7194
|
+
pruneDrainDeferred = false;
|
|
7195
|
+
removing = /* @__PURE__ */ new Set();
|
|
6382
7196
|
lastEnqueuedJobIdByKey = /* @__PURE__ */ new Map();
|
|
6383
7197
|
inboxChunkChain = Promise.resolve();
|
|
6384
7198
|
constructor(logger, remoteStorage, cursorStorage, deadLetterStorage, channelFactory, operationIndex, reactor, eventBus, driveContainerTypes, config = {}) {
|
|
@@ -6430,18 +7244,19 @@ var SyncManager = class {
|
|
|
6430
7244
|
await channel.init();
|
|
6431
7245
|
} catch (error) {
|
|
6432
7246
|
this.logger.error("Error initializing channel for remote (@name, @error)", record.name, error instanceof Error ? error.message : String(error));
|
|
6433
|
-
this.
|
|
7247
|
+
await this.dropRemoteAfterFailedInit(remote, false);
|
|
6434
7248
|
continue;
|
|
6435
7249
|
}
|
|
6436
7250
|
const outboxAckOrdinal = remote.channel.outbox.ackOrdinal;
|
|
6437
7251
|
if (outboxAckOrdinal > 0) {
|
|
6438
7252
|
const backfillController = new AbortController();
|
|
6439
7253
|
this.backfillAbortControllers.set(record.name, backfillController);
|
|
6440
|
-
this.updateOutbox(remote, outboxAckOrdinal,
|
|
7254
|
+
this.updateOutbox(remote, outboxAckOrdinal, "backfill", backfillController.signal).catch((error) => {
|
|
6441
7255
|
if (backfillController.signal.aborted) return;
|
|
6442
7256
|
this.logger.error("Backfill failed for remote @RemoteName: @Error", remote.meta.name, error instanceof Error ? error : new Error(String(error)));
|
|
6443
7257
|
}).finally(() => {
|
|
6444
7258
|
this.backfillAbortControllers.delete(record.name);
|
|
7259
|
+
this.drainPrunes();
|
|
6445
7260
|
});
|
|
6446
7261
|
}
|
|
6447
7262
|
}
|
|
@@ -6455,6 +7270,8 @@ var SyncManager = class {
|
|
|
6455
7270
|
this.backfillAbortControllers.clear();
|
|
6456
7271
|
this.planKeyToJobUuid.clear();
|
|
6457
7272
|
this.lastEnqueuedJobIdByKey.clear();
|
|
7273
|
+
this.prunePending.clear();
|
|
7274
|
+
this.pruneDrainDeferred = false;
|
|
6458
7275
|
this.batchAggregator.clear();
|
|
6459
7276
|
if (this.eventUnsubscribe) {
|
|
6460
7277
|
this.eventUnsubscribe();
|
|
@@ -6548,17 +7365,17 @@ var SyncManager = class {
|
|
|
6548
7365
|
try {
|
|
6549
7366
|
await channel.init();
|
|
6550
7367
|
} catch (error) {
|
|
6551
|
-
this.
|
|
6552
|
-
await this.remoteStorage.remove(name);
|
|
7368
|
+
await this.dropRemoteAfterFailedInit(remote, !isCredentialOrNetworkError(error));
|
|
6553
7369
|
throw error;
|
|
6554
7370
|
}
|
|
6555
7371
|
const backfillController = new AbortController();
|
|
6556
7372
|
this.backfillAbortControllers.set(name, backfillController);
|
|
6557
|
-
this.updateOutbox(remote, 0,
|
|
7373
|
+
this.updateOutbox(remote, 0, "backfill", backfillController.signal).catch((error) => {
|
|
6558
7374
|
if (backfillController.signal.aborted) return;
|
|
6559
7375
|
this.logger.error("Backfill failed for remote @RemoteName: @Error", remote.meta.name, error instanceof Error ? error : new Error(String(error)));
|
|
6560
7376
|
}).finally(() => {
|
|
6561
7377
|
this.backfillAbortControllers.delete(name);
|
|
7378
|
+
this.drainPrunes();
|
|
6562
7379
|
});
|
|
6563
7380
|
return remote;
|
|
6564
7381
|
}
|
|
@@ -6570,21 +7387,62 @@ var SyncManager = class {
|
|
|
6570
7387
|
async remove(name) {
|
|
6571
7388
|
const remote = this.remotes.get(name);
|
|
6572
7389
|
if (!remote) throw new Error(`Remote with name '${name}' does not exist`);
|
|
7390
|
+
this.removing.add(name);
|
|
7391
|
+
try {
|
|
7392
|
+
await this.teardownRemoteResources(remote);
|
|
7393
|
+
await this.remoteStorage.remove(name);
|
|
7394
|
+
await this.cursorStorage.remove(name);
|
|
7395
|
+
} finally {
|
|
7396
|
+
this.remotes.delete(name);
|
|
7397
|
+
this.removing.delete(name);
|
|
7398
|
+
}
|
|
7399
|
+
}
|
|
7400
|
+
/**
|
|
7401
|
+
* Drops a remote whose channel.init() rejected, optionally removing its
|
|
7402
|
+
* stored record. Ordered like remove(): the registry slot is released last,
|
|
7403
|
+
* so a concurrent add of the same name cannot slip in and have its record
|
|
7404
|
+
* deleted by the removal below. A failure to tear down must not replace the
|
|
7405
|
+
* init error the caller has to classify.
|
|
7406
|
+
*/
|
|
7407
|
+
async dropRemoteAfterFailedInit(remote, removeStorageRecord) {
|
|
7408
|
+
const name = remote.meta.name;
|
|
7409
|
+
this.removing.add(name);
|
|
7410
|
+
try {
|
|
7411
|
+
await this.teardownRemoteResources(remote);
|
|
7412
|
+
if (removeStorageRecord) await this.remoteStorage.remove(name);
|
|
7413
|
+
} catch (error) {
|
|
7414
|
+
this.logger.error("Error tearing down remote after failed init (@name, @error)", name, error instanceof Error ? error.message : String(error));
|
|
7415
|
+
} finally {
|
|
7416
|
+
this.remotes.delete(name);
|
|
7417
|
+
this.removing.delete(name);
|
|
7418
|
+
}
|
|
7419
|
+
}
|
|
7420
|
+
/**
|
|
7421
|
+
* Releases everything wiring a remote up holds: the in-flight backfill, the
|
|
7422
|
+
* channel, the status tracker entry and the connection-state subscription.
|
|
7423
|
+
* The registry slot is deliberately NOT released here -- the caller holds it
|
|
7424
|
+
* until any storage removal has finished, so a concurrent add of the same
|
|
7425
|
+
* name is refused for the whole teardown.
|
|
7426
|
+
*/
|
|
7427
|
+
async teardownRemoteResources(remote) {
|
|
7428
|
+
const name = remote.meta.name;
|
|
6573
7429
|
const backfillController = this.backfillAbortControllers.get(name);
|
|
6574
7430
|
if (backfillController) {
|
|
6575
7431
|
backfillController.abort();
|
|
6576
7432
|
this.backfillAbortControllers.delete(name);
|
|
6577
7433
|
}
|
|
6578
|
-
|
|
6579
|
-
|
|
6580
|
-
|
|
6581
|
-
|
|
6582
|
-
|
|
6583
|
-
|
|
6584
|
-
|
|
6585
|
-
|
|
7434
|
+
try {
|
|
7435
|
+
await remote.channel.shutdown();
|
|
7436
|
+
} finally {
|
|
7437
|
+
this.syncStatusTracker.untrackRemote(name);
|
|
7438
|
+
const unsub = this.connectionStateUnsubscribes.get(name);
|
|
7439
|
+
if (unsub) {
|
|
7440
|
+
unsub();
|
|
7441
|
+
this.connectionStateUnsubscribes.delete(name);
|
|
7442
|
+
}
|
|
7443
|
+
this.evictedOutboxFloors.delete(name);
|
|
7444
|
+
this.prunePending.delete(name);
|
|
6586
7445
|
}
|
|
6587
|
-
this.remotes.delete(name);
|
|
6588
7446
|
}
|
|
6589
7447
|
list() {
|
|
6590
7448
|
return Array.from(this.remotes.values());
|
|
@@ -6632,7 +7490,7 @@ var SyncManager = class {
|
|
|
6632
7490
|
scopes: syncOp.scopes,
|
|
6633
7491
|
branch: syncOp.branch,
|
|
6634
7492
|
operations: syncOp.operations,
|
|
6635
|
-
errorSource: syncOp.error?.source ??
|
|
7493
|
+
errorSource: syncOp.error?.source ?? "none",
|
|
6636
7494
|
errorMessage: syncOp.error?.error.message ?? "unknown",
|
|
6637
7495
|
errorType
|
|
6638
7496
|
};
|
|
@@ -6679,7 +7537,7 @@ var SyncManager = class {
|
|
|
6679
7537
|
this.logger.debug("Loaded @count persisted dead letters for remote @name", records.length, remote.meta.name);
|
|
6680
7538
|
}
|
|
6681
7539
|
getRemotesForCollection(collectionId) {
|
|
6682
|
-
return Array.from(this.remotes.values()).filter((remote) => remote.meta.collectionId.key === collectionId);
|
|
7540
|
+
return Array.from(this.remotes.values()).filter((remote) => remote.meta.collectionId.key === collectionId && !this.removing.has(remote.meta.name));
|
|
6683
7541
|
}
|
|
6684
7542
|
async processCompleteBatch(batch) {
|
|
6685
7543
|
if (this.isShutdown) return;
|
|
@@ -6690,7 +7548,11 @@ var SyncManager = class {
|
|
|
6690
7548
|
for (const remote of remotes) if (!affectedRemotes.includes(remote)) affectedRemotes.push(remote);
|
|
6691
7549
|
}
|
|
6692
7550
|
for (const remote of affectedRemotes) trimMailboxFromBatch(remote.channel.inbox, batch);
|
|
6693
|
-
for (const remote of affectedRemotes)
|
|
7551
|
+
for (const remote of affectedRemotes) {
|
|
7552
|
+
if (!this.remotes.has(remote.meta.name) || this.removing.has(remote.meta.name)) continue;
|
|
7553
|
+
await this.updateOutbox(remote, remote.channel.outbox.latestOrdinal, "batch-triggered");
|
|
7554
|
+
}
|
|
7555
|
+
await this.drainPrunes();
|
|
6694
7556
|
}
|
|
6695
7557
|
handleInboxAdded(remote, syncOps) {
|
|
6696
7558
|
if (this.isShutdown) return;
|
|
@@ -6730,7 +7592,7 @@ var SyncManager = class {
|
|
|
6730
7592
|
if (this.isShutdown) return;
|
|
6731
7593
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
6732
7594
|
this.logger.error("Failed to load operations from inbox (@remote, @documentId, @error)", remote.meta.name, syncOp.documentId, err.message);
|
|
6733
|
-
const channelError = new ChannelError(
|
|
7595
|
+
const channelError = new ChannelError("inbox", err);
|
|
6734
7596
|
syncOp.failed(channelError);
|
|
6735
7597
|
remote.channel.deadLetter.add(syncOp);
|
|
6736
7598
|
remote.channel.inbox.remove(syncOp);
|
|
@@ -6743,14 +7605,14 @@ var SyncManager = class {
|
|
|
6743
7605
|
if (this.isShutdown) return;
|
|
6744
7606
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
6745
7607
|
this.logger.error("Failed to wait for job completion (@remote, @documentId, @jobId, @error)", remote.meta.name, syncOp.documentId, jobInfo.id, err.message);
|
|
6746
|
-
const channelError = new ChannelError(
|
|
7608
|
+
const channelError = new ChannelError("inbox", err);
|
|
6747
7609
|
syncOp.failed(channelError);
|
|
6748
7610
|
remote.channel.deadLetter.add(syncOp);
|
|
6749
7611
|
remote.channel.inbox.remove(syncOp);
|
|
6750
7612
|
return;
|
|
6751
7613
|
}
|
|
6752
7614
|
if (this.isShutdown) return;
|
|
6753
|
-
if (completedJobInfo.status ===
|
|
7615
|
+
if (completedJobInfo.status === "FAILED") {
|
|
6754
7616
|
const errorMessage = completedJobInfo.error?.message || "Unknown error";
|
|
6755
7617
|
this.logger.error("Failed to apply operations from inbox (@remote, @documentId, @jobId, @error)", remote.meta.name, syncOp.documentId, completedJobInfo.id, errorMessage);
|
|
6756
7618
|
syncOp.failed(this.inboxFailure(completedJobInfo.error));
|
|
@@ -6793,7 +7655,7 @@ var SyncManager = class {
|
|
|
6793
7655
|
if (this.isShutdown) return;
|
|
6794
7656
|
for (const { remote, syncOp } of items) {
|
|
6795
7657
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
6796
|
-
syncOp.failed(new ChannelError(
|
|
7658
|
+
syncOp.failed(new ChannelError("inbox", err));
|
|
6797
7659
|
remote.channel.deadLetter.add(syncOp);
|
|
6798
7660
|
remote.channel.inbox.remove(syncOp);
|
|
6799
7661
|
}
|
|
@@ -6810,7 +7672,7 @@ var SyncManager = class {
|
|
|
6810
7672
|
for (const { remote, syncOp } of items) {
|
|
6811
7673
|
if (!(syncOp.jobId in result.jobs)) {
|
|
6812
7674
|
this.logger.error("Job key missing from batch load result (@remote, @documentId, @jobId)", remote.meta.name, syncOp.documentId, syncOp.jobId);
|
|
6813
|
-
const error = new ChannelError(
|
|
7675
|
+
const error = new ChannelError("inbox", /* @__PURE__ */ new Error(`Job key '${syncOp.jobId}' missing from batch load result`));
|
|
6814
7676
|
syncOp.failed(error);
|
|
6815
7677
|
remote.channel.deadLetter.add(syncOp);
|
|
6816
7678
|
remote.channel.inbox.remove(syncOp);
|
|
@@ -6823,13 +7685,13 @@ var SyncManager = class {
|
|
|
6823
7685
|
} catch (error) {
|
|
6824
7686
|
if (this.isShutdown) continue;
|
|
6825
7687
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
6826
|
-
syncOp.failed(new ChannelError(
|
|
7688
|
+
syncOp.failed(new ChannelError("inbox", err));
|
|
6827
7689
|
remote.channel.deadLetter.add(syncOp);
|
|
6828
7690
|
remote.channel.inbox.remove(syncOp);
|
|
6829
7691
|
continue;
|
|
6830
7692
|
}
|
|
6831
7693
|
if (this.isShutdown) return;
|
|
6832
|
-
if (completedJobInfo.status ===
|
|
7694
|
+
if (completedJobInfo.status === "FAILED") {
|
|
6833
7695
|
syncOp.failed(this.inboxFailure(completedJobInfo.error));
|
|
6834
7696
|
remote.channel.deadLetter.add(syncOp);
|
|
6835
7697
|
} else syncOp.executed();
|
|
@@ -6848,7 +7710,7 @@ var SyncManager = class {
|
|
|
6848
7710
|
*/
|
|
6849
7711
|
inboxFailure(error) {
|
|
6850
7712
|
const message = error?.message || "Unknown error";
|
|
6851
|
-
return new ChannelError(
|
|
7713
|
+
return new ChannelError("inbox", /* @__PURE__ */ new Error(`Failed to apply operations: ${message}`), classifyJobFailure(error?.name ?? "Error"));
|
|
6852
7714
|
}
|
|
6853
7715
|
/**
|
|
6854
7716
|
* Where the next outbox derivation starts: at the evicted floor when there is
|
|
@@ -6875,7 +7737,7 @@ var SyncManager = class {
|
|
|
6875
7737
|
*/
|
|
6876
7738
|
evictPastOutboxBound(remote) {
|
|
6877
7739
|
const cap = this.config.maxHeldOperationsPerRemote;
|
|
6878
|
-
const queued = remote.channel.outbox.items.filter((syncOp) => syncOp.status ===
|
|
7740
|
+
const queued = remote.channel.outbox.items.filter((syncOp) => syncOp.status === -1).sort((a, b) => firstOrdinalOf(a) - firstOrdinalOf(b));
|
|
6879
7741
|
let kept = 0;
|
|
6880
7742
|
let cut = queued.length;
|
|
6881
7743
|
for (let i = 0; i < queued.length; i++) {
|
|
@@ -6891,15 +7753,95 @@ var SyncManager = class {
|
|
|
6891
7753
|
const floor = firstOrdinalOf(evicted[0]);
|
|
6892
7754
|
const known = this.evictedOutboxFloors.get(remote.meta.name);
|
|
6893
7755
|
this.evictedOutboxFloors.set(remote.meta.name, known === void 0 ? floor : Math.min(known, floor));
|
|
6894
|
-
this.
|
|
7756
|
+
const staleMs = this.stalePollAgeMs(remote);
|
|
7757
|
+
if (staleMs !== void 0) {
|
|
7758
|
+
let held = kept;
|
|
7759
|
+
for (const syncOp of evicted) held += syncOp.operations.length;
|
|
7760
|
+
if (!this.prunePending.has(remote.meta.name)) {
|
|
7761
|
+
this.prunePending.add(remote.meta.name);
|
|
7762
|
+
this.logger.warn("Outbox for @RemoteName (@Collection) is past its bound of @Cap operations holding @Held, and it has not been polled for @StaleMs ms; marking the channel for removal once this derivation ends", remote.meta.name, remote.meta.collectionId.key, cap, held, staleMs);
|
|
7763
|
+
}
|
|
7764
|
+
} else this.logger.warn("Outbox for @RemoteName is past its bound of @Cap operations; evicting @Count entries from ordinal @Floor, to be derived again once it drains", remote.meta.name, cap, evicted.length, floor);
|
|
6895
7765
|
remote.channel.outbox.remove(...evicted);
|
|
6896
7766
|
}
|
|
7767
|
+
/**
|
|
7768
|
+
* How long a served remote's holder has been silent, if past the window.
|
|
7769
|
+
*
|
|
7770
|
+
* The channel is asked, rather than its config inspected: SyncManager serves
|
|
7771
|
+
* and subscribes with the same interface, both kinds report lastSuccessUtcMs,
|
|
7772
|
+
* and the caller-supplied channelConfig.type is a free-form string the
|
|
7773
|
+
* factories do not read -- so neither could tell a dead served channel from a
|
|
7774
|
+
* client whose switchboard is merely unreachable. Only a channel that claims
|
|
7775
|
+
* a holder by reporting when it last heard from one can be removed for that
|
|
7776
|
+
* holder's silence; a channel that reports nothing (or 0) is never pruned.
|
|
7777
|
+
*/
|
|
7778
|
+
stalePollAgeMs(remote) {
|
|
7779
|
+
const last = remote.channel.lastHolderPollUtcMs();
|
|
7780
|
+
if (last === void 0 || last <= 0) return;
|
|
7781
|
+
const age = Date.now() - last;
|
|
7782
|
+
return age >= this.config.staleRemotePollWindowMs ? age : void 0;
|
|
7783
|
+
}
|
|
7784
|
+
/**
|
|
7785
|
+
* Removes the remotes marked stale during eviction.
|
|
7786
|
+
*
|
|
7787
|
+
* Removing a remote that a derivation is still iterating would pull its
|
|
7788
|
+
* mailboxes out from under it, so a drain that arrives during one is deferred
|
|
7789
|
+
* rather than run; the last derivation to finish re-arms it.
|
|
7790
|
+
*/
|
|
7791
|
+
drainPrunes() {
|
|
7792
|
+
if (this.derivingOutboxes > 0) {
|
|
7793
|
+
this.pruneDrainDeferred = true;
|
|
7794
|
+
return Promise.resolve();
|
|
7795
|
+
}
|
|
7796
|
+
const next = this.pruneChain.then(async () => {
|
|
7797
|
+
if (this.isShutdown) return;
|
|
7798
|
+
for (const name of [...this.prunePending]) {
|
|
7799
|
+
if (this.derivingOutboxes > 0) {
|
|
7800
|
+
this.pruneDrainDeferred = true;
|
|
7801
|
+
return;
|
|
7802
|
+
}
|
|
7803
|
+
this.prunePending.delete(name);
|
|
7804
|
+
const remote = this.remotes.get(name);
|
|
7805
|
+
if (!remote) continue;
|
|
7806
|
+
if (this.stalePollAgeMs(remote) === void 0) {
|
|
7807
|
+
this.logger.info("Stale removal of @name revoked: its holder polled while the outbox was being derived", name);
|
|
7808
|
+
continue;
|
|
7809
|
+
}
|
|
7810
|
+
try {
|
|
7811
|
+
await this.remove(name);
|
|
7812
|
+
} catch (error) {
|
|
7813
|
+
this.logger.error("Failed to remove stale remote (@name, @error)", name, error instanceof Error ? error.message : String(error));
|
|
7814
|
+
}
|
|
7815
|
+
}
|
|
7816
|
+
});
|
|
7817
|
+
this.pruneChain = next.catch(() => {});
|
|
7818
|
+
return next;
|
|
7819
|
+
}
|
|
6897
7820
|
outboxOperationCount(remote) {
|
|
6898
7821
|
let count = 0;
|
|
6899
7822
|
for (const syncOp of remote.channel.outbox.items) count += syncOp.operations.length;
|
|
6900
7823
|
return count;
|
|
6901
7824
|
}
|
|
6902
|
-
|
|
7825
|
+
/**
|
|
7826
|
+
* Derives this remote's outbox, holding off prunes for the duration.
|
|
7827
|
+
*
|
|
7828
|
+
* A backfill elsewhere can finish at any await in here and drain the prunes
|
|
7829
|
+
* it marked; the count is what keeps that drain from removing the remote this
|
|
7830
|
+
* call is still adding to.
|
|
7831
|
+
*/
|
|
7832
|
+
async updateOutbox(remote, ackOrdinal, mode = "backfill", signal) {
|
|
7833
|
+
this.derivingOutboxes++;
|
|
7834
|
+
try {
|
|
7835
|
+
await this.deriveOutbox(remote, ackOrdinal, mode, signal);
|
|
7836
|
+
} finally {
|
|
7837
|
+
this.derivingOutboxes--;
|
|
7838
|
+
if (this.derivingOutboxes === 0 && this.pruneDrainDeferred) {
|
|
7839
|
+
this.pruneDrainDeferred = false;
|
|
7840
|
+
this.drainPrunes();
|
|
7841
|
+
}
|
|
7842
|
+
}
|
|
7843
|
+
}
|
|
7844
|
+
async deriveOutbox(remote, ackOrdinal, mode, signal) {
|
|
6903
7845
|
const composedSignal = signal ? AbortSignal.any([signal, this.abortController.signal]) : this.abortController.signal;
|
|
6904
7846
|
const startOrdinal = this.refillOrdinal(remote, ackOrdinal);
|
|
6905
7847
|
let maxOrdinal = startOrdinal;
|
|
@@ -6915,11 +7857,11 @@ var SyncManager = class {
|
|
|
6915
7857
|
const prevJobId = lastJobByDoc.get(batch.documentId);
|
|
6916
7858
|
const deps = [];
|
|
6917
7859
|
if (prevJobId) deps.push(prevJobId);
|
|
6918
|
-
if (mode ===
|
|
7860
|
+
if (mode === "batch-triggered" && prevChainJobId && prevChainJobId !== prevJobId) deps.push(prevChainJobId);
|
|
6919
7861
|
const syncOp = new SyncOperation(crypto.randomUUID(), jobId, deps, remote.meta.name, batch.documentId, [batch.scope], batch.branch, batch.operations);
|
|
6920
7862
|
syncOps.push(syncOp);
|
|
6921
7863
|
lastJobByDoc.set(batch.documentId, jobId);
|
|
6922
|
-
if (mode ===
|
|
7864
|
+
if (mode === "batch-triggered") prevChainJobId = jobId;
|
|
6923
7865
|
}
|
|
6924
7866
|
remote.channel.outbox.add(...syncOps);
|
|
6925
7867
|
this.evictPastOutboxBound(remote);
|
|
@@ -7001,6 +7943,10 @@ var SyncBuilder = class {
|
|
|
7001
7943
|
this.config.maxHeldOperationsPerRemote = limit;
|
|
7002
7944
|
return this;
|
|
7003
7945
|
}
|
|
7946
|
+
withStaleRemotePollWindowMs(windowMs) {
|
|
7947
|
+
this.config.staleRemotePollWindowMs = windowMs;
|
|
7948
|
+
return this;
|
|
7949
|
+
}
|
|
7004
7950
|
build(reactor, logger, operationIndex, eventBus, db, driveContainerTypes) {
|
|
7005
7951
|
return this.buildModule(reactor, logger, operationIndex, eventBus, db, driveContainerTypes).syncManager;
|
|
7006
7952
|
}
|
|
@@ -7166,6 +8112,16 @@ var Reactor = class {
|
|
|
7166
8112
|
throwIfAborted(signal, () => new AbortError());
|
|
7167
8113
|
return relationships.results.map((rel) => rel.sourceId);
|
|
7168
8114
|
}
|
|
8115
|
+
async getOutgoingRelationshipEdges(sourceId, relationshipType, paging, consistencyToken, signal) {
|
|
8116
|
+
const relationships = await this.documentIndexer.getOutgoing(sourceId, relationshipType ? [relationshipType] : void 0, paging, consistencyToken, signal);
|
|
8117
|
+
throwIfAborted(signal, () => new AbortError());
|
|
8118
|
+
return relationships;
|
|
8119
|
+
}
|
|
8120
|
+
async getIncomingRelationshipEdges(targetId, relationshipType, paging, consistencyToken, signal) {
|
|
8121
|
+
const relationships = await this.documentIndexer.getIncoming(targetId, relationshipType ? [relationshipType] : void 0, paging, consistencyToken, signal);
|
|
8122
|
+
throwIfAborted(signal, () => new AbortError());
|
|
8123
|
+
return relationships;
|
|
8124
|
+
}
|
|
7169
8125
|
async getOperations(documentId, view, filter, paging, consistencyToken, signal) {
|
|
7170
8126
|
this.logger.verbose("getOperations(@documentId, @view, @filter, @paging)", documentId, view, filter, paging);
|
|
7171
8127
|
const branch = view?.branch || "main";
|
|
@@ -7213,7 +8169,7 @@ var Reactor = class {
|
|
|
7213
8169
|
this.logger.verbose("create(@id, @type, @slug)", document.header.id, document.header.documentType, document.header.slug);
|
|
7214
8170
|
const createdAtUtcIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
7215
8171
|
throwIfAborted(signal, () => new AbortError());
|
|
7216
|
-
|
|
8172
|
+
const createAction = createDocumentAction({
|
|
7217
8173
|
model: document.header.documentType,
|
|
7218
8174
|
version: 0,
|
|
7219
8175
|
documentId: document.header.id,
|
|
@@ -7229,14 +8185,20 @@ var Reactor = class {
|
|
|
7229
8185
|
branch: document.header.branch,
|
|
7230
8186
|
meta: document.header.meta,
|
|
7231
8187
|
protocolVersions: document.header.protocolVersions ?? { "base-reducer": 2 }
|
|
7232
|
-
})
|
|
8188
|
+
});
|
|
8189
|
+
const upgradeAction = upgradeDocumentAction({
|
|
7233
8190
|
documentId: document.header.id,
|
|
7234
8191
|
model: document.header.documentType,
|
|
7235
8192
|
fromVersion: 0,
|
|
7236
8193
|
toVersion: normalizeDocumentModelVersion(document.state.document?.version),
|
|
7237
8194
|
initialState: document.state
|
|
7238
|
-
})
|
|
7239
|
-
|
|
8195
|
+
});
|
|
8196
|
+
const branch = document.header.branch || "main";
|
|
8197
|
+
let actions = [createAction, upgradeAction];
|
|
8198
|
+
if (signer) actions = await signActions(actions, signer, {
|
|
8199
|
+
documentId: document.header.id,
|
|
8200
|
+
branch
|
|
8201
|
+
}, signal);
|
|
7240
8202
|
const jobId = v4();
|
|
7241
8203
|
const jobMeta = buildSingleJobMeta(jobId, meta);
|
|
7242
8204
|
const job = {
|
|
@@ -7244,7 +8206,7 @@ var Reactor = class {
|
|
|
7244
8206
|
kind: "mutation",
|
|
7245
8207
|
documentId: document.header.id,
|
|
7246
8208
|
scope: "document",
|
|
7247
|
-
branch
|
|
8209
|
+
branch,
|
|
7248
8210
|
actions,
|
|
7249
8211
|
operations: [],
|
|
7250
8212
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -7256,7 +8218,7 @@ var Reactor = class {
|
|
|
7256
8218
|
const jobInfo = {
|
|
7257
8219
|
id: jobId,
|
|
7258
8220
|
documentId: job.documentId,
|
|
7259
|
-
status:
|
|
8221
|
+
status: "PENDING",
|
|
7260
8222
|
createdAtUtcIso,
|
|
7261
8223
|
consistencyToken: {
|
|
7262
8224
|
version: 1,
|
|
@@ -7275,7 +8237,10 @@ var Reactor = class {
|
|
|
7275
8237
|
const createdAtUtcIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
7276
8238
|
throwIfAborted(signal, () => new AbortError());
|
|
7277
8239
|
let action = deleteDocumentAction(id);
|
|
7278
|
-
if (signer) action = await signAction(action, signer,
|
|
8240
|
+
if (signer) action = await signAction(action, signer, {
|
|
8241
|
+
documentId: id,
|
|
8242
|
+
branch: "main"
|
|
8243
|
+
}, signal);
|
|
7279
8244
|
const jobId = v4();
|
|
7280
8245
|
const jobMeta = buildSingleJobMeta(jobId, meta);
|
|
7281
8246
|
const job = {
|
|
@@ -7295,7 +8260,7 @@ var Reactor = class {
|
|
|
7295
8260
|
const jobInfo = {
|
|
7296
8261
|
id: jobId,
|
|
7297
8262
|
documentId: job.documentId,
|
|
7298
|
-
status:
|
|
8263
|
+
status: "PENDING",
|
|
7299
8264
|
createdAtUtcIso,
|
|
7300
8265
|
consistencyToken: {
|
|
7301
8266
|
version: 1,
|
|
@@ -7333,7 +8298,7 @@ var Reactor = class {
|
|
|
7333
8298
|
const jobInfo = {
|
|
7334
8299
|
id: jobId,
|
|
7335
8300
|
documentId: job.documentId,
|
|
7336
|
-
status:
|
|
8301
|
+
status: "PENDING",
|
|
7337
8302
|
createdAtUtcIso,
|
|
7338
8303
|
consistencyToken: {
|
|
7339
8304
|
version: 1,
|
|
@@ -7373,7 +8338,7 @@ var Reactor = class {
|
|
|
7373
8338
|
const jobInfo = {
|
|
7374
8339
|
id: jobId,
|
|
7375
8340
|
documentId: job.documentId,
|
|
7376
|
-
status:
|
|
8341
|
+
status: "PENDING",
|
|
7377
8342
|
createdAtUtcIso,
|
|
7378
8343
|
consistencyToken: {
|
|
7379
8344
|
version: 1,
|
|
@@ -7408,7 +8373,7 @@ var Reactor = class {
|
|
|
7408
8373
|
const jobInfo = {
|
|
7409
8374
|
id: planKeyToJobId.get(jobPlan.key),
|
|
7410
8375
|
documentId: jobPlan.documentId,
|
|
7411
|
-
status:
|
|
8376
|
+
status: "PENDING",
|
|
7412
8377
|
createdAtUtcIso,
|
|
7413
8378
|
consistencyToken: {
|
|
7414
8379
|
version: 1,
|
|
@@ -7478,7 +8443,7 @@ var Reactor = class {
|
|
|
7478
8443
|
const jobInfo = {
|
|
7479
8444
|
id: planKeyToJobId.get(jobPlan.key),
|
|
7480
8445
|
documentId: jobPlan.documentId,
|
|
7481
|
-
status:
|
|
8446
|
+
status: "PENDING",
|
|
7482
8447
|
createdAtUtcIso,
|
|
7483
8448
|
consistencyToken: {
|
|
7484
8449
|
version: 1,
|
|
@@ -7528,18 +8493,34 @@ var Reactor = class {
|
|
|
7528
8493
|
}
|
|
7529
8494
|
return { jobs: Object.fromEntries(jobInfos) };
|
|
7530
8495
|
}
|
|
7531
|
-
async addRelationship(sourceId, targetId, relationshipType, branch = "main", signer, signal) {
|
|
7532
|
-
this.logger.verbose("addRelationship(@sourceId, @targetId, @relationshipType, @branch)", sourceId, targetId, relationshipType, branch);
|
|
8496
|
+
async addRelationship(sourceId, targetId, relationshipType, metadata, branch = "main", signer, signal) {
|
|
8497
|
+
this.logger.verbose("addRelationship(@sourceId, @targetId, @relationshipType, @metadata, @branch)", sourceId, targetId, relationshipType, metadata, branch);
|
|
8498
|
+
throwIfAborted(signal, () => new AbortError());
|
|
8499
|
+
let actions = [addRelationshipAction(sourceId, targetId, relationshipType, metadata)];
|
|
8500
|
+
if (signer) actions = await signActions(actions, signer, {
|
|
8501
|
+
documentId: sourceId,
|
|
8502
|
+
branch
|
|
8503
|
+
}, signal);
|
|
8504
|
+
return await this.execute(sourceId, branch, actions, signal);
|
|
8505
|
+
}
|
|
8506
|
+
async updateRelationship(sourceId, targetId, relationshipType, metadata, branch = "main", signer, signal) {
|
|
8507
|
+
this.logger.verbose("updateRelationship(@sourceId, @targetId, @relationshipType, @metadata, @branch)", sourceId, targetId, relationshipType, metadata, branch);
|
|
7533
8508
|
throwIfAborted(signal, () => new AbortError());
|
|
7534
|
-
let actions = [
|
|
7535
|
-
if (signer) actions = await signActions(actions, signer,
|
|
8509
|
+
let actions = [updateRelationshipAction(sourceId, targetId, relationshipType, metadata)];
|
|
8510
|
+
if (signer) actions = await signActions(actions, signer, {
|
|
8511
|
+
documentId: sourceId,
|
|
8512
|
+
branch
|
|
8513
|
+
}, signal);
|
|
7536
8514
|
return await this.execute(sourceId, branch, actions, signal);
|
|
7537
8515
|
}
|
|
7538
8516
|
async removeRelationship(sourceId, targetId, relationshipType, branch = "main", signer, signal) {
|
|
7539
8517
|
this.logger.verbose("removeRelationship(@sourceId, @targetId, @relationshipType, @branch)", sourceId, targetId, relationshipType, branch);
|
|
7540
8518
|
throwIfAborted(signal, () => new AbortError());
|
|
7541
8519
|
let actions = [removeRelationshipAction(sourceId, targetId, relationshipType)];
|
|
7542
|
-
if (signer) actions = await signActions(actions, signer,
|
|
8520
|
+
if (signer) actions = await signActions(actions, signer, {
|
|
8521
|
+
documentId: sourceId,
|
|
8522
|
+
branch
|
|
8523
|
+
}, signal);
|
|
7543
8524
|
return await this.execute(sourceId, branch, actions, signal);
|
|
7544
8525
|
}
|
|
7545
8526
|
getJobStatus(jobId, signal) {
|
|
@@ -7551,7 +8532,7 @@ var Reactor = class {
|
|
|
7551
8532
|
return Promise.resolve({
|
|
7552
8533
|
id: jobId,
|
|
7553
8534
|
documentId: "",
|
|
7554
|
-
status:
|
|
8535
|
+
status: "FAILED",
|
|
7555
8536
|
createdAtUtcIso: now,
|
|
7556
8537
|
completedAtUtcIso: now,
|
|
7557
8538
|
error: toErrorInfo$1("Job not found"),
|
|
@@ -7643,8 +8624,11 @@ var ReactorBuilder = class {
|
|
|
7643
8624
|
eventBus;
|
|
7644
8625
|
readModelCoordinator;
|
|
7645
8626
|
readModelCoordinatorFactory;
|
|
7646
|
-
signatureVerifier;
|
|
7647
8627
|
kyselyInstance;
|
|
8628
|
+
signer;
|
|
8629
|
+
workerSigner;
|
|
8630
|
+
trustPolicy;
|
|
8631
|
+
workerTrustPolicy;
|
|
7648
8632
|
signalHandlersEnabled = false;
|
|
7649
8633
|
queueInstance;
|
|
7650
8634
|
channelScheme;
|
|
@@ -7743,10 +8727,34 @@ var ReactorBuilder = class {
|
|
|
7743
8727
|
this.eventBus = eventBus;
|
|
7744
8728
|
return this;
|
|
7745
8729
|
}
|
|
7746
|
-
|
|
7747
|
-
|
|
8730
|
+
/**
|
|
8731
|
+
* Signs the operations the executor synthesizes: the NOOP an UNDO becomes and
|
|
8732
|
+
* the action a REDO rebuilds. Without one they are stored unsigned. Pooled
|
|
8733
|
+
* workers import `workerSigner` to build the same signer.
|
|
8734
|
+
*/
|
|
8735
|
+
withSigner(signer, workerSigner) {
|
|
8736
|
+
this.signer = signer;
|
|
8737
|
+
this.workerSigner = workerSigner;
|
|
8738
|
+
return this;
|
|
8739
|
+
}
|
|
8740
|
+
/** Whether {@link withSigner} was called. */
|
|
8741
|
+
hasSigner() {
|
|
8742
|
+
return this.signer !== void 0;
|
|
8743
|
+
}
|
|
8744
|
+
/**
|
|
8745
|
+
* Decides at admission whether a key may sign as the user it claims; see
|
|
8746
|
+
* {@link SignatureTrustPolicy} for the contract and the default. Pooled
|
|
8747
|
+
* workers import `workerTrustPolicy` to build the same policy.
|
|
8748
|
+
*/
|
|
8749
|
+
withTrustPolicy(trustPolicy, workerTrustPolicy) {
|
|
8750
|
+
this.trustPolicy = trustPolicy;
|
|
8751
|
+
this.workerTrustPolicy = workerTrustPolicy;
|
|
7748
8752
|
return this;
|
|
7749
8753
|
}
|
|
8754
|
+
/** Whether {@link withTrustPolicy} was called. */
|
|
8755
|
+
hasTrustPolicy() {
|
|
8756
|
+
return this.trustPolicy !== void 0;
|
|
8757
|
+
}
|
|
7750
8758
|
withKysely(kysely) {
|
|
7751
8759
|
this.kyselyInstance = kysely;
|
|
7752
8760
|
return this;
|
|
@@ -7800,8 +8808,7 @@ var ReactorBuilder = class {
|
|
|
7800
8808
|
* this enables the pool — there is no `enabled` flag. Provide `db`
|
|
7801
8809
|
* (each worker opens its own Postgres pool; the parent database is built
|
|
7802
8810
|
* from it too unless {@link withKysely} is set) or a custom `factory`
|
|
7803
|
-
* transport.
|
|
7804
|
-
* omitted = no executor-side signature verification.
|
|
8811
|
+
* transport.
|
|
7805
8812
|
*/
|
|
7806
8813
|
withWorkerPool(options) {
|
|
7807
8814
|
this.workerPool = options;
|
|
@@ -7876,11 +8883,12 @@ var ReactorBuilder = class {
|
|
|
7876
8883
|
if (resolver instanceof DocumentModelResolver) resolver.setModelLoadedHook((documentType) => eventBus.emit(ReactorEventTypes.MODEL_LOADED, { documentType }));
|
|
7877
8884
|
const queue = this.queueInstance ?? new InMemoryQueue(eventBus, resolver);
|
|
7878
8885
|
const jobTracker = new InMemoryJobTracker(eventBus);
|
|
7879
|
-
const
|
|
8886
|
+
const cacheConfig = {
|
|
7880
8887
|
maxDocuments: this.writeCacheConfig?.maxDocuments ?? 100,
|
|
7881
8888
|
ringBufferSize: this.writeCacheConfig?.ringBufferSize ?? 10,
|
|
7882
8889
|
keyframeInterval: this.writeCacheConfig?.keyframeInterval ?? 10
|
|
7883
|
-
}
|
|
8890
|
+
};
|
|
8891
|
+
const writeCache = new KyselyWriteCache(keyframeStore, operationStore, documentModelRegistry, cacheConfig);
|
|
7884
8892
|
await writeCache.startup();
|
|
7885
8893
|
const operationIndex = new KyselyOperationIndex(database);
|
|
7886
8894
|
const documentMetaCache = new DocumentMetaCache(operationStore, { maxDocuments: 1e3 });
|
|
@@ -7889,33 +8897,49 @@ var ReactorBuilder = class {
|
|
|
7889
8897
|
const executionScope = new KyselyExecutionScope(database, operationStore, operationIndex, keyframeStore, writeCache, documentMetaCache, collectionMembershipCache);
|
|
7890
8898
|
let executorManager = this.executorManager;
|
|
7891
8899
|
let executorStartCount = this.executorConfig.maxConcurrency ?? 1;
|
|
7892
|
-
if (!executorManager)
|
|
7893
|
-
|
|
7894
|
-
|
|
7895
|
-
|
|
7896
|
-
if (
|
|
7897
|
-
|
|
7898
|
-
|
|
7899
|
-
|
|
7900
|
-
|
|
7901
|
-
|
|
7902
|
-
|
|
7903
|
-
|
|
8900
|
+
if (!executorManager) {
|
|
8901
|
+
if (this.workerPool) {
|
|
8902
|
+
const pool = this.workerPool;
|
|
8903
|
+
let factory = pool.factory;
|
|
8904
|
+
if (!factory) {
|
|
8905
|
+
if (pool.db === void 0) throw new Error("unreachable: worker pool configured without db or factory");
|
|
8906
|
+
if (this.signer && !this.workerSigner) this.logger.warn("Worker pool has no signer spec; pooled workers store synthesized operations unsigned");
|
|
8907
|
+
if (this.trustPolicy && !this.workerTrustPolicy) this.logger.warn("Worker pool has no trust policy spec; pooled workers apply the default trust policy");
|
|
8908
|
+
factory = await this.createDefaultWorkerFactory(pool.numWorkers, pool.db);
|
|
8909
|
+
}
|
|
8910
|
+
const poolManager = new WorkerPoolJobExecutorManager(factory, eventBus, queue, jobTracker, this.logger, resolver, operationIndex, this.executorConfig.jobTimeoutMs, this.executorConfig.deferredJobTtlMs);
|
|
8911
|
+
executorManager = poolManager;
|
|
8912
|
+
executorStartCount = pool.numWorkers;
|
|
8913
|
+
if (resolver instanceof DocumentModelResolver) resolver.setBroadcastHook((entry) => poolManager.loadModel(entry));
|
|
8914
|
+
} else executorManager = new SimpleJobExecutorManager(() => new SimpleJobExecutor(this.logger, documentModelRegistry, operationStore, eventBus, writeCache, operationIndex, documentMetaCache, collectionMembershipCache, this.driveContainerTypes, this.executorConfig, executionScope, this.signer, this.trustPolicy), eventBus, queue, jobTracker, this.logger, resolver, this.executorConfig.jobTimeoutMs, this.executorConfig.deferredJobTtlMs);
|
|
8915
|
+
}
|
|
7904
8916
|
await executorManager.start(executorStartCount);
|
|
7905
|
-
const callerReadModels = Array.from(new Set([...this.readModels]));
|
|
8917
|
+
const callerReadModels = Array.from(/* @__PURE__ */ new Set([...this.readModels]));
|
|
8918
|
+
const degradedComponents = [];
|
|
8919
|
+
const startDegraded = (component, error) => {
|
|
8920
|
+
degradedComponents.push({
|
|
8921
|
+
component,
|
|
8922
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
8923
|
+
});
|
|
8924
|
+
this.logger?.error("Reactor component started degraded: @component", component, error);
|
|
8925
|
+
};
|
|
8926
|
+
const readModelIndexing = {
|
|
8927
|
+
commitChunkSize: 50,
|
|
8928
|
+
yieldDeadlineMs: this.executorConfig.yieldDeadlineMs ?? 50
|
|
8929
|
+
};
|
|
7906
8930
|
const documentViewConsistencyTracker = new ConsistencyTracker();
|
|
7907
|
-
const documentView = new KyselyDocumentView(database, operationStore, operationIndex, writeCache, documentViewConsistencyTracker, featureFlags.documentDecisions);
|
|
8931
|
+
const documentView = new KyselyDocumentView(database, operationStore, operationIndex, writeCache, documentViewConsistencyTracker, featureFlags.documentDecisions ? "StateAtDeletion" : "NotFound", readModelIndexing);
|
|
7908
8932
|
try {
|
|
7909
8933
|
await documentView.init();
|
|
7910
8934
|
} catch (error) {
|
|
7911
|
-
|
|
8935
|
+
startDegraded("document view", error);
|
|
7912
8936
|
}
|
|
7913
8937
|
const documentIndexerConsistencyTracker = new ConsistencyTracker();
|
|
7914
|
-
const documentIndexer = new KyselyDocumentIndexer(database, operationIndex, writeCache, documentIndexerConsistencyTracker);
|
|
8938
|
+
const documentIndexer = new KyselyDocumentIndexer(database, operationIndex, writeCache, documentIndexerConsistencyTracker, readModelIndexing);
|
|
7915
8939
|
try {
|
|
7916
8940
|
await documentIndexer.init();
|
|
7917
8941
|
} catch (error) {
|
|
7918
|
-
|
|
8942
|
+
startDegraded("document indexer", error);
|
|
7919
8943
|
}
|
|
7920
8944
|
const subscriptionManager = new ReactorSubscriptionManager(new DefaultSubscriptionErrorHandler());
|
|
7921
8945
|
const subscriptionNotificationReadModel = new SubscriptionNotificationReadModel(subscriptionManager, documentView);
|
|
@@ -7924,9 +8948,9 @@ var ReactorBuilder = class {
|
|
|
7924
8948
|
try {
|
|
7925
8949
|
await processorManager.init();
|
|
7926
8950
|
} catch (error) {
|
|
7927
|
-
|
|
8951
|
+
startDegraded("processor manager", error);
|
|
7928
8952
|
}
|
|
7929
|
-
for (const factory of this.readModelFactories) {
|
|
8953
|
+
for (const [index, factory] of this.readModelFactories.entries()) try {
|
|
7930
8954
|
const readModel = await factory({
|
|
7931
8955
|
documentModelRegistry,
|
|
7932
8956
|
operationIndex,
|
|
@@ -7934,6 +8958,8 @@ var ReactorBuilder = class {
|
|
|
7934
8958
|
processorManagerConsistencyTracker
|
|
7935
8959
|
});
|
|
7936
8960
|
callerReadModels.push(readModel);
|
|
8961
|
+
} catch (error) {
|
|
8962
|
+
startDegraded(`read model ${index}${factory.name ? ` (${factory.name})` : ""}`, error);
|
|
7937
8963
|
}
|
|
7938
8964
|
const readModelInstances = [
|
|
7939
8965
|
...callerReadModels,
|
|
@@ -7952,13 +8978,13 @@ var ReactorBuilder = class {
|
|
|
7952
8978
|
processorManager,
|
|
7953
8979
|
documentView,
|
|
7954
8980
|
documentIndexer,
|
|
7955
|
-
createProjectionShardManager: (config) => this.createProjectionShardManager(config, eventBus, hostTrackers, false),
|
|
8981
|
+
createProjectionShardManager: (config) => this.createProjectionShardManager(config, eventBus, hostTrackers, readModelIndexing, false),
|
|
7956
8982
|
registerShutdownHook: (hook) => this.shutdownHooks.push(hook)
|
|
7957
|
-
}) : this.projectionShardConfig ? await this.createProjectionShardManager(this.projectionShardConfig, eventBus, hostTrackers, true) : new ReadModelCoordinator(eventBus, readModelInstances, [subscriptionNotificationReadModel, processorManager]);
|
|
8983
|
+
}) : this.projectionShardConfig ? await this.createProjectionShardManager(this.projectionShardConfig, eventBus, hostTrackers, readModelIndexing, true) : new ReadModelCoordinator(eventBus, readModelInstances, [subscriptionNotificationReadModel, processorManager]);
|
|
7958
8984
|
const reactor = new Reactor(this.logger, documentModelRegistry, queue, jobTracker, readModelCoordinator, this.features, documentView, documentIndexer, operationStore, eventBus, executorManager);
|
|
7959
8985
|
let syncModule = void 0;
|
|
7960
8986
|
if (this.channelScheme) {
|
|
7961
|
-
const factory = this.channelScheme ===
|
|
8987
|
+
const factory = this.channelScheme === "connect" ? new GqlRequestChannelFactory(this.logger, this.jwtHandler, queue) : new GqlResponseChannelFactory(this.logger);
|
|
7962
8988
|
syncModule = new SyncBuilder().withChannelFactory(factory).buildModule(reactor, this.logger, operationIndex, eventBus, database, this.driveContainerTypes);
|
|
7963
8989
|
await syncModule.syncManager.startup();
|
|
7964
8990
|
} else if (this.syncBuilder) {
|
|
@@ -7993,8 +9019,10 @@ var ReactorBuilder = class {
|
|
|
7993
9019
|
syncModule,
|
|
7994
9020
|
reactor,
|
|
7995
9021
|
groupReevaluationTrigger,
|
|
7996
|
-
pools: this.instrumentedPools
|
|
9022
|
+
pools: this.instrumentedPools,
|
|
9023
|
+
degradedComponents
|
|
7997
9024
|
};
|
|
9025
|
+
if (degradedComponents.length > 0) this.logger.warn("Reactor started with @count degraded component(s): @components", degradedComponents.length, degradedComponents.map(({ component }) => component).join(", "));
|
|
7998
9026
|
if (this.signalHandlersEnabled) this.attachSignalHandlers(module);
|
|
7999
9027
|
return module;
|
|
8000
9028
|
}
|
|
@@ -8023,11 +9051,15 @@ var ReactorBuilder = class {
|
|
|
8023
9051
|
* models never index an operation, so the manager advances these from the
|
|
8024
9052
|
* shards' relayed indexing reports; without them every read carrying a
|
|
8025
9053
|
* consistency token waits forever.
|
|
9054
|
+
* @param indexing The chunking bounds the host's own read models index
|
|
9055
|
+
* under. The shards' read models are built inside the worker, so without
|
|
9056
|
+
* this they would fall back to the library default and a host that tuned
|
|
9057
|
+
* the cadence would silently get it on the in-process path only.
|
|
8026
9058
|
* @param registerShutdownHook Whether the builder owns `manager.shutdown()`
|
|
8027
9059
|
* at signal time. False for the coordinator-factory path, whose factory
|
|
8028
9060
|
* registers its own hook so host chains drain before the worker stops.
|
|
8029
9061
|
*/
|
|
8030
|
-
async createProjectionShardManager(config, eventBus, consistencyTrackers, registerShutdownHook) {
|
|
9062
|
+
async createProjectionShardManager(config, eventBus, consistencyTrackers, indexing, registerShutdownHook) {
|
|
8031
9063
|
const parentDb = this.resolveReactorDbConfig();
|
|
8032
9064
|
const baseDb = config.db ?? parentDb;
|
|
8033
9065
|
const caller = this.projectionShardConfig !== void 0 ? "withProjectionShards" : "withReadModelCoordinatorFactory";
|
|
@@ -8053,13 +9085,14 @@ var ReactorBuilder = class {
|
|
|
8053
9085
|
poolInstrumentations.push(forwarder);
|
|
8054
9086
|
this.instrumentedPools.push(forwarder);
|
|
8055
9087
|
}
|
|
8056
|
-
const { ProjectionShardManager } = await import("./projection-shard-manager-
|
|
9088
|
+
const { ProjectionShardManager } = await import("./projection-shard-manager-BpcqDtnQ.js");
|
|
8057
9089
|
const manager = new ProjectionShardManager({
|
|
8058
9090
|
shardCount: config.shardCount,
|
|
8059
9091
|
db,
|
|
8060
9092
|
models,
|
|
8061
9093
|
preReadyKinds: config.preReadyKinds,
|
|
8062
9094
|
postReadyKinds: config.postReadyKinds,
|
|
9095
|
+
indexing,
|
|
8063
9096
|
factory,
|
|
8064
9097
|
logger: this.logger,
|
|
8065
9098
|
hostBus: eventBus,
|
|
@@ -8086,7 +9119,7 @@ var ReactorBuilder = class {
|
|
|
8086
9119
|
return manager;
|
|
8087
9120
|
}
|
|
8088
9121
|
async createDefaultProjectionWorkerFactory() {
|
|
8089
|
-
const [{ createProjectionThreadTransport }, { projectionWorkerEntryPath }] = await Promise.all([import("./transport-
|
|
9122
|
+
const [{ createProjectionThreadTransport }, { projectionWorkerEntryPath }] = await Promise.all([import("./transport-B3G50KVd.js"), import("./projection-worker-D8uNFdwX.js")]);
|
|
8090
9123
|
return () => createProjectionThreadTransport(projectionWorkerEntryPath);
|
|
8091
9124
|
}
|
|
8092
9125
|
/**
|
|
@@ -8094,11 +9127,11 @@ var ReactorBuilder = class {
|
|
|
8094
9127
|
* custom `factory`. Each worker spawns a real `node:worker_threads`
|
|
8095
9128
|
* Worker pointing at the compiled `worker/entry.js`.
|
|
8096
9129
|
*/
|
|
8097
|
-
async createDefaultWorkerFactory(numWorkers, db
|
|
9130
|
+
async createDefaultWorkerFactory(numWorkers, db) {
|
|
8098
9131
|
const [{ WorkerHandle }, { createThreadTransport }, { workerEntryPath }] = await Promise.all([
|
|
8099
|
-
import("./worker-handle-
|
|
8100
|
-
import("./transport-
|
|
8101
|
-
|
|
9132
|
+
import("./worker-handle-C2rLUHwv.js"),
|
|
9133
|
+
import("./transport-CZDHzfvy.js"),
|
|
9134
|
+
Promise.resolve().then(() => worker_exports)
|
|
8102
9135
|
]);
|
|
8103
9136
|
const poolConfig = {
|
|
8104
9137
|
enabled: true,
|
|
@@ -8118,9 +9151,10 @@ var ReactorBuilder = class {
|
|
|
8118
9151
|
initPayload: {
|
|
8119
9152
|
poolConfig,
|
|
8120
9153
|
db,
|
|
8121
|
-
signatureVerifier,
|
|
8122
9154
|
models,
|
|
8123
|
-
executorConfig: this.executorConfig
|
|
9155
|
+
executorConfig: this.executorConfig,
|
|
9156
|
+
signer: this.workerSigner,
|
|
9157
|
+
trustPolicy: this.workerTrustPolicy
|
|
8124
9158
|
},
|
|
8125
9159
|
logger,
|
|
8126
9160
|
poolInstrumentation
|
|
@@ -8194,7 +9228,7 @@ var ReactorBuilder = class {
|
|
|
8194
9228
|
}
|
|
8195
9229
|
this.logger.info("Shutdown complete");
|
|
8196
9230
|
nodeProcess.exit = realExit;
|
|
8197
|
-
realExit(pendingExitCode ?? 0);
|
|
9231
|
+
realExit(pendingExitCode || Number(nodeProcess.exitCode ?? 0));
|
|
8198
9232
|
};
|
|
8199
9233
|
nodeProcess.prependListener("SIGINT", () => void handler("SIGINT"));
|
|
8200
9234
|
nodeProcess.prependListener("SIGTERM", () => void handler("SIGTERM"));
|
|
@@ -8363,30 +9397,6 @@ function createHybridProjectionCoordinatorFactory(options = {}) {
|
|
|
8363
9397
|
};
|
|
8364
9398
|
}
|
|
8365
9399
|
//#endregion
|
|
8366
|
-
//#region src/signer/passthrough-signer.ts
|
|
8367
|
-
/**
|
|
8368
|
-
* A no-op signer that returns empty values for all methods.
|
|
8369
|
-
* Used when signing is not required.
|
|
8370
|
-
*/
|
|
8371
|
-
var PassthroughSigner = class {
|
|
8372
|
-
publicKey = {};
|
|
8373
|
-
sign() {
|
|
8374
|
-
return Promise.resolve(new Uint8Array(0));
|
|
8375
|
-
}
|
|
8376
|
-
verify() {
|
|
8377
|
-
return Promise.resolve();
|
|
8378
|
-
}
|
|
8379
|
-
signAction() {
|
|
8380
|
-
return Promise.resolve([
|
|
8381
|
-
"",
|
|
8382
|
-
"",
|
|
8383
|
-
"",
|
|
8384
|
-
"",
|
|
8385
|
-
""
|
|
8386
|
-
]);
|
|
8387
|
-
}
|
|
8388
|
-
};
|
|
8389
|
-
//#endregion
|
|
8390
9400
|
//#region src/core/reactor-client-builder.ts
|
|
8391
9401
|
/**
|
|
8392
9402
|
* Builder class for constructing ReactorClient instances with proper configuration
|
|
@@ -8399,7 +9409,10 @@ var ReactorClientBuilder = class {
|
|
|
8399
9409
|
documentIndexer;
|
|
8400
9410
|
documentView;
|
|
8401
9411
|
signer;
|
|
8402
|
-
|
|
9412
|
+
workerSigner;
|
|
9413
|
+
trustPolicy;
|
|
9414
|
+
workerTrustPolicy;
|
|
9415
|
+
createSignaturePolicy = DEFAULT_SIGNATURE_POLICY;
|
|
8403
9416
|
subscriptionManager;
|
|
8404
9417
|
jobAwaiter;
|
|
8405
9418
|
documentModelLoader;
|
|
@@ -8433,17 +9446,28 @@ var ReactorClientBuilder = class {
|
|
|
8433
9446
|
return this;
|
|
8434
9447
|
}
|
|
8435
9448
|
/**
|
|
8436
|
-
*
|
|
8437
|
-
*
|
|
8438
|
-
*
|
|
9449
|
+
* Signs submitted actions, and the operations a reactor built from
|
|
9450
|
+
* `withReactorBuilder` synthesizes unless that builder has its own signer.
|
|
9451
|
+
* A `trustPolicy` reaches that builder unless it has its own.
|
|
8439
9452
|
*/
|
|
8440
9453
|
withSigner(config) {
|
|
8441
9454
|
if ("signer" in config) {
|
|
8442
9455
|
this.signer = config.signer;
|
|
8443
|
-
this.
|
|
9456
|
+
this.workerSigner = config.workerSigner;
|
|
9457
|
+
this.trustPolicy = config.trustPolicy;
|
|
9458
|
+
this.workerTrustPolicy = config.workerTrustPolicy;
|
|
8444
9459
|
} else this.signer = config;
|
|
8445
9460
|
return this;
|
|
8446
9461
|
}
|
|
9462
|
+
/**
|
|
9463
|
+
* What the client creates new documents as when a call does not say:
|
|
9464
|
+
* `v2-required` (the default) or `legacy`, for a fleet with peers that
|
|
9465
|
+
* predate v2-required documents. Existing documents keep their policy.
|
|
9466
|
+
*/
|
|
9467
|
+
withCreateSignaturePolicy(policy) {
|
|
9468
|
+
this.createSignaturePolicy = policy;
|
|
9469
|
+
return this;
|
|
9470
|
+
}
|
|
8447
9471
|
withSubscriptionManager(subscriptionManager) {
|
|
8448
9472
|
this.subscriptionManager = subscriptionManager;
|
|
8449
9473
|
return this;
|
|
@@ -8508,8 +9532,9 @@ var ReactorClientBuilder = class {
|
|
|
8508
9532
|
let documentView;
|
|
8509
9533
|
let reactorModule;
|
|
8510
9534
|
if (this.reactorBuilder) {
|
|
8511
|
-
if (this.signatureVerifier) this.reactorBuilder.withSignatureVerifier(this.signatureVerifier);
|
|
8512
9535
|
if (this.documentModelLoader) this.reactorBuilder.withDocumentModelLoader(this.documentModelLoader);
|
|
9536
|
+
if (this.signer && !this.reactorBuilder.hasSigner()) this.reactorBuilder.withSigner(this.signer, this.workerSigner);
|
|
9537
|
+
if (this.trustPolicy && !this.reactorBuilder.hasTrustPolicy()) this.reactorBuilder.withTrustPolicy(this.trustPolicy, this.workerTrustPolicy);
|
|
8513
9538
|
reactorModule = await this.reactorBuilder.buildModule();
|
|
8514
9539
|
reactor = reactorModule.reactor;
|
|
8515
9540
|
eventBus = reactorModule.eventBus;
|
|
@@ -8527,7 +9552,7 @@ var ReactorClientBuilder = class {
|
|
|
8527
9552
|
const jobAwaiter = this.jobAwaiter ?? new JobAwaiter(eventBus, (jobId, signal) => reactor.getJobStatus(jobId, signal));
|
|
8528
9553
|
const decisionModel = reactorModule ? readDecisionModel(reactorModule.featureFlags, reactorModule.documentModelRegistry) : void 0;
|
|
8529
9554
|
return {
|
|
8530
|
-
client: new ReactorClient(this.logger, reactor, signer, subscriptionManager, jobAwaiter, documentIndexer, documentView, this.readGate ?? this.resolveReadGate(reactorModule, documentView, decisionModel), this.resolveActionEvaluation(reactorModule, decisionModel)),
|
|
9555
|
+
client: new ReactorClient(this.logger, reactor, signer, subscriptionManager, jobAwaiter, documentIndexer, documentView, this.readGate ?? this.resolveReadGate(reactorModule, documentView, decisionModel), this.resolveActionEvaluation(reactorModule, decisionModel), this.createSignaturePolicy),
|
|
8531
9556
|
reactor,
|
|
8532
9557
|
eventBus,
|
|
8533
9558
|
documentIndexer,
|
|
@@ -8565,6 +9590,38 @@ function driveIdFromUrl(url) {
|
|
|
8565
9590
|
return url.split("/").pop() ?? "";
|
|
8566
9591
|
}
|
|
8567
9592
|
//#endregion
|
|
9593
|
+
//#region src/signer/types.ts
|
|
9594
|
+
const SIGNATURE_REFUSAL_CODES = [
|
|
9595
|
+
"UNSIGNED_REQUIRED",
|
|
9596
|
+
"UNSIGNED_IDENTITY",
|
|
9597
|
+
"KEY_MISMATCH",
|
|
9598
|
+
"MALFORMED_TUPLE",
|
|
9599
|
+
"TIMESTAMP_MISMATCH",
|
|
9600
|
+
"HASH_MISMATCH",
|
|
9601
|
+
"BAD_SIGNATURE",
|
|
9602
|
+
"SCHEME_BELOW_POLICY",
|
|
9603
|
+
"ID_MISMATCH",
|
|
9604
|
+
"DUPLICATE_ACTION",
|
|
9605
|
+
"SIGNER_UNAUTHORIZED"
|
|
9606
|
+
];
|
|
9607
|
+
//#endregion
|
|
9608
|
+
//#region src/executor/worker/index.ts
|
|
9609
|
+
var worker_exports = /* @__PURE__ */ __exportAll({
|
|
9610
|
+
createForwardingLogger: () => createForwardingLogger,
|
|
9611
|
+
errorToInfo: () => errorToInfo,
|
|
9612
|
+
sanitizeArg: () => sanitizeArg,
|
|
9613
|
+
workerEntryPath: () => workerEntryPath
|
|
9614
|
+
});
|
|
9615
|
+
function resolveWorkerEntryPath() {
|
|
9616
|
+
const url = new URL("./entry.js", import.meta.url);
|
|
9617
|
+
if (url.protocol !== "file:") return url.href;
|
|
9618
|
+
let pathname = decodeURIComponent(url.pathname);
|
|
9619
|
+
if (/^\/[A-Za-z]:[/\\]/.test(pathname)) pathname = pathname.slice(1);
|
|
9620
|
+
return pathname;
|
|
9621
|
+
}
|
|
9622
|
+
/** Absolute path to the worker entry script for use with `new Worker(workerEntryPath)`. */
|
|
9623
|
+
const workerEntryPath = resolveWorkerEntryPath();
|
|
9624
|
+
//#endregion
|
|
8568
9625
|
//#region src/decision/sync-scope-gate.ts
|
|
8569
9626
|
/** What a subject may read of a document that is not there to be read. */
|
|
8570
9627
|
const META_ONLY = (scope) => ALWAYS_READABLE_SCOPES.has(scope);
|
|
@@ -8576,6 +9633,9 @@ const META_ONLY = (scope) => ALWAYS_READABLE_SCOPES.has(scope);
|
|
|
8576
9633
|
* path's own job, and it is the only thing this adds to the gate it wraps.
|
|
8577
9634
|
*/
|
|
8578
9635
|
var SyncScopeGate = class {
|
|
9636
|
+
gate;
|
|
9637
|
+
documentView;
|
|
9638
|
+
logger;
|
|
8579
9639
|
constructor(gate, documentView, logger) {
|
|
8580
9640
|
this.gate = gate;
|
|
8581
9641
|
this.documentView = documentView;
|
|
@@ -8792,6 +9852,6 @@ var DocumentIntegrityService = class {
|
|
|
8792
9852
|
}
|
|
8793
9853
|
};
|
|
8794
9854
|
//#endregion
|
|
8795
|
-
export { ALWAYS_READABLE_SCOPES, APPEND_CONDITION_FAILED_PREFIX, AppendConditionFailedError, AuthEnforcementDisabledError, BareReadGate, BaseReadModel, ChannelError, ChannelErrorSource, ChannelScheme, ConsistencyTracker, DEFAULT_DRIVE_CONTAINER_TYPES, DOCUMENT_INDEXER_READ_MODEL, DOCUMENT_VIEW_READ_MODEL, DRIVE_AUTH_ERROR_MESSAGES, DefaultSubscriptionErrorHandler, DocumentAlreadyExistsError, DocumentChangeType, DocumentExistence, DocumentIntegrityService, DocumentModelRegistry, DocumentModelResolver, DriveClient, DriveCollectionId, DuplicateManifestError, DuplicateModuleError, DuplicateOperationError, EventBus, EventBusAggregateError, GqlRequestChannel, GqlRequestChannelFactory, GqlResponseChannel, GqlResponseChannelFactory, HybridProjectionCoordinator, SimpleJobExecutor as InMemoryJobExecutor, SimpleJobExecutor, InMemoryJobTracker, InMemoryQueue, IntervalPollTimer, InvalidModuleError, JobAwaiter, JobExecutorEventTypes, JobStatus, KyselyDocumentIndexer, KyselyDocumentView, KyselyKeyframeStore, KyselyOperationStore, KyselySyncCursorStorage, KyselySyncRemoteStorage, KyselyWriteCache, Mailbox, ModelReadGate, ModuleNotFoundError, NullDocumentModelResolver, OptimisticLockError, PollBehavior, PollingChannelError, ProcessorManager, PropagationMode, QueueEventTypes, REACTOR_SCHEMA, RECOVERABLE_GRAPHQL_ERROR_CODES, Reactor, ReactorBuilder, ReactorClient, ReactorClientBuilder, ReactorEventTypes, ReactorSubscriptionManager, ReadModelCoordinator, RelationalDbProcessor, RelationshipChangeType, RetryAccounting, RevisionMismatchError, SeededStateReader, SimpleJobExecutorManager, SyncBuilder, SyncEventTypes, SyncOperation, SyncOperationAggregateError, SyncOperationStatus, SyncScopeGate, SyncStatus, SyncStatusTracker, addRelationshipAction, authDecisionModel, batchOperationsByDocument, buildDecisionModel, classifyJobFailure, consolidateSyncOperations, createDocumentAction, createForwardingLogger, createHybridProjectionCoordinatorFactory, createMutableShutdownStatus, createReactorHostModuleBase, createRelationalDb, decideAtHead, deleteDocumentAction, documentActions, documentDecisionModel, driveIdFromUrl, envelopesToSyncOperations, errorToInfo, firstOutOfOrderPair, getMigrationStatus, instrumentPgPool, isDriveAuthError, isRecoverableGraphQLError, makeConsistencyKey, parseDriveUrl, parsePagingOptions, quarantinesDocument, readDecisionModel, removeRelationshipAction, runMigrations, sanitizeArg, selectDecisionModel, supportsLiveReadModelRegistration, syncOperationErrorType, trimMailboxFromAckOrdinal, updateRelationshipAction, upgradeDocumentAction, workerEntryPath };
|
|
9855
|
+
export { ALWAYS_READABLE_SCOPES, APPEND_CONDITION_FAILED_PREFIX, AppendConditionFailedError, AuthEnforcementDisabledError, BareReadGate, BaseReadModel, ChannelError, ChannelErrorSource, ChannelScheme, ConsistencyTracker, DEFAULT_COMMIT_CHUNK_SIZE, DEFAULT_DRIVE_CONTAINER_TYPES, DEFAULT_READ_MODEL_YIELD_DEADLINE_MS, DOCUMENT_INDEXER_READ_MODEL, DOCUMENT_VIEW_READ_MODEL, DRIVE_AUTH_ERROR_MESSAGES, DefaultSubscriptionErrorHandler, DeletedDocumentRead, DocumentAlreadyExistsError, DocumentChangeType, DocumentExistence, DocumentIntegrityService, DocumentModelRegistry, DocumentModelResolver, DriveClient, DriveCollectionId, DriveRequestError, DuplicateManifestError, DuplicateModuleError, DuplicateOperationError, EventBus, EventBusAggregateError, GqlRequestChannel, GqlRequestChannelFactory, GqlResponseChannel, GqlResponseChannelFactory, HybridProjectionCoordinator, SimpleJobExecutor as InMemoryJobExecutor, SimpleJobExecutor, InMemoryJobTracker, InMemoryQueue, IntervalPollTimer, InvalidModuleError, InvalidSignatureError, JobAwaiter, JobExecutorEventTypes, JobStatus, KyselyDocumentIndexer, KyselyDocumentView, KyselyKeyframeStore, KyselyOperationStore, KyselySyncCursorStorage, KyselySyncRemoteStorage, KyselyWriteCache, Mailbox, ModelReadGate, ModuleNotFoundError, NullDocumentModelResolver, OptimisticLockError, PollBehavior, PollingChannelError, ProcessorManager, PropagationMode, QueueEventTypes, REACTOR_SCHEMA, RECOVERABLE_GRAPHQL_ERROR_CODES, Reactor, ReactorBuilder, ReactorClient, ReactorClientBuilder, ReactorEventTypes, ReactorSubscriptionManager, ReadModelCoordinator, RelationalDbProcessor, RelationshipChangeType, RelationshipNotFoundError, RetryAccounting, RevisionMismatchError, SIGNATURE_REFUSAL_CODES, SeededStateReader, SimpleJobExecutorManager, SyncBuilder, SyncEventTypes, SyncOperation, SyncOperationAggregateError, SyncOperationStatus, SyncScopeGate, SyncStatus, SyncStatusTracker, addRelationshipAction, authDecisionModel, batchOperationsByDocument, buildDecisionModel, classifyJobFailure, consolidateSyncOperations, createDocumentAction, createForwardingLogger, createHybridProjectionCoordinatorFactory, createMutableShutdownStatus, createReactorHostModuleBase, createRelationalDb, decideAtHead, defaultReadModelIndexingConfig, deleteDocumentAction, documentActions, documentDecisionModel, driveIdFromUrl, envelopesToSyncOperations, errorToInfo, firstOutOfOrderPair, getMigrationStatus, instrumentPgPool, isDriveAuthError, isRecoverableGraphQLError, makeConsistencyKey, parseDriveUrl, parsePagingOptions, quarantinesDocument, readDecisionModel, removeRelationshipAction, runMigrations, sanitizeArg, selectDecisionModel, supportsLiveReadModelRegistration, syncOperationErrorType, trimMailboxFromAckOrdinal, unchunkedReadModelIndexingConfig, updateRelationshipAction, upgradeDocumentAction, verifyActionSignature, workerEntryPath };
|
|
8796
9856
|
|
|
8797
9857
|
//# sourceMappingURL=index.js.map
|