@powerhousedao/reactor 6.2.3-dev.50 → 6.2.3-dev.52
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/{document-indexer-DwAMDVRI.js → document-indexer-3fkVWCaN.js} +26 -14
- package/dist/document-indexer-3fkVWCaN.js.map +1 -0
- package/dist/{drive-container-types-Ci1pdv4X.js → drive-container-types-BAfFzBOf.js} +133 -7
- package/dist/drive-container-types-BAfFzBOf.js.map +1 -0
- package/dist/entry.js +2 -2
- package/dist/{forwarding-logger-ByfBFgMS.js → forwarding-logger-B9LLXItm.js} +111 -79
- package/dist/forwarding-logger-B9LLXItm.js.map +1 -0
- package/dist/{index-CNhT69De.d.ts → index-dDm0fUBT.d.ts} +518 -68
- package/dist/index-dDm0fUBT.d.ts.map +1 -0
- package/dist/index.d.ts +1040 -138
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3110 -776
- package/dist/index.js.map +1 -1
- package/dist/projection-entry.js +2 -2
- package/dist/{projection-shard-manager-CQ5FwQmv.js → projection-shard-manager-Drl9w8Eo.js} +3 -13
- package/dist/projection-shard-manager-Drl9w8Eo.js.map +1 -0
- package/dist/rpc.d.ts +32 -6
- package/dist/rpc.d.ts.map +1 -1
- package/dist/rpc.js +8 -4
- package/dist/rpc.js.map +1 -1
- package/dist/{worker-handle-CN2-oRv-.js → worker-handle-Nso8i5eU.js} +2 -2
- package/dist/{worker-handle-CN2-oRv-.js.map → worker-handle-Nso8i5eU.js.map} +1 -1
- package/dist/worker-pool-router-BROOEQSE.js +35 -0
- package/dist/worker-pool-router-BROOEQSE.js.map +1 -0
- package/package.json +3 -3
- package/dist/document-indexer-DwAMDVRI.js.map +0 -1
- package/dist/drive-container-types-Ci1pdv4X.js.map +0 -1
- package/dist/forwarding-logger-ByfBFgMS.js.map +0 -1
- package/dist/index-CNhT69De.d.ts.map +0 -1
- package/dist/projection-shard-manager-CQ5FwQmv.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import { $ as DocumentExistence, A as GATED_DOCUMENT_ACTIONS, C as describeWaitingSessions, Ct as
|
|
2
|
-
import { _ as
|
|
1
|
+
import { $ as DocumentExistence, A as GATED_DOCUMENT_ACTIONS, C as describeWaitingSessions, Ct as ReactorEventTypes, D as CollectionMembershipCache, Dt as parsePagingOptions, E as DocumentMetaCache, Et as matchesScope, J as targetDocumentId, K as submittedActionIds, L as createConsistencyToken, Ot as throwIfAborted, Q as DocumentAlreadyExistsError, R as createDocumentFromAction, S as createKyselyWatermarkProbe, St as EventBusAggregateError, T as snapshotFunctionsFor, X as APPEND_CONDITION_FAILED_PREFIX, Z as AppendConditionFailedError, _ as acquirePurgeLocks, _t as RelationshipNotFoundError, a as REACTOR_SCHEMA, at as AuthorizationDeniedError, b as ProbeSettler, bt as UnsupportedStoredProtocolError, c as createForwardingPoolInstrumentation, ct as DocumentNotDeletedError, d as KyselyKeyframeStore, dt as ExcessiveReshuffleError, et as DuplicateOperationError, f as EventBus, ft as GroupInUseError, g as PURGE_NS, gt as PurgeTooLargeError, h as PURGE_LOCK_BUCKETS, ht as JOB_NOT_FOUND_ERROR_NAME, it as AuthTimestampNotMonotonicError, jt as __exportAll, kt as withDeadline, l as instrumentPgPool, lt as DocumentNotFoundError, m as KyselyOperationIndex, mt as InvalidSignatureError, n as errorToInfo, nt as RevisionMismatchError, o as getMigrationStatus, ot as BatchJobFailedError, p as KyselyWriteCache, pt as InvalidOperationTimestampError, q as summarizeSubmittedActions, r as sanitizeArg, rt as AuthEnforcementDisabledError, s as runMigrations, st as DocumentDeletedError, t as createForwardingLogger, tt as OptimisticLockError, u as KyselyOperationStore, ut as DocumentPurgedError, v as findPurged, vt as ReservedActionError, w as parseSnapshot, wt as TIMED_OUT, x as SettledWatermark, xt as UpgradePreconditionFailedError, y as listPurged, yt as UnsupportedProtocolVersionError, z as createEmptyConsistencyToken } from "./forwarding-logger-B9LLXItm.js";
|
|
2
|
+
import { C as PropagationMode, S as JobStatus, T as cursorProtectedLoadMeta, _ as selectDecisionModel, a as NoopStorageFlusher, b as buildDecisionModel, c as DEFAULT_PURGE_DELETE_BATCH, d as resolveFeatureFlags, f as DEFAULT_DEFERRED_JOB_TTL_MS, g as decideAtHead, h as JobExecutorEventTypes, i as PassthroughSigner, l as KyselyDocumentPurger, m as DEFAULT_MAX_PURGE_OPERATIONS, n as SimpleJobExecutor, o as StoragePoisonedError, p as DEFAULT_JOB_TIMEOUT_MS, r as verifyActionSignature, s as KyselyExecutionScope, t as DEFAULT_DRIVE_CONTAINER_TYPES, u as appliedDelete, v as documentDecisionModel, w as RelationshipChangeType, x as DriveCollectionId, y as authDecisionModel } from "./drive-container-types-BAfFzBOf.js";
|
|
3
3
|
import { a as DuplicateModuleError, c as ModuleNotFoundError, i as DuplicateManifestError, n as loadDocumentModelSpec, o as InvalidModuleError, r as DocumentModelRegistry, s as ModelNotWorkerImportableError } from "./load-spec-ZLjuyhxz.js";
|
|
4
4
|
import { i as WorkerInitFailedError, r as WorkerExitedError, t as WorkerAbortTimeoutError } from "./errors-BM_ULVWd.js";
|
|
5
|
-
import {
|
|
5
|
+
import { n as hashDocumentId, t as bucketFor } from "./worker-pool-router-BROOEQSE.js";
|
|
6
|
+
import { S as isCatchUpConsumer, _ as defaultReadModelIndexingConfig, a as KyselyDocumentView, b as defaultCatchUpConfig, c as ReadModelCoordinator, d as releaseBatch, f as reserveBatch, g as DEFAULT_READ_MODEL_YIELD_DEADLINE_MS, h as DEFAULT_COMMIT_CHUNK_SIZE, i as DeletedDocumentRead, l as indexReserved, m as BaseReadModel, n as ConsistencyTracker, o as DOCUMENT_INDEXER_READ_MODEL, p as spliceReadModel, r as makeConsistencyKey, s as DOCUMENT_VIEW_READ_MODEL, t as KyselyDocumentIndexer, u as indexStage, v as unchunkedReadModelIndexingConfig, x as CatchUpScheduler, y as ContiguousCursor } from "./document-indexer-3fkVWCaN.js";
|
|
6
7
|
import { DEFAULT_SIGNATURE_POLICY, DOCUMENT_PURGE_PROTOCOL, DowngradeNotSupportedError, MARKER_REFUSAL_FEATURE, MAX_AUTH_GRANTS, PEER_CAPABILITIES, PURGE_DOCUMENT, UnsupportedDocumentModelVersionError, actionSignerIdentity, actionSigningTarget, actions, actions as documentActions, canonicalJson, coversLocal, createCopyHeader, decide, deserializeSignature, garbageCollect, generateId, groupDocumentType, groupMembershipActionTypes, hashDocumentStateForScope, holdReason, isOlderManifest, isPurgeMarker, isPurgeMarker as isPurgeMarker$1, legacySupports, localPeerManifest, localSupports, mergePeerCapabilities, normalizeDocumentModelVersion, peerSupports, purgedProtocolVersions, readPeerManifest, requestedSignaturePolicy, selectProtocolVersions, sortOperations, toTransportAction, withSignaturePolicy } from "@powerhousedao/shared/document-model";
|
|
7
8
|
import { addFile, addFolder, copyNode, deleteNode, driveCreateDocument, generateNodesCopy, getDescendants, handleTargetNameCollisions, isFileNode, isFolderNode, moveNode, updateNode } from "@powerhousedao/shared/document-drive";
|
|
8
9
|
import { v4 } from "uuid";
|
|
9
10
|
import { ConsoleLogger, childLogger } from "document-model";
|
|
10
|
-
import { CompiledQuery, sql } from "kysely";
|
|
11
|
+
import { CompiledQuery, Kysely, sql } from "kysely";
|
|
11
12
|
import { PGliteDialect } from "kysely-pglite-dialect";
|
|
12
13
|
import { RelationalDbProcessor, createRelationalDb } from "@powerhousedao/shared/processors";
|
|
13
14
|
//#region src/actions/index.ts
|
|
@@ -271,49 +272,7 @@ function buildSingleJobMeta(jobId, callerMeta) {
|
|
|
271
272
|
};
|
|
272
273
|
}
|
|
273
274
|
//#endregion
|
|
274
|
-
//#region src/shared/types.ts
|
|
275
|
-
/**
|
|
276
|
-
* Enum that determines deletion propagation.
|
|
277
|
-
*/
|
|
278
|
-
let PropagationMode = /* @__PURE__ */ function(PropagationMode) {
|
|
279
|
-
PropagationMode["None"] = "none";
|
|
280
|
-
PropagationMode["Cascade"] = "cascade";
|
|
281
|
-
return PropagationMode;
|
|
282
|
-
}({});
|
|
283
|
-
/**
|
|
284
|
-
* Enum that describes the type of relationship change.
|
|
285
|
-
*/
|
|
286
|
-
let RelationshipChangeType = /* @__PURE__ */ function(RelationshipChangeType) {
|
|
287
|
-
RelationshipChangeType["Added"] = "added";
|
|
288
|
-
RelationshipChangeType["Removed"] = "removed";
|
|
289
|
-
return RelationshipChangeType;
|
|
290
|
-
}({});
|
|
291
|
-
/**
|
|
292
|
-
* Job execution statuses
|
|
293
|
-
*/
|
|
294
|
-
let JobStatus = /* @__PURE__ */ function(JobStatus) {
|
|
295
|
-
/** Job is queued but not yet started */
|
|
296
|
-
JobStatus["PENDING"] = "PENDING";
|
|
297
|
-
/** Job is currently being executed */
|
|
298
|
-
JobStatus["RUNNING"] = "RUNNING";
|
|
299
|
-
/** Operations have been written to the operation store (JOB_WRITE_READY event) */
|
|
300
|
-
JobStatus["WRITE_READY"] = "WRITE_READY";
|
|
301
|
-
/** Read models have finished indexing operations (JOB_READ_READY event) */
|
|
302
|
-
JobStatus["READ_READY"] = "READ_READY";
|
|
303
|
-
/** Job failed (may be retried) */
|
|
304
|
-
JobStatus["FAILED"] = "FAILED";
|
|
305
|
-
return JobStatus;
|
|
306
|
-
}({});
|
|
307
|
-
//#endregion
|
|
308
275
|
//#region src/client/drive-client.ts
|
|
309
|
-
/**
|
|
310
|
-
* Implementation of {@link IDriveClient}.
|
|
311
|
-
*
|
|
312
|
-
* Holds a back-reference to its parent {@link IReactorClient} for read and
|
|
313
|
-
* single-document write primitives, plus direct access to {@link IReactor}
|
|
314
|
-
* for batch execution. The back-reference is captured but never invoked
|
|
315
|
-
* during construction, so the partial-`this` hazard does not apply.
|
|
316
|
-
*/
|
|
317
276
|
/** A copy keeps its source's versions; selection fills only the keys it lacks. */
|
|
318
277
|
function withSelectedVersions(header, selected) {
|
|
319
278
|
return {
|
|
@@ -327,15 +286,16 @@ function withSelectedVersions(header, selected) {
|
|
|
327
286
|
var DriveClient = class {
|
|
328
287
|
client;
|
|
329
288
|
logger;
|
|
330
|
-
reactor;
|
|
331
289
|
signer;
|
|
332
290
|
resolveReference;
|
|
333
|
-
|
|
291
|
+
executeBatch;
|
|
292
|
+
/** `submitter` may still be an IReactor, whose executeBatch is used. */
|
|
293
|
+
constructor(client, logger, submitter, signer, resolveReference) {
|
|
334
294
|
this.client = client;
|
|
335
295
|
this.logger = logger;
|
|
336
|
-
this.reactor = reactor;
|
|
337
296
|
this.signer = signer;
|
|
338
297
|
this.resolveReference = resolveReference;
|
|
298
|
+
this.executeBatch = typeof submitter === "function" ? submitter : (request, signal) => submitter.executeBatch(request, signal);
|
|
339
299
|
}
|
|
340
300
|
async create(input, signal) {
|
|
341
301
|
this.logger.verbose("drives.create(@input)", input);
|
|
@@ -535,7 +495,7 @@ var DriveClient = class {
|
|
|
535
495
|
* collision has to be told apart from a transient failure.
|
|
536
496
|
*/
|
|
537
497
|
async runJobs(jobs, signal) {
|
|
538
|
-
const batchResult = await this.
|
|
498
|
+
const batchResult = await this.executeBatch({ jobs }, signal);
|
|
539
499
|
const completedJobs = await Promise.all(Object.values(batchResult.jobs).map((job) => this.client.waitForJob(job, signal)));
|
|
540
500
|
for (const job of completedJobs) if (job.status === "FAILED") throw this.toError(job);
|
|
541
501
|
return completedJobs;
|
|
@@ -592,6 +552,139 @@ var DriveClient = class {
|
|
|
592
552
|
}
|
|
593
553
|
};
|
|
594
554
|
//#endregion
|
|
555
|
+
//#region src/client/types.ts
|
|
556
|
+
/**
|
|
557
|
+
* Describes the types of document changes that can occur.
|
|
558
|
+
*/
|
|
559
|
+
let DocumentChangeType = /* @__PURE__ */ function(DocumentChangeType) {
|
|
560
|
+
DocumentChangeType["Created"] = "created";
|
|
561
|
+
DocumentChangeType["Deleted"] = "deleted";
|
|
562
|
+
DocumentChangeType["Updated"] = "updated";
|
|
563
|
+
DocumentChangeType["ParentAdded"] = "parent_added";
|
|
564
|
+
DocumentChangeType["ParentRemoved"] = "parent_removed";
|
|
565
|
+
DocumentChangeType["ChildAdded"] = "child_added";
|
|
566
|
+
DocumentChangeType["ChildRemoved"] = "child_removed";
|
|
567
|
+
return DocumentChangeType;
|
|
568
|
+
}({});
|
|
569
|
+
//#endregion
|
|
570
|
+
//#region src/client/derivations.ts
|
|
571
|
+
/** The signed jobs that create `document`, and attach it to `parentId`. */
|
|
572
|
+
async function buildCreateJobs(document, parentId, signer, signal) {
|
|
573
|
+
const documentId = document.header.id;
|
|
574
|
+
const branch = document.header.branch || "main";
|
|
575
|
+
const createInput = {
|
|
576
|
+
model: document.header.documentType,
|
|
577
|
+
version: 0,
|
|
578
|
+
documentId,
|
|
579
|
+
signing: {
|
|
580
|
+
signature: documentId,
|
|
581
|
+
publicKey: document.header.sig.publicKey,
|
|
582
|
+
nonce: document.header.sig.nonce,
|
|
583
|
+
createdAtUtcIso: document.header.createdAtUtcIso,
|
|
584
|
+
documentType: document.header.documentType
|
|
585
|
+
},
|
|
586
|
+
slug: document.header.slug,
|
|
587
|
+
name: document.header.name,
|
|
588
|
+
branch: document.header.branch,
|
|
589
|
+
meta: document.header.meta,
|
|
590
|
+
protocolVersions: document.header.protocolVersions ?? { "base-reducer": 2 }
|
|
591
|
+
};
|
|
592
|
+
const createActions = await signActions([createDocumentAction(createInput), upgradeDocumentAction({
|
|
593
|
+
documentId,
|
|
594
|
+
model: document.header.documentType,
|
|
595
|
+
fromVersion: 0,
|
|
596
|
+
toVersion: normalizeDocumentModelVersion(document.state.document?.version),
|
|
597
|
+
initialState: document.state
|
|
598
|
+
})], signer, {
|
|
599
|
+
documentId,
|
|
600
|
+
branch
|
|
601
|
+
}, signal);
|
|
602
|
+
const jobs = [{
|
|
603
|
+
key: "create",
|
|
604
|
+
documentId,
|
|
605
|
+
scope: getSharedActionScope(createActions),
|
|
606
|
+
branch,
|
|
607
|
+
actions: createActions,
|
|
608
|
+
dependsOn: []
|
|
609
|
+
}];
|
|
610
|
+
if (parentId) {
|
|
611
|
+
const parentActions = await signActions([addRelationshipAction(parentId, documentId, "child")], signer, {
|
|
612
|
+
documentId: parentId,
|
|
613
|
+
branch: "main"
|
|
614
|
+
}, signal);
|
|
615
|
+
jobs.push({
|
|
616
|
+
key: "parent",
|
|
617
|
+
documentId: parentId,
|
|
618
|
+
scope: getSharedActionScope(parentActions),
|
|
619
|
+
branch: "main",
|
|
620
|
+
actions: parentActions,
|
|
621
|
+
dependsOn: ["create"]
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
return jobs;
|
|
625
|
+
}
|
|
626
|
+
/** The registered module for a type: the requested version, else the latest. */
|
|
627
|
+
function selectDocumentModelModule(modules, documentModelType, documentModelVersion) {
|
|
628
|
+
const matching = modules.filter((m) => m.documentModel.global.id === documentModelType);
|
|
629
|
+
if (documentModelVersion !== void 0) {
|
|
630
|
+
const requested = normalizeDocumentModelVersion(documentModelVersion);
|
|
631
|
+
const module = matching.find((m) => normalizeDocumentModelVersion(m.version) === requested);
|
|
632
|
+
if (!module) throw new Error(`Document model not found for type: ${documentModelType} with version: ${documentModelVersion}`);
|
|
633
|
+
return module;
|
|
634
|
+
}
|
|
635
|
+
const latest = matching.reduce((best, current) => {
|
|
636
|
+
if (best === void 0) return current;
|
|
637
|
+
return normalizeDocumentModelVersion(current.version) > normalizeDocumentModelVersion(best.version) ? current : best;
|
|
638
|
+
}, void 0);
|
|
639
|
+
if (!latest) throw new Error(`Document model not found for type: ${documentModelType}`);
|
|
640
|
+
return latest;
|
|
641
|
+
}
|
|
642
|
+
/** A new, empty document of `module`, under the create defaults given. */
|
|
643
|
+
function createEmptyDocument(module, options, defaultPolicy, baseVersions) {
|
|
644
|
+
const document = withSignaturePolicy(module.utils.createDocument(), requestedSignaturePolicy(options, defaultPolicy), { protocolVersions: {
|
|
645
|
+
...baseVersions,
|
|
646
|
+
...options?.protocolVersions
|
|
647
|
+
} });
|
|
648
|
+
document.state.document.version = normalizeDocumentModelVersion(module.version);
|
|
649
|
+
return document;
|
|
650
|
+
}
|
|
651
|
+
/** Retries from a fresh read when a concurrent edit invalidates the snapshot. */
|
|
652
|
+
async function upgradeDocumentWith(deps, documentIdentifier, toVersion, options, signal) {
|
|
653
|
+
const maxConflictRetries = options?.maxConflictRetries ?? 3;
|
|
654
|
+
let lastConflictMessage = "";
|
|
655
|
+
for (let attempt = 0; attempt <= maxConflictRetries; attempt++) {
|
|
656
|
+
const document = await deps.read(documentIdentifier, void 0, void 0, signal);
|
|
657
|
+
const documentId = document.header.id;
|
|
658
|
+
const documentType = document.header.documentType;
|
|
659
|
+
const branch = document.header.branch || "main";
|
|
660
|
+
const fromVersion = normalizeDocumentModelVersion(document.state.document?.version);
|
|
661
|
+
let targetVersion = toVersion;
|
|
662
|
+
if (targetVersion === void 0) {
|
|
663
|
+
const module = await deps.getDocumentModelModule(documentType);
|
|
664
|
+
targetVersion = normalizeDocumentModelVersion(module.version);
|
|
665
|
+
}
|
|
666
|
+
if (targetVersion === fromVersion) return document;
|
|
667
|
+
if (targetVersion < fromVersion) throw new DowngradeNotSupportedError(documentType, fromVersion, targetVersion);
|
|
668
|
+
const action = upgradeDocumentAction({
|
|
669
|
+
documentId,
|
|
670
|
+
model: documentType,
|
|
671
|
+
fromVersion,
|
|
672
|
+
toVersion: targetVersion,
|
|
673
|
+
revision: { ...document.header.revision }
|
|
674
|
+
});
|
|
675
|
+
const signedActions = await signActions([action], deps.signer, {
|
|
676
|
+
documentId,
|
|
677
|
+
branch
|
|
678
|
+
}, signal);
|
|
679
|
+
const jobInfo = await deps.submit(documentId, branch, signedActions, signal);
|
|
680
|
+
const completedJob = await deps.waitForJob(jobInfo, signal);
|
|
681
|
+
if (completedJob.status !== "FAILED") return deps.read(documentId, branch, completedJob.consistencyToken, signal);
|
|
682
|
+
if (completedJob.error?.name !== "UpgradePreconditionFailedError") throw new Error(completedJob.error?.message);
|
|
683
|
+
lastConflictMessage = completedJob.error.message;
|
|
684
|
+
}
|
|
685
|
+
throw new Error(`Upgrade of document ${documentIdentifier} conflicted with concurrent edits after ${maxConflictRetries + 1} attempts: ${lastConflictMessage}`);
|
|
686
|
+
}
|
|
687
|
+
//#endregion
|
|
595
688
|
//#region src/shared/awaiter.ts
|
|
596
689
|
/**
|
|
597
690
|
* Checks if a job status is terminal (job has finished).
|
|
@@ -726,21 +819,6 @@ function decodeCompositeCursor(cursor) {
|
|
|
726
819
|
}
|
|
727
820
|
}
|
|
728
821
|
//#endregion
|
|
729
|
-
//#region src/client/types.ts
|
|
730
|
-
/**
|
|
731
|
-
* Describes the types of document changes that can occur.
|
|
732
|
-
*/
|
|
733
|
-
let DocumentChangeType = /* @__PURE__ */ function(DocumentChangeType) {
|
|
734
|
-
DocumentChangeType["Created"] = "created";
|
|
735
|
-
DocumentChangeType["Deleted"] = "deleted";
|
|
736
|
-
DocumentChangeType["Updated"] = "updated";
|
|
737
|
-
DocumentChangeType["ParentAdded"] = "parent_added";
|
|
738
|
-
DocumentChangeType["ParentRemoved"] = "parent_removed";
|
|
739
|
-
DocumentChangeType["ChildAdded"] = "child_added";
|
|
740
|
-
DocumentChangeType["ChildRemoved"] = "child_removed";
|
|
741
|
-
return DocumentChangeType;
|
|
742
|
-
}({});
|
|
743
|
-
//#endregion
|
|
744
822
|
//#region src/decision/read-gate.ts
|
|
745
823
|
/**
|
|
746
824
|
* Scopes every holder of a document may read, whatever the grants say. Denying
|
|
@@ -1293,7 +1371,7 @@ var ReactorClient = class {
|
|
|
1293
1371
|
this.eventReads = new EventReadsSource(reactor, documentView, readGate);
|
|
1294
1372
|
this.actionEvaluation = actionEvaluation;
|
|
1295
1373
|
this.createSignaturePolicy = createSignaturePolicy;
|
|
1296
|
-
this.drives = new DriveClient(this, logger, reactor, signer, (id, signal) => this.resolveReference(id, "main", signal));
|
|
1374
|
+
this.drives = new DriveClient(this, logger, (request, signal) => reactor.executeBatch(request, signal), signer, (id, signal) => this.resolveReference(id, "main", signal));
|
|
1297
1375
|
this.logger.verbose("ReactorClient initialized");
|
|
1298
1376
|
}
|
|
1299
1377
|
/**
|
|
@@ -1555,58 +1633,7 @@ var ReactorClient = class {
|
|
|
1555
1633
|
return this.submitCreate(document, parentIdentifier, signal);
|
|
1556
1634
|
}
|
|
1557
1635
|
async submitCreate(document, parentIdentifier, signal) {
|
|
1558
|
-
const
|
|
1559
|
-
const branch = document.header.branch || "main";
|
|
1560
|
-
const parentId = parentIdentifier ? await this.resolveReference(parentIdentifier, "main", signal) : void 0;
|
|
1561
|
-
const createInput = {
|
|
1562
|
-
model: document.header.documentType,
|
|
1563
|
-
version: 0,
|
|
1564
|
-
documentId,
|
|
1565
|
-
signing: {
|
|
1566
|
-
signature: documentId,
|
|
1567
|
-
publicKey: document.header.sig.publicKey,
|
|
1568
|
-
nonce: document.header.sig.nonce,
|
|
1569
|
-
createdAtUtcIso: document.header.createdAtUtcIso,
|
|
1570
|
-
documentType: document.header.documentType
|
|
1571
|
-
},
|
|
1572
|
-
slug: document.header.slug,
|
|
1573
|
-
name: document.header.name,
|
|
1574
|
-
branch: document.header.branch,
|
|
1575
|
-
meta: document.header.meta,
|
|
1576
|
-
protocolVersions: document.header.protocolVersions ?? { "base-reducer": 2 }
|
|
1577
|
-
};
|
|
1578
|
-
const createActions = await signActions([createDocumentAction(createInput), upgradeDocumentAction({
|
|
1579
|
-
documentId,
|
|
1580
|
-
model: document.header.documentType,
|
|
1581
|
-
fromVersion: 0,
|
|
1582
|
-
toVersion: normalizeDocumentModelVersion(document.state.document?.version),
|
|
1583
|
-
initialState: document.state
|
|
1584
|
-
})], this.signer, {
|
|
1585
|
-
documentId,
|
|
1586
|
-
branch
|
|
1587
|
-
}, signal);
|
|
1588
|
-
const jobs = [{
|
|
1589
|
-
key: "create",
|
|
1590
|
-
documentId,
|
|
1591
|
-
scope: getSharedActionScope(createActions),
|
|
1592
|
-
branch,
|
|
1593
|
-
actions: createActions,
|
|
1594
|
-
dependsOn: []
|
|
1595
|
-
}];
|
|
1596
|
-
if (parentId) {
|
|
1597
|
-
const parentActions = await signActions([addRelationshipAction(parentId, documentId, "child")], this.signer, {
|
|
1598
|
-
documentId: parentId,
|
|
1599
|
-
branch: "main"
|
|
1600
|
-
}, signal);
|
|
1601
|
-
jobs.push({
|
|
1602
|
-
key: "parent",
|
|
1603
|
-
documentId: parentId,
|
|
1604
|
-
scope: getSharedActionScope(parentActions),
|
|
1605
|
-
branch: "main",
|
|
1606
|
-
actions: parentActions,
|
|
1607
|
-
dependsOn: ["create"]
|
|
1608
|
-
});
|
|
1609
|
-
}
|
|
1636
|
+
const jobs = await buildCreateJobs(document, parentIdentifier ? await this.resolveReference(parentIdentifier, "main", signal) : void 0, this.signer, signal);
|
|
1610
1637
|
return this.reactor.executeBatch({ jobs }, signal);
|
|
1611
1638
|
}
|
|
1612
1639
|
/**
|
|
@@ -1626,26 +1653,9 @@ var ReactorClient = class {
|
|
|
1626
1653
|
return this.submitCreate(document, options?.parentIdentifier, signal);
|
|
1627
1654
|
}
|
|
1628
1655
|
async emptyDocument(documentModelType, options, signal) {
|
|
1629
|
-
const
|
|
1630
|
-
let module;
|
|
1631
|
-
if (options?.documentModelVersion !== void 0) {
|
|
1632
|
-
const requestedVersion = normalizeDocumentModelVersion(options.documentModelVersion);
|
|
1633
|
-
module = matchingModules.find((m) => normalizeDocumentModelVersion(m.version) === requestedVersion);
|
|
1634
|
-
if (!module) throw new Error(`Document model not found for type: ${documentModelType} with version: ${options.documentModelVersion}`);
|
|
1635
|
-
} else {
|
|
1636
|
-
module = matchingModules.reduce((latest, current) => {
|
|
1637
|
-
if (latest === void 0) return current;
|
|
1638
|
-
return normalizeDocumentModelVersion(current.version) > normalizeDocumentModelVersion(latest.version) ? current : latest;
|
|
1639
|
-
}, void 0);
|
|
1640
|
-
if (!module) throw new Error(`Document model not found for type: ${documentModelType}`);
|
|
1641
|
-
}
|
|
1656
|
+
const module = selectDocumentModelModule((await this.reactor.getDocumentModels(void 0, void 0, signal)).results, documentModelType, options?.documentModelVersion);
|
|
1642
1657
|
const base = await this.getCreateProtocolVersions(options?.parentIdentifier, signal);
|
|
1643
|
-
|
|
1644
|
-
...base,
|
|
1645
|
-
...options?.protocolVersions
|
|
1646
|
-
} });
|
|
1647
|
-
document.state.document.version = normalizeDocumentModelVersion(module.version);
|
|
1648
|
-
return document;
|
|
1658
|
+
return createEmptyDocument(module, options, this.createSignaturePolicy, base);
|
|
1649
1659
|
}
|
|
1650
1660
|
/**
|
|
1651
1661
|
* Upgrades a document to a newer document model version by dispatching an
|
|
@@ -1660,42 +1670,14 @@ var ReactorClient = class {
|
|
|
1660
1670
|
*/
|
|
1661
1671
|
async upgradeDocument(documentIdentifier, toVersion, options, signal) {
|
|
1662
1672
|
this.logger.verbose("upgradeDocument(@documentIdentifier, @toVersion)", documentIdentifier, toVersion);
|
|
1663
|
-
const
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
let targetVersion = toVersion;
|
|
1672
|
-
if (targetVersion === void 0) {
|
|
1673
|
-
const module = await this.getDocumentModelModule(documentType);
|
|
1674
|
-
targetVersion = normalizeDocumentModelVersion(module.version);
|
|
1675
|
-
}
|
|
1676
|
-
if (targetVersion === fromVersion) return this.gateDocument(document, { branch }, signal);
|
|
1677
|
-
if (targetVersion < fromVersion) throw new DowngradeNotSupportedError(documentType, fromVersion, targetVersion);
|
|
1678
|
-
const action = upgradeDocumentAction({
|
|
1679
|
-
documentId,
|
|
1680
|
-
model: documentType,
|
|
1681
|
-
fromVersion,
|
|
1682
|
-
toVersion: targetVersion,
|
|
1683
|
-
revision: { ...document.header.revision }
|
|
1684
|
-
});
|
|
1685
|
-
const signedActions = await signActions([action], this.signer, {
|
|
1686
|
-
documentId,
|
|
1687
|
-
branch
|
|
1688
|
-
}, signal);
|
|
1689
|
-
const jobInfo = await this.reactor.execute(documentId, branch, signedActions, signal);
|
|
1690
|
-
const completedJob = await this.waitForJob(jobInfo, signal);
|
|
1691
|
-
if (completedJob.status !== "FAILED") {
|
|
1692
|
-
const upgraded = await this.reactor.getByIdOrSlug(documentId, { branch }, completedJob.consistencyToken, signal);
|
|
1693
|
-
return this.gateDocument(upgraded, { branch }, signal);
|
|
1694
|
-
}
|
|
1695
|
-
if (completedJob.error?.name !== "UpgradePreconditionFailedError") throw new Error(completedJob.error?.message);
|
|
1696
|
-
lastConflictMessage = completedJob.error.message;
|
|
1697
|
-
}
|
|
1698
|
-
throw new Error(`Upgrade of document ${documentIdentifier} conflicted with concurrent edits after ${maxConflictRetries + 1} attempts: ${lastConflictMessage}`);
|
|
1673
|
+
const upgraded = await upgradeDocumentWith({
|
|
1674
|
+
signer: this.signer,
|
|
1675
|
+
read: (identifier, branch, consistencyToken, readSignal) => this.reactor.getByIdOrSlug(identifier, branch === void 0 ? void 0 : { branch }, consistencyToken, readSignal),
|
|
1676
|
+
getDocumentModelModule: (documentType) => this.getDocumentModelModule(documentType),
|
|
1677
|
+
submit: (documentId, branch, actions, submitSignal) => this.reactor.execute(documentId, branch, actions, submitSignal),
|
|
1678
|
+
waitForJob: (job, waitSignal) => this.waitForJob(job, waitSignal)
|
|
1679
|
+
}, documentIdentifier, toVersion, options, signal);
|
|
1680
|
+
return this.gateDocument(upgraded, { branch: upgraded.header.branch || "main" }, signal);
|
|
1699
1681
|
}
|
|
1700
1682
|
/**
|
|
1701
1683
|
* Creates an empty document in a drive as a single batched operation.
|
|
@@ -2294,6 +2276,10 @@ function toErrorInfo(error) {
|
|
|
2294
2276
|
stack: (/* @__PURE__ */ new Error()).stack || ""
|
|
2295
2277
|
};
|
|
2296
2278
|
}
|
|
2279
|
+
/** Whether a retry charged to the job's limit is still within it. */
|
|
2280
|
+
function hasRetriesLeft(job) {
|
|
2281
|
+
return (job.retryCount || 0) < (job.maxRetries || 0);
|
|
2282
|
+
}
|
|
2297
2283
|
var JobResultHandler = class {
|
|
2298
2284
|
queue;
|
|
2299
2285
|
jobTracker;
|
|
@@ -2322,7 +2308,7 @@ var JobResultHandler = class {
|
|
|
2322
2308
|
return;
|
|
2323
2309
|
}
|
|
2324
2310
|
}
|
|
2325
|
-
if (modelLoaded) {
|
|
2311
|
+
if (modelLoaded && hasRetriesLeft(handle.job)) {
|
|
2326
2312
|
const errorInfo = toErrorInfo(result.error);
|
|
2327
2313
|
try {
|
|
2328
2314
|
await this.queue.retryJob(handle.job.id, errorInfo);
|
|
@@ -2347,7 +2333,7 @@ var JobResultHandler = class {
|
|
|
2347
2333
|
return;
|
|
2348
2334
|
}
|
|
2349
2335
|
const retryCount = handle.job.retryCount || 0;
|
|
2350
|
-
if (
|
|
2336
|
+
if (hasRetriesLeft(handle.job)) {
|
|
2351
2337
|
const currentErrorInfo = result.error ? toErrorInfo(result.error) : toErrorInfo("Unknown error");
|
|
2352
2338
|
try {
|
|
2353
2339
|
await this.queue.retryJob(handle.job.id, currentErrorInfo);
|
|
@@ -2530,38 +2516,6 @@ var DeferredJobs = class {
|
|
|
2530
2516
|
}
|
|
2531
2517
|
};
|
|
2532
2518
|
//#endregion
|
|
2533
|
-
//#region src/executor/worker-pool-router.ts
|
|
2534
|
-
/**
|
|
2535
|
-
* Sticky-by-document routing for the executor worker pool.
|
|
2536
|
-
*
|
|
2537
|
-
* Hashes `documentId` to a stable bucket so every job on a given document
|
|
2538
|
-
* lands on the same worker. The queue already enforces "at most one job
|
|
2539
|
-
* executing per document at a time", so a sticky worker always sees a fresh
|
|
2540
|
-
* post-commit snapshot before its next job on that document — which keeps
|
|
2541
|
-
* the per-worker `IWriteCache` and `IDocumentMetaCache` coherent for free.
|
|
2542
|
-
*
|
|
2543
|
-
* The hash is FNV-1a 32-bit: deterministic, dependency-free, and stable
|
|
2544
|
-
* across processes (so the same routing decision can be reproduced anywhere
|
|
2545
|
-
* the documentId is known).
|
|
2546
|
-
*
|
|
2547
|
-
* @see Executor Worker Pool Design wiki page
|
|
2548
|
-
* (Powerhouse board wiki id: d400d711-f07e-4389-a226-4e9fdd4fa8ba)
|
|
2549
|
-
*/
|
|
2550
|
-
const FNV_OFFSET_BASIS = 2166136261;
|
|
2551
|
-
const FNV_PRIME = 16777619;
|
|
2552
|
-
function hashDocumentId(documentId) {
|
|
2553
|
-
let hash = FNV_OFFSET_BASIS;
|
|
2554
|
-
for (let i = 0; i < documentId.length; i++) {
|
|
2555
|
-
hash ^= documentId.charCodeAt(i);
|
|
2556
|
-
hash = Math.imul(hash, FNV_PRIME);
|
|
2557
|
-
}
|
|
2558
|
-
return hash >>> 0;
|
|
2559
|
-
}
|
|
2560
|
-
function bucketFor(documentId, numWorkers) {
|
|
2561
|
-
if (numWorkers < 1) throw new Error(`bucketFor: numWorkers must be >= 1 (got ${numWorkers})`);
|
|
2562
|
-
return hashDocumentId(documentId) % numWorkers;
|
|
2563
|
-
}
|
|
2564
|
-
//#endregion
|
|
2565
2519
|
//#region src/executor/worker-pool-job-executor-manager.ts
|
|
2566
2520
|
/**
|
|
2567
2521
|
* Manages a pool of executor workers and dispatches jobs across them with
|
|
@@ -2714,20 +2668,11 @@ var WorkerPoolJobExecutorManager = class {
|
|
|
2714
2668
|
} catch (error) {
|
|
2715
2669
|
const errorInfo = toErrorInfo(error instanceof Error ? error : String(error));
|
|
2716
2670
|
if (isWorkerTransportError(error)) {
|
|
2717
|
-
await this.handleWorkerTransportFailure(worker, handle
|
|
2671
|
+
await this.handleWorkerTransportFailure(worker, handle, errorInfo);
|
|
2718
2672
|
return;
|
|
2719
2673
|
}
|
|
2720
|
-
handle.fail(errorInfo);
|
|
2721
2674
|
this.activeJobs--;
|
|
2722
|
-
this.
|
|
2723
|
-
const failedEvent = {
|
|
2724
|
-
job: handle.job,
|
|
2725
|
-
error: errorInfo.message,
|
|
2726
|
-
willRetry: false,
|
|
2727
|
-
retryCount: 0,
|
|
2728
|
-
workerId
|
|
2729
|
-
};
|
|
2730
|
-
this.eventBus.emit(JobExecutorEventTypes.JOB_FAILED, failedEvent).catch(() => {});
|
|
2675
|
+
this.failInFlight(handle, errorInfo, workerId);
|
|
2731
2676
|
await this.tryDispatchFor(worker);
|
|
2732
2677
|
return;
|
|
2733
2678
|
}
|
|
@@ -2790,20 +2735,41 @@ var WorkerPoolJobExecutorManager = class {
|
|
|
2790
2735
|
this.logger.error("Failed to emit JOB_WRITE_READY event: @Error", error);
|
|
2791
2736
|
}
|
|
2792
2737
|
}
|
|
2738
|
+
failInFlight(handle, errorInfo, workerId) {
|
|
2739
|
+
handle.fail(errorInfo);
|
|
2740
|
+
this.jobTracker.markFailed(handle.job.id, errorInfo, handle.job);
|
|
2741
|
+
const failedEvent = {
|
|
2742
|
+
job: handle.job,
|
|
2743
|
+
error: errorInfo.message,
|
|
2744
|
+
willRetry: false,
|
|
2745
|
+
retryCount: handle.job.retryCount || 0,
|
|
2746
|
+
workerId
|
|
2747
|
+
};
|
|
2748
|
+
this.eventBus.emit(JobExecutorEventTypes.JOB_FAILED, failedEvent).catch(() => {});
|
|
2749
|
+
}
|
|
2793
2750
|
/**
|
|
2794
2751
|
* Handle a worker-transport failure (worker exited / init failed / abort
|
|
2795
|
-
* timed out) detected while `worker.execute` was in flight.
|
|
2796
|
-
*
|
|
2797
|
-
*
|
|
2798
|
-
*
|
|
2799
|
-
* not failed, only the worker is.
|
|
2752
|
+
* timed out) detected while `worker.execute` was in flight. The dead
|
|
2753
|
+
* worker is replaced and dispatch resumes on its bucket. The in-flight
|
|
2754
|
+
* job is retried via `queue.retryJob` while it has retries left, and
|
|
2755
|
+
* failed with the transport error once they are spent.
|
|
2800
2756
|
*/
|
|
2801
|
-
async handleWorkerTransportFailure(dead,
|
|
2757
|
+
async handleWorkerTransportFailure(dead, handle, errorInfo) {
|
|
2758
|
+
const job = handle.job;
|
|
2759
|
+
this.activeJobs--;
|
|
2760
|
+
if (!hasRetriesLeft(job)) {
|
|
2761
|
+
this.logger.error("worker transport error during execute; job @jobId is out of retries: @error", {
|
|
2762
|
+
jobId: job.id,
|
|
2763
|
+
workerId: dead.workerId
|
|
2764
|
+
}, errorInfo.message);
|
|
2765
|
+
this.failInFlight(handle, errorInfo, dead.workerId);
|
|
2766
|
+
await this.replaceWorker(dead);
|
|
2767
|
+
return;
|
|
2768
|
+
}
|
|
2802
2769
|
this.logger.warn("worker transport error during execute; retrying job @jobId on a replacement worker: @error", {
|
|
2803
2770
|
jobId: job.id,
|
|
2804
2771
|
workerId: dead.workerId
|
|
2805
2772
|
}, errorInfo.message);
|
|
2806
|
-
this.activeJobs--;
|
|
2807
2773
|
await this.replaceWorker(dead);
|
|
2808
2774
|
try {
|
|
2809
2775
|
await this.queue.retryJob(job.id, errorInfo);
|
|
@@ -3009,7 +2975,7 @@ var SimpleJobExecutorManager = class {
|
|
|
3009
2975
|
deferredJobs;
|
|
3010
2976
|
resultHandler;
|
|
3011
2977
|
jobTimeoutMs;
|
|
3012
|
-
constructor(executorFactory, eventBus, queue, jobTracker, logger, resolver, jobTimeoutMs =
|
|
2978
|
+
constructor(executorFactory, eventBus, queue, jobTracker, logger, resolver, jobTimeoutMs = DEFAULT_JOB_TIMEOUT_MS, deferredJobTtlMs = DEFAULT_DEFERRED_JOB_TTL_MS) {
|
|
3013
2979
|
this.executorFactory = executorFactory;
|
|
3014
2980
|
this.eventBus = eventBus;
|
|
3015
2981
|
this.queue = queue;
|
|
@@ -3079,7 +3045,12 @@ var SimpleJobExecutorManager = class {
|
|
|
3079
3045
|
workerId
|
|
3080
3046
|
};
|
|
3081
3047
|
this.eventBus.emit(JobExecutorEventTypes.JOB_STARTED, startedEvent).catch(() => {});
|
|
3082
|
-
const
|
|
3048
|
+
const timeout = new AbortController();
|
|
3049
|
+
const timer = setTimeout(() => {
|
|
3050
|
+
timeout.abort(new DOMException("The operation was aborted due to timeout", "TimeoutError"));
|
|
3051
|
+
}, this.jobTimeoutMs);
|
|
3052
|
+
timer.unref?.();
|
|
3053
|
+
const signal = timeout.signal;
|
|
3083
3054
|
const toError = (reason) => reason instanceof Error ? reason : new Error(String(reason));
|
|
3084
3055
|
const abortPromise = new Promise((_, reject) => {
|
|
3085
3056
|
if (signal.aborted) {
|
|
@@ -3090,8 +3061,9 @@ var SimpleJobExecutorManager = class {
|
|
|
3090
3061
|
});
|
|
3091
3062
|
let result;
|
|
3092
3063
|
try {
|
|
3093
|
-
result = await Promise.race([executor.executeJob(handle.job, signal), abortPromise]);
|
|
3064
|
+
result = await Promise.race([executor.executeJob(handle.job, signal, () => clearTimeout(timer)), abortPromise]);
|
|
3094
3065
|
} catch (error) {
|
|
3066
|
+
clearTimeout(timer);
|
|
3095
3067
|
const errorInfo = toErrorInfo(error instanceof Error ? error : String(error));
|
|
3096
3068
|
handle.fail(errorInfo);
|
|
3097
3069
|
this.activeJobs--;
|
|
@@ -3107,6 +3079,7 @@ var SimpleJobExecutorManager = class {
|
|
|
3107
3079
|
await this.checkForMoreJobs();
|
|
3108
3080
|
return;
|
|
3109
3081
|
}
|
|
3082
|
+
clearTimeout(timer);
|
|
3110
3083
|
if (result.success) this.totalJobsProcessed++;
|
|
3111
3084
|
if (result.success) {
|
|
3112
3085
|
const completedEvent = {
|
|
@@ -4542,6 +4515,8 @@ var InMemoryQueue = class {
|
|
|
4542
4515
|
jobIdToDocId = /* @__PURE__ */ new Map();
|
|
4543
4516
|
completedJobs = /* @__PURE__ */ new Set();
|
|
4544
4517
|
jobIndex = /* @__PURE__ */ new Map();
|
|
4518
|
+
jobIdToSeq = /* @__PURE__ */ new Map();
|
|
4519
|
+
nextSeq = 0;
|
|
4545
4520
|
isBlocked = false;
|
|
4546
4521
|
onDrainedCallback;
|
|
4547
4522
|
isPausedFlag = false;
|
|
@@ -4636,7 +4611,15 @@ var InMemoryQueue = class {
|
|
|
4636
4611
|
async enqueue(job) {
|
|
4637
4612
|
if (this.isBlocked) throw new Error("Queue is blocked");
|
|
4638
4613
|
const queueKey = this.createQueueKey(job.documentId, job.scope, job.branch);
|
|
4639
|
-
this.getQueue(queueKey)
|
|
4614
|
+
const queue = this.getQueue(queueKey);
|
|
4615
|
+
const seq = this.jobIdToSeq.get(job.id);
|
|
4616
|
+
if (seq === void 0) {
|
|
4617
|
+
this.jobIdToSeq.set(job.id, this.nextSeq++);
|
|
4618
|
+
queue.push(job);
|
|
4619
|
+
} else {
|
|
4620
|
+
const later = queue.findIndex((queued) => (this.jobIdToSeq.get(queued.id) ?? Infinity) > seq);
|
|
4621
|
+
queue.splice(later === -1 ? queue.length : later, 0, job);
|
|
4622
|
+
}
|
|
4640
4623
|
this.jobIdToQueueKey.set(job.id, queueKey);
|
|
4641
4624
|
this.jobIndex.set(job.id, job);
|
|
4642
4625
|
const documentType = this.getCreateDocumentType(job);
|
|
@@ -4774,6 +4757,7 @@ var InMemoryQueue = class {
|
|
|
4774
4757
|
queue.splice(jobIdx, 1);
|
|
4775
4758
|
this.jobIdToQueueKey.delete(jobId);
|
|
4776
4759
|
this.jobIndex.delete(jobId);
|
|
4760
|
+
this.jobIdToSeq.delete(jobId);
|
|
4777
4761
|
if (queue.length === 0) this.queues.delete(queueKey);
|
|
4778
4762
|
return Promise.resolve(true);
|
|
4779
4763
|
}
|
|
@@ -4784,6 +4768,7 @@ var InMemoryQueue = class {
|
|
|
4784
4768
|
for (const job of queue) {
|
|
4785
4769
|
this.jobIdToQueueKey.delete(job.id);
|
|
4786
4770
|
this.jobIndex.delete(job.id);
|
|
4771
|
+
this.jobIdToSeq.delete(job.id);
|
|
4787
4772
|
}
|
|
4788
4773
|
this.queues.delete(queueKey);
|
|
4789
4774
|
}
|
|
@@ -4792,6 +4777,7 @@ var InMemoryQueue = class {
|
|
|
4792
4777
|
clearAll() {
|
|
4793
4778
|
this.jobIdToQueueKey.clear();
|
|
4794
4779
|
this.jobIndex.clear();
|
|
4780
|
+
this.jobIdToSeq.clear();
|
|
4795
4781
|
this.completedJobs.clear();
|
|
4796
4782
|
this.queues.clear();
|
|
4797
4783
|
return Promise.resolve();
|
|
@@ -4804,6 +4790,7 @@ var InMemoryQueue = class {
|
|
|
4804
4790
|
if (documentId) this.markJobComplete(jobId, documentId);
|
|
4805
4791
|
this.completedJobs.add(jobId);
|
|
4806
4792
|
this.jobIndex.delete(jobId);
|
|
4793
|
+
this.jobIdToSeq.delete(jobId);
|
|
4807
4794
|
await this.remove(jobId);
|
|
4808
4795
|
this.checkDrained();
|
|
4809
4796
|
}
|
|
@@ -4816,6 +4803,7 @@ var InMemoryQueue = class {
|
|
|
4816
4803
|
if (error) job.errorHistory.push(error);
|
|
4817
4804
|
}
|
|
4818
4805
|
this.jobIndex.delete(jobId);
|
|
4806
|
+
this.jobIdToSeq.delete(jobId);
|
|
4819
4807
|
this.completedJobs.add(jobId);
|
|
4820
4808
|
await this.remove(jobId);
|
|
4821
4809
|
if (job) {
|
|
@@ -5158,112 +5146,763 @@ var NullDocumentModelResolver = class {
|
|
|
5158
5146
|
return this.ensureModelLoaded(documentType);
|
|
5159
5147
|
}
|
|
5160
5148
|
};
|
|
5161
|
-
|
|
5162
|
-
|
|
5163
|
-
|
|
5164
|
-
|
|
5165
|
-
|
|
5166
|
-
*/
|
|
5167
|
-
var DefaultSubscriptionErrorHandler = class {
|
|
5168
|
-
handleError(error, context) {
|
|
5169
|
-
const errorMessage = `Subscription error in ${context.eventType} (${context.subscriptionId})`;
|
|
5170
|
-
if (error instanceof Error) {
|
|
5171
|
-
const enhancedError = /* @__PURE__ */ new Error(`${errorMessage}: ${error.message}`);
|
|
5172
|
-
enhancedError.cause = error;
|
|
5173
|
-
enhancedError.stack = error.stack;
|
|
5174
|
-
throw enhancedError;
|
|
5175
|
-
} else throw new Error(`${errorMessage}: ${String(error)}`);
|
|
5149
|
+
/** The filesystem sync never settled. */
|
|
5150
|
+
var PGliteFlushSyncTimeoutError = class extends Error {
|
|
5151
|
+
constructor(timeoutMs) {
|
|
5152
|
+
super(`The PGlite filesystem sync neither resolved nor rejected within ${timeoutMs}ms; no durability barrier was established.`);
|
|
5153
|
+
this.name = "PGliteFlushSyncTimeoutError";
|
|
5176
5154
|
}
|
|
5177
5155
|
};
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
|
|
5197
|
-
|
|
5198
|
-
|
|
5156
|
+
/** Group-commits PGlite's per-statement sync; see `withGroupCommitPGlite`. */
|
|
5157
|
+
var GroupCommitPGliteClient = class {
|
|
5158
|
+
instance;
|
|
5159
|
+
options;
|
|
5160
|
+
deferred;
|
|
5161
|
+
/** The instance's own `syncToFs`, captured before deferral replaces it. */
|
|
5162
|
+
instanceSync;
|
|
5163
|
+
/** Statements started. */
|
|
5164
|
+
sequence = 0;
|
|
5165
|
+
/** The highest {@link sequence} a completed flush made durable. */
|
|
5166
|
+
flushed = 0;
|
|
5167
|
+
/** `covers` is 0 until the snapshot starts; earlier requests join it. */
|
|
5168
|
+
running = void 0;
|
|
5169
|
+
syncFailures = 0;
|
|
5170
|
+
/** PGlite returns at once from a sync requested while one is scheduled. */
|
|
5171
|
+
lastSync = Promise.resolve();
|
|
5172
|
+
inFlight = 0;
|
|
5173
|
+
admittedRuns = 0;
|
|
5174
|
+
idleWaiters = [];
|
|
5175
|
+
gate = void 0;
|
|
5176
|
+
releaseGate = () => void 0;
|
|
5177
|
+
poisonCause = void 0;
|
|
5178
|
+
/** Rejects once the session is poisoned; every wait races it. */
|
|
5179
|
+
poisonedSignal;
|
|
5180
|
+
signalPoisoned = () => void 0;
|
|
5181
|
+
constructor(instance, options = {}) {
|
|
5182
|
+
this.instance = instance;
|
|
5183
|
+
this.options = {
|
|
5184
|
+
onDiagnostic: options.onDiagnostic ?? ((message, error) => {
|
|
5185
|
+
console.error(`[group-commit-pglite] ${message}`, error);
|
|
5186
|
+
}),
|
|
5187
|
+
onSyncStuck: options.onSyncStuck ?? (() => void 0),
|
|
5188
|
+
flushSyncTimeoutMs: options.flushSyncTimeoutMs ?? 12e4,
|
|
5189
|
+
maxConsecutiveSyncFailures: options.maxConsecutiveSyncFailures ?? 3,
|
|
5190
|
+
closeTimeoutMs: options.closeTimeoutMs ?? 3e4
|
|
5199
5191
|
};
|
|
5200
|
-
|
|
5201
|
-
|
|
5202
|
-
const id = `deleted-${++this.subscriptionCounter}`;
|
|
5203
|
-
this.deletedSubscriptions.set(id, {
|
|
5204
|
-
id,
|
|
5205
|
-
callback,
|
|
5206
|
-
search
|
|
5192
|
+
this.poisonedSignal = new Promise((_resolve, reject) => {
|
|
5193
|
+
this.signalPoisoned = reject;
|
|
5207
5194
|
});
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
|
|
5195
|
+
this.poisonedSignal.catch(() => void 0);
|
|
5196
|
+
const candidate = instance.syncToFs;
|
|
5197
|
+
this.deferred = typeof candidate === "function";
|
|
5198
|
+
this.instanceSync = this.deferred ? candidate.bind(instance) : () => Promise.resolve();
|
|
5199
|
+
if (!this.deferred) this.options.onDiagnostic("this PGlite session exposes no filesystem sync, so statements keep their own durability");
|
|
5200
|
+
this.setStatementSync(!this.deferred);
|
|
5201
|
+
}
|
|
5202
|
+
query(sql, params) {
|
|
5203
|
+
const run = () => this.instance.query(sql, params);
|
|
5204
|
+
return this.admittedRuns > 0 ? run() : this.admit(run);
|
|
5205
|
+
}
|
|
5206
|
+
exec(sql) {
|
|
5207
|
+
const run = () => this.instance.exec(sql);
|
|
5208
|
+
return this.admittedRuns > 0 ? run() : this.admit(run);
|
|
5209
|
+
}
|
|
5210
|
+
/** Runs a statement past the flush gate; the calls it makes are not gated again. */
|
|
5211
|
+
async admit(run) {
|
|
5212
|
+
await this.enterStatement();
|
|
5213
|
+
this.admittedRuns += 1;
|
|
5214
|
+
try {
|
|
5215
|
+
return await run();
|
|
5216
|
+
} finally {
|
|
5217
|
+
this.admittedRuns -= 1;
|
|
5218
|
+
this.endStatement();
|
|
5219
|
+
}
|
|
5211
5220
|
}
|
|
5212
|
-
|
|
5213
|
-
|
|
5214
|
-
this.updatedSubscriptions.set(id, {
|
|
5215
|
-
id,
|
|
5216
|
-
callback,
|
|
5217
|
-
search,
|
|
5218
|
-
view
|
|
5219
|
-
});
|
|
5220
|
-
return () => {
|
|
5221
|
-
this.updatedSubscriptions.delete(id);
|
|
5222
|
-
};
|
|
5221
|
+
isInTransaction() {
|
|
5222
|
+
return this.instance.isInTransaction();
|
|
5223
5223
|
}
|
|
5224
|
-
|
|
5225
|
-
|
|
5226
|
-
this.
|
|
5227
|
-
id,
|
|
5228
|
-
callback,
|
|
5229
|
-
search
|
|
5230
|
-
});
|
|
5231
|
-
return () => {
|
|
5232
|
-
this.relationshipSubscriptions.delete(id);
|
|
5233
|
-
};
|
|
5224
|
+
/** Whether statements have stopped flushing themselves. */
|
|
5225
|
+
get deferringStatementFlush() {
|
|
5226
|
+
return this.deferred;
|
|
5234
5227
|
}
|
|
5235
|
-
/**
|
|
5236
|
-
|
|
5237
|
-
|
|
5238
|
-
|
|
5239
|
-
|
|
5240
|
-
|
|
5241
|
-
|
|
5242
|
-
|
|
5243
|
-
|
|
5228
|
+
/** Every later and parked flush rejects: nothing can be made durable again. */
|
|
5229
|
+
markPoisoned(cause) {
|
|
5230
|
+
if (this.poisonCause) return;
|
|
5231
|
+
this.poisonCause = new StoragePoisonedError("The PGlite session is poisoned; nothing more can be made durable until the host restarts.", cause);
|
|
5232
|
+
this.releaseGate();
|
|
5233
|
+
this.idleWaiters = [];
|
|
5234
|
+
this.signalPoisoned(this.poisonCause);
|
|
5235
|
+
}
|
|
5236
|
+
/** Shares one sync between callers whose statements it covers. */
|
|
5237
|
+
async flush() {
|
|
5238
|
+
if (!this.deferred) return;
|
|
5239
|
+
const target = this.sequence;
|
|
5240
|
+
for (;;) {
|
|
5241
|
+
this.assertHealthy();
|
|
5242
|
+
if (this.flushed >= target) return;
|
|
5243
|
+
const running = this.running;
|
|
5244
|
+
if (running === void 0) {
|
|
5245
|
+
await this.startFlush();
|
|
5246
|
+
continue;
|
|
5244
5247
|
}
|
|
5245
|
-
|
|
5246
|
-
|
|
5247
|
-
|
|
5248
|
-
if (filteredIds.length > 0) try {
|
|
5249
|
-
subscription.callback({
|
|
5250
|
-
...result,
|
|
5251
|
-
results: filteredIds
|
|
5252
|
-
});
|
|
5253
|
-
} catch (error) {
|
|
5254
|
-
this.errorHandler.handleError(error, {
|
|
5255
|
-
eventType: "created",
|
|
5256
|
-
subscriptionId: subscription.id,
|
|
5257
|
-
eventData: filteredIds
|
|
5258
|
-
});
|
|
5248
|
+
if (running.covers === 0 || running.covers >= target) {
|
|
5249
|
+
await running.promise;
|
|
5250
|
+
continue;
|
|
5259
5251
|
}
|
|
5252
|
+
await running.promise.catch(() => void 0);
|
|
5260
5253
|
}
|
|
5261
5254
|
}
|
|
5262
|
-
/**
|
|
5263
|
-
|
|
5264
|
-
|
|
5265
|
-
|
|
5266
|
-
|
|
5255
|
+
/** PGlite's own close relies on the per-statement sync, so it is restored. */
|
|
5256
|
+
async close() {
|
|
5257
|
+
const flushing = this.flush();
|
|
5258
|
+
flushing.catch(() => void 0);
|
|
5259
|
+
let failure;
|
|
5260
|
+
try {
|
|
5261
|
+
if (await this.bounded(flushing) === TIMED_OUT) failure = /* @__PURE__ */ new Error("the closing flush did not settle");
|
|
5262
|
+
} catch (error) {
|
|
5263
|
+
failure = error;
|
|
5264
|
+
}
|
|
5265
|
+
if (failure !== void 0) this.options.onDiagnostic("the closing flush failed; writes since the last group commit are lost", failure);
|
|
5266
|
+
this.setStatementSync(true);
|
|
5267
|
+
const closing = this.instance.close();
|
|
5268
|
+
if (await this.bounded(closing) === TIMED_OUT) this.options.onDiagnostic("closing the PGlite instance did not settle");
|
|
5269
|
+
}
|
|
5270
|
+
/** Hands the instance back to per-statement durability, unclosed. */
|
|
5271
|
+
restoreStatementSync() {
|
|
5272
|
+
this.setStatementSync(true);
|
|
5273
|
+
}
|
|
5274
|
+
/** A statement that died in flight would otherwise hold a close forever. */
|
|
5275
|
+
bounded(pending) {
|
|
5276
|
+
const timeoutMs = this.options.closeTimeoutMs;
|
|
5277
|
+
return timeoutMs > 0 ? withDeadline(pending, timeoutMs) : pending;
|
|
5278
|
+
}
|
|
5279
|
+
assertHealthy() {
|
|
5280
|
+
if (this.poisonCause) throw this.poisonCause;
|
|
5281
|
+
}
|
|
5282
|
+
/** Restores by assignment so a prototype or own-field `syncToFs` both work. */
|
|
5283
|
+
setStatementSync(enabled) {
|
|
5284
|
+
if (!this.deferred) return;
|
|
5285
|
+
const target = this.instance;
|
|
5286
|
+
target.syncToFs = enabled ? this.instanceSync : () => Promise.resolve();
|
|
5287
|
+
}
|
|
5288
|
+
startFlush() {
|
|
5289
|
+
const running = {
|
|
5290
|
+
promise: Promise.resolve(),
|
|
5291
|
+
covers: 0
|
|
5292
|
+
};
|
|
5293
|
+
running.promise = this.runFlush(running).finally(() => {
|
|
5294
|
+
if (this.running === running) this.running = void 0;
|
|
5295
|
+
});
|
|
5296
|
+
this.running = running;
|
|
5297
|
+
return running.promise;
|
|
5298
|
+
}
|
|
5299
|
+
async runFlush(running) {
|
|
5300
|
+
const release = await this.holdStatements();
|
|
5301
|
+
running.covers = this.sequence;
|
|
5302
|
+
const covered = running.covers;
|
|
5303
|
+
try {
|
|
5304
|
+
await this.boundedSync();
|
|
5305
|
+
} finally {
|
|
5306
|
+
release();
|
|
5307
|
+
}
|
|
5308
|
+
this.flushed = Math.max(this.flushed, covered);
|
|
5309
|
+
}
|
|
5310
|
+
/** Chained, so PGlite's scheduled-sync shortcut never stands in for a sync. */
|
|
5311
|
+
async boundedSync() {
|
|
5312
|
+
const started = this.lastSync.then(() => this.instanceSync());
|
|
5313
|
+
this.lastSync = started.then(() => void 0, () => void 0);
|
|
5314
|
+
const syncing = Promise.race([started, this.poisonedSignal]);
|
|
5315
|
+
syncing.catch(() => void 0);
|
|
5316
|
+
let failure;
|
|
5317
|
+
try {
|
|
5318
|
+
const timeoutMs = this.options.flushSyncTimeoutMs;
|
|
5319
|
+
if ((timeoutMs > 0 ? await withDeadline(syncing, timeoutMs) : await syncing) !== TIMED_OUT) {
|
|
5320
|
+
this.syncFailures = 0;
|
|
5321
|
+
return;
|
|
5322
|
+
}
|
|
5323
|
+
failure = new PGliteFlushSyncTimeoutError(timeoutMs);
|
|
5324
|
+
} catch (error) {
|
|
5325
|
+
this.assertHealthy();
|
|
5326
|
+
this.syncFailures += 1;
|
|
5327
|
+
if (this.syncFailures < this.options.maxConsecutiveSyncFailures) throw error;
|
|
5328
|
+
failure = error instanceof Error ? error : new Error(String(error));
|
|
5329
|
+
}
|
|
5330
|
+
this.options.onDiagnostic("the PGlite filesystem sync is stuck; treating the session as poisoned", failure);
|
|
5331
|
+
this.markPoisoned(failure);
|
|
5332
|
+
try {
|
|
5333
|
+
this.options.onSyncStuck(failure);
|
|
5334
|
+
} catch (error) {
|
|
5335
|
+
this.options.onDiagnostic("onSyncStuck threw", error);
|
|
5336
|
+
}
|
|
5337
|
+
throw this.poisonCause ?? failure;
|
|
5338
|
+
}
|
|
5339
|
+
/** Waits for the statement in flight to finish and holds the next one back. */
|
|
5340
|
+
async holdStatements() {
|
|
5341
|
+
while (this.gate !== void 0) await Promise.race([this.gate, this.poisonedSignal]);
|
|
5342
|
+
this.assertHealthy();
|
|
5343
|
+
this.gate = new Promise((resolve) => {
|
|
5344
|
+
this.releaseGate = () => {
|
|
5345
|
+
this.gate = void 0;
|
|
5346
|
+
this.releaseGate = () => void 0;
|
|
5347
|
+
resolve();
|
|
5348
|
+
};
|
|
5349
|
+
});
|
|
5350
|
+
const release = () => this.releaseGate();
|
|
5351
|
+
if (this.inFlight === 0) return release;
|
|
5352
|
+
try {
|
|
5353
|
+
await Promise.race([new Promise((resolve) => this.idleWaiters.push(resolve)), this.poisonedSignal]);
|
|
5354
|
+
} catch (error) {
|
|
5355
|
+
release();
|
|
5356
|
+
throw error;
|
|
5357
|
+
}
|
|
5358
|
+
return release;
|
|
5359
|
+
}
|
|
5360
|
+
async enterStatement() {
|
|
5361
|
+
while (this.gate !== void 0) await this.gate;
|
|
5362
|
+
this.inFlight += 1;
|
|
5363
|
+
this.sequence += 1;
|
|
5364
|
+
}
|
|
5365
|
+
endStatement() {
|
|
5366
|
+
this.inFlight -= 1;
|
|
5367
|
+
if (this.inFlight > 0) return;
|
|
5368
|
+
const waiters = this.idleWaiters;
|
|
5369
|
+
this.idleWaiters = [];
|
|
5370
|
+
for (const waiter of waiters) waiter();
|
|
5371
|
+
}
|
|
5372
|
+
};
|
|
5373
|
+
//#endregion
|
|
5374
|
+
//#region src/storage/storage-health.ts
|
|
5375
|
+
/** Fed by the poison path so "connected" never reads green over a dead session. */
|
|
5376
|
+
var StorageHealthTracker = class {
|
|
5377
|
+
healthy = true;
|
|
5378
|
+
markPoisoned() {
|
|
5379
|
+
this.healthy = false;
|
|
5380
|
+
}
|
|
5381
|
+
getStorageHealth() {
|
|
5382
|
+
return { healthy: this.healthy };
|
|
5383
|
+
}
|
|
5384
|
+
};
|
|
5385
|
+
//#endregion
|
|
5386
|
+
//#region src/storage/kysely/pglite-dialect.ts
|
|
5387
|
+
var pglite_dialect_exports = /* @__PURE__ */ __exportAll({
|
|
5388
|
+
DEFAULT_LONG_STATEMENT_TIMEOUT_MS: () => DEFAULT_LONG_STATEMENT_TIMEOUT_MS,
|
|
5389
|
+
DEFAULT_RECOVERY_TIMEOUT_MS: () => DEFAULT_RECOVERY_TIMEOUT_MS,
|
|
5390
|
+
DEFAULT_STATEMENT_TIMEOUT_MS: () => DEFAULT_STATEMENT_TIMEOUT_MS,
|
|
5391
|
+
HardenedPGliteDialect: () => HardenedPGliteDialect,
|
|
5392
|
+
PGliteAbortedTransactionError: () => PGliteAbortedTransactionError,
|
|
5393
|
+
PGliteAcquireTimeoutError: () => PGliteAcquireTimeoutError,
|
|
5394
|
+
PGliteSessionError: () => PGliteSessionError,
|
|
5395
|
+
PGliteSessionPoisonedError: () => PGliteSessionPoisonedError,
|
|
5396
|
+
PGliteStatementTimeoutError: () => PGliteStatementTimeoutError,
|
|
5397
|
+
isLongRunningStatement: () => isLongRunningStatement,
|
|
5398
|
+
queryThroughDialect: () => queryThroughDialect
|
|
5399
|
+
});
|
|
5400
|
+
const DEFAULT_STATEMENT_TIMEOUT_MS = 12e4;
|
|
5401
|
+
const DEFAULT_LONG_STATEMENT_TIMEOUT_MS = 9e5;
|
|
5402
|
+
const DEFAULT_RECOVERY_TIMEOUT_MS = 15e3;
|
|
5403
|
+
const LONG_STATEMENT_PREFIXES = [
|
|
5404
|
+
"vacuum",
|
|
5405
|
+
"analyze",
|
|
5406
|
+
"reindex",
|
|
5407
|
+
"cluster",
|
|
5408
|
+
"checkpoint",
|
|
5409
|
+
"copy",
|
|
5410
|
+
"alter ",
|
|
5411
|
+
"create ",
|
|
5412
|
+
"drop ",
|
|
5413
|
+
"truncate",
|
|
5414
|
+
"refresh "
|
|
5415
|
+
];
|
|
5416
|
+
/** Maintenance and DDL, matched on the leading keyword of the compiled SQL. */
|
|
5417
|
+
function isLongRunningStatement(statement) {
|
|
5418
|
+
const normalized = statement.trimStart().toLowerCase();
|
|
5419
|
+
return LONG_STATEMENT_PREFIXES.some((prefix) => normalized.startsWith(prefix));
|
|
5420
|
+
}
|
|
5421
|
+
function summarize(statement) {
|
|
5422
|
+
const collapsed = statement.replace(/\s+/g, " ").trim();
|
|
5423
|
+
return collapsed.length > 160 ? `${collapsed.slice(0, 160)}...` : collapsed;
|
|
5424
|
+
}
|
|
5425
|
+
/** COMMIT in an aborted transaction silently rolls back; the guard raises first. */
|
|
5426
|
+
const COMMIT_WITH_GUARD = "select 1 as __commit_guard; commit";
|
|
5427
|
+
const PROBE = "select 1 as __session_probe";
|
|
5428
|
+
const ABORTED_TRANSACTION_CODE = "25P02";
|
|
5429
|
+
var PGliteSessionError = class extends Error {
|
|
5430
|
+
cause;
|
|
5431
|
+
constructor(message, cause) {
|
|
5432
|
+
super(message);
|
|
5433
|
+
this.cause = cause;
|
|
5434
|
+
this.name = "PGliteSessionError";
|
|
5435
|
+
}
|
|
5436
|
+
};
|
|
5437
|
+
var PGliteAcquireTimeoutError = class extends PGliteSessionError {
|
|
5438
|
+
constructor(timeoutMs) {
|
|
5439
|
+
super(`Timed out after ${timeoutMs}ms waiting for the PGlite connection. The single session is held by a statement that never completed: a transaction awaiting a nested query on the base handle, or an abandoned stream iterator.`);
|
|
5440
|
+
this.name = "PGliteAcquireTimeoutError";
|
|
5441
|
+
}
|
|
5442
|
+
};
|
|
5443
|
+
/** The call cannot be aborted, so it is abandoned and the session treated as poisoned. */
|
|
5444
|
+
var PGliteStatementTimeoutError = class extends PGliteSessionError {
|
|
5445
|
+
constructor(timeoutMs, statement) {
|
|
5446
|
+
super(`A PGlite statement neither resolved nor rejected within ${timeoutMs}ms, so its wasm call is presumed dead and the session poisoned: ${summarize(statement)}`);
|
|
5447
|
+
this.name = "PGliteStatementTimeoutError";
|
|
5448
|
+
}
|
|
5449
|
+
};
|
|
5450
|
+
var PGliteAbortedTransactionError = class extends PGliteSessionError {
|
|
5451
|
+
constructor(cause) {
|
|
5452
|
+
super(`Refusing to report a commit for an aborted transaction: COMMIT would have been answered with a ROLLBACK tag and nothing written. Underlying error: ${errorOf(cause).message}`, cause);
|
|
5453
|
+
this.name = "PGliteAbortedTransactionError";
|
|
5454
|
+
}
|
|
5455
|
+
};
|
|
5456
|
+
/** No statement can clear the session (e.g. a stuck active portal). */
|
|
5457
|
+
var PGliteSessionPoisonedError = class extends PGliteSessionError {
|
|
5458
|
+
constructor(cause) {
|
|
5459
|
+
super(`The PGlite session is unrecoverable and cannot be reused; the component owning it must be restarted. Original error: ${errorOf(cause).message}`, cause);
|
|
5460
|
+
this.name = "PGliteSessionPoisonedError";
|
|
5461
|
+
}
|
|
5462
|
+
};
|
|
5463
|
+
function errorOf(error) {
|
|
5464
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
5465
|
+
}
|
|
5466
|
+
function isAbortedTransactionError(error) {
|
|
5467
|
+
if (error?.code === ABORTED_TRANSACTION_CODE) return true;
|
|
5468
|
+
return errorOf(error).message.includes("current transaction is aborted");
|
|
5469
|
+
}
|
|
5470
|
+
/** `kysely-pglite-dialect` hardened for one shared session that a fault must not brick. */
|
|
5471
|
+
var HardenedPGliteDialect = class {
|
|
5472
|
+
client;
|
|
5473
|
+
inner;
|
|
5474
|
+
options;
|
|
5475
|
+
constructor(client, options = {}) {
|
|
5476
|
+
this.client = client;
|
|
5477
|
+
this.inner = new PGliteDialect(client);
|
|
5478
|
+
this.options = {
|
|
5479
|
+
acquireTimeoutMs: options.acquireTimeoutMs ?? 0,
|
|
5480
|
+
statementTimeoutMs: options.statementTimeoutMs ?? 12e4,
|
|
5481
|
+
longStatementTimeoutMs: options.longStatementTimeoutMs ?? 9e5,
|
|
5482
|
+
recoveryTimeoutMs: options.recoveryTimeoutMs ?? 15e3,
|
|
5483
|
+
onDiagnostic: options.onDiagnostic ?? ((message, error) => {
|
|
5484
|
+
console.error(`[pglite-dialect] ${message}`, error);
|
|
5485
|
+
}),
|
|
5486
|
+
onPoisoned: options.onPoisoned ?? (() => void 0)
|
|
5487
|
+
};
|
|
5488
|
+
}
|
|
5489
|
+
createAdapter() {
|
|
5490
|
+
return this.inner.createAdapter();
|
|
5491
|
+
}
|
|
5492
|
+
createDriver() {
|
|
5493
|
+
return new HardenedPGliteDriver(this.inner.createDriver(), this.client, this.options);
|
|
5494
|
+
}
|
|
5495
|
+
createQueryCompiler() {
|
|
5496
|
+
return this.inner.createQueryCompiler();
|
|
5497
|
+
}
|
|
5498
|
+
createIntrospector(db) {
|
|
5499
|
+
return this.inner.createIntrospector(db);
|
|
5500
|
+
}
|
|
5501
|
+
};
|
|
5502
|
+
var HardenedPGliteConnection = class {
|
|
5503
|
+
inner;
|
|
5504
|
+
guard;
|
|
5505
|
+
failure = void 0;
|
|
5506
|
+
rollbackFailure = void 0;
|
|
5507
|
+
transactionOpen = false;
|
|
5508
|
+
constructor(inner, guard) {
|
|
5509
|
+
this.inner = inner;
|
|
5510
|
+
this.guard = guard;
|
|
5511
|
+
}
|
|
5512
|
+
/** Never retried: a replay could commit alone a write meant for another consumer's transaction. */
|
|
5513
|
+
async executeQuery(compiledQuery) {
|
|
5514
|
+
const generation = this.guard.generation;
|
|
5515
|
+
try {
|
|
5516
|
+
return await this.guard.runStatement(compiledQuery.sql, () => this.inner.executeQuery(compiledQuery));
|
|
5517
|
+
} catch (error) {
|
|
5518
|
+
this.guard.recordFailure(this, generation, error);
|
|
5519
|
+
throw error;
|
|
5520
|
+
}
|
|
5521
|
+
}
|
|
5522
|
+
streamQuery(compiledQuery, chunkSize) {
|
|
5523
|
+
const inner = this.inner.streamQuery(compiledQuery, chunkSize);
|
|
5524
|
+
return this.deadlinedStream(compiledQuery, inner);
|
|
5525
|
+
}
|
|
5526
|
+
/** Upstream issues the whole query on the first pull, so each pull is bounded like a statement. */
|
|
5527
|
+
async *deadlinedStream(compiledQuery, inner) {
|
|
5528
|
+
for (;;) {
|
|
5529
|
+
const generation = this.guard.generation;
|
|
5530
|
+
let next;
|
|
5531
|
+
try {
|
|
5532
|
+
next = await this.guard.runStatement(compiledQuery.sql, () => inner.next());
|
|
5533
|
+
} catch (error) {
|
|
5534
|
+
this.guard.recordFailure(this, generation, error);
|
|
5535
|
+
throw error;
|
|
5536
|
+
}
|
|
5537
|
+
if (next.done === true) return;
|
|
5538
|
+
yield next.value;
|
|
5539
|
+
}
|
|
5540
|
+
}
|
|
5541
|
+
get suspect() {
|
|
5542
|
+
return this.failure !== void 0 || this.rollbackFailure !== void 0 || this.transactionOpen;
|
|
5543
|
+
}
|
|
5544
|
+
};
|
|
5545
|
+
var HardenedPGliteDriver = class {
|
|
5546
|
+
inner;
|
|
5547
|
+
client;
|
|
5548
|
+
options;
|
|
5549
|
+
/** Set when release could not reset the session; each acquire retries the reset. */
|
|
5550
|
+
sessionFault = void 0;
|
|
5551
|
+
/** Lets an abandoned call's late settlement be told apart from current failures. */
|
|
5552
|
+
statementGeneration = 0;
|
|
5553
|
+
/** A call abandoned at its deadline; PGlite would queue every later call behind it, so none is issued. */
|
|
5554
|
+
deadCall = void 0;
|
|
5555
|
+
poisonReported = false;
|
|
5556
|
+
constructor(inner, client, options) {
|
|
5557
|
+
this.inner = inner;
|
|
5558
|
+
this.client = client;
|
|
5559
|
+
this.options = options;
|
|
5560
|
+
}
|
|
5561
|
+
get generation() {
|
|
5562
|
+
return this.statementGeneration;
|
|
5563
|
+
}
|
|
5564
|
+
/** Bounds one statement; an expiry is escalated as a poisoned session. */
|
|
5565
|
+
async runStatement(statement, execute) {
|
|
5566
|
+
if (this.deadCall !== void 0) throw new PGliteSessionPoisonedError(this.deadCall);
|
|
5567
|
+
const timeoutMs = this.timeoutFor(statement);
|
|
5568
|
+
if (timeoutMs <= 0) return execute();
|
|
5569
|
+
return this.admitted(async () => {
|
|
5570
|
+
if (this.deadCall !== void 0) throw new PGliteSessionPoisonedError(this.deadCall);
|
|
5571
|
+
const generation = this.statementGeneration;
|
|
5572
|
+
const outcome = await withDeadline(execute(), timeoutMs);
|
|
5573
|
+
if (outcome !== TIMED_OUT) return outcome;
|
|
5574
|
+
throw this.escalateHungStatement(new PGliteStatementTimeoutError(timeoutMs, statement), generation);
|
|
5575
|
+
});
|
|
5576
|
+
}
|
|
5577
|
+
/** Time spent waiting to be admitted is bounded by the session, not the deadline. */
|
|
5578
|
+
admitted(run) {
|
|
5579
|
+
return this.client.admit ? this.client.admit(run) : run();
|
|
5580
|
+
}
|
|
5581
|
+
recordFailure(connection, generation, error) {
|
|
5582
|
+
if (generation === this.statementGeneration) connection.failure = errorOf(error);
|
|
5583
|
+
}
|
|
5584
|
+
async init() {
|
|
5585
|
+
await this.inner.init();
|
|
5586
|
+
}
|
|
5587
|
+
async acquireConnection() {
|
|
5588
|
+
const innerConnection = await this.acquireWithTimeout();
|
|
5589
|
+
if (this.deadCall !== void 0) {
|
|
5590
|
+
await this.inner.releaseConnection(innerConnection);
|
|
5591
|
+
throw new PGliteSessionPoisonedError(this.deadCall);
|
|
5592
|
+
}
|
|
5593
|
+
if (this.sessionFault !== void 0) {
|
|
5594
|
+
const fault = this.sessionFault;
|
|
5595
|
+
let recovered = false;
|
|
5596
|
+
try {
|
|
5597
|
+
recovered = await this.tryRecoverSession(false);
|
|
5598
|
+
} finally {
|
|
5599
|
+
if (!recovered) await this.inner.releaseConnection(innerConnection);
|
|
5600
|
+
}
|
|
5601
|
+
if (!recovered) {
|
|
5602
|
+
this.reportPoisoned(fault);
|
|
5603
|
+
throw new PGliteSessionPoisonedError(fault);
|
|
5604
|
+
}
|
|
5605
|
+
this.sessionFault = void 0;
|
|
5606
|
+
}
|
|
5607
|
+
return new HardenedPGliteConnection(innerConnection, this);
|
|
5608
|
+
}
|
|
5609
|
+
/** Kysely never rolls back after a failed BEGIN, so an aborted session is reset here. */
|
|
5610
|
+
async beginTransaction(connection, settings) {
|
|
5611
|
+
const wrapper = asWrapper(connection);
|
|
5612
|
+
const generation = this.statementGeneration;
|
|
5613
|
+
try {
|
|
5614
|
+
await this.runStatement("begin", () => this.inner.beginTransaction(wrapper.inner, settings));
|
|
5615
|
+
} catch (error) {
|
|
5616
|
+
this.recordFailure(wrapper, generation, error);
|
|
5617
|
+
if (!isAbortedTransactionError(error)) throw error;
|
|
5618
|
+
if (!await this.tryRecoverSession(false)) {
|
|
5619
|
+
this.sessionFault = errorOf(error);
|
|
5620
|
+
throw error;
|
|
5621
|
+
}
|
|
5622
|
+
const retryGeneration = this.statementGeneration;
|
|
5623
|
+
try {
|
|
5624
|
+
await this.runStatement("begin", () => this.inner.beginTransaction(wrapper.inner, settings));
|
|
5625
|
+
} catch (retryError) {
|
|
5626
|
+
this.recordFailure(wrapper, retryGeneration, retryError);
|
|
5627
|
+
throw retryError;
|
|
5628
|
+
}
|
|
5629
|
+
wrapper.failure = void 0;
|
|
5630
|
+
}
|
|
5631
|
+
wrapper.transactionOpen = true;
|
|
5632
|
+
}
|
|
5633
|
+
async commitTransaction(connection) {
|
|
5634
|
+
const wrapper = asWrapper(connection);
|
|
5635
|
+
const generation = this.statementGeneration;
|
|
5636
|
+
try {
|
|
5637
|
+
await this.runStatement(COMMIT_WITH_GUARD, () => this.client.exec(COMMIT_WITH_GUARD));
|
|
5638
|
+
} catch (error) {
|
|
5639
|
+
this.recordFailure(wrapper, generation, error);
|
|
5640
|
+
if (isAbortedTransactionError(error)) throw new PGliteAbortedTransactionError(error);
|
|
5641
|
+
throw error;
|
|
5642
|
+
}
|
|
5643
|
+
wrapper.transactionOpen = false;
|
|
5644
|
+
wrapper.failure = void 0;
|
|
5645
|
+
}
|
|
5646
|
+
/** Swallowed so the original failure propagates; release resets the session. */
|
|
5647
|
+
async rollbackTransaction(connection) {
|
|
5648
|
+
const wrapper = asWrapper(connection);
|
|
5649
|
+
const generation = this.statementGeneration;
|
|
5650
|
+
try {
|
|
5651
|
+
await this.runStatement("rollback", () => this.inner.rollbackTransaction(wrapper.inner));
|
|
5652
|
+
wrapper.transactionOpen = false;
|
|
5653
|
+
} catch (error) {
|
|
5654
|
+
if (generation === this.statementGeneration) wrapper.rollbackFailure = errorOf(error);
|
|
5655
|
+
this.options.onDiagnostic("rollback failed; preserving the original failure and recovering the session on release", error);
|
|
5656
|
+
}
|
|
5657
|
+
}
|
|
5658
|
+
async releaseConnection(connection) {
|
|
5659
|
+
const wrapper = asWrapper(connection);
|
|
5660
|
+
try {
|
|
5661
|
+
if (wrapper.suspect && this.deadCall === void 0) {
|
|
5662
|
+
if (!await this.tryRecoverSession(wrapper.transactionOpen)) {
|
|
5663
|
+
this.sessionFault = wrapper.rollbackFailure ?? wrapper.failure ?? /* @__PURE__ */ new Error("session left in a transaction");
|
|
5664
|
+
this.options.onDiagnostic("the PGlite session could not be reset and is being marked unrecoverable", this.sessionFault);
|
|
5665
|
+
}
|
|
5666
|
+
}
|
|
5667
|
+
} finally {
|
|
5668
|
+
await this.inner.releaseConnection(wrapper.inner);
|
|
5669
|
+
}
|
|
5670
|
+
}
|
|
5671
|
+
async destroy() {
|
|
5672
|
+
await this.inner.destroy();
|
|
5673
|
+
}
|
|
5674
|
+
reportPoisoned(cause) {
|
|
5675
|
+
if (this.poisonReported) return;
|
|
5676
|
+
this.poisonReported = true;
|
|
5677
|
+
try {
|
|
5678
|
+
this.options.onPoisoned(cause);
|
|
5679
|
+
} catch (error) {
|
|
5680
|
+
this.options.onDiagnostic("onPoisoned threw", error);
|
|
5681
|
+
}
|
|
5682
|
+
}
|
|
5683
|
+
timeoutFor(statement) {
|
|
5684
|
+
return isLongRunningStatement(statement) ? this.options.longStatementTimeoutMs : this.options.statementTimeoutMs;
|
|
5685
|
+
}
|
|
5686
|
+
/** Marks the session faulted once per generation; a stale expiry reports only itself. */
|
|
5687
|
+
escalateHungStatement(cause, generation) {
|
|
5688
|
+
if (generation !== this.statementGeneration) return cause;
|
|
5689
|
+
this.statementGeneration += 1;
|
|
5690
|
+
this.deadCall = cause;
|
|
5691
|
+
this.reportPoisoned(cause);
|
|
5692
|
+
this.options.onDiagnostic("a PGlite statement never settled within its deadline; treating the session as poisoned", cause);
|
|
5693
|
+
return new PGliteSessionPoisonedError(cause);
|
|
5694
|
+
}
|
|
5695
|
+
/** A waiter that gave up still passes the lease on, so it cannot wedge the queue. */
|
|
5696
|
+
async acquireWithTimeout() {
|
|
5697
|
+
const timeoutMs = this.options.acquireTimeoutMs;
|
|
5698
|
+
if (timeoutMs <= 0) return this.inner.acquireConnection();
|
|
5699
|
+
let abandoned = false;
|
|
5700
|
+
const pending = this.inner.acquireConnection().then((connection) => {
|
|
5701
|
+
if (!abandoned) return connection;
|
|
5702
|
+
this.inner.releaseConnection(connection).catch(() => void 0);
|
|
5703
|
+
throw new PGliteAcquireTimeoutError(timeoutMs);
|
|
5704
|
+
});
|
|
5705
|
+
let handle;
|
|
5706
|
+
const expiry = new Promise((_resolve, reject) => {
|
|
5707
|
+
handle = setTimeout(() => {
|
|
5708
|
+
abandoned = true;
|
|
5709
|
+
reject(new PGliteAcquireTimeoutError(timeoutMs));
|
|
5710
|
+
}, timeoutMs);
|
|
5711
|
+
});
|
|
5712
|
+
try {
|
|
5713
|
+
return await Promise.race([pending, expiry]);
|
|
5714
|
+
} finally {
|
|
5715
|
+
clearTimeout(handle);
|
|
5716
|
+
}
|
|
5717
|
+
}
|
|
5718
|
+
/** {@link recoverSession}, with a recovery that throws counted as failed. */
|
|
5719
|
+
async tryRecoverSession(mustEndTransaction) {
|
|
5720
|
+
try {
|
|
5721
|
+
return await this.recoverSession(mustEndTransaction);
|
|
5722
|
+
} catch (error) {
|
|
5723
|
+
this.options.onDiagnostic("session recovery threw", error);
|
|
5724
|
+
return false;
|
|
5725
|
+
}
|
|
5726
|
+
}
|
|
5727
|
+
/** Rolls back only when told to or when the probe fails, sparing another consumer's live transaction. */
|
|
5728
|
+
async recoverSession(mustEndTransaction) {
|
|
5729
|
+
if (!mustEndTransaction && !this.client.isInTransaction() && await this.probeSession()) return true;
|
|
5730
|
+
if (!await this.recoveryExec("rollback")) this.options.onDiagnostic("session rollback did not complete; the session stays suspect");
|
|
5731
|
+
if (!await this.probeSession()) return false;
|
|
5732
|
+
return !this.client.isInTransaction();
|
|
5733
|
+
}
|
|
5734
|
+
async probeSession() {
|
|
5735
|
+
return this.recoveryExec(PROBE);
|
|
5736
|
+
}
|
|
5737
|
+
/** Kysely awaits release, so an unbounded recovery statement would hold the lease forever. */
|
|
5738
|
+
async recoveryExec(statement) {
|
|
5739
|
+
if (this.deadCall !== void 0) return false;
|
|
5740
|
+
const timeoutMs = this.options.recoveryTimeoutMs;
|
|
5741
|
+
const run = () => this.client.exec(statement).then(() => true, (error) => {
|
|
5742
|
+
this.options.onDiagnostic("recovery statement failed", error);
|
|
5743
|
+
return false;
|
|
5744
|
+
});
|
|
5745
|
+
if (timeoutMs <= 0) return run();
|
|
5746
|
+
return this.admitted(async () => {
|
|
5747
|
+
if (this.deadCall !== void 0) return false;
|
|
5748
|
+
const outcome = await withDeadline(run(), timeoutMs);
|
|
5749
|
+
if (outcome === TIMED_OUT) {
|
|
5750
|
+
this.deadCall ??= new PGliteStatementTimeoutError(timeoutMs, statement);
|
|
5751
|
+
this.reportPoisoned(this.deadCall);
|
|
5752
|
+
this.options.onDiagnostic(`a PGlite recovery statement did not settle within ${timeoutMs}ms; the session is unrecoverable from SQL`);
|
|
5753
|
+
return false;
|
|
5754
|
+
}
|
|
5755
|
+
return outcome;
|
|
5756
|
+
});
|
|
5757
|
+
}
|
|
5758
|
+
};
|
|
5759
|
+
function asWrapper(connection) {
|
|
5760
|
+
if (!(connection instanceof HardenedPGliteConnection)) throw new PGliteSessionError("Connection was not created by HardenedPGliteDialect");
|
|
5761
|
+
return connection;
|
|
5762
|
+
}
|
|
5763
|
+
/** Raw SQL through the dialect's queue, instead of into whatever transaction holds the session. */
|
|
5764
|
+
async function queryThroughDialect(db, sql, params) {
|
|
5765
|
+
return [...(await db.executeQuery(CompiledQuery.raw(sql, params ?? []))).rows];
|
|
5766
|
+
}
|
|
5767
|
+
//#endregion
|
|
5768
|
+
//#region src/storage/kysely/group-commit-storage.ts
|
|
5769
|
+
/** One unit, so the deferral never exists without its flusher and poison path. */
|
|
5770
|
+
function createGroupCommitStorage(options) {
|
|
5771
|
+
const onDiagnostic = options.onDiagnostic ?? ((message, error) => {
|
|
5772
|
+
console.error(`[group-commit-pglite] ${message}`, error);
|
|
5773
|
+
});
|
|
5774
|
+
const health = new StorageHealthTracker();
|
|
5775
|
+
let poisoned = false;
|
|
5776
|
+
const poison = (cause) => {
|
|
5777
|
+
if (poisoned) return;
|
|
5778
|
+
poisoned = true;
|
|
5779
|
+
health.markPoisoned();
|
|
5780
|
+
client.markPoisoned(cause);
|
|
5781
|
+
options.onUnrecoverable(cause);
|
|
5782
|
+
};
|
|
5783
|
+
const client = new GroupCommitPGliteClient(options.pg, {
|
|
5784
|
+
...options.client,
|
|
5785
|
+
onDiagnostic,
|
|
5786
|
+
onSyncStuck: poison
|
|
5787
|
+
});
|
|
5788
|
+
return {
|
|
5789
|
+
db: new Kysely({ dialect: new HardenedPGliteDialect(client, {
|
|
5790
|
+
...options.dialect,
|
|
5791
|
+
onDiagnostic,
|
|
5792
|
+
onPoisoned: poison
|
|
5793
|
+
}) }),
|
|
5794
|
+
flusher: client,
|
|
5795
|
+
health,
|
|
5796
|
+
close: () => client.close(),
|
|
5797
|
+
restoreStatementSync: () => client.restoreStatementSync()
|
|
5798
|
+
};
|
|
5799
|
+
}
|
|
5800
|
+
//#endregion
|
|
5801
|
+
//#region src/subs/default-error-handler.ts
|
|
5802
|
+
/**
|
|
5803
|
+
* Default error handler that re-throws subscription errors.
|
|
5804
|
+
* This ensures that errors are not silently swallowed.
|
|
5805
|
+
*/
|
|
5806
|
+
var DefaultSubscriptionErrorHandler = class {
|
|
5807
|
+
handleError(error, context) {
|
|
5808
|
+
const errorMessage = `Subscription error in ${context.eventType} (${context.subscriptionId})`;
|
|
5809
|
+
if (error instanceof Error) {
|
|
5810
|
+
const enhancedError = /* @__PURE__ */ new Error(`${errorMessage}: ${error.message}`);
|
|
5811
|
+
enhancedError.cause = error;
|
|
5812
|
+
enhancedError.stack = error.stack;
|
|
5813
|
+
throw enhancedError;
|
|
5814
|
+
} else throw new Error(`${errorMessage}: ${String(error)}`);
|
|
5815
|
+
}
|
|
5816
|
+
};
|
|
5817
|
+
//#endregion
|
|
5818
|
+
//#region src/subs/react-subscription-manager.ts
|
|
5819
|
+
var ReactorSubscriptionManager = class {
|
|
5820
|
+
createdSubscriptions = /* @__PURE__ */ new Map();
|
|
5821
|
+
deletedSubscriptions = /* @__PURE__ */ new Map();
|
|
5822
|
+
updatedSubscriptions = /* @__PURE__ */ new Map();
|
|
5823
|
+
relationshipSubscriptions = /* @__PURE__ */ new Map();
|
|
5824
|
+
subscriptionCounter = 0;
|
|
5825
|
+
errorHandler;
|
|
5826
|
+
constructor(errorHandler) {
|
|
5827
|
+
this.errorHandler = errorHandler;
|
|
5828
|
+
}
|
|
5829
|
+
onDocumentCreated(callback, search) {
|
|
5830
|
+
const id = `created-${++this.subscriptionCounter}`;
|
|
5831
|
+
this.createdSubscriptions.set(id, {
|
|
5832
|
+
id,
|
|
5833
|
+
callback,
|
|
5834
|
+
search
|
|
5835
|
+
});
|
|
5836
|
+
return () => {
|
|
5837
|
+
this.createdSubscriptions.delete(id);
|
|
5838
|
+
};
|
|
5839
|
+
}
|
|
5840
|
+
onDocumentDeleted(callback, search) {
|
|
5841
|
+
const id = `deleted-${++this.subscriptionCounter}`;
|
|
5842
|
+
this.deletedSubscriptions.set(id, {
|
|
5843
|
+
id,
|
|
5844
|
+
callback,
|
|
5845
|
+
search
|
|
5846
|
+
});
|
|
5847
|
+
return () => {
|
|
5848
|
+
this.deletedSubscriptions.delete(id);
|
|
5849
|
+
};
|
|
5850
|
+
}
|
|
5851
|
+
onDocumentStateUpdated(callback, search, view) {
|
|
5852
|
+
const id = `updated-${++this.subscriptionCounter}`;
|
|
5853
|
+
this.updatedSubscriptions.set(id, {
|
|
5854
|
+
id,
|
|
5855
|
+
callback,
|
|
5856
|
+
search,
|
|
5857
|
+
view
|
|
5858
|
+
});
|
|
5859
|
+
return () => {
|
|
5860
|
+
this.updatedSubscriptions.delete(id);
|
|
5861
|
+
};
|
|
5862
|
+
}
|
|
5863
|
+
onRelationshipChanged(callback, search) {
|
|
5864
|
+
const id = `relationship-${++this.subscriptionCounter}`;
|
|
5865
|
+
this.relationshipSubscriptions.set(id, {
|
|
5866
|
+
id,
|
|
5867
|
+
callback,
|
|
5868
|
+
search
|
|
5869
|
+
});
|
|
5870
|
+
return () => {
|
|
5871
|
+
this.relationshipSubscriptions.delete(id);
|
|
5872
|
+
};
|
|
5873
|
+
}
|
|
5874
|
+
/**
|
|
5875
|
+
* Notify subscribers about created documents
|
|
5876
|
+
*/
|
|
5877
|
+
notifyDocumentsCreated(documentIds, documentTypes, parentIds) {
|
|
5878
|
+
const result = {
|
|
5879
|
+
results: documentIds,
|
|
5880
|
+
options: {
|
|
5881
|
+
cursor: "",
|
|
5882
|
+
limit: documentIds.length
|
|
5883
|
+
}
|
|
5884
|
+
};
|
|
5885
|
+
for (const subscription of this.createdSubscriptions.values()) {
|
|
5886
|
+
const filteredIds = this.filterDocumentIds(documentIds, subscription.search, documentTypes, parentIds);
|
|
5887
|
+
if (filteredIds.length > 0) try {
|
|
5888
|
+
subscription.callback({
|
|
5889
|
+
...result,
|
|
5890
|
+
results: filteredIds
|
|
5891
|
+
});
|
|
5892
|
+
} catch (error) {
|
|
5893
|
+
this.errorHandler.handleError(error, {
|
|
5894
|
+
eventType: "created",
|
|
5895
|
+
subscriptionId: subscription.id,
|
|
5896
|
+
eventData: filteredIds
|
|
5897
|
+
});
|
|
5898
|
+
}
|
|
5899
|
+
}
|
|
5900
|
+
}
|
|
5901
|
+
/**
|
|
5902
|
+
* Notify subscribers about deleted documents
|
|
5903
|
+
*/
|
|
5904
|
+
notifyDocumentsDeleted(documentIds, documentTypes, parentIds, info) {
|
|
5905
|
+
for (const subscription of this.deletedSubscriptions.values()) {
|
|
5267
5906
|
const filteredIds = this.filterDocumentIds(documentIds, subscription.search, documentTypes, parentIds);
|
|
5268
5907
|
if (filteredIds.length > 0) try {
|
|
5269
5908
|
if (info) subscription.callback(filteredIds, info);
|
|
@@ -5539,6 +6178,62 @@ var GroupReevaluationTrigger = class extends BaseReadModel {
|
|
|
5539
6178
|
}
|
|
5540
6179
|
};
|
|
5541
6180
|
//#endregion
|
|
6181
|
+
//#region src/sync/channels/composite-channel-factory.ts
|
|
6182
|
+
/**
|
|
6183
|
+
* An {@link IChannelFactory} that dispatches to one of several factories on
|
|
6184
|
+
* {@link ChannelConfig.type}.
|
|
6185
|
+
*
|
|
6186
|
+
* This is the seam that lets ONE reactor hold remotes of more than one
|
|
6187
|
+
* transport at once -- gql remotes to a Switchboard AND brokered
|
|
6188
|
+
* `LocalChannel` peers (multi-reactor W3.0). Before it, a reactor wired a
|
|
6189
|
+
* single factory, so a local-sync reactor was local-only and a gql reactor
|
|
6190
|
+
* could not adopt a local peer; every mixed topology was out of reach.
|
|
6191
|
+
*
|
|
6192
|
+
* Routing is strict and total: a config whose type no registered factory
|
|
6193
|
+
* claims is refused by name rather than handed to whichever factory happens to
|
|
6194
|
+
* be first. That refusal is the point -- the single-factory world answered a
|
|
6195
|
+
* `{ type: "local" }` config on a gql reactor with a complaint about a missing
|
|
6196
|
+
* `url`, which says nothing about what is actually wrong.
|
|
6197
|
+
*
|
|
6198
|
+
* The composite adds no behaviour of its own: it holds no queue, no transport
|
|
6199
|
+
* and no logger, and every argument is passed through untouched. Which types a
|
|
6200
|
+
* reactor registers is the builder's decision (`ReactorBuilder`), not this
|
|
6201
|
+
* class's.
|
|
6202
|
+
*/
|
|
6203
|
+
var CompositeChannelFactory = class {
|
|
6204
|
+
factories;
|
|
6205
|
+
/**
|
|
6206
|
+
* @param factories - (channel-config type -> factory) pairs. A duplicated
|
|
6207
|
+
* type is refused rather than silently resolved to the last entry: two
|
|
6208
|
+
* factories claiming one type is a configuration mistake with no correct
|
|
6209
|
+
* answer.
|
|
6210
|
+
* @throws Error if `factories` is empty or names a type twice
|
|
6211
|
+
*/
|
|
6212
|
+
constructor(factories) {
|
|
6213
|
+
this.factories = /* @__PURE__ */ new Map();
|
|
6214
|
+
for (const [type, factory] of factories) {
|
|
6215
|
+
if (this.factories.has(type)) throw new Error(`CompositeChannelFactory was given two factories for the channel type "${type}"`);
|
|
6216
|
+
if (factory.channelTypes && !factory.channelTypes.includes(type)) throw new Error(`CompositeChannelFactory was given a factory for "${type}" that builds only [${factory.channelTypes.join(", ")}]`);
|
|
6217
|
+
this.factories.set(type, factory);
|
|
6218
|
+
}
|
|
6219
|
+
if (this.factories.size === 0) throw new Error("CompositeChannelFactory requires at least one (type, factory) entry");
|
|
6220
|
+
}
|
|
6221
|
+
/**
|
|
6222
|
+
* Creates a channel with the factory registered for `config.type`.
|
|
6223
|
+
*
|
|
6224
|
+
* @throws Error if no factory is registered for that type
|
|
6225
|
+
*/
|
|
6226
|
+
instance(remoteId, remoteName, config, cursorStorage, collectionId, filter, operationIndex, options) {
|
|
6227
|
+
const factory = this.factories.get(config.type);
|
|
6228
|
+
if (!factory) throw new Error(`This reactor has no "${config.type}" channel factory: it composes factories for [${this.channelTypes.join(", ")}]`);
|
|
6229
|
+
return factory.instance(remoteId, remoteName, config, cursorStorage, collectionId, filter, operationIndex, options);
|
|
6230
|
+
}
|
|
6231
|
+
/** The types it routes, in registration order. */
|
|
6232
|
+
get channelTypes() {
|
|
6233
|
+
return [...this.factories.keys()];
|
|
6234
|
+
}
|
|
6235
|
+
};
|
|
6236
|
+
//#endregion
|
|
5542
6237
|
//#region src/sync/types.ts
|
|
5543
6238
|
let ChannelScheme = /* @__PURE__ */ function(ChannelScheme) {
|
|
5544
6239
|
ChannelScheme["CONNECT"] = "connect";
|
|
@@ -5556,6 +6251,19 @@ let PollBehavior = /* @__PURE__ */ function(PollBehavior) {
|
|
|
5556
6251
|
PollBehavior["Manual"] = "manual";
|
|
5557
6252
|
return PollBehavior;
|
|
5558
6253
|
}({});
|
|
6254
|
+
/**
|
|
6255
|
+
* Whether a remote outlives the process that added it.
|
|
6256
|
+
*
|
|
6257
|
+
* A `Session` remote's transport is a live object of this session, such as a
|
|
6258
|
+
* brokered `MessagePort`. Its record is written like any other, so its cursors,
|
|
6259
|
+
* holds and dead letters have a parent row, and the next `startup()` removes
|
|
6260
|
+
* it instead of rehydrating it.
|
|
6261
|
+
*/
|
|
6262
|
+
let RemotePersistence = /* @__PURE__ */ function(RemotePersistence) {
|
|
6263
|
+
RemotePersistence["Durable"] = "durable";
|
|
6264
|
+
RemotePersistence["Session"] = "session";
|
|
6265
|
+
return RemotePersistence;
|
|
6266
|
+
}({});
|
|
5559
6267
|
let SyncOperationStatus = /* @__PURE__ */ function(SyncOperationStatus) {
|
|
5560
6268
|
SyncOperationStatus[SyncOperationStatus["Unknown"] = -1] = "Unknown";
|
|
5561
6269
|
SyncOperationStatus[SyncOperationStatus["TransportPending"] = 0] = "TransportPending";
|
|
@@ -5599,10 +6307,14 @@ var MailboxAggregateError = class extends Error {
|
|
|
5599
6307
|
};
|
|
5600
6308
|
var Mailbox = class {
|
|
5601
6309
|
holdAckBelowMarkers;
|
|
6310
|
+
holdAckBelowUnapplied;
|
|
5602
6311
|
itemsMap = /* @__PURE__ */ new Map();
|
|
5603
6312
|
/** Unapplied items carrying a marker, so the held ack reads only these. */
|
|
5604
6313
|
heldMarkers = /* @__PURE__ */ new Set();
|
|
5605
|
-
|
|
6314
|
+
/** Lowest ordinal of each item neither applied nor failed, read once at add. */
|
|
6315
|
+
unapplied = /* @__PURE__ */ new Map();
|
|
6316
|
+
/** Null when stale; a valid cache may be positive infinity. */
|
|
6317
|
+
unappliedFloorCache = Number.POSITIVE_INFINITY;
|
|
5606
6318
|
addedCallbacks = [];
|
|
5607
6319
|
removedCallbacks = [];
|
|
5608
6320
|
paused = false;
|
|
@@ -5612,6 +6324,7 @@ var Mailbox = class {
|
|
|
5612
6324
|
_latestOrdinal = 0;
|
|
5613
6325
|
constructor(options = {}) {
|
|
5614
6326
|
this.holdAckBelowMarkers = options.holdAckBelowMarkers ?? false;
|
|
6327
|
+
this.holdAckBelowUnapplied = options.holdAckBelowUnapplied ?? true;
|
|
5615
6328
|
}
|
|
5616
6329
|
init(ackOrdinal) {
|
|
5617
6330
|
this._ack = this._latestOrdinal = ackOrdinal;
|
|
@@ -5623,22 +6336,17 @@ var Mailbox = class {
|
|
|
5623
6336
|
return Array.from(this.itemsMap.values());
|
|
5624
6337
|
}
|
|
5625
6338
|
get ackOrdinal() {
|
|
5626
|
-
|
|
5627
|
-
|
|
5628
|
-
for (const item of [...this.heldMarkers, ...this.held]) for (const op of item.operations) {
|
|
6339
|
+
let floor = this.unappliedFloor();
|
|
6340
|
+
for (const item of this.heldMarkers) for (const op of item.operations) {
|
|
5629
6341
|
const ordinal = op.context.ordinal;
|
|
5630
6342
|
if (ordinal > 0 && ordinal < floor) floor = ordinal;
|
|
5631
6343
|
}
|
|
6344
|
+
if (floor === Number.POSITIVE_INFINITY) return this._ack;
|
|
5632
6345
|
return Math.min(this._ack, floor - 1);
|
|
5633
6346
|
}
|
|
5634
6347
|
get latestOrdinal() {
|
|
5635
6348
|
return this._latestOrdinal;
|
|
5636
6349
|
}
|
|
5637
|
-
hold(item) {
|
|
5638
|
-
if (item.status === 2) return;
|
|
5639
|
-
if (this.itemsMap.get(item.id) !== item) return;
|
|
5640
|
-
this.held.add(item);
|
|
5641
|
-
}
|
|
5642
6350
|
get(id) {
|
|
5643
6351
|
return this.itemsMap.get(id);
|
|
5644
6352
|
}
|
|
@@ -5647,19 +6355,28 @@ var Mailbox = class {
|
|
|
5647
6355
|
const replaced = this.itemsMap.get(item.id);
|
|
5648
6356
|
if (replaced !== void 0 && replaced !== item) {
|
|
5649
6357
|
this.heldMarkers.delete(replaced);
|
|
5650
|
-
this.
|
|
6358
|
+
this.forgetUnapplied(replaced);
|
|
5651
6359
|
}
|
|
5652
6360
|
this.itemsMap.set(item.id, item);
|
|
5653
6361
|
let marker = false;
|
|
6362
|
+
let lowest = Number.POSITIVE_INFINITY;
|
|
5654
6363
|
for (const op of item.operations) {
|
|
5655
|
-
|
|
6364
|
+
const ordinal = op.context.ordinal;
|
|
6365
|
+
this._latestOrdinal = Math.max(this._latestOrdinal, ordinal);
|
|
6366
|
+
if (ordinal > 0 && ordinal < lowest) lowest = ordinal;
|
|
5656
6367
|
if (isPurgeMarker$1(op)) marker = true;
|
|
5657
6368
|
}
|
|
6369
|
+
if (this.holdAckBelowUnapplied && lowest !== Number.POSITIVE_INFINITY && item.status !== 2 && item.status !== 3) {
|
|
6370
|
+
this.unapplied.set(item, lowest);
|
|
6371
|
+
const cached = this.unappliedFloorCache;
|
|
6372
|
+
if (cached !== null && lowest < cached) this.unappliedFloorCache = lowest;
|
|
6373
|
+
}
|
|
5658
6374
|
if (this.holdAckBelowMarkers && marker && item.status !== 2) this.heldMarkers.add(item);
|
|
5659
6375
|
item.on((syncOp, _, next) => {
|
|
6376
|
+
if (next === 3) this.forgetUnapplied(syncOp);
|
|
5660
6377
|
if (next === 2) {
|
|
5661
6378
|
this.heldMarkers.delete(syncOp);
|
|
5662
|
-
this.
|
|
6379
|
+
this.forgetUnapplied(syncOp);
|
|
5663
6380
|
for (const op of syncOp.operations) this._ack = Math.max(this._ack, op.context.ordinal);
|
|
5664
6381
|
}
|
|
5665
6382
|
});
|
|
@@ -5679,9 +6396,14 @@ var Mailbox = class {
|
|
|
5679
6396
|
}
|
|
5680
6397
|
remove(...items) {
|
|
5681
6398
|
for (const item of items) {
|
|
6399
|
+
const current = this.itemsMap.get(item.id);
|
|
6400
|
+
if (current !== void 0 && current !== item) {
|
|
6401
|
+
this.heldMarkers.delete(current);
|
|
6402
|
+
this.forgetUnapplied(current);
|
|
6403
|
+
}
|
|
5682
6404
|
this.itemsMap.delete(item.id);
|
|
5683
6405
|
this.heldMarkers.delete(item);
|
|
5684
|
-
this.
|
|
6406
|
+
this.forgetUnapplied(item);
|
|
5685
6407
|
}
|
|
5686
6408
|
if (this.paused) {
|
|
5687
6409
|
this.removedBuffer.push(...items);
|
|
@@ -5736,6 +6458,20 @@ var Mailbox = class {
|
|
|
5736
6458
|
isPaused() {
|
|
5737
6459
|
return this.paused;
|
|
5738
6460
|
}
|
|
6461
|
+
/** Recomputed only once the item that set it has resolved. */
|
|
6462
|
+
unappliedFloor() {
|
|
6463
|
+
if (this.unappliedFloorCache !== null) return this.unappliedFloorCache;
|
|
6464
|
+
let floor = Number.POSITIVE_INFINITY;
|
|
6465
|
+
for (const ordinal of this.unapplied.values()) if (ordinal < floor) floor = ordinal;
|
|
6466
|
+
this.unappliedFloorCache = floor;
|
|
6467
|
+
return floor;
|
|
6468
|
+
}
|
|
6469
|
+
forgetUnapplied(item) {
|
|
6470
|
+
const ordinal = this.unapplied.get(item);
|
|
6471
|
+
if (ordinal === void 0) return;
|
|
6472
|
+
this.unapplied.delete(item);
|
|
6473
|
+
if (ordinal === this.unappliedFloorCache) this.unappliedFloorCache = null;
|
|
6474
|
+
}
|
|
5739
6475
|
};
|
|
5740
6476
|
//#endregion
|
|
5741
6477
|
//#region src/sync/buffered-mailbox.ts
|
|
@@ -5965,6 +6701,15 @@ var SyncRepairRefusedError = class extends Error {
|
|
|
5965
6701
|
this.name = "SyncRepairRefusedError";
|
|
5966
6702
|
}
|
|
5967
6703
|
};
|
|
6704
|
+
/** A dead-letter page cursor that is not a row offset. */
|
|
6705
|
+
var InvalidDeadLetterCursorError = class extends Error {
|
|
6706
|
+
cursor;
|
|
6707
|
+
constructor(cursor) {
|
|
6708
|
+
super(`Invalid dead-letter cursor: ${JSON.stringify(cursor)}`);
|
|
6709
|
+
this.cursor = cursor;
|
|
6710
|
+
this.name = "InvalidDeadLetterCursorError";
|
|
6711
|
+
}
|
|
6712
|
+
};
|
|
5968
6713
|
var PollingChannelError = class extends Error {
|
|
5969
6714
|
constructor(message) {
|
|
5970
6715
|
super(message);
|
|
@@ -7695,6 +8440,13 @@ var GqlRequestChannel = class {
|
|
|
7695
8440
|
//#endregion
|
|
7696
8441
|
//#region src/sync/channels/gql-request-channel-factory.ts
|
|
7697
8442
|
/**
|
|
8443
|
+
* The {@link ChannelConfig.type} a GqlRequestChannel is created from.
|
|
8444
|
+
*
|
|
8445
|
+
* Named so {@link CompositeChannelFactory} registration and the callers that
|
|
8446
|
+
* build a `{ type: "gql" }` config agree on one spelling.
|
|
8447
|
+
*/
|
|
8448
|
+
const GQL_CHANNEL_TYPE = "gql";
|
|
8449
|
+
/**
|
|
7698
8450
|
* Factory for creating GqlRequestChannel instances.
|
|
7699
8451
|
*
|
|
7700
8452
|
* Extracts GraphQL-specific configuration from ChannelConfig.parameters and
|
|
@@ -7704,6 +8456,7 @@ var GqlRequestChannel = class {
|
|
|
7704
8456
|
* which is useful for short-lived tokens with audience-specific claims.
|
|
7705
8457
|
*/
|
|
7706
8458
|
var GqlRequestChannelFactory = class {
|
|
8459
|
+
channelTypes = ["gql"];
|
|
7707
8460
|
logger;
|
|
7708
8461
|
jwtHandler;
|
|
7709
8462
|
queue;
|
|
@@ -7815,7 +8568,7 @@ var GqlResponseChannel = class {
|
|
|
7815
8568
|
this.cursorStorage = cursorStorage;
|
|
7816
8569
|
this.isShutdown = false;
|
|
7817
8570
|
this.inbox = new Mailbox({ holdAckBelowMarkers: true });
|
|
7818
|
-
this.outbox = new Mailbox();
|
|
8571
|
+
this.outbox = new Mailbox({ holdAckBelowUnapplied: false });
|
|
7819
8572
|
this.deadLetter = new Mailbox();
|
|
7820
8573
|
this.outbox.onRemoved((syncOps) => {
|
|
7821
8574
|
this.rememberUnserved(syncOps);
|
|
@@ -7964,9 +8717,20 @@ var GqlResponseChannel = class {
|
|
|
7964
8717
|
//#endregion
|
|
7965
8718
|
//#region src/sync/channels/gql-response-channel-factory.ts
|
|
7966
8719
|
/**
|
|
8720
|
+
* The {@link ChannelConfig.type} a GqlResponseChannel is created from.
|
|
8721
|
+
*
|
|
8722
|
+
* A response channel is never configured by this reactor: it is registered by
|
|
8723
|
+
* the peer that polls it, over the `registerChannel` mutation, whose resolver
|
|
8724
|
+
* (`packages/reactor-api`) writes exactly this type. Named here so
|
|
8725
|
+
* {@link CompositeChannelFactory} registration for the SWITCHBOARD scheme
|
|
8726
|
+
* routes the configs that actually arrive.
|
|
8727
|
+
*/
|
|
8728
|
+
const POLLING_CHANNEL_TYPE = "polling";
|
|
8729
|
+
/**
|
|
7967
8730
|
* Factory for creating GqlResponseChannel instances.
|
|
7968
8731
|
*/
|
|
7969
8732
|
var GqlResponseChannelFactory = class {
|
|
8733
|
+
channelTypes = [POLLING_CHANNEL_TYPE];
|
|
7970
8734
|
logger;
|
|
7971
8735
|
constructor(logger) {
|
|
7972
8736
|
this.logger = logger;
|
|
@@ -7976,6 +8740,36 @@ var GqlResponseChannelFactory = class {
|
|
|
7976
8740
|
}
|
|
7977
8741
|
};
|
|
7978
8742
|
//#endregion
|
|
8743
|
+
//#region src/storage/flush-guarded-sync-cursor-storage.ts
|
|
8744
|
+
/**
|
|
8745
|
+
* Durability boundary 1 as a seam around whatever cursor storage sync uses: no
|
|
8746
|
+
* cursor row is written before a flush covering its operations, and a failed
|
|
8747
|
+
* flush takes the write with it. The row itself is not flushed after: a crash
|
|
8748
|
+
* then loses the advance, never the data, and a re-pull is idempotent.
|
|
8749
|
+
*/
|
|
8750
|
+
var FlushGuardedSyncCursorStorage = class {
|
|
8751
|
+
inner;
|
|
8752
|
+
flusher;
|
|
8753
|
+
constructor(inner, flusher) {
|
|
8754
|
+
this.inner = inner;
|
|
8755
|
+
this.flusher = flusher;
|
|
8756
|
+
}
|
|
8757
|
+
list(remoteName, signal) {
|
|
8758
|
+
return this.inner.list(remoteName, signal);
|
|
8759
|
+
}
|
|
8760
|
+
get(remoteName, cursorType, signal) {
|
|
8761
|
+
return this.inner.get(remoteName, cursorType, signal);
|
|
8762
|
+
}
|
|
8763
|
+
async upsert(cursor, signal) {
|
|
8764
|
+
await this.flusher.flush();
|
|
8765
|
+
await this.inner.upsert(cursor, signal);
|
|
8766
|
+
}
|
|
8767
|
+
/** Forgetting an advance is the safe direction, so it needs no barrier. */
|
|
8768
|
+
remove(remoteName, signal) {
|
|
8769
|
+
return this.inner.remove(remoteName, signal);
|
|
8770
|
+
}
|
|
8771
|
+
};
|
|
8772
|
+
//#endregion
|
|
7979
8773
|
//#region src/storage/kysely/delivery-lookup.ts
|
|
7980
8774
|
/** The index row at `ordinal` and every membership of its document. */
|
|
7981
8775
|
async function deliveryAt(db, documentId, ordinal) {
|
|
@@ -8315,7 +9109,8 @@ function rowToRemoteRecord(row) {
|
|
|
8315
9109
|
},
|
|
8316
9110
|
options: {
|
|
8317
9111
|
sinceTimestampUtcMs: "0",
|
|
8318
|
-
boundAddress: row.bound_address ?? void 0
|
|
9112
|
+
boundAddress: row.bound_address ?? void 0,
|
|
9113
|
+
...row.persistence === "session" ? { persistence: "session" } : {}
|
|
8319
9114
|
},
|
|
8320
9115
|
status: {
|
|
8321
9116
|
push: {
|
|
@@ -8358,6 +9153,7 @@ function remoteRecordToRow(remote) {
|
|
|
8358
9153
|
pull_last_failure_utc_ms: remote.status.pull.lastFailureUtcMs ? new Date(remote.status.pull.lastFailureUtcMs).toISOString() : null,
|
|
8359
9154
|
pull_failure_count: remote.status.pull.failureCount,
|
|
8360
9155
|
bound_address: remote.options.boundAddress ?? null,
|
|
9156
|
+
persistence: remote.options.persistence ?? "durable",
|
|
8361
9157
|
peer_manifest: remote.peer?.manifest ? canonicalJson(remote.peer.manifest, "peer manifest") : null,
|
|
8362
9158
|
peer_manifest_at_utc_ms: remote.peer?.receivedAtUtcMs ?? null
|
|
8363
9159
|
};
|
|
@@ -8789,6 +9585,15 @@ var SyncAwaiter = class {
|
|
|
8789
9585
|
}
|
|
8790
9586
|
};
|
|
8791
9587
|
//#endregion
|
|
9588
|
+
//#region src/sync/sync-inspection.ts
|
|
9589
|
+
function deriveConnectionHealth(snapshot, nowMs = Date.now()) {
|
|
9590
|
+
return {
|
|
9591
|
+
snapshot,
|
|
9592
|
+
neverSucceeded: snapshot.state === "connected" && snapshot.lastSuccessUtcMs === 0,
|
|
9593
|
+
stalenessMs: snapshot.lastSuccessUtcMs > 0 ? Math.max(0, nowMs - snapshot.lastSuccessUtcMs) : void 0
|
|
9594
|
+
};
|
|
9595
|
+
}
|
|
9596
|
+
//#endregion
|
|
8792
9597
|
//#region src/sync/sync-status-tracker.ts
|
|
8793
9598
|
let SyncStatus = /* @__PURE__ */ function(SyncStatus) {
|
|
8794
9599
|
SyncStatus["Synced"] = "SYNCED";
|
|
@@ -8904,7 +9709,8 @@ const defaultSyncManagerConfig = {
|
|
|
8904
9709
|
maxHeldOperationsPerRemote: 1e4,
|
|
8905
9710
|
staleRemotePollWindowMs: 3e5,
|
|
8906
9711
|
markerRetryBaseDelayMs: 1e3,
|
|
8907
|
-
markerRetryMaxDelayMs: 6e4
|
|
9712
|
+
markerRetryMaxDelayMs: 6e4,
|
|
9713
|
+
maxConcurrentInboxChunks: 8
|
|
8908
9714
|
};
|
|
8909
9715
|
const PLAN_KEY_TO_JOB_UUID_CAP = 1e4;
|
|
8910
9716
|
/**
|
|
@@ -8916,6 +9722,9 @@ function isCredentialOrNetworkError(error) {
|
|
|
8916
9722
|
if (isDriveAuthError(error)) return true;
|
|
8917
9723
|
return error instanceof GraphQLRequestError && (error.category === "network" || error.category === "timeout");
|
|
8918
9724
|
}
|
|
9725
|
+
function isSessionScoped(options) {
|
|
9726
|
+
return options.persistence === "session";
|
|
9727
|
+
}
|
|
8919
9728
|
const holdKey = (documentId, branch) => `${documentId}\u0000${branch}`;
|
|
8920
9729
|
const VERSION_CACHE_CAP = 1e4;
|
|
8921
9730
|
/** The versions a CREATE_DOCUMENT among `operations` fixes for the document. */
|
|
@@ -8984,7 +9793,9 @@ var SyncManager = class {
|
|
|
8984
9793
|
derivedThrough = /* @__PURE__ */ new Map();
|
|
8985
9794
|
sweptThrough = 0;
|
|
8986
9795
|
settledUnsubscribe;
|
|
8987
|
-
|
|
9796
|
+
inboxLanes = /* @__PURE__ */ new Map();
|
|
9797
|
+
inboxChunksInFlight = 0;
|
|
9798
|
+
inboxSlotWaiters = [];
|
|
8988
9799
|
capabilities;
|
|
8989
9800
|
manifest;
|
|
8990
9801
|
localSupport;
|
|
@@ -8998,6 +9809,7 @@ var SyncManager = class {
|
|
|
8998
9809
|
peerUpdates = /* @__PURE__ */ new Map();
|
|
8999
9810
|
peerUnsubscribes = /* @__PURE__ */ new Map();
|
|
9000
9811
|
receivedMarkers = /* @__PURE__ */ new Map();
|
|
9812
|
+
appliedMarkers = /* @__PURE__ */ new Map();
|
|
9001
9813
|
markerStorage;
|
|
9002
9814
|
refusalStorage;
|
|
9003
9815
|
markerWrites = /* @__PURE__ */ new Map();
|
|
@@ -9047,6 +9859,14 @@ var SyncManager = class {
|
|
|
9047
9859
|
}
|
|
9048
9860
|
async startup() {
|
|
9049
9861
|
if (this.isShutdown) throw new Error("SyncManager is already shutdown and cannot be started");
|
|
9862
|
+
const remoteRecords = [];
|
|
9863
|
+
for (const record of await this.remoteStorage.list()) {
|
|
9864
|
+
if (isSessionScoped(record.options)) {
|
|
9865
|
+
await this.purgeSessionRemote(record.name);
|
|
9866
|
+
continue;
|
|
9867
|
+
}
|
|
9868
|
+
remoteRecords.push(record);
|
|
9869
|
+
}
|
|
9050
9870
|
try {
|
|
9051
9871
|
const quarantinedIds = await this.deadLetterStorage.listQuarantinedDocumentIds();
|
|
9052
9872
|
for (const id of quarantinedIds) this.quarantinedDocumentIds.add(id);
|
|
@@ -9063,30 +9883,19 @@ var SyncManager = class {
|
|
|
9063
9883
|
} catch (error) {
|
|
9064
9884
|
this.logger.error("Failed to load sync holds (@error)", error instanceof Error ? error.message : String(error));
|
|
9065
9885
|
}
|
|
9066
|
-
const remoteRecords = await this.remoteStorage.list();
|
|
9067
9886
|
const head = await this.watermarkHead();
|
|
9068
9887
|
this.sweptThrough = Math.max(this.sweptThrough, head);
|
|
9069
9888
|
for (const record of remoteRecords) {
|
|
9070
|
-
|
|
9071
|
-
|
|
9072
|
-
|
|
9073
|
-
|
|
9074
|
-
|
|
9075
|
-
|
|
9076
|
-
|
|
9077
|
-
|
|
9078
|
-
|
|
9079
|
-
|
|
9080
|
-
},
|
|
9081
|
-
channel
|
|
9082
|
-
};
|
|
9083
|
-
this.remotes.set(record.name, remote);
|
|
9084
|
-
this.owe(record.name, head);
|
|
9085
|
-
this.records.set(record.name, remote.meta);
|
|
9086
|
-
await this.loadDeadLetters(remote);
|
|
9087
|
-
await this.restoreReceivedMarkers(remote);
|
|
9088
|
-
const unheard = [...remote.channel.inbox.items];
|
|
9089
|
-
this.wireChannelCallbacks(remote);
|
|
9889
|
+
let rehydrated;
|
|
9890
|
+
try {
|
|
9891
|
+
rehydrated = await this.rehydrateRemote(record, head);
|
|
9892
|
+
} catch (error) {
|
|
9893
|
+
await this.degradeRemoteAfterFailedRehydration(record, error);
|
|
9894
|
+
continue;
|
|
9895
|
+
}
|
|
9896
|
+
const remote = rehydrated.remote;
|
|
9897
|
+
const channel = remote.channel;
|
|
9898
|
+
const unheard = rehydrated.unheard;
|
|
9090
9899
|
try {
|
|
9091
9900
|
await channel.init();
|
|
9092
9901
|
} catch (error) {
|
|
@@ -9120,6 +9929,8 @@ var SyncManager = class {
|
|
|
9120
9929
|
this.backfillAbortControllers.clear();
|
|
9121
9930
|
this.planKeyToJobUuid.clear();
|
|
9122
9931
|
this.lastEnqueuedJobIdByKey.clear();
|
|
9932
|
+
this.inboxLanes.clear();
|
|
9933
|
+
for (const waiter of this.inboxSlotWaiters.splice(0)) waiter();
|
|
9123
9934
|
this.prunePending.clear();
|
|
9124
9935
|
this.pruneDrainDeferred = false;
|
|
9125
9936
|
this.batchAggregator.clear();
|
|
@@ -9510,6 +10321,16 @@ var SyncManager = class {
|
|
|
9510
10321
|
}
|
|
9511
10322
|
return kept;
|
|
9512
10323
|
}
|
|
10324
|
+
/** A previous session's transport is gone, so its remote and rows go too. */
|
|
10325
|
+
async purgeSessionRemote(name) {
|
|
10326
|
+
try {
|
|
10327
|
+
await this.remoteStorage.remove(name);
|
|
10328
|
+
await this.cursorStorage.remove(name);
|
|
10329
|
+
await this.forgetRemote(name);
|
|
10330
|
+
} catch (error) {
|
|
10331
|
+
this.logger.error("Error removing a previous session's remote at startup (@name, @error)", name, error instanceof Error ? error.message : String(error));
|
|
10332
|
+
}
|
|
10333
|
+
}
|
|
9513
10334
|
recordOf(meta) {
|
|
9514
10335
|
return {
|
|
9515
10336
|
id: meta.id,
|
|
@@ -9546,8 +10367,8 @@ var SyncManager = class {
|
|
|
9546
10367
|
receivedAtUtcMs: Date.now()
|
|
9547
10368
|
}
|
|
9548
10369
|
};
|
|
9549
|
-
await this.remoteStorage.upsert(this.recordOf(meta));
|
|
9550
10370
|
const channel = this.channelFactory.instance(remoteId, name, channelConfig, this.cursorStorage, collectionId, filter, this.operationIndex, options);
|
|
10371
|
+
await this.remoteStorage.upsert(this.recordOf(meta));
|
|
9551
10372
|
const remote = {
|
|
9552
10373
|
meta,
|
|
9553
10374
|
channel
|
|
@@ -9587,6 +10408,72 @@ var SyncManager = class {
|
|
|
9587
10408
|
}
|
|
9588
10409
|
}
|
|
9589
10410
|
/**
|
|
10411
|
+
* Rebuilds one stored remote's channel and wires it up, short of init().
|
|
10412
|
+
*
|
|
10413
|
+
* Everything here can throw -- the factory rejects an unusable config, the
|
|
10414
|
+
* dead-letter and marker reads touch storage -- so startup() calls it inside
|
|
10415
|
+
* a guard and degrades just this remote when it does. Nothing is left half
|
|
10416
|
+
* registered on the throwing path that {@link degradeRemoteAfterFailedRehydration}
|
|
10417
|
+
* does not then clear.
|
|
10418
|
+
*/
|
|
10419
|
+
async rehydrateRemote(record, head) {
|
|
10420
|
+
const channel = this.channelFactory.instance(record.id, record.name, record.channelConfig, this.cursorStorage, record.collectionId, record.filter, this.operationIndex, record.options);
|
|
10421
|
+
const remote = {
|
|
10422
|
+
meta: {
|
|
10423
|
+
id: record.id,
|
|
10424
|
+
name: record.name,
|
|
10425
|
+
collectionId: record.collectionId,
|
|
10426
|
+
channelConfig: record.channelConfig,
|
|
10427
|
+
filter: record.filter,
|
|
10428
|
+
options: record.options,
|
|
10429
|
+
peer: record.peer
|
|
10430
|
+
},
|
|
10431
|
+
channel
|
|
10432
|
+
};
|
|
10433
|
+
this.remotes.set(record.name, remote);
|
|
10434
|
+
this.owe(record.name, head);
|
|
10435
|
+
this.records.set(record.name, remote.meta);
|
|
10436
|
+
await this.loadDeadLetters(remote);
|
|
10437
|
+
await this.restoreReceivedMarkers(remote);
|
|
10438
|
+
const unheard = [...remote.channel.inbox.items];
|
|
10439
|
+
this.wireChannelCallbacks(remote);
|
|
10440
|
+
return {
|
|
10441
|
+
remote,
|
|
10442
|
+
unheard
|
|
10443
|
+
};
|
|
10444
|
+
}
|
|
10445
|
+
/**
|
|
10446
|
+
* Records a stored remote startup() could not bring up and moves on.
|
|
10447
|
+
*
|
|
10448
|
+
* Classified exactly as a failed init is: a credential or network failure
|
|
10449
|
+
* says nothing about the configuration, so the row stays and a retry after
|
|
10450
|
+
* sign-in can re-add it; anything else -- a factory with no scheme for this
|
|
10451
|
+
* config, a transport this session cannot resolve -- says the record itself
|
|
10452
|
+
* is unusable, so it is dropped rather than left to fail every boot. Either
|
|
10453
|
+
* way the boot carries on without it.
|
|
10454
|
+
*/
|
|
10455
|
+
async degradeRemoteAfterFailedRehydration(record, error) {
|
|
10456
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
10457
|
+
this.logger.error("Error rehydrating stored remote at startup (@name, @error)", record.name, message);
|
|
10458
|
+
const recordKept = isCredentialOrNetworkError(error);
|
|
10459
|
+
this.remotes.delete(record.name);
|
|
10460
|
+
this.owed.delete(record.name);
|
|
10461
|
+
this.derivedThrough.delete(record.name);
|
|
10462
|
+
this.connectionStateUnsubscribes.get(record.name)?.();
|
|
10463
|
+
this.connectionStateUnsubscribes.delete(record.name);
|
|
10464
|
+
if (recordKept) return;
|
|
10465
|
+
try {
|
|
10466
|
+
await this.remoteStorage.remove(record.name);
|
|
10467
|
+
} catch (removeError) {
|
|
10468
|
+
this.logger.error("Error removing unusable remote record at startup (@name, @error)", record.name, removeError instanceof Error ? removeError.message : String(removeError));
|
|
10469
|
+
}
|
|
10470
|
+
try {
|
|
10471
|
+
await this.forgetRemote(record.name);
|
|
10472
|
+
} catch (forgetError) {
|
|
10473
|
+
this.logger.error("Error forgetting unusable remote at startup (@name, @error)", record.name, forgetError instanceof Error ? forgetError.message : String(forgetError));
|
|
10474
|
+
}
|
|
10475
|
+
}
|
|
10476
|
+
/**
|
|
9590
10477
|
* Drops a remote whose channel.init() rejected, optionally removing its
|
|
9591
10478
|
* stored record. Ordered like remove(): the registry slot is released last,
|
|
9592
10479
|
* so a concurrent add of the same name cannot slip in and have its record
|
|
@@ -9641,6 +10528,7 @@ var SyncManager = class {
|
|
|
9641
10528
|
this.derivedThrough.delete(name);
|
|
9642
10529
|
this.prunePending.delete(name);
|
|
9643
10530
|
this.receivedMarkers.delete(name);
|
|
10531
|
+
this.appliedMarkers.delete(name);
|
|
9644
10532
|
for (const [id, retry] of [...this.markerRetries]) {
|
|
9645
10533
|
if (retry.remoteName !== name) continue;
|
|
9646
10534
|
clearTimeout(retry.timer);
|
|
@@ -9654,6 +10542,55 @@ var SyncManager = class {
|
|
|
9654
10542
|
list() {
|
|
9655
10543
|
return Array.from(this.remotes.values());
|
|
9656
10544
|
}
|
|
10545
|
+
async inspectRemote(remoteName) {
|
|
10546
|
+
const remote = this.getByName(remoteName);
|
|
10547
|
+
return this.inspectionOf(remote);
|
|
10548
|
+
}
|
|
10549
|
+
async inspectRemotes() {
|
|
10550
|
+
const out = [];
|
|
10551
|
+
for (const remote of [...this.remotes.values()]) out.push(await this.inspectionOf(remote));
|
|
10552
|
+
return out;
|
|
10553
|
+
}
|
|
10554
|
+
async listDeadLetters(remoteName, cursor, limit) {
|
|
10555
|
+
this.getByName(remoteName);
|
|
10556
|
+
if (cursor && !(/^\d+$/.test(cursor) && Number.isSafeInteger(Number(cursor)))) throw new InvalidDeadLetterCursorError(cursor);
|
|
10557
|
+
const max = this.config.maxDeadLettersPerRemote;
|
|
10558
|
+
const page = await this.deadLetterStorage.list(remoteName, {
|
|
10559
|
+
cursor: cursor || "0",
|
|
10560
|
+
limit: limit === void 0 || !Number.isFinite(limit) ? max : Math.min(Math.max(Math.trunc(limit), 1), max)
|
|
10561
|
+
});
|
|
10562
|
+
return {
|
|
10563
|
+
remoteName,
|
|
10564
|
+
results: page.results,
|
|
10565
|
+
nextCursor: page.nextCursor
|
|
10566
|
+
};
|
|
10567
|
+
}
|
|
10568
|
+
async inspectionOf(remote) {
|
|
10569
|
+
const cursors = await this.cursorStorage.list(remote.meta.name);
|
|
10570
|
+
const cursorInfo = (cursorType, mailbox) => {
|
|
10571
|
+
const stored = cursors.find((c) => c.cursorType === cursorType);
|
|
10572
|
+
return {
|
|
10573
|
+
cursorType,
|
|
10574
|
+
cursorOrdinal: stored?.cursorOrdinal ?? 0,
|
|
10575
|
+
lastSyncedAtUtcMs: stored?.lastSyncedAtUtcMs,
|
|
10576
|
+
liveAckOrdinal: mailbox.ackOrdinal,
|
|
10577
|
+
liveLatestOrdinal: mailbox.latestOrdinal
|
|
10578
|
+
};
|
|
10579
|
+
};
|
|
10580
|
+
const { channel } = remote;
|
|
10581
|
+
return {
|
|
10582
|
+
remoteName: remote.meta.name,
|
|
10583
|
+
remoteId: remote.meta.id,
|
|
10584
|
+
inboxCursor: cursorInfo("inbox", channel.inbox),
|
|
10585
|
+
outboxCursor: cursorInfo("outbox", channel.outbox),
|
|
10586
|
+
mailboxDepths: {
|
|
10587
|
+
inbox: channel.inbox.items.length,
|
|
10588
|
+
outbox: channel.outbox.items.length,
|
|
10589
|
+
deadLetter: channel.deadLetter.items.length
|
|
10590
|
+
},
|
|
10591
|
+
connection: deriveConnectionHealth(channel.getConnectionState())
|
|
10592
|
+
};
|
|
10593
|
+
}
|
|
9657
10594
|
waitForSync(jobId, signal) {
|
|
9658
10595
|
return this.syncAwaiter.waitForSync(jobId, signal);
|
|
9659
10596
|
}
|
|
@@ -9667,6 +10604,13 @@ var SyncManager = class {
|
|
|
9667
10604
|
async receiptsStored(remoteNames) {
|
|
9668
10605
|
await Promise.all(this.markerWritesOf(remoteNames));
|
|
9669
10606
|
}
|
|
10607
|
+
async resetSettled(name) {
|
|
10608
|
+
for (;;) {
|
|
10609
|
+
const pending = this.queuedResets.get(name) ?? this.resets.get(name);
|
|
10610
|
+
if (!pending) return;
|
|
10611
|
+
await pending.catch(() => void 0);
|
|
10612
|
+
}
|
|
10613
|
+
}
|
|
9670
10614
|
/** Never joins a running rebuild: it may have read state older than the request. */
|
|
9671
10615
|
resetChannel(remoteName) {
|
|
9672
10616
|
const queued = this.queuedResets.get(remoteName);
|
|
@@ -9743,7 +10687,6 @@ var SyncManager = class {
|
|
|
9743
10687
|
if (!this.parkedByQuarantine.has(item) || requeued.has(item.id)) continue;
|
|
9744
10688
|
const copy = new SyncOperation(item.id, item.jobId, item.jobDependencies, item.remoteName, item.documentId, item.scopes, item.branch, item.operations);
|
|
9745
10689
|
fresh.channel.inbox.add(copy);
|
|
9746
|
-
fresh.channel.inbox.hold?.(copy);
|
|
9747
10690
|
}
|
|
9748
10691
|
}
|
|
9749
10692
|
/** Before a fresh channel is registered: nothing to tear down. */
|
|
@@ -10107,13 +11050,15 @@ var SyncManager = class {
|
|
|
10107
11050
|
const eligible = [];
|
|
10108
11051
|
const dropped = [];
|
|
10109
11052
|
const received = this.receivedMarkersOf(remote.meta.name);
|
|
11053
|
+
const applied = this.appliedMarkers.get(remote.meta.name);
|
|
10110
11054
|
const loading = (id, syncOp) => {
|
|
10111
11055
|
const item = received.get(id);
|
|
10112
11056
|
return item !== void 0 && item !== syncOp && item.status !== 2 && remote.channel.inbox.get(item.id) === item;
|
|
10113
11057
|
};
|
|
10114
11058
|
for (const syncOp of syncOps) if (carriesMarker(syncOp)) {
|
|
10115
11059
|
const ids = markerIdsOf(syncOp);
|
|
10116
|
-
if (ids.every((id) =>
|
|
11060
|
+
if (applied !== void 0 && ids.every((id) => applied.has(id))) dropped.push(syncOp);
|
|
11061
|
+
else if (ids.every((id) => loading(id, syncOp))) {
|
|
10117
11062
|
this.storeReceivedMarker(remote, syncOp);
|
|
10118
11063
|
dropped.push(syncOp);
|
|
10119
11064
|
} else {
|
|
@@ -10126,10 +11071,7 @@ var SyncManager = class {
|
|
|
10126
11071
|
this.dropRequeuedDeadLetter(syncOp.id, remote.meta.name);
|
|
10127
11072
|
} else if (!this.quarantinedDocumentIds.has(syncOp.documentId)) eligible.push(syncOp);
|
|
10128
11073
|
else if (!this.requeuedDeadLetterIds.has(syncOp.id) && this.parkedCopyOf(remote, syncOp)) dropped.push(syncOp);
|
|
10129
|
-
else
|
|
10130
|
-
this.parkedByQuarantine.add(syncOp);
|
|
10131
|
-
remote.channel.inbox.hold?.(syncOp);
|
|
10132
|
-
}
|
|
11074
|
+
else this.parkedByQuarantine.add(syncOp);
|
|
10133
11075
|
for (const syncOp of dropped) {
|
|
10134
11076
|
this.logger.debug("Dropping received operations of a purged, already-loading or already-parked document (@remote, @documentId)", remote.meta.name, syncOp.documentId);
|
|
10135
11077
|
syncOp.executed();
|
|
@@ -10156,14 +11098,34 @@ var SyncManager = class {
|
|
|
10156
11098
|
}
|
|
10157
11099
|
/** Forgets a marker once the item loading it leaves the inbox. */
|
|
10158
11100
|
handleInboxRemoved(remote, syncOps) {
|
|
10159
|
-
const received = this.receivedMarkers.get(remote.meta.name);
|
|
10160
|
-
if (received === void 0 || received.size === 0) return;
|
|
10161
11101
|
const name = remote.meta.name;
|
|
10162
|
-
|
|
11102
|
+
const received = this.receivedMarkers.get(name);
|
|
11103
|
+
if (received !== void 0 && received.size > 0) for (const syncOp of syncOps) for (const id of markerIdsOf(syncOp)) {
|
|
10163
11104
|
if (received.get(id) !== syncOp) continue;
|
|
10164
11105
|
received.delete(id);
|
|
10165
11106
|
this.writeMarker(name, id, () => this.markerStorage.remove(name, id));
|
|
11107
|
+
if (syncOp.status === 2) this.rememberAppliedMarker(name, id, syncOp);
|
|
10166
11108
|
}
|
|
11109
|
+
this.forgetAckedMarkers(remote);
|
|
11110
|
+
}
|
|
11111
|
+
/** Kept until the ack passes it: the pusher re-pushes everything above the ack. */
|
|
11112
|
+
rememberAppliedMarker(name, id, syncOp) {
|
|
11113
|
+
let applied = this.appliedMarkers.get(name);
|
|
11114
|
+
if (applied === void 0) {
|
|
11115
|
+
applied = /* @__PURE__ */ new Map();
|
|
11116
|
+
this.appliedMarkers.set(name, applied);
|
|
11117
|
+
}
|
|
11118
|
+
let ordinal = 0;
|
|
11119
|
+
for (const op of syncOp.operations) ordinal = Math.max(ordinal, op.context.ordinal);
|
|
11120
|
+
applied.set(id, ordinal);
|
|
11121
|
+
}
|
|
11122
|
+
forgetAckedMarkers(remote) {
|
|
11123
|
+
const name = remote.meta.name;
|
|
11124
|
+
const applied = this.appliedMarkers.get(name);
|
|
11125
|
+
if (applied === void 0) return;
|
|
11126
|
+
const ack = remote.channel.inbox.ackOrdinal;
|
|
11127
|
+
for (const [id, ordinal] of applied) if (ordinal <= ack) applied.delete(id);
|
|
11128
|
+
if (applied.size === 0) this.appliedMarkers.delete(name);
|
|
10167
11129
|
}
|
|
10168
11130
|
markerWritesOf(remoteNames) {
|
|
10169
11131
|
if (remoteNames === void 0) return [...this.markerWrites.values()];
|
|
@@ -10229,23 +11191,91 @@ var SyncManager = class {
|
|
|
10229
11191
|
return received;
|
|
10230
11192
|
}
|
|
10231
11193
|
processInboxChunks(chunks) {
|
|
10232
|
-
|
|
10233
|
-
|
|
10234
|
-
|
|
10235
|
-
|
|
10236
|
-
|
|
10237
|
-
|
|
10238
|
-
|
|
11194
|
+
return Promise.all(chunks.map((chunk) => this.queueInboxChunk(chunk))).then(() => void 0);
|
|
11195
|
+
}
|
|
11196
|
+
/**
|
|
11197
|
+
* Queues a chunk behind every chunk it shares a lane key with and beside the
|
|
11198
|
+
* rest, so one document's stalled or failing load does not hold up another's.
|
|
11199
|
+
*/
|
|
11200
|
+
queueInboxChunk(chunk) {
|
|
11201
|
+
const keys = this.inboxLaneKeys(chunk);
|
|
11202
|
+
const predecessors = [];
|
|
11203
|
+
for (const key of keys) {
|
|
11204
|
+
const tail = this.inboxLanes.get(key);
|
|
11205
|
+
if (tail !== void 0) predecessors.push(tail);
|
|
11206
|
+
}
|
|
11207
|
+
const lane = Promise.all(predecessors).then(() => this.runInboxChunk(chunk)).catch((err) => {
|
|
10239
11208
|
this.logger.error("Inbox chunk processing failed (@error)", err instanceof Error ? err.message : String(err));
|
|
10240
11209
|
});
|
|
10241
|
-
|
|
11210
|
+
for (const key of keys) this.inboxLanes.set(key, lane);
|
|
11211
|
+
lane.then(() => {
|
|
11212
|
+
for (const key of keys) if (this.inboxLanes.get(key) === lane) this.inboxLanes.delete(key);
|
|
11213
|
+
});
|
|
11214
|
+
return lane;
|
|
11215
|
+
}
|
|
11216
|
+
/**
|
|
11217
|
+
* The documents a chunk writes, since the FIFO dependency reads the job last
|
|
11218
|
+
* enqueued for its document, and the plan keys its edges name, since a
|
|
11219
|
+
* cross-chunk edge resolves only once its provider is enqueued. Per document
|
|
11220
|
+
* rather than per scope or branch: the queue serialises a document across both.
|
|
11221
|
+
*/
|
|
11222
|
+
inboxLaneKeys(chunk) {
|
|
11223
|
+
const keys = /* @__PURE__ */ new Set();
|
|
11224
|
+
for (const { syncOp } of chunk) {
|
|
11225
|
+
keys.add(`doc ${syncOp.documentId}`);
|
|
11226
|
+
keys.add(`plan ${syncOp.jobId}`);
|
|
11227
|
+
for (const dep of syncOp.jobDependencies) if (dep) keys.add(`plan ${dep}`);
|
|
11228
|
+
}
|
|
11229
|
+
return [...keys];
|
|
11230
|
+
}
|
|
11231
|
+
/**
|
|
11232
|
+
* Settles once the chunk is enqueued, which is all its lane waits for; the
|
|
11233
|
+
* queue orders the writes. The slot is released there too, and the items
|
|
11234
|
+
* then resolve beside one another, unbounded: each is a passive wait on a
|
|
11235
|
+
* job for an item the inbox already holds.
|
|
11236
|
+
*/
|
|
11237
|
+
async runInboxChunk(chunk) {
|
|
11238
|
+
if (this.isShutdown) return;
|
|
11239
|
+
await this.acquireInboxSlot();
|
|
11240
|
+
if (this.isShutdown) {
|
|
11241
|
+
this.releaseInboxSlot();
|
|
11242
|
+
return;
|
|
11243
|
+
}
|
|
11244
|
+
let enqueued;
|
|
11245
|
+
try {
|
|
11246
|
+
enqueued = await this.enqueueInboxBatch(chunk);
|
|
11247
|
+
} finally {
|
|
11248
|
+
this.releaseInboxSlot();
|
|
11249
|
+
}
|
|
11250
|
+
if (enqueued === void 0) return;
|
|
11251
|
+
this.resolveInboxBatch(enqueued).catch((err) => {
|
|
11252
|
+
this.logger.error("Inbox chunk resolution failed (@error)", err instanceof Error ? err.message : String(err));
|
|
11253
|
+
});
|
|
11254
|
+
}
|
|
11255
|
+
acquireInboxSlot() {
|
|
11256
|
+
if (this.inboxChunksInFlight < this.config.maxConcurrentInboxChunks) {
|
|
11257
|
+
this.inboxChunksInFlight++;
|
|
11258
|
+
return Promise.resolve();
|
|
11259
|
+
}
|
|
11260
|
+
return new Promise((resolve) => {
|
|
11261
|
+
this.inboxSlotWaiters.push(resolve);
|
|
11262
|
+
});
|
|
11263
|
+
}
|
|
11264
|
+
/** Hands the slot straight to a waiter rather than freeing it. */
|
|
11265
|
+
releaseInboxSlot() {
|
|
11266
|
+
const waiter = this.inboxSlotWaiters.shift();
|
|
11267
|
+
if (waiter !== void 0) {
|
|
11268
|
+
waiter();
|
|
11269
|
+
return;
|
|
11270
|
+
}
|
|
11271
|
+
this.inboxChunksInFlight--;
|
|
10242
11272
|
}
|
|
10243
11273
|
async applyInboxJob(remote, syncOp) {
|
|
10244
11274
|
if ((await this.refuseOnReceipt(remote, [syncOp])).size > 0) return;
|
|
10245
11275
|
const operations = syncOp.operations.map((op) => op.operation);
|
|
10246
11276
|
let jobInfo;
|
|
10247
11277
|
try {
|
|
10248
|
-
jobInfo = await this.reactor.load(syncOp.documentId, syncOp.branch, operations, this.abortController.signal,
|
|
11278
|
+
jobInfo = await this.reactor.load(syncOp.documentId, syncOp.branch, operations, this.abortController.signal, cursorProtectedLoadMeta(remote.meta.name));
|
|
10249
11279
|
} catch (error) {
|
|
10250
11280
|
if (this.isShutdown) return;
|
|
10251
11281
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
@@ -10335,14 +11365,15 @@ var SyncManager = class {
|
|
|
10335
11365
|
}
|
|
10336
11366
|
await this.applyInboxJob(remote, syncOp);
|
|
10337
11367
|
}
|
|
10338
|
-
|
|
11368
|
+
/** Enqueues one chunk's loads; the resolution runs outside the lane. */
|
|
11369
|
+
async enqueueInboxBatch(received) {
|
|
10339
11370
|
const refused = /* @__PURE__ */ new Set();
|
|
10340
11371
|
for (const { remote, syncOp } of received) {
|
|
10341
11372
|
const refusals = await this.refuseOnReceipt(remote, [syncOp]);
|
|
10342
11373
|
for (const refusal of refusals) refused.add(refusal);
|
|
10343
11374
|
}
|
|
10344
11375
|
const items = received.filter(({ syncOp }) => !refused.has(syncOp));
|
|
10345
|
-
if (items.length === 0) return;
|
|
11376
|
+
if (items.length === 0) return void 0;
|
|
10346
11377
|
const sourceRemote = items[0].remote.meta.name;
|
|
10347
11378
|
const chunkKeys = new Set(items.map(({ syncOp }) => syncOp.jobId));
|
|
10348
11379
|
const jobs = items.map(({ syncOp }) => {
|
|
@@ -10372,9 +11403,9 @@ var SyncManager = class {
|
|
|
10372
11403
|
const request = { jobs };
|
|
10373
11404
|
let result;
|
|
10374
11405
|
try {
|
|
10375
|
-
result = await this.reactor.loadBatch(request, this.abortController.signal,
|
|
11406
|
+
result = await this.reactor.loadBatch(request, this.abortController.signal, cursorProtectedLoadMeta(sourceRemote));
|
|
10376
11407
|
} catch (error) {
|
|
10377
|
-
if (this.isShutdown) return;
|
|
11408
|
+
if (this.isShutdown) return void 0;
|
|
10378
11409
|
for (const { remote, syncOp } of items) {
|
|
10379
11410
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
10380
11411
|
if (carriesMarker(syncOp)) {
|
|
@@ -10387,7 +11418,7 @@ var SyncManager = class {
|
|
|
10387
11418
|
}
|
|
10388
11419
|
return;
|
|
10389
11420
|
}
|
|
10390
|
-
if (this.isShutdown) return;
|
|
11421
|
+
if (this.isShutdown) return void 0;
|
|
10391
11422
|
for (const plan of jobs) {
|
|
10392
11423
|
if (!(plan.key in result.jobs)) continue;
|
|
10393
11424
|
const info = result.jobs[plan.key];
|
|
@@ -10395,51 +11426,59 @@ var SyncManager = class {
|
|
|
10395
11426
|
const fifoKey = `${plan.documentId}:${plan.scope}:${plan.branch}`;
|
|
10396
11427
|
this.lastEnqueuedJobIdByKey.set(fifoKey, info.id);
|
|
10397
11428
|
}
|
|
10398
|
-
|
|
10399
|
-
|
|
10400
|
-
|
|
10401
|
-
|
|
10402
|
-
|
|
10403
|
-
|
|
10404
|
-
|
|
10405
|
-
|
|
10406
|
-
|
|
10407
|
-
|
|
10408
|
-
|
|
10409
|
-
|
|
10410
|
-
|
|
10411
|
-
|
|
10412
|
-
|
|
10413
|
-
|
|
10414
|
-
|
|
10415
|
-
|
|
10416
|
-
|
|
10417
|
-
|
|
10418
|
-
|
|
10419
|
-
|
|
10420
|
-
|
|
10421
|
-
continue;
|
|
10422
|
-
}
|
|
11429
|
+
return {
|
|
11430
|
+
items,
|
|
11431
|
+
result
|
|
11432
|
+
};
|
|
11433
|
+
}
|
|
11434
|
+
/** Each item awaits its own job, so one deferred load holds no other. */
|
|
11435
|
+
async resolveInboxBatch(enqueued) {
|
|
11436
|
+
await Promise.all(enqueued.items.map(({ remote, syncOp }) => this.resolveInboxItem(remote, syncOp, enqueued.result)));
|
|
11437
|
+
}
|
|
11438
|
+
async resolveInboxItem(remote, syncOp, result) {
|
|
11439
|
+
if (!(syncOp.jobId in result.jobs)) {
|
|
11440
|
+
this.logger.error("Job key missing from batch load result (@remote, @documentId, @jobId)", remote.meta.name, syncOp.documentId, syncOp.jobId);
|
|
11441
|
+
const error = new ChannelError("inbox", /* @__PURE__ */ new Error(`Job key '${syncOp.jobId}' missing from batch load result`));
|
|
11442
|
+
syncOp.failed(error);
|
|
11443
|
+
remote.channel.deadLetter.add(syncOp);
|
|
11444
|
+
remote.channel.inbox.remove(syncOp);
|
|
11445
|
+
return;
|
|
11446
|
+
}
|
|
11447
|
+
const jobInfo = result.jobs[syncOp.jobId];
|
|
11448
|
+
let completedJobInfo;
|
|
11449
|
+
try {
|
|
11450
|
+
completedJobInfo = await this.awaiter.waitForJob(jobInfo.id, this.abortController.signal);
|
|
11451
|
+
} catch (error) {
|
|
10423
11452
|
if (this.isShutdown) return;
|
|
10424
|
-
|
|
10425
|
-
if (
|
|
10426
|
-
syncOp.
|
|
10427
|
-
|
|
10428
|
-
resolved = true;
|
|
10429
|
-
} else if (isPurgedFailure(completedJobInfo.error, syncOp.documentId)) {
|
|
10430
|
-
this.tombstone(syncOp.documentId);
|
|
10431
|
-
syncOp.executed();
|
|
10432
|
-
resolved = true;
|
|
10433
|
-
} else if (carriesMarker(syncOp) && !isRefusedMarker(completedJobInfo.error)) {
|
|
10434
|
-
this.retryMarker(remote, syncOp, completedJobInfo.error?.message || "Unknown error");
|
|
10435
|
-
continue;
|
|
10436
|
-
} else {
|
|
10437
|
-
syncOp.failed(this.inboxFailure(syncOp, completedJobInfo.error));
|
|
10438
|
-
remote.channel.deadLetter.add(syncOp);
|
|
11453
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
11454
|
+
if (carriesMarker(syncOp)) {
|
|
11455
|
+
this.retryMarker(remote, syncOp, err.message);
|
|
11456
|
+
return;
|
|
10439
11457
|
}
|
|
11458
|
+
syncOp.failed(new ChannelError("inbox", err));
|
|
11459
|
+
remote.channel.deadLetter.add(syncOp);
|
|
10440
11460
|
remote.channel.inbox.remove(syncOp);
|
|
10441
|
-
|
|
11461
|
+
return;
|
|
11462
|
+
}
|
|
11463
|
+
if (this.isShutdown) return;
|
|
11464
|
+
let resolved = false;
|
|
11465
|
+
if (completedJobInfo.status !== "FAILED") {
|
|
11466
|
+
syncOp.executed();
|
|
11467
|
+
if (carriesMarker(syncOp)) this.tombstone(syncOp.documentId);
|
|
11468
|
+
resolved = true;
|
|
11469
|
+
} else if (isPurgedFailure(completedJobInfo.error, syncOp.documentId)) {
|
|
11470
|
+
this.tombstone(syncOp.documentId);
|
|
11471
|
+
syncOp.executed();
|
|
11472
|
+
resolved = true;
|
|
11473
|
+
} else if (carriesMarker(syncOp) && !isRefusedMarker(completedJobInfo.error)) {
|
|
11474
|
+
this.retryMarker(remote, syncOp, completedJobInfo.error?.message || "Unknown error");
|
|
11475
|
+
return;
|
|
11476
|
+
} else {
|
|
11477
|
+
syncOp.failed(this.inboxFailure(syncOp, completedJobInfo.error));
|
|
11478
|
+
remote.channel.deadLetter.add(syncOp);
|
|
10442
11479
|
}
|
|
11480
|
+
remote.channel.inbox.remove(syncOp);
|
|
11481
|
+
if (resolved) await this.dropRequeuedDeadLetter(syncOp.id, remote.meta.name);
|
|
10443
11482
|
}
|
|
10444
11483
|
/**
|
|
10445
11484
|
* The dead letter for a load job the executor failed.
|
|
@@ -10671,6 +11710,7 @@ var SyncBuilder = class {
|
|
|
10671
11710
|
holdStorage;
|
|
10672
11711
|
receivedMarkerStorage;
|
|
10673
11712
|
purgeRefusalStorage;
|
|
11713
|
+
storageFlusher = new NoopStorageFlusher();
|
|
10674
11714
|
config = {};
|
|
10675
11715
|
withChannelFactory(factory) {
|
|
10676
11716
|
this.channelFactory = factory;
|
|
@@ -10684,6 +11724,11 @@ var SyncBuilder = class {
|
|
|
10684
11724
|
this.cursorStorage = storage;
|
|
10685
11725
|
return this;
|
|
10686
11726
|
}
|
|
11727
|
+
/** The barrier every cursor write, including a custom storage's, goes behind. */
|
|
11728
|
+
withStorageFlusher(flusher) {
|
|
11729
|
+
this.storageFlusher = flusher;
|
|
11730
|
+
return this;
|
|
11731
|
+
}
|
|
10687
11732
|
withDeadLetterStorage(storage) {
|
|
10688
11733
|
this.deadLetterStorage = storage;
|
|
10689
11734
|
return this;
|
|
@@ -10708,6 +11753,10 @@ var SyncBuilder = class {
|
|
|
10708
11753
|
this.config.maxInboxBatchSize = limit;
|
|
10709
11754
|
return this;
|
|
10710
11755
|
}
|
|
11756
|
+
withMaxConcurrentInboxChunks(limit) {
|
|
11757
|
+
this.config.maxConcurrentInboxChunks = limit;
|
|
11758
|
+
return this;
|
|
11759
|
+
}
|
|
10711
11760
|
withMaxHeldOperationsPerRemote(limit) {
|
|
10712
11761
|
this.config.maxHeldOperationsPerRemote = limit;
|
|
10713
11762
|
return this;
|
|
@@ -10722,7 +11771,7 @@ var SyncBuilder = class {
|
|
|
10722
11771
|
buildModule(reactor, logger, operationIndex, eventBus, db, driveContainerTypes, watermark, localPeer) {
|
|
10723
11772
|
if (!this.channelFactory) throw new Error("Channel factory is required");
|
|
10724
11773
|
const remoteStorage = this.remoteStorage ?? new KyselySyncRemoteStorage(db);
|
|
10725
|
-
const cursorStorage = this.cursorStorage ?? new KyselySyncCursorStorage(db);
|
|
11774
|
+
const cursorStorage = new FlushGuardedSyncCursorStorage(this.cursorStorage ?? new KyselySyncCursorStorage(db), this.storageFlusher);
|
|
10726
11775
|
const deadLetterStorage = this.deadLetterStorage ?? new KyselySyncDeadLetterStorage(db);
|
|
10727
11776
|
const holdStorage = this.holdStorage ?? new KyselySyncHoldStorage(db);
|
|
10728
11777
|
const receivedMarkerStorage = this.receivedMarkerStorage ?? new KyselySyncReceivedMarkerStorage(db);
|
|
@@ -10735,7 +11784,8 @@ var SyncBuilder = class {
|
|
|
10735
11784
|
holdStorage,
|
|
10736
11785
|
channelFactory: this.channelFactory,
|
|
10737
11786
|
syncManager,
|
|
10738
|
-
syncAdmin: syncManager
|
|
11787
|
+
syncAdmin: syncManager,
|
|
11788
|
+
syncInspector: syncManager
|
|
10739
11789
|
};
|
|
10740
11790
|
}
|
|
10741
11791
|
};
|
|
@@ -10748,6 +11798,38 @@ async function createDefaultDatabase() {
|
|
|
10748
11798
|
return new Kysely({ dialect: new HardenedPGliteDialect(new PGlite()) });
|
|
10749
11799
|
}
|
|
10750
11800
|
//#endregion
|
|
11801
|
+
//#region src/inspector/storage-facts.ts
|
|
11802
|
+
const UNKNOWN_STORAGE_FACTS = Object.freeze({
|
|
11803
|
+
engine: "unknown",
|
|
11804
|
+
persistence: "unknown",
|
|
11805
|
+
durable: false,
|
|
11806
|
+
selfHeal: false
|
|
11807
|
+
});
|
|
11808
|
+
const POSTGRES_STORAGE_FACTS = Object.freeze({
|
|
11809
|
+
engine: "postgres",
|
|
11810
|
+
persistence: "server",
|
|
11811
|
+
durable: true,
|
|
11812
|
+
selfHeal: false
|
|
11813
|
+
});
|
|
11814
|
+
const IN_MEMORY_PGLITE_STORAGE_FACTS = Object.freeze({
|
|
11815
|
+
engine: "pglite",
|
|
11816
|
+
persistence: "memory",
|
|
11817
|
+
durable: false,
|
|
11818
|
+
selfHeal: false
|
|
11819
|
+
});
|
|
11820
|
+
const PGLITE_PATH_STORAGE_FACTS = Object.freeze({
|
|
11821
|
+
engine: "pglite",
|
|
11822
|
+
persistence: "path",
|
|
11823
|
+
durable: true,
|
|
11824
|
+
selfHeal: false
|
|
11825
|
+
});
|
|
11826
|
+
const PGLITE_IDB_STORAGE_FACTS = Object.freeze({
|
|
11827
|
+
engine: "pglite",
|
|
11828
|
+
persistence: "idb",
|
|
11829
|
+
durable: true,
|
|
11830
|
+
selfHeal: false
|
|
11831
|
+
});
|
|
11832
|
+
//#endregion
|
|
10751
11833
|
//#region src/shared/factories.ts
|
|
10752
11834
|
/**
|
|
10753
11835
|
* Factory method to create a ShutdownStatus that can be updated
|
|
@@ -11322,7 +12404,10 @@ var Reactor = class {
|
|
|
11322
12404
|
status: "FAILED",
|
|
11323
12405
|
createdAtUtcIso: now,
|
|
11324
12406
|
completedAtUtcIso: now,
|
|
11325
|
-
error:
|
|
12407
|
+
error: {
|
|
12408
|
+
...toErrorInfo$1("Job not found"),
|
|
12409
|
+
name: JOB_NOT_FOUND_ERROR_NAME
|
|
12410
|
+
},
|
|
11326
12411
|
consistencyToken: {
|
|
11327
12412
|
version: 1,
|
|
11328
12413
|
createdAtUtcIso: now,
|
|
@@ -11401,6 +12486,19 @@ function validateBuiltInKindCoverage(preReadyKinds, postReadyKinds) {
|
|
|
11401
12486
|
const problems = [missing.length > 0 ? `never named: ${missing.join(", ")} (would be indexed by no shard, leaving those reads permanently stale and their consistency-token waits unresolvable)` : void 0, duplicated.length > 0 ? `named more than once: ${duplicated.join(", ")} (would be indexed twice per operation)` : void 0].filter((problem) => problem !== void 0);
|
|
11402
12487
|
throw new Error(`withProjectionShards requires preReadyKinds and postReadyKinds to name each built-in read model (${BUILT_IN_READ_MODEL_KINDS.join(", ")}) exactly once between them; ${problems.join("; ")}`);
|
|
11403
12488
|
}
|
|
12489
|
+
function schemeChannelDescriptor(scheme) {
|
|
12490
|
+
switch (scheme) {
|
|
12491
|
+
case "connect": return {
|
|
12492
|
+
type: "gql",
|
|
12493
|
+
create: (logger, jwtHandler, queue) => new GqlRequestChannelFactory(logger, jwtHandler, queue)
|
|
12494
|
+
};
|
|
12495
|
+
case "switchboard": return {
|
|
12496
|
+
type: POLLING_CHANNEL_TYPE,
|
|
12497
|
+
create: (logger) => new GqlResponseChannelFactory(logger)
|
|
12498
|
+
};
|
|
12499
|
+
default: throw new Error(`Unsupported channel scheme: ${JSON.stringify(scheme)}`);
|
|
12500
|
+
}
|
|
12501
|
+
}
|
|
11404
12502
|
var ReactorBuilder = class {
|
|
11405
12503
|
logger;
|
|
11406
12504
|
documentModelSources = [];
|
|
@@ -11421,6 +12519,8 @@ var ReactorBuilder = class {
|
|
|
11421
12519
|
readModelCoordinator;
|
|
11422
12520
|
readModelCoordinatorFactory;
|
|
11423
12521
|
kyselyInstance;
|
|
12522
|
+
storageFactsOverride;
|
|
12523
|
+
groupCommitPGlite;
|
|
11424
12524
|
signer;
|
|
11425
12525
|
workerSigner;
|
|
11426
12526
|
trustPolicy;
|
|
@@ -11428,6 +12528,7 @@ var ReactorBuilder = class {
|
|
|
11428
12528
|
signalHandlersEnabled = false;
|
|
11429
12529
|
queueInstance;
|
|
11430
12530
|
channelScheme;
|
|
12531
|
+
additionalChannelFactories = /* @__PURE__ */ new Map();
|
|
11431
12532
|
jwtHandler;
|
|
11432
12533
|
documentModelLoader;
|
|
11433
12534
|
shutdownHooks = [];
|
|
@@ -11585,6 +12686,23 @@ var ReactorBuilder = class {
|
|
|
11585
12686
|
this.kyselyInstance = kysely;
|
|
11586
12687
|
return this;
|
|
11587
12688
|
}
|
|
12689
|
+
/** Declares the store's facts; without it a caller-supplied store is unknown. */
|
|
12690
|
+
withStorageFacts(facts) {
|
|
12691
|
+
this.storageFactsOverride = facts;
|
|
12692
|
+
return this;
|
|
12693
|
+
}
|
|
12694
|
+
/**
|
|
12695
|
+
* Runs the reactor on an embedded PGlite with group commit: statements stop
|
|
12696
|
+
* syncing the filesystem, and the flush moves to a job's write-ready and
|
|
12697
|
+
* every sync cursor write. A poisoned session goes to `onUnrecoverable`,
|
|
12698
|
+
* since a fallback would leave in-memory read-model positions ahead of the
|
|
12699
|
+
* store. The caller keeps `pg` if the build fails; once built, the module's
|
|
12700
|
+
* `groupCommitStorage` closes it.
|
|
12701
|
+
*/
|
|
12702
|
+
withGroupCommitPGlite(options) {
|
|
12703
|
+
this.groupCommitPGlite = options;
|
|
12704
|
+
return this;
|
|
12705
|
+
}
|
|
11588
12706
|
/**
|
|
11589
12707
|
* Register an externally-constructed pg.Pool's {@link PoolInstrumentation}
|
|
11590
12708
|
* so it surfaces through {@link ReactorModule.pools}. Use this when the
|
|
@@ -11600,11 +12718,24 @@ var ReactorBuilder = class {
|
|
|
11600
12718
|
this.queueInstance = queue;
|
|
11601
12719
|
return this;
|
|
11602
12720
|
}
|
|
11603
|
-
/**
|
|
12721
|
+
/**
|
|
12722
|
+
* Mutually exclusive with {@link withSync}; setting both fails the build.
|
|
12723
|
+
* Compose further transports onto the scheme with {@link withAdditionalChannelFactory}.
|
|
12724
|
+
*/
|
|
11604
12725
|
withChannelScheme(scheme) {
|
|
11605
12726
|
this.channelScheme = scheme;
|
|
11606
12727
|
return this;
|
|
11607
12728
|
}
|
|
12729
|
+
/**
|
|
12730
|
+
* Routes `type` to `factory` beside the {@link withChannelScheme} scheme's
|
|
12731
|
+
* own factory, through a {@link CompositeChannelFactory}. Build refuses it
|
|
12732
|
+
* without a scheme, or when `type` is the scheme's own.
|
|
12733
|
+
*/
|
|
12734
|
+
withAdditionalChannelFactory(type, factory) {
|
|
12735
|
+
if (this.additionalChannelFactories.has(type)) throw new Error(`A channel factory for the type "${type}" is already registered on this ReactorBuilder`);
|
|
12736
|
+
this.additionalChannelFactories.set(type, factory);
|
|
12737
|
+
return this;
|
|
12738
|
+
}
|
|
11608
12739
|
withJwtHandler(handler) {
|
|
11609
12740
|
this.jwtHandler = handler;
|
|
11610
12741
|
return this;
|
|
@@ -11680,6 +12811,43 @@ var ReactorBuilder = class {
|
|
|
11680
12811
|
return (await this.buildModule()).reactor;
|
|
11681
12812
|
}
|
|
11682
12813
|
async buildModule() {
|
|
12814
|
+
this.assertStorageConfiguration();
|
|
12815
|
+
const groupCommit = this.groupCommitPGlite ? createGroupCommitStorage(this.groupCommitPGlite) : void 0;
|
|
12816
|
+
try {
|
|
12817
|
+
return await this.assembleModule(groupCommit);
|
|
12818
|
+
} catch (error) {
|
|
12819
|
+
groupCommit?.restoreStatementSync();
|
|
12820
|
+
throw error;
|
|
12821
|
+
}
|
|
12822
|
+
}
|
|
12823
|
+
/** Refuses what would bypass the flush or leave a poison with no owner. */
|
|
12824
|
+
assertStorageConfiguration() {
|
|
12825
|
+
const options = this.groupCommitPGlite;
|
|
12826
|
+
if (options === void 0) return;
|
|
12827
|
+
if (typeof options.onUnrecoverable !== "function") throw new Error("withGroupCommitPGlite needs onUnrecoverable: a poisoned session is never reused, so without it the store stays wedged");
|
|
12828
|
+
if (this.kyselyInstance !== void 0) throw new Error("withGroupCommitPGlite and withKysely are mutually exclusive: both supply the reactor's database");
|
|
12829
|
+
if (this.workerPool !== void 0 || this.projectionShardConfig) throw new Error("withGroupCommitPGlite cannot be combined with withWorkerPool or withProjectionShards: those open their own Postgres connections and never see the embedded store or its flush");
|
|
12830
|
+
if (this.executorManager !== void 0) throw new Error("withExecutor cannot be combined with withGroupCommitPGlite: only the executor this builder constructs flushes before announcing a job write-ready");
|
|
12831
|
+
this.fitJobTimeoutToDurability(options);
|
|
12832
|
+
}
|
|
12833
|
+
/**
|
|
12834
|
+
* A committed job waits for its flush, the statement in flight then one
|
|
12835
|
+
* sync, and withholds past that wait. Statements before the commit queue
|
|
12836
|
+
* behind the same flush, so the job timeout outlasts the wait too.
|
|
12837
|
+
*/
|
|
12838
|
+
fitJobTimeoutToDurability(options) {
|
|
12839
|
+
const requested = this.executorConfig.durabilityWaitMs;
|
|
12840
|
+
const durabilityWaitMs = requested ?? (options.dialect?.statementTimeoutMs ?? 12e4) + (options.client?.flushSyncTimeoutMs ?? 12e4);
|
|
12841
|
+
if (!(Number.isFinite(durabilityWaitMs) && durabilityWaitMs > 0)) throw new Error(requested === void 0 ? `withGroupCommitPGlite needs a positive, finite durabilityWaitMs when its statement and flush sync bounds add up to ${durabilityWaitMs}: otherwise a committed job whose flush never lands waits forever` : `withGroupCommitPGlite needs a positive, finite durabilityWaitMs (got ${requested}): otherwise a committed job whose flush never lands waits forever`);
|
|
12842
|
+
const configured = this.executorConfig.jobTimeoutMs;
|
|
12843
|
+
if (configured !== void 0 && configured <= durabilityWaitMs) throw new Error(`jobTimeoutMs (${configured}) must exceed withGroupCommitPGlite's durability wait (${durabilityWaitMs}ms: statement + flush sync bounds): statements before a job's commit queue behind the same flush`);
|
|
12844
|
+
this.executorConfig = {
|
|
12845
|
+
...this.executorConfig,
|
|
12846
|
+
durabilityWaitMs,
|
|
12847
|
+
jobTimeoutMs: configured ?? durabilityWaitMs + 3e4
|
|
12848
|
+
};
|
|
12849
|
+
}
|
|
12850
|
+
async assembleModule(groupCommit) {
|
|
11683
12851
|
if (!this.logger) this.logger = new ConsoleLogger(["reactor"]);
|
|
11684
12852
|
const featureFlags = resolveFeatureFlags(this.executorConfig.featureFlags);
|
|
11685
12853
|
if (this.executorConfig.protocolSupport === void 0) this.executorConfig = {
|
|
@@ -11688,6 +12856,7 @@ var ReactorBuilder = class {
|
|
|
11688
12856
|
};
|
|
11689
12857
|
if (this.channelScheme !== void 0 && this.syncBuilder !== void 0) throw new Error("withChannelScheme and withSync are mutually exclusive; use one (the scheme builds its own SyncBuilder)");
|
|
11690
12858
|
if (this.readModelCoordinator !== void 0 && this.readModelCoordinatorFactory !== void 0) throw new Error("withReadModelCoordinator and withReadModelCoordinatorFactory are mutually exclusive; register one coordinator source");
|
|
12859
|
+
this.assertAdditionalChannelFactories();
|
|
11691
12860
|
if (this.projectionShardConfig !== void 0 && this.readModelCoordinatorFactory !== void 0) throw new Error("withProjectionShards and withReadModelCoordinatorFactory are mutually exclusive; the factory owns the projection worker through its createProjectionShardManager dependency");
|
|
11692
12861
|
if (this.projectionShardConfig !== void 0 && this.readModelFactories.length > 0) throw new Error("withProjectionShards does not support read models registered through withReadModelFactory; projection workers cannot receive host-only factory dependencies");
|
|
11693
12862
|
if (this.projectionShardConfig !== void 0 && this.readModels.length > 0) throw new Error("withProjectionShards does not support read models registered through withReadModel; projection workers build their own read models from the shard config and would silently omit these");
|
|
@@ -11712,11 +12881,13 @@ var ReactorBuilder = class {
|
|
|
11712
12881
|
for (const result of results) if (result.status === "error") this.logger.error("Failed to register document model: @error", result.error.message);
|
|
11713
12882
|
}
|
|
11714
12883
|
const reactorDbConfig = this.resolveReactorDbConfig();
|
|
11715
|
-
const baseDatabase = this.kyselyInstance ?? (reactorDbConfig ? await this.createPostgresDatabase(reactorDbConfig) : await createDefaultDatabase());
|
|
12884
|
+
const baseDatabase = this.kyselyInstance ?? groupCommit?.db ?? (reactorDbConfig ? await this.createPostgresDatabase(reactorDbConfig) : await createDefaultDatabase());
|
|
12885
|
+
const storageFacts = this.storageFactsOverride ?? (this.kyselyInstance || groupCommit ? UNKNOWN_STORAGE_FACTS : reactorDbConfig ? POSTGRES_STORAGE_FACTS : IN_MEMORY_PGLITE_STORAGE_FACTS);
|
|
11716
12886
|
if (this.migrationStrategy === "auto") {
|
|
11717
12887
|
const result = await runMigrations(baseDatabase, REACTOR_SCHEMA);
|
|
11718
12888
|
if (!result.success && result.error) throw new Error(`Database migration failed: ${result.error.message}`);
|
|
11719
12889
|
}
|
|
12890
|
+
await groupCommit?.flusher.flush();
|
|
11720
12891
|
await checkStoredProtocols(baseDatabase, REACTOR_SCHEMA, this.executorConfig.protocolSupport ?? {}, this.unsupportedStoredDocuments, this.logger);
|
|
11721
12892
|
const database = baseDatabase.withSchema(REACTOR_SCHEMA);
|
|
11722
12893
|
const operationStore = new KyselyOperationStore(database);
|
|
@@ -11773,7 +12944,7 @@ var ReactorBuilder = class {
|
|
|
11773
12944
|
executorManager = poolManager;
|
|
11774
12945
|
executorStartCount = pool.numWorkers;
|
|
11775
12946
|
if (resolver instanceof DocumentModelResolver) resolver.setBroadcastHook((entry) => poolManager.loadModel(entry));
|
|
11776
|
-
} 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);
|
|
12947
|
+
} else executorManager = new SimpleJobExecutorManager(() => new SimpleJobExecutor(this.logger, documentModelRegistry, operationStore, eventBus, writeCache, operationIndex, documentMetaCache, collectionMembershipCache, this.driveContainerTypes, this.executorConfig, executionScope, this.signer, this.trustPolicy, groupCommit?.flusher), eventBus, queue, jobTracker, this.logger, resolver, this.executorConfig.jobTimeoutMs, this.executorConfig.deferredJobTtlMs);
|
|
11777
12948
|
}
|
|
11778
12949
|
await executorManager.start(executorStartCount);
|
|
11779
12950
|
const callerReadModels = Array.from(/* @__PURE__ */ new Set([...this.readModels]));
|
|
@@ -11868,10 +13039,12 @@ var ReactorBuilder = class {
|
|
|
11868
13039
|
};
|
|
11869
13040
|
let syncModule = void 0;
|
|
11870
13041
|
if (this.channelScheme) {
|
|
11871
|
-
const
|
|
11872
|
-
|
|
13042
|
+
const syncBuilder = new SyncBuilder().withChannelFactory(this.buildSchemeChannelFactory(this.channelScheme, this.logger, queue));
|
|
13043
|
+
if (groupCommit) syncBuilder.withStorageFlusher(groupCommit.flusher);
|
|
13044
|
+
syncModule = syncBuilder.buildModule(reactor, this.logger, operationIndex, eventBus, database, this.driveContainerTypes, settledWatermark, localPeer);
|
|
11873
13045
|
await syncModule.syncManager.startup();
|
|
11874
13046
|
} else if (this.syncBuilder) {
|
|
13047
|
+
if (groupCommit) this.syncBuilder.withStorageFlusher(groupCommit.flusher);
|
|
11875
13048
|
syncModule = this.syncBuilder.buildModule(reactor, this.logger, operationIndex, eventBus, database, this.driveContainerTypes, settledWatermark, localPeer);
|
|
11876
13049
|
await syncModule.syncManager.startup();
|
|
11877
13050
|
}
|
|
@@ -11909,13 +13082,34 @@ var ReactorBuilder = class {
|
|
|
11909
13082
|
degradedComponents,
|
|
11910
13083
|
catchUp,
|
|
11911
13084
|
settledWatermark,
|
|
11912
|
-
documentPurgeService: new DocumentPurgeService(queue, jobTracker, eventBus)
|
|
13085
|
+
documentPurgeService: new DocumentPurgeService(queue, jobTracker, eventBus),
|
|
13086
|
+
storageFacts: { ...storageFacts },
|
|
13087
|
+
groupCommitStorage: groupCommit && {
|
|
13088
|
+
health: groupCommit.health,
|
|
13089
|
+
close: () => groupCommit.close()
|
|
13090
|
+
}
|
|
11913
13091
|
};
|
|
11914
13092
|
catchUp.start();
|
|
11915
13093
|
if (degradedComponents.length > 0) this.logger.warn("Reactor started with @count degraded component(s): @components", degradedComponents.length, degradedComponents.map(({ component }) => component).join(", "));
|
|
11916
13094
|
if (this.signalHandlersEnabled) this.attachSignalHandlers(module);
|
|
11917
13095
|
return module;
|
|
11918
13096
|
}
|
|
13097
|
+
assertAdditionalChannelFactories() {
|
|
13098
|
+
if (this.additionalChannelFactories.size === 0) return;
|
|
13099
|
+
if (!this.channelScheme) {
|
|
13100
|
+
const types = [...this.additionalChannelFactories.keys()].join(", ");
|
|
13101
|
+
throw new Error(`withAdditionalChannelFactory([${types}]) needs a withChannelScheme to compose with: without a scheme there is no factory to compose, and a withSync SyncBuilder owns its own. Pass a CompositeChannelFactory to that SyncBuilder instead.`);
|
|
13102
|
+
}
|
|
13103
|
+
const { type } = schemeChannelDescriptor(this.channelScheme);
|
|
13104
|
+
if (this.additionalChannelFactories.has(type)) throw new Error(`withAdditionalChannelFactory("${type}", ...) collides with the "${this.channelScheme}" channel scheme, which already serves that channel type`);
|
|
13105
|
+
}
|
|
13106
|
+
/** Bare without additional factories, so an existing reactor routes as before. */
|
|
13107
|
+
buildSchemeChannelFactory(scheme, logger, queue) {
|
|
13108
|
+
const { type, create } = schemeChannelDescriptor(scheme);
|
|
13109
|
+
const schemeFactory = create(logger, this.jwtHandler, queue);
|
|
13110
|
+
if (this.additionalChannelFactories.size === 0) return schemeFactory;
|
|
13111
|
+
return new CompositeChannelFactory([[type, schemeFactory], ...this.additionalChannelFactories]);
|
|
13112
|
+
}
|
|
11919
13113
|
/**
|
|
11920
13114
|
* The single Postgres config for the parent, executor workers, and
|
|
11921
13115
|
* projection shards. They must share one physical database (the parent
|
|
@@ -11976,7 +13170,7 @@ var ReactorBuilder = class {
|
|
|
11976
13170
|
poolInstrumentations.push(forwarder);
|
|
11977
13171
|
this.instrumentedPools.push(forwarder);
|
|
11978
13172
|
}
|
|
11979
|
-
const { ProjectionShardManager } = await import("./projection-shard-manager-
|
|
13173
|
+
const { ProjectionShardManager } = await import("./projection-shard-manager-Drl9w8Eo.js");
|
|
11980
13174
|
const manager = new ProjectionShardManager({
|
|
11981
13175
|
shardCount: config.shardCount,
|
|
11982
13176
|
db,
|
|
@@ -12021,7 +13215,7 @@ var ReactorBuilder = class {
|
|
|
12021
13215
|
*/
|
|
12022
13216
|
async createDefaultWorkerFactory(numWorkers, db) {
|
|
12023
13217
|
const [{ WorkerHandle }, { createThreadTransport }, { workerEntryPath }] = await Promise.all([
|
|
12024
|
-
import("./worker-handle-
|
|
13218
|
+
import("./worker-handle-Nso8i5eU.js"),
|
|
12025
13219
|
import("./transport-CZDHzfvy.js"),
|
|
12026
13220
|
Promise.resolve().then(() => worker_exports)
|
|
12027
13221
|
]);
|
|
@@ -12178,6 +13372,9 @@ var HybridProjectionCoordinator = class {
|
|
|
12178
13372
|
else this.postReady.push(readModel);
|
|
12179
13373
|
this.readModels.push(readModel);
|
|
12180
13374
|
}
|
|
13375
|
+
removeReadModel(readModel) {
|
|
13376
|
+
return spliceReadModel(readModel, this.preReady, this.postReady, this.readModels);
|
|
13377
|
+
}
|
|
12181
13378
|
getChainDepth() {
|
|
12182
13379
|
return this.manager.getChainDepth() + this.chains.size;
|
|
12183
13380
|
}
|
|
@@ -12214,11 +13411,7 @@ var HybridProjectionCoordinator = class {
|
|
|
12214
13411
|
async runHostBatch(event, enqueuedAt, reservations) {
|
|
12215
13412
|
const chainWaitDurationMs = performance.now() - enqueuedAt;
|
|
12216
13413
|
const preReadyStart = performance.now();
|
|
12217
|
-
|
|
12218
|
-
await Promise.all(this.preReady.map((readModel) => this.indexWithTiming(readModel, "pre_ready", event, reservations)));
|
|
12219
|
-
} catch (error) {
|
|
12220
|
-
this.logger.error("Host pre-ready read model indexing failed for job @jobId: @Error", { jobId: event.jobId }, error);
|
|
12221
|
-
}
|
|
13414
|
+
await indexStage([...this.preReady], (readModel) => this.indexWithTiming(readModel, "pre_ready", event, reservations), (error) => this.logger.error("Host pre-ready read model indexing failed for job @jobId: @Error", { jobId: event.jobId }, error));
|
|
12222
13415
|
const preReadyDurationMs = performance.now() - preReadyStart;
|
|
12223
13416
|
const emitStart = performance.now();
|
|
12224
13417
|
try {
|
|
@@ -12228,11 +13421,7 @@ var HybridProjectionCoordinator = class {
|
|
|
12228
13421
|
}
|
|
12229
13422
|
const emitDurationMs = performance.now() - emitStart;
|
|
12230
13423
|
const postReadyStart = performance.now();
|
|
12231
|
-
|
|
12232
|
-
await Promise.all(this.postReady.map((readModel) => this.indexWithTiming(readModel, "post_ready", event, reservations)));
|
|
12233
|
-
} catch (error) {
|
|
12234
|
-
this.logger.error("Host post-ready read model indexing failed for job @jobId: @Error", { jobId: event.jobId }, error);
|
|
12235
|
-
}
|
|
13424
|
+
await indexStage([...this.postReady], (readModel) => this.indexWithTiming(readModel, "post_ready", event, reservations), (error) => this.logger.error("Host post-ready read model indexing failed for job @jobId: @Error", { jobId: event.jobId }, error));
|
|
12236
13425
|
const postReadyDurationMs = performance.now() - postReadyStart;
|
|
12237
13426
|
this.emitBatchCompleted({
|
|
12238
13427
|
jobId: event.jobId,
|
|
@@ -12618,394 +13807,813 @@ function firstOutOfOrderPair(operations, options) {
|
|
|
12618
13807
|
}
|
|
12619
13808
|
}
|
|
12620
13809
|
//#endregion
|
|
12621
|
-
//#region src/
|
|
12622
|
-
|
|
12623
|
-
|
|
12624
|
-
DEFAULT_RECOVERY_TIMEOUT_MS: () => DEFAULT_RECOVERY_TIMEOUT_MS,
|
|
12625
|
-
DEFAULT_STATEMENT_TIMEOUT_MS: () => DEFAULT_STATEMENT_TIMEOUT_MS,
|
|
12626
|
-
HardenedPGliteDialect: () => HardenedPGliteDialect,
|
|
12627
|
-
PGliteAbortedTransactionError: () => PGliteAbortedTransactionError,
|
|
12628
|
-
PGliteAcquireTimeoutError: () => PGliteAcquireTimeoutError,
|
|
12629
|
-
PGliteSessionError: () => PGliteSessionError,
|
|
12630
|
-
PGliteSessionPoisonedError: () => PGliteSessionPoisonedError,
|
|
12631
|
-
PGliteStatementTimeoutError: () => PGliteStatementTimeoutError,
|
|
12632
|
-
isLongRunningStatement: () => isLongRunningStatement,
|
|
12633
|
-
queryThroughDialect: () => queryThroughDialect
|
|
12634
|
-
});
|
|
12635
|
-
const DEFAULT_STATEMENT_TIMEOUT_MS = 12e4;
|
|
12636
|
-
const DEFAULT_LONG_STATEMENT_TIMEOUT_MS = 9e5;
|
|
12637
|
-
const DEFAULT_RECOVERY_TIMEOUT_MS = 15e3;
|
|
12638
|
-
const LONG_STATEMENT_PREFIXES = [
|
|
12639
|
-
"vacuum",
|
|
12640
|
-
"analyze",
|
|
12641
|
-
"reindex",
|
|
12642
|
-
"cluster",
|
|
12643
|
-
"checkpoint",
|
|
12644
|
-
"copy",
|
|
12645
|
-
"alter ",
|
|
12646
|
-
"create ",
|
|
12647
|
-
"drop ",
|
|
12648
|
-
"truncate",
|
|
12649
|
-
"refresh "
|
|
12650
|
-
];
|
|
12651
|
-
/** Maintenance and DDL, matched on the leading keyword of the compiled SQL. */
|
|
12652
|
-
function isLongRunningStatement(statement) {
|
|
12653
|
-
const normalized = statement.trimStart().toLowerCase();
|
|
12654
|
-
return LONG_STATEMENT_PREFIXES.some((prefix) => normalized.startsWith(prefix));
|
|
13810
|
+
//#region src/read-models/interfaces.ts
|
|
13811
|
+
function supportsLiveReadModelRegistration(coordinator) {
|
|
13812
|
+
return "addReadModel" in coordinator && typeof coordinator.addReadModel === "function" && "removeReadModel" in coordinator && typeof coordinator.removeReadModel === "function";
|
|
12655
13813
|
}
|
|
12656
|
-
|
|
12657
|
-
|
|
12658
|
-
|
|
13814
|
+
//#endregion
|
|
13815
|
+
//#region src/sync/channels/channel-factory-types.ts
|
|
13816
|
+
/** The channel types a built factory declares; empty when it declares none. */
|
|
13817
|
+
function channelFactoryTypes(factory) {
|
|
13818
|
+
return factory.channelTypes ?? [];
|
|
12659
13819
|
}
|
|
12660
|
-
|
|
12661
|
-
|
|
12662
|
-
|
|
12663
|
-
|
|
12664
|
-
|
|
12665
|
-
|
|
12666
|
-
|
|
12667
|
-
|
|
12668
|
-
|
|
12669
|
-
|
|
12670
|
-
|
|
12671
|
-
|
|
12672
|
-
|
|
12673
|
-
|
|
12674
|
-
|
|
12675
|
-
|
|
12676
|
-
|
|
12677
|
-
}
|
|
12678
|
-
|
|
12679
|
-
|
|
12680
|
-
|
|
12681
|
-
|
|
12682
|
-
|
|
13820
|
+
//#endregion
|
|
13821
|
+
//#region src/sync/channels/local-wire.ts
|
|
13822
|
+
/**
|
|
13823
|
+
* Whether a received value is a well-formed wire message.
|
|
13824
|
+
*
|
|
13825
|
+
* A push is validated down to what `envelopesToSyncOperations` dereferences:
|
|
13826
|
+
* each envelope's `channelMeta.id` and, per operation, the `operation.action`
|
|
13827
|
+
* and the `context` fields it batches on.
|
|
13828
|
+
*/
|
|
13829
|
+
function isLocalWireMessage(data) {
|
|
13830
|
+
if (!isObject(data)) return false;
|
|
13831
|
+
const kind = data.kind;
|
|
13832
|
+
if (kind === "push") return Array.isArray(data.envelopes) && data.envelopes.every(isEnvelope);
|
|
13833
|
+
return kind === "hello" || kind === "ack" || kind === "resend";
|
|
13834
|
+
}
|
|
13835
|
+
function isObject(value) {
|
|
13836
|
+
return typeof value === "object" && value !== null;
|
|
13837
|
+
}
|
|
13838
|
+
function isEnvelope(envelope) {
|
|
13839
|
+
if (!isObject(envelope)) return false;
|
|
13840
|
+
if (!isObject(envelope.channelMeta)) return false;
|
|
13841
|
+
if (typeof envelope.channelMeta.id !== "string") return false;
|
|
13842
|
+
const operations = envelope.operations;
|
|
13843
|
+
return operations === void 0 || operations === null || Array.isArray(operations) && operations.every(isWireOperation);
|
|
13844
|
+
}
|
|
13845
|
+
function isWireOperation(entry) {
|
|
13846
|
+
if (!isObject(entry)) return false;
|
|
13847
|
+
const { operation, context } = entry;
|
|
13848
|
+
return isObject(operation) && isObject(operation.action) && isObject(context) && typeof context.documentId === "string" && typeof context.scope === "string" && typeof context.branch === "string" && typeof context.ordinal === "number";
|
|
13849
|
+
}
|
|
13850
|
+
//#endregion
|
|
13851
|
+
//#region src/sync/channels/local-channel.ts
|
|
13852
|
+
/** Base delay for the backoff that re-pushes after a transient transport fault. */
|
|
13853
|
+
const PUSH_RETRY_BASE_DELAY_MS = 500;
|
|
13854
|
+
/** Ceiling for that backoff. */
|
|
13855
|
+
const PUSH_RETRY_MAX_DELAY_MS = 3e4;
|
|
13856
|
+
/**
|
|
13857
|
+
* A symmetric, push-capable synchronization channel between two reactors over a
|
|
13858
|
+
* message port.
|
|
13859
|
+
*
|
|
13860
|
+
* Unlike the gql channels -- one polls and pushes over HTTP, the other never
|
|
13861
|
+
* initiates and is filled by resolvers -- a LocalChannel is peer-to-peer: either
|
|
13862
|
+
* side may push envelopes to the other at any time, with no GraphQL, no HTTP and
|
|
13863
|
+
* no Switchboard. It is the productionized sibling of the test channel: a real
|
|
13864
|
+
* wire protocol over an injected {@link LocalChannelPort}, the peer-manifest
|
|
13865
|
+
* handshake wired into the sync manager, cursor persistence, and ACK-driven
|
|
13866
|
+
* outbox trimming.
|
|
13867
|
+
*
|
|
13868
|
+
* The inbox holds its ack below unapplied purge markers, as the gql channels'
|
|
13869
|
+
* inboxes do.
|
|
13870
|
+
*/
|
|
13871
|
+
var LocalChannel = class {
|
|
13872
|
+
inbox;
|
|
13873
|
+
outbox;
|
|
13874
|
+
deadLetter;
|
|
13875
|
+
logger;
|
|
13876
|
+
channelId;
|
|
13877
|
+
remoteName;
|
|
13878
|
+
cursorStorage;
|
|
13879
|
+
port;
|
|
13880
|
+
collectionId;
|
|
13881
|
+
filter;
|
|
13882
|
+
isShutdown = false;
|
|
13883
|
+
unsubscribeTransport;
|
|
13884
|
+
localManifestProvider;
|
|
13885
|
+
/** Undefined until the first handshake; null for a peer that announces nothing. */
|
|
13886
|
+
peerManifest = void 0;
|
|
13887
|
+
peerManifestCallbacks = /* @__PURE__ */ new Set();
|
|
13888
|
+
connectionState = "connecting";
|
|
13889
|
+
/** Whether any valid frame has arrived since init. */
|
|
13890
|
+
heardPeer = false;
|
|
13891
|
+
failureCount = 0;
|
|
13892
|
+
lastSuccessUtcMs;
|
|
13893
|
+
lastFailureUtcMs;
|
|
13894
|
+
/** The highest inbox ack already reported to the peer; only an advance re-posts. */
|
|
13895
|
+
lastPostedAckOrdinal = 0;
|
|
13896
|
+
pushFailureCount = 0;
|
|
13897
|
+
pushRetryTimer;
|
|
13898
|
+
connectionStateCallbacks = /* @__PURE__ */ new Set();
|
|
13899
|
+
cursorWriters = {
|
|
13900
|
+
inbox: {
|
|
13901
|
+
persisted: 0,
|
|
13902
|
+
requested: 0,
|
|
13903
|
+
tail: Promise.resolve()
|
|
13904
|
+
},
|
|
13905
|
+
outbox: {
|
|
13906
|
+
persisted: 0,
|
|
13907
|
+
requested: 0,
|
|
13908
|
+
tail: Promise.resolve()
|
|
13909
|
+
}
|
|
13910
|
+
};
|
|
13911
|
+
constructor(logger, channelId, remoteName, cursorStorage, port, collectionId, filter) {
|
|
13912
|
+
this.logger = logger;
|
|
13913
|
+
this.channelId = channelId;
|
|
13914
|
+
this.remoteName = remoteName;
|
|
13915
|
+
this.cursorStorage = cursorStorage;
|
|
13916
|
+
this.port = port;
|
|
13917
|
+
this.collectionId = collectionId;
|
|
13918
|
+
this.filter = filter;
|
|
13919
|
+
this.inbox = new Mailbox({ holdAckBelowMarkers: true });
|
|
13920
|
+
this.outbox = new Mailbox();
|
|
13921
|
+
this.deadLetter = new Mailbox();
|
|
13922
|
+
this.outbox.onAdded((added) => this.onOutboxAdded(added));
|
|
13923
|
+
this.outbox.onRemoved((syncOps) => this.onOutboxRemoved(syncOps));
|
|
13924
|
+
this.inbox.onRemoved(() => this.onInboxRemoved());
|
|
12683
13925
|
}
|
|
12684
|
-
|
|
12685
|
-
|
|
12686
|
-
|
|
12687
|
-
|
|
12688
|
-
|
|
13926
|
+
/**
|
|
13927
|
+
* Loads the persisted cursors and initialises both mailboxes BEFORE
|
|
13928
|
+
* subscribing to the transport, so no inbound message is ever processed
|
|
13929
|
+
* against a zeroed mailbox. A frame processed before `inbox.init` runs would
|
|
13930
|
+
* be added below an ack of 0, and the init that followed would overwrite the
|
|
13931
|
+
* ack while leaving that frame pinning the floor -- dragging the live ack
|
|
13932
|
+
* below the durable cursor and re-loading already-applied ops as fresh jobs.
|
|
13933
|
+
*
|
|
13934
|
+
* Loading first relies on the port holding frames until a listener attaches.
|
|
13935
|
+
* A `LocalChannelPortRegistry` port does: it keeps one listener on the
|
|
13936
|
+
* raw port for its whole registration and replays, in order, what arrived
|
|
13937
|
+
* while no channel was attached. A raw browser MessagePort handed straight to
|
|
13938
|
+
* a channel does not once started, so a frame arriving between a shutdown
|
|
13939
|
+
* and the next init on it is lost.
|
|
13940
|
+
*/
|
|
13941
|
+
async init() {
|
|
13942
|
+
const cursors = await this.cursorStorage.list(this.remoteName);
|
|
13943
|
+
if (this.isShutdown) return;
|
|
13944
|
+
const inboxOrdinal = cursors.find((c) => c.cursorType === "inbox")?.cursorOrdinal ?? 0;
|
|
13945
|
+
const outboxOrdinal = cursors.find((c) => c.cursorType === "outbox")?.cursorOrdinal ?? 0;
|
|
13946
|
+
this.inbox.init(inboxOrdinal);
|
|
13947
|
+
this.outbox.init(outboxOrdinal);
|
|
13948
|
+
this.cursorWriters.inbox.persisted = inboxOrdinal;
|
|
13949
|
+
this.cursorWriters.outbox.persisted = outboxOrdinal;
|
|
13950
|
+
this.lastPostedAckOrdinal = inboxOrdinal;
|
|
13951
|
+
this.unsubscribeTransport = this.port.onMessage((data) => this.receive(data));
|
|
13952
|
+
this.sendHello(false);
|
|
12689
13953
|
}
|
|
12690
|
-
|
|
12691
|
-
|
|
12692
|
-
|
|
12693
|
-
|
|
12694
|
-
|
|
12695
|
-
this.
|
|
13954
|
+
/** Leaves the port open: whoever registered it closes it. */
|
|
13955
|
+
async shutdown() {
|
|
13956
|
+
this.isShutdown = true;
|
|
13957
|
+
this.clearPushRetry();
|
|
13958
|
+
this.unsubscribeTransport?.();
|
|
13959
|
+
this.unsubscribeTransport = void 0;
|
|
13960
|
+
this.transitionConnectionState("disconnected");
|
|
13961
|
+
return Promise.resolve();
|
|
12696
13962
|
}
|
|
12697
|
-
|
|
12698
|
-
|
|
12699
|
-
|
|
12700
|
-
|
|
12701
|
-
|
|
12702
|
-
|
|
12703
|
-
|
|
12704
|
-
|
|
12705
|
-
|
|
12706
|
-
|
|
12707
|
-
|
|
12708
|
-
try {
|
|
12709
|
-
return await Promise.race([pending, expiry]);
|
|
12710
|
-
} finally {
|
|
12711
|
-
clearTimeout(handle);
|
|
13963
|
+
getConnectionState() {
|
|
13964
|
+
return {
|
|
13965
|
+
state: this.connectionState,
|
|
13966
|
+
failureCount: this.failureCount,
|
|
13967
|
+
lastSuccessUtcMs: this.lastSuccessUtcMs ?? 0,
|
|
13968
|
+
lastFailureUtcMs: this.lastFailureUtcMs ?? 0,
|
|
13969
|
+
pushBlocked: this.pushRetryTimer !== void 0,
|
|
13970
|
+
pushFailureCount: this.pushFailureCount,
|
|
13971
|
+
receivingPages: false,
|
|
13972
|
+
requiresAuth: false
|
|
13973
|
+
};
|
|
12712
13974
|
}
|
|
12713
|
-
|
|
12714
|
-
|
|
12715
|
-
|
|
12716
|
-
|
|
12717
|
-
}
|
|
12718
|
-
|
|
12719
|
-
|
|
12720
|
-
|
|
12721
|
-
|
|
12722
|
-
|
|
12723
|
-
|
|
12724
|
-
this.
|
|
12725
|
-
this.
|
|
12726
|
-
|
|
12727
|
-
|
|
12728
|
-
|
|
12729
|
-
|
|
12730
|
-
|
|
12731
|
-
|
|
12732
|
-
|
|
12733
|
-
|
|
12734
|
-
|
|
13975
|
+
onConnectionStateChange(callback) {
|
|
13976
|
+
this.connectionStateCallbacks.add(callback);
|
|
13977
|
+
return () => {
|
|
13978
|
+
this.connectionStateCallbacks.delete(callback);
|
|
13979
|
+
};
|
|
13980
|
+
}
|
|
13981
|
+
/**
|
|
13982
|
+
* A push transport has nothing to poll, so this asks the peer to re-push
|
|
13983
|
+
* whatever it still holds unacked for this side, from this side's inbox ack.
|
|
13984
|
+
*/
|
|
13985
|
+
triggerPull() {
|
|
13986
|
+
if (this.isShutdown) return;
|
|
13987
|
+
this.post({
|
|
13988
|
+
kind: "resend",
|
|
13989
|
+
channelId: this.channelId,
|
|
13990
|
+
sinceOrdinal: this.inbox.ackOrdinal
|
|
13991
|
+
});
|
|
13992
|
+
}
|
|
13993
|
+
/** This channel pushes to its peer; it has no holder to hear from. */
|
|
13994
|
+
notePoll() {}
|
|
13995
|
+
/** No holder, so nothing this channel reports may strand one. */
|
|
13996
|
+
lastHolderPollUtcMs() {}
|
|
13997
|
+
setLocalManifest(provider) {
|
|
13998
|
+
this.localManifestProvider = provider;
|
|
13999
|
+
}
|
|
14000
|
+
onPeerManifest(callback) {
|
|
14001
|
+
this.peerManifestCallbacks.add(callback);
|
|
14002
|
+
return () => {
|
|
14003
|
+
this.peerManifestCallbacks.delete(callback);
|
|
12735
14004
|
};
|
|
12736
14005
|
}
|
|
12737
|
-
|
|
12738
|
-
|
|
14006
|
+
/** Pushes freshly added outbox items that have not since been removed. */
|
|
14007
|
+
onOutboxAdded(added) {
|
|
14008
|
+
if (this.isShutdown) return;
|
|
14009
|
+
const syncOps = added.filter((op) => this.outbox.get(op.id) === op);
|
|
14010
|
+
if (syncOps.length > 0) this.pushOutbox(syncOps);
|
|
12739
14011
|
}
|
|
12740
|
-
|
|
12741
|
-
|
|
14012
|
+
/**
|
|
14013
|
+
* Persists the outbox cursor from the applied ordinal of what was removed,
|
|
14014
|
+
* clamped below the lowest ordinal still in flight. Items for different
|
|
14015
|
+
* documents apply out of order, so the highest applied ordinal can pass one
|
|
14016
|
+
* still unacked; a restart from it would skip that one.
|
|
14017
|
+
*/
|
|
14018
|
+
onOutboxRemoved(syncOps) {
|
|
14019
|
+
const ordinal = Math.min(getLatestAppliedOrdinal(syncOps), this.unappliedOutboxFloor() - 1);
|
|
14020
|
+
this.persistCursor("outbox", ordinal);
|
|
12742
14021
|
}
|
|
12743
|
-
|
|
12744
|
-
|
|
14022
|
+
/**
|
|
14023
|
+
* Persists the inbox ack and reports it to the peer so it trims its outbox.
|
|
14024
|
+
* The ack is posted only when it strictly advances, so a burst of removals
|
|
14025
|
+
* that does not move the floor does not flood the peer with duplicate acks.
|
|
14026
|
+
*/
|
|
14027
|
+
onInboxRemoved() {
|
|
14028
|
+
const ackOrdinal = this.inbox.ackOrdinal;
|
|
14029
|
+
this.persistCursor("inbox", ackOrdinal);
|
|
14030
|
+
if (ackOrdinal > this.lastPostedAckOrdinal) this.postAck();
|
|
12745
14031
|
}
|
|
12746
|
-
|
|
12747
|
-
|
|
14032
|
+
/** Unconditional, so a peer re-pushing what this side applied can trim. */
|
|
14033
|
+
postAck() {
|
|
14034
|
+
const ackOrdinal = this.inbox.ackOrdinal;
|
|
14035
|
+
this.lastPostedAckOrdinal = Math.max(this.lastPostedAckOrdinal, ackOrdinal);
|
|
14036
|
+
this.post({
|
|
14037
|
+
kind: "ack",
|
|
14038
|
+
channelId: this.channelId,
|
|
14039
|
+
ackOrdinal
|
|
14040
|
+
});
|
|
12748
14041
|
}
|
|
12749
|
-
|
|
12750
|
-
|
|
12751
|
-
|
|
12752
|
-
|
|
12753
|
-
|
|
12754
|
-
|
|
12755
|
-
|
|
12756
|
-
|
|
12757
|
-
this.
|
|
12758
|
-
|
|
14042
|
+
receive(data) {
|
|
14043
|
+
if (this.isShutdown) return;
|
|
14044
|
+
if (!isLocalWireMessage(data)) {
|
|
14045
|
+
this.recordFailure(/* @__PURE__ */ new Error("LocalChannel received a malformed wire message"));
|
|
14046
|
+
return;
|
|
14047
|
+
}
|
|
14048
|
+
this.heardPeer = true;
|
|
14049
|
+
this.markSuccess();
|
|
14050
|
+
this.transitionConnectionState("connected");
|
|
14051
|
+
switch (data.kind) {
|
|
14052
|
+
case "hello":
|
|
14053
|
+
this.receiveHello(data);
|
|
14054
|
+
return;
|
|
14055
|
+
case "push":
|
|
14056
|
+
this.receivePush(data);
|
|
14057
|
+
return;
|
|
14058
|
+
case "ack":
|
|
14059
|
+
if (data.ackOrdinal > 0) trimMailboxFromAckOrdinal(this.outbox, data.ackOrdinal);
|
|
14060
|
+
return;
|
|
14061
|
+
case "resend":
|
|
14062
|
+
this.receiveResend(data);
|
|
14063
|
+
return;
|
|
14064
|
+
}
|
|
12759
14065
|
}
|
|
12760
|
-
/**
|
|
12761
|
-
|
|
12762
|
-
|
|
14066
|
+
/**
|
|
14067
|
+
* A HELLO's sinceOrdinal is the peer's inbox ack, so the outbox is trimmed to
|
|
14068
|
+
* it exactly as an ACK would, letting a reconnect resume without re-serving
|
|
14069
|
+
* applied ops. An opening hello then re-pushes the remaining unacked items in
|
|
14070
|
+
* case the peer reconnected and lost them. A reply does not: it answers this
|
|
14071
|
+
* side's own opening, so the peer did not reset, and re-pushing would hand
|
|
14072
|
+
* it every unacked op a second time.
|
|
14073
|
+
*/
|
|
14074
|
+
receiveHello(message) {
|
|
14075
|
+
if (message.sinceOrdinal > 0) trimMailboxFromAckOrdinal(this.outbox, message.sinceOrdinal);
|
|
14076
|
+
if (!message.reply) this.sendHello(true);
|
|
14077
|
+
this.hearPeer(message.manifest);
|
|
14078
|
+
if (!message.reply) this.rePushUnacked();
|
|
14079
|
+
}
|
|
14080
|
+
/**
|
|
14081
|
+
* Ingests a push into the inbox, dropping operations the inbox has already
|
|
14082
|
+
* acked. A reconnecting peer may re-push durable ops before it hears this
|
|
14083
|
+
* side's ack; adding one back as a fresh sync job would transiently drag the
|
|
14084
|
+
* ack floor below the persisted cursor, so a batch entirely at or below the
|
|
14085
|
+
* ack is skipped, and the ack is posted again so the peer can trim them. A
|
|
14086
|
+
* frame {@link isLocalWireMessage} rejects never reaches here; one that still
|
|
14087
|
+
* fails conversion is recorded as a failure.
|
|
14088
|
+
*/
|
|
14089
|
+
receivePush(message) {
|
|
14090
|
+
const ackFloor = this.inbox.ackOrdinal;
|
|
14091
|
+
const syncOps = [];
|
|
14092
|
+
let dropped = false;
|
|
14093
|
+
let converted;
|
|
12763
14094
|
try {
|
|
12764
|
-
|
|
14095
|
+
converted = message.envelopes.flatMap((envelope) => envelopesToSyncOperations(envelope, this.remoteName));
|
|
12765
14096
|
} catch (error) {
|
|
12766
|
-
this.
|
|
12767
|
-
|
|
14097
|
+
this.recordFailure(error instanceof Error ? error : new Error(String(error)));
|
|
14098
|
+
return;
|
|
12768
14099
|
}
|
|
12769
|
-
|
|
12770
|
-
|
|
12771
|
-
|
|
12772
|
-
|
|
12773
|
-
}
|
|
12774
|
-
/** Upstream issues the whole query on the first pull, so each pull is bounded like a statement. */
|
|
12775
|
-
async *deadlinedStream(compiledQuery, inner) {
|
|
12776
|
-
for (;;) {
|
|
12777
|
-
const generation = this.guard.generation;
|
|
12778
|
-
let next;
|
|
12779
|
-
try {
|
|
12780
|
-
next = await this.guard.runStatement(compiledQuery.sql, () => inner.next());
|
|
12781
|
-
} catch (error) {
|
|
12782
|
-
this.guard.recordFailure(this, generation, error);
|
|
12783
|
-
throw error;
|
|
14100
|
+
for (const syncOp of converted) {
|
|
14101
|
+
if (this.highestOrdinal(syncOp) <= ackFloor) {
|
|
14102
|
+
dropped = true;
|
|
14103
|
+
continue;
|
|
12784
14104
|
}
|
|
12785
|
-
|
|
12786
|
-
|
|
14105
|
+
syncOp.transported();
|
|
14106
|
+
syncOps.push(syncOp);
|
|
14107
|
+
}
|
|
14108
|
+
if (dropped) this.postAck();
|
|
14109
|
+
if (syncOps.length === 0) return;
|
|
14110
|
+
try {
|
|
14111
|
+
this.inbox.add(...syncOps);
|
|
14112
|
+
} catch (error) {
|
|
14113
|
+
this.recordFailure(error instanceof Error ? error : new Error(String(error)));
|
|
12787
14114
|
}
|
|
12788
14115
|
}
|
|
12789
|
-
|
|
12790
|
-
|
|
14116
|
+
receiveResend(message) {
|
|
14117
|
+
if (message.sinceOrdinal > 0) trimMailboxFromAckOrdinal(this.outbox, message.sinceOrdinal);
|
|
14118
|
+
this.rePushUnacked();
|
|
12791
14119
|
}
|
|
12792
|
-
|
|
12793
|
-
|
|
12794
|
-
|
|
12795
|
-
|
|
12796
|
-
|
|
12797
|
-
|
|
12798
|
-
|
|
12799
|
-
|
|
12800
|
-
|
|
12801
|
-
|
|
12802
|
-
deadCall = void 0;
|
|
12803
|
-
poisonReported = false;
|
|
12804
|
-
constructor(inner, client, options) {
|
|
12805
|
-
this.inner = inner;
|
|
12806
|
-
this.client = client;
|
|
12807
|
-
this.options = options;
|
|
14120
|
+
sendHello(reply) {
|
|
14121
|
+
this.post({
|
|
14122
|
+
kind: "hello",
|
|
14123
|
+
channelId: this.channelId,
|
|
14124
|
+
collectionId: this.collectionId.key,
|
|
14125
|
+
filter: this.filter,
|
|
14126
|
+
sinceOrdinal: this.inbox.ackOrdinal,
|
|
14127
|
+
manifest: this.localManifestProvider?.() ?? null,
|
|
14128
|
+
...reply ? { reply } : {}
|
|
14129
|
+
});
|
|
12808
14130
|
}
|
|
12809
|
-
|
|
12810
|
-
|
|
14131
|
+
/**
|
|
14132
|
+
* Pushes the given outbox items to the peer, marking them in flight.
|
|
14133
|
+
*
|
|
14134
|
+
* A transport throw is classified rather than treated as terminal: only a
|
|
14135
|
+
* structured-clone failure, which recurs identically on every retry, dead-
|
|
14136
|
+
* letters its ops. Any other fault is transient -- the port is momentarily
|
|
14137
|
+
* unusable -- so the ops stay in the outbox and {@link handlePushFailure}
|
|
14138
|
+
* schedules a backoff re-push, keeping behaviour in step with the gql
|
|
14139
|
+
* sibling's recoverable/unrecoverable split.
|
|
14140
|
+
*/
|
|
14141
|
+
pushOutbox(syncOps) {
|
|
14142
|
+
if (this.isShutdown || syncOps.length === 0) return;
|
|
14143
|
+
for (const syncOp of syncOps) syncOp.started();
|
|
14144
|
+
try {
|
|
14145
|
+
this.port.postMessage({
|
|
14146
|
+
kind: "push",
|
|
14147
|
+
channelId: this.channelId,
|
|
14148
|
+
envelopes: this.envelopesFor(syncOps)
|
|
14149
|
+
});
|
|
14150
|
+
} catch (error) {
|
|
14151
|
+
this.handlePushFailure(syncOps, error instanceof Error ? error : new Error(String(error)));
|
|
14152
|
+
return;
|
|
14153
|
+
}
|
|
14154
|
+
this.clearPushRetry();
|
|
14155
|
+
if (this.heardPeer) {
|
|
14156
|
+
this.markSuccess();
|
|
14157
|
+
this.transitionConnectionState("connected");
|
|
14158
|
+
} else this.transitionConnectionState("connecting");
|
|
12811
14159
|
}
|
|
12812
|
-
/**
|
|
12813
|
-
|
|
12814
|
-
if (this.
|
|
12815
|
-
const
|
|
12816
|
-
|
|
12817
|
-
|
|
12818
|
-
|
|
12819
|
-
|
|
12820
|
-
|
|
14160
|
+
/** Re-pushes every outbox item the peer has not acknowledged. */
|
|
14161
|
+
rePushUnacked() {
|
|
14162
|
+
if (this.isShutdown) return;
|
|
14163
|
+
const unacked = this.outbox.items.filter((syncOp) => syncOp.status !== 2);
|
|
14164
|
+
this.pushOutbox(unacked);
|
|
14165
|
+
}
|
|
14166
|
+
/**
|
|
14167
|
+
* Routes a push transport failure: a structured-clone (`DataCloneError`)
|
|
14168
|
+
* failure is unrecoverable and dead-letters its ops; any other error is a
|
|
14169
|
+
* transient transport fault, so the ops are left in the outbox to be
|
|
14170
|
+
* re-pushed by the backoff retry and by the next hello/resend recovery.
|
|
14171
|
+
*/
|
|
14172
|
+
handlePushFailure(syncOps, error) {
|
|
14173
|
+
if (this.classifyPostError(error) === "unrecoverable") {
|
|
14174
|
+
const channelError = new ChannelError("outbox", error);
|
|
14175
|
+
for (const syncOp of syncOps) syncOp.failed(channelError);
|
|
14176
|
+
this.deadLetter.add(...syncOps);
|
|
14177
|
+
this.outbox.remove(...syncOps);
|
|
14178
|
+
this.recordFailure(error);
|
|
14179
|
+
return;
|
|
14180
|
+
}
|
|
14181
|
+
this.recordFailure(error);
|
|
14182
|
+
this.schedulePushRetry();
|
|
12821
14183
|
}
|
|
12822
|
-
|
|
12823
|
-
|
|
14184
|
+
/** A structured-clone failure cannot self-heal; every other fault can. */
|
|
14185
|
+
classifyPostError(error) {
|
|
14186
|
+
return error.name === "DataCloneError" ? "unrecoverable" : "recoverable";
|
|
12824
14187
|
}
|
|
12825
|
-
|
|
12826
|
-
|
|
14188
|
+
/** Schedules a single backoff re-push of the unacked outbox. */
|
|
14189
|
+
schedulePushRetry() {
|
|
14190
|
+
if (this.isShutdown || this.pushRetryTimer !== void 0) return;
|
|
14191
|
+
const delay = calculateBackoffDelay(this.pushFailureCount, PUSH_RETRY_BASE_DELAY_MS, PUSH_RETRY_MAX_DELAY_MS, Math.random());
|
|
14192
|
+
this.pushFailureCount++;
|
|
14193
|
+
this.pushRetryTimer = setTimeout(() => {
|
|
14194
|
+
this.pushRetryTimer = void 0;
|
|
14195
|
+
this.rePushUnacked();
|
|
14196
|
+
}, delay);
|
|
12827
14197
|
}
|
|
12828
|
-
|
|
12829
|
-
|
|
12830
|
-
|
|
12831
|
-
|
|
12832
|
-
|
|
14198
|
+
/** Clears the backoff state once a push lands or the channel stops. */
|
|
14199
|
+
clearPushRetry() {
|
|
14200
|
+
this.pushFailureCount = 0;
|
|
14201
|
+
if (this.pushRetryTimer !== void 0) {
|
|
14202
|
+
clearTimeout(this.pushRetryTimer);
|
|
14203
|
+
this.pushRetryTimer = void 0;
|
|
12833
14204
|
}
|
|
12834
|
-
|
|
12835
|
-
|
|
12836
|
-
|
|
12837
|
-
|
|
12838
|
-
|
|
12839
|
-
|
|
12840
|
-
|
|
14205
|
+
}
|
|
14206
|
+
/** One envelope per SyncOperation, with key/dependsOn for batch ordering. */
|
|
14207
|
+
envelopesFor(syncOps) {
|
|
14208
|
+
const jobIdToKeys = /* @__PURE__ */ new Map();
|
|
14209
|
+
const envelopes = [];
|
|
14210
|
+
for (let i = 0; i < syncOps.length; i++) {
|
|
14211
|
+
const syncOp = syncOps[i];
|
|
14212
|
+
const key = String(i);
|
|
14213
|
+
if (syncOp.jobId) {
|
|
14214
|
+
const keys = jobIdToKeys.get(syncOp.jobId) ?? [];
|
|
14215
|
+
keys.push(key);
|
|
14216
|
+
jobIdToKeys.set(syncOp.jobId, keys);
|
|
12841
14217
|
}
|
|
12842
|
-
|
|
12843
|
-
|
|
12844
|
-
|
|
14218
|
+
const dependsOn = [];
|
|
14219
|
+
for (const dep of syncOp.jobDependencies) {
|
|
14220
|
+
const depKeys = jobIdToKeys.get(dep);
|
|
14221
|
+
if (depKeys) dependsOn.push(...depKeys);
|
|
12845
14222
|
}
|
|
12846
|
-
|
|
14223
|
+
envelopes.push({
|
|
14224
|
+
type: "operations",
|
|
14225
|
+
channelMeta: { id: this.channelId },
|
|
14226
|
+
operations: syncOp.operations,
|
|
14227
|
+
key,
|
|
14228
|
+
dependsOn
|
|
14229
|
+
});
|
|
12847
14230
|
}
|
|
12848
|
-
return
|
|
14231
|
+
return envelopes.map((envelope) => serializeEnvelope(envelope));
|
|
12849
14232
|
}
|
|
12850
|
-
/**
|
|
12851
|
-
|
|
12852
|
-
|
|
12853
|
-
|
|
12854
|
-
|
|
12855
|
-
|
|
12856
|
-
|
|
12857
|
-
|
|
12858
|
-
|
|
12859
|
-
if (!await this.tryRecoverSession(false)) {
|
|
12860
|
-
this.sessionFault = errorOf(error);
|
|
12861
|
-
throw error;
|
|
12862
|
-
}
|
|
12863
|
-
const retryGeneration = this.statementGeneration;
|
|
14233
|
+
/**
|
|
14234
|
+
* Hears the peer's manifest, deduped like the gql channel: an older or
|
|
14235
|
+
* identical revision is ignored, so a reconnect that re-announces the same
|
|
14236
|
+
* manifest does not re-run the sync manager's hold reconciliation.
|
|
14237
|
+
*/
|
|
14238
|
+
async hearPeer(manifest) {
|
|
14239
|
+
if (this.peerManifest !== void 0 && (this.peerManifest?.revision ?? null) === (manifest?.revision ?? null) || isOlderManifest(manifest, this.peerManifest)) return;
|
|
14240
|
+
this.peerManifest = manifest;
|
|
14241
|
+
await Promise.all([...this.peerManifestCallbacks].map(async (callback) => {
|
|
12864
14242
|
try {
|
|
12865
|
-
await
|
|
12866
|
-
} catch (
|
|
12867
|
-
this.
|
|
12868
|
-
throw retryError;
|
|
14243
|
+
await callback(manifest);
|
|
14244
|
+
} catch (error) {
|
|
14245
|
+
this.logger.error("LocalChannel @ChannelId peer manifest callback error: @Error", this.channelId, error);
|
|
12869
14246
|
}
|
|
12870
|
-
|
|
12871
|
-
}
|
|
12872
|
-
wrapper.transactionOpen = true;
|
|
14247
|
+
}));
|
|
12873
14248
|
}
|
|
12874
|
-
|
|
12875
|
-
|
|
12876
|
-
|
|
14249
|
+
/** Sends a message, returning whether the transport accepted it. */
|
|
14250
|
+
post(message) {
|
|
14251
|
+
if (this.isShutdown) return false;
|
|
12877
14252
|
try {
|
|
12878
|
-
|
|
14253
|
+
this.port.postMessage(message);
|
|
14254
|
+
return true;
|
|
12879
14255
|
} catch (error) {
|
|
12880
|
-
this.recordFailure(
|
|
12881
|
-
|
|
12882
|
-
throw error;
|
|
14256
|
+
this.recordFailure(error instanceof Error ? error : new Error(String(error)));
|
|
14257
|
+
return false;
|
|
12883
14258
|
}
|
|
12884
|
-
wrapper.transactionOpen = false;
|
|
12885
|
-
wrapper.failure = void 0;
|
|
12886
14259
|
}
|
|
12887
|
-
/**
|
|
12888
|
-
|
|
12889
|
-
|
|
12890
|
-
|
|
12891
|
-
|
|
12892
|
-
|
|
12893
|
-
|
|
14260
|
+
/** Leaves the push retry alone: only a push that lands clears it. */
|
|
14261
|
+
markSuccess() {
|
|
14262
|
+
this.lastSuccessUtcMs = Date.now();
|
|
14263
|
+
this.failureCount = 0;
|
|
14264
|
+
}
|
|
14265
|
+
recordFailure(error) {
|
|
14266
|
+
this.failureCount++;
|
|
14267
|
+
this.lastFailureUtcMs = Date.now();
|
|
14268
|
+
this.logger.error("LocalChannel @ChannelId transport error (@FailureCount): @Error", this.channelId, this.failureCount, error);
|
|
14269
|
+
this.transitionConnectionState("error");
|
|
14270
|
+
}
|
|
14271
|
+
transitionConnectionState(next) {
|
|
14272
|
+
if (this.connectionState === next) return;
|
|
14273
|
+
this.connectionState = next;
|
|
14274
|
+
const snapshot = this.getConnectionState();
|
|
14275
|
+
for (const callback of this.connectionStateCallbacks) try {
|
|
14276
|
+
callback(snapshot);
|
|
12894
14277
|
} catch (error) {
|
|
12895
|
-
|
|
12896
|
-
this.options.onDiagnostic("rollback failed; preserving the original failure and recovering the session on release", error);
|
|
14278
|
+
this.logger.error("LocalChannel @ChannelId connection state callback error: @Error", this.channelId, error);
|
|
12897
14279
|
}
|
|
12898
14280
|
}
|
|
12899
|
-
|
|
12900
|
-
|
|
12901
|
-
|
|
12902
|
-
|
|
12903
|
-
|
|
12904
|
-
|
|
12905
|
-
|
|
12906
|
-
|
|
14281
|
+
/** The greatest operation ordinal a sync op carries, or 0 when it carries none. */
|
|
14282
|
+
highestOrdinal(syncOp) {
|
|
14283
|
+
let highest = 0;
|
|
14284
|
+
for (const op of syncOp.operations) if (op.context.ordinal > highest) highest = op.context.ordinal;
|
|
14285
|
+
return highest;
|
|
14286
|
+
}
|
|
14287
|
+
/** The lowest ordinal of an outbox item the peer has not applied. */
|
|
14288
|
+
unappliedOutboxFloor() {
|
|
14289
|
+
let floor = Number.POSITIVE_INFINITY;
|
|
14290
|
+
for (const syncOp of this.outbox.items) {
|
|
14291
|
+
if (syncOp.status === 2) continue;
|
|
14292
|
+
for (const op of syncOp.operations) {
|
|
14293
|
+
const ordinal = op.context.ordinal;
|
|
14294
|
+
if (ordinal > 0 && ordinal < floor) floor = ordinal;
|
|
12907
14295
|
}
|
|
12908
|
-
} finally {
|
|
12909
|
-
await this.inner.releaseConnection(wrapper.inner);
|
|
12910
14296
|
}
|
|
14297
|
+
return floor;
|
|
12911
14298
|
}
|
|
12912
|
-
|
|
12913
|
-
|
|
14299
|
+
/**
|
|
14300
|
+
* Advances a persisted cursor only once the write has landed, and never with
|
|
14301
|
+
* two writes for the same row outstanding. See the gql request channel for
|
|
14302
|
+
* the full argument: concurrent writers for one row are last-writer-wins, and
|
|
14303
|
+
* the storage promises no FIFO, so a burst of removals is coalesced into one
|
|
14304
|
+
* ordered write rather than a race.
|
|
14305
|
+
*/
|
|
14306
|
+
persistCursor(cursorType, ordinal) {
|
|
14307
|
+
const writer = this.cursorWriters[cursorType];
|
|
14308
|
+
if (ordinal <= Math.max(writer.persisted, writer.requested)) return;
|
|
14309
|
+
writer.requested = ordinal;
|
|
14310
|
+
writer.tail = writer.tail.then(() => this.writeCursor(cursorType));
|
|
12914
14311
|
}
|
|
12915
|
-
|
|
12916
|
-
|
|
12917
|
-
|
|
14312
|
+
/** One link of a cursor write chain; never rejects, so the chain survives. */
|
|
14313
|
+
async writeCursor(cursorType) {
|
|
14314
|
+
const writer = this.cursorWriters[cursorType];
|
|
14315
|
+
const ordinal = writer.requested;
|
|
14316
|
+
if (ordinal <= writer.persisted) return;
|
|
14317
|
+
writer.requested = 0;
|
|
12918
14318
|
try {
|
|
12919
|
-
this.
|
|
14319
|
+
await this.cursorStorage.upsert({
|
|
14320
|
+
remoteName: this.remoteName,
|
|
14321
|
+
cursorType,
|
|
14322
|
+
cursorOrdinal: ordinal,
|
|
14323
|
+
lastSyncedAtUtcMs: Date.now()
|
|
14324
|
+
});
|
|
12920
14325
|
} catch (error) {
|
|
12921
|
-
this.
|
|
14326
|
+
this.logger.error("LocalChannel @ChannelId failed to persist @CursorType cursor at @Ordinal; the watermark stays put so the next advance retries it: @Error", this.channelId, cursorType, ordinal, error);
|
|
14327
|
+
return;
|
|
12922
14328
|
}
|
|
14329
|
+
writer.persisted = Math.max(writer.persisted, ordinal);
|
|
12923
14330
|
}
|
|
12924
|
-
|
|
12925
|
-
|
|
14331
|
+
};
|
|
14332
|
+
//#endregion
|
|
14333
|
+
//#region src/sync/channels/local-channel-factory.ts
|
|
14334
|
+
/** The {@link ChannelConfig.type} a LocalChannel is created from. */
|
|
14335
|
+
const LOCAL_CHANNEL_TYPE = "local";
|
|
14336
|
+
/**
|
|
14337
|
+
* Factory for {@link LocalChannel} instances.
|
|
14338
|
+
*
|
|
14339
|
+
* The channel's config is `{ type: "local", parameters: { peerId, channelName } }`
|
|
14340
|
+
* -- no url, because there is no server. The factory does not take a port
|
|
14341
|
+
* directly: {@link IChannelFactory.instance} has no seam for a live object, and
|
|
14342
|
+
* a MessagePort is not clone-safe config that could ride in `parameters`.
|
|
14343
|
+
* Instead the factory holds a {@link LocalChannelTransportProvider} and resolves
|
|
14344
|
+
* the port from the (peerId, channelName) the config names. W1.2 supplies a real
|
|
14345
|
+
* brokered MessagePort by registering it with the provider under that key, with
|
|
14346
|
+
* no change to the channel or this factory.
|
|
14347
|
+
*/
|
|
14348
|
+
var LocalChannelFactory = class {
|
|
14349
|
+
channelTypes = [LOCAL_CHANNEL_TYPE];
|
|
14350
|
+
logger;
|
|
14351
|
+
transportProvider;
|
|
14352
|
+
constructor(logger, transportProvider) {
|
|
14353
|
+
this.logger = logger;
|
|
14354
|
+
this.transportProvider = transportProvider;
|
|
14355
|
+
}
|
|
14356
|
+
instance(remoteId, remoteName, config, cursorStorage, collectionId, filter, _operationIndex, _options) {
|
|
14357
|
+
if (config.type !== "local") throw new Error(`This reactor has no "${config.type}" channel factory: it is provisioned local-only, and LocalChannelFactory builds only "${LOCAL_CHANNEL_TYPE}" channels`);
|
|
14358
|
+
const peerId = config.parameters.peerId;
|
|
14359
|
+
if (typeof peerId !== "string" || !peerId) throw new Error("LocalChannelFactory requires a non-empty \"peerId\" string in config.parameters");
|
|
14360
|
+
const channelName = config.parameters.channelName;
|
|
14361
|
+
if (typeof channelName !== "string" || !channelName) throw new Error("LocalChannelFactory requires a non-empty \"channelName\" string in config.parameters");
|
|
14362
|
+
const port = this.transportProvider(peerId, channelName);
|
|
14363
|
+
if (!port) throw new Error(`LocalChannelFactory has no transport for peer '${peerId}' channel '${channelName}'`);
|
|
14364
|
+
return new LocalChannel(this.logger, remoteId, remoteName, cursorStorage, port, collectionId, filter);
|
|
12926
14365
|
}
|
|
12927
|
-
|
|
12928
|
-
|
|
12929
|
-
|
|
12930
|
-
|
|
12931
|
-
|
|
12932
|
-
|
|
12933
|
-
|
|
12934
|
-
|
|
14366
|
+
};
|
|
14367
|
+
//#endregion
|
|
14368
|
+
//#region src/sync/channels/local-channel-registry.ts
|
|
14369
|
+
const DEFAULT_MAX_QUEUED_FRAMES = 1024;
|
|
14370
|
+
function frameKind(data) {
|
|
14371
|
+
return typeof data === "object" && data !== null ? data.kind : void 0;
|
|
14372
|
+
}
|
|
14373
|
+
/**
|
|
14374
|
+
* The port a channel sees. It holds the registry's one listener on the raw port
|
|
14375
|
+
* for the whole registration, so a frame that arrives between two channels
|
|
14376
|
+
* (a reset) is queued and replayed to the next one instead of lost.
|
|
14377
|
+
*
|
|
14378
|
+
* Past the bound, every queued push is discarded, and so is every later one
|
|
14379
|
+
* until a channel is attached and hears a live hello: the inbox ack is the
|
|
14380
|
+
* highest applied ordinal, so delivering pushes after a gap would ack past the
|
|
14381
|
+
* missing ones. The peer still holds them unacked and re-pushes them right
|
|
14382
|
+
* after the hello it answers the fresh channel's hello with, or after its own
|
|
14383
|
+
* opening hello's backfill.
|
|
14384
|
+
*/
|
|
14385
|
+
var RegisteredPort = class {
|
|
14386
|
+
raw;
|
|
14387
|
+
label;
|
|
14388
|
+
logger;
|
|
14389
|
+
maxQueuedFrames;
|
|
14390
|
+
subscriber;
|
|
14391
|
+
queued = [];
|
|
14392
|
+
discardingPushes = false;
|
|
14393
|
+
detachRaw;
|
|
14394
|
+
constructor(raw, label, logger, maxQueuedFrames) {
|
|
14395
|
+
this.raw = raw;
|
|
14396
|
+
this.label = label;
|
|
14397
|
+
this.logger = logger;
|
|
14398
|
+
this.maxQueuedFrames = maxQueuedFrames;
|
|
14399
|
+
this.detachRaw = raw.onMessage((data) => this.dispatch(data));
|
|
12935
14400
|
}
|
|
12936
|
-
|
|
12937
|
-
|
|
12938
|
-
const timeoutMs = this.options.acquireTimeoutMs;
|
|
12939
|
-
if (timeoutMs <= 0) return this.inner.acquireConnection();
|
|
12940
|
-
let abandoned = false;
|
|
12941
|
-
const pending = this.inner.acquireConnection().then((connection) => {
|
|
12942
|
-
if (!abandoned) return connection;
|
|
12943
|
-
this.inner.releaseConnection(connection).catch(() => void 0);
|
|
12944
|
-
throw new PGliteAcquireTimeoutError(timeoutMs);
|
|
12945
|
-
});
|
|
12946
|
-
let handle;
|
|
12947
|
-
const expiry = new Promise((_resolve, reject) => {
|
|
12948
|
-
handle = setTimeout(() => {
|
|
12949
|
-
abandoned = true;
|
|
12950
|
-
reject(new PGliteAcquireTimeoutError(timeoutMs));
|
|
12951
|
-
}, timeoutMs);
|
|
12952
|
-
});
|
|
12953
|
-
try {
|
|
12954
|
-
return await Promise.race([pending, expiry]);
|
|
12955
|
-
} finally {
|
|
12956
|
-
clearTimeout(handle);
|
|
12957
|
-
}
|
|
14401
|
+
postMessage(data) {
|
|
14402
|
+
this.raw.postMessage(data);
|
|
12958
14403
|
}
|
|
12959
|
-
/**
|
|
12960
|
-
|
|
12961
|
-
|
|
12962
|
-
|
|
12963
|
-
|
|
12964
|
-
|
|
12965
|
-
|
|
14404
|
+
/**
|
|
14405
|
+
* One channel at a time; a newer attach supersedes an older one. A receive
|
|
14406
|
+
* that throws during replay detaches it and leaves the rest queued.
|
|
14407
|
+
*/
|
|
14408
|
+
onMessage(callback) {
|
|
14409
|
+
this.subscriber = callback;
|
|
14410
|
+
const detach = () => {
|
|
14411
|
+
if (this.subscriber === callback) this.subscriber = void 0;
|
|
14412
|
+
};
|
|
14413
|
+
while (this.queued.length > 0 && this.subscriber === callback) {
|
|
14414
|
+
const frame = this.queued.shift();
|
|
14415
|
+
try {
|
|
14416
|
+
callback(frame);
|
|
14417
|
+
} catch (error) {
|
|
14418
|
+
detach();
|
|
14419
|
+
if (frameKind(frame) === "push") this.discardPushes();
|
|
14420
|
+
throw error;
|
|
14421
|
+
}
|
|
12966
14422
|
}
|
|
14423
|
+
return detach;
|
|
12967
14424
|
}
|
|
12968
|
-
/**
|
|
12969
|
-
|
|
12970
|
-
|
|
12971
|
-
|
|
12972
|
-
|
|
12973
|
-
|
|
12974
|
-
|
|
12975
|
-
async probeSession() {
|
|
12976
|
-
return this.recoveryExec(PROBE);
|
|
14425
|
+
/** The registry closes the raw port on unregister; a channel cannot. */
|
|
14426
|
+
close() {}
|
|
14427
|
+
release() {
|
|
14428
|
+
this.subscriber = void 0;
|
|
14429
|
+
this.queued.length = 0;
|
|
14430
|
+
this.detachRaw();
|
|
14431
|
+
this.raw.close();
|
|
12977
14432
|
}
|
|
12978
|
-
|
|
12979
|
-
|
|
12980
|
-
|
|
12981
|
-
|
|
12982
|
-
|
|
12983
|
-
|
|
12984
|
-
|
|
14433
|
+
dispatch(data) {
|
|
14434
|
+
if (this.discardingPushes) {
|
|
14435
|
+
const kind = frameKind(data);
|
|
14436
|
+
if (kind === "push") return;
|
|
14437
|
+
if (kind === "hello" && this.subscriber) this.discardingPushes = false;
|
|
14438
|
+
}
|
|
14439
|
+
if (this.subscriber) {
|
|
14440
|
+
this.subscriber(data);
|
|
14441
|
+
return;
|
|
14442
|
+
}
|
|
14443
|
+
this.queued.push(data);
|
|
14444
|
+
if (this.queued.length <= this.maxQueuedFrames) return;
|
|
14445
|
+
if (!this.discardingPushes) this.logger.warn("Local sync port @Label queued more than @Max frames with no channel attached; discarding queued pushes for the peer to re-push", this.label, this.maxQueuedFrames);
|
|
14446
|
+
this.discardPushes();
|
|
14447
|
+
this.compact();
|
|
14448
|
+
}
|
|
14449
|
+
/** Until a live hello, so no later push lands across the gap. */
|
|
14450
|
+
discardPushes() {
|
|
14451
|
+
this.discardingPushes = true;
|
|
14452
|
+
this.queued = this.queued.filter((frame) => frameKind(frame) !== "push");
|
|
14453
|
+
}
|
|
14454
|
+
/** Keeps only the latest hello or resend and the latest ack, in order. */
|
|
14455
|
+
compact() {
|
|
14456
|
+
const handshake = this.queued.findLast((frame) => {
|
|
14457
|
+
const kind = frameKind(frame);
|
|
14458
|
+
return kind === "hello" || kind === "resend";
|
|
12985
14459
|
});
|
|
12986
|
-
|
|
12987
|
-
|
|
12988
|
-
|
|
12989
|
-
|
|
12990
|
-
|
|
12991
|
-
|
|
12992
|
-
|
|
14460
|
+
const ack = this.queued.findLast((frame) => frameKind(frame) === "ack");
|
|
14461
|
+
this.queued = this.queued.filter((frame) => frame === handshake || frame === ack);
|
|
14462
|
+
}
|
|
14463
|
+
};
|
|
14464
|
+
/** Brokered local-sync ports by (peerId, channelName); owns closing them. */
|
|
14465
|
+
var LocalChannelPortRegistry = class {
|
|
14466
|
+
ports = /* @__PURE__ */ new Map();
|
|
14467
|
+
closedKeys = /* @__PURE__ */ new Set();
|
|
14468
|
+
closed = false;
|
|
14469
|
+
logger;
|
|
14470
|
+
maxQueuedFrames;
|
|
14471
|
+
constructor(options = {}) {
|
|
14472
|
+
this.logger = options.logger ?? new ConsoleLogger(["local-sync"]);
|
|
14473
|
+
this.maxQueuedFrames = options.maxQueuedFrames ?? DEFAULT_MAX_QUEUED_FRAMES;
|
|
14474
|
+
}
|
|
14475
|
+
/** Hand this to the {@link LocalChannelFactory}. */
|
|
14476
|
+
provider = (peerId, channelName) => {
|
|
14477
|
+
const key = this.key(peerId, channelName);
|
|
14478
|
+
const port = this.ports.get(key);
|
|
14479
|
+
if (port) return port;
|
|
14480
|
+
if (this.closedKeys.has(key)) throw new Error(`Local sync port for peer '${peerId}' channel '${channelName}' has been closed; the link is severed and must be brokered again`);
|
|
14481
|
+
};
|
|
14482
|
+
/** Refuses a live key; a closed key may be registered again (a re-link). */
|
|
14483
|
+
register(peerId, channelName, port) {
|
|
14484
|
+
if (this.closed) throw new Error(`This reactor's local sync ports are closed; peer '${peerId}' channel '${channelName}' cannot be brokered here`);
|
|
14485
|
+
const key = this.key(peerId, channelName);
|
|
14486
|
+
if (this.ports.has(key)) throw new Error(`A local sync port is already registered for peer '${peerId}' channel '${channelName}'; unlink the existing link before brokering another`);
|
|
14487
|
+
this.closedKeys.delete(key);
|
|
14488
|
+
this.ports.set(key, new RegisteredPort(port, key, this.logger, this.maxQueuedFrames));
|
|
14489
|
+
}
|
|
14490
|
+
has(peerId, channelName) {
|
|
14491
|
+
return this.ports.has(this.key(peerId, channelName));
|
|
14492
|
+
}
|
|
14493
|
+
isClosed(peerId, channelName) {
|
|
14494
|
+
return this.closedKeys.has(this.key(peerId, channelName));
|
|
14495
|
+
}
|
|
14496
|
+
/** Detaches the registry's listener and closes the port. */
|
|
14497
|
+
unregister(peerId, channelName) {
|
|
14498
|
+
const key = this.key(peerId, channelName);
|
|
14499
|
+
const port = this.ports.get(key);
|
|
14500
|
+
this.ports.delete(key);
|
|
14501
|
+
this.closedKeys.add(key);
|
|
14502
|
+
port?.release();
|
|
14503
|
+
}
|
|
14504
|
+
/** Closes every port and refuses later registrations; the host is going away. */
|
|
14505
|
+
close() {
|
|
14506
|
+
this.closed = true;
|
|
14507
|
+
for (const [key, port] of [...this.ports]) {
|
|
14508
|
+
this.ports.delete(key);
|
|
14509
|
+
this.closedKeys.add(key);
|
|
14510
|
+
try {
|
|
14511
|
+
port.release();
|
|
14512
|
+
} catch (error) {
|
|
14513
|
+
this.logger.error("Closing local sync port @Label failed: @Error", key, error);
|
|
14514
|
+
}
|
|
12993
14515
|
}
|
|
12994
|
-
|
|
14516
|
+
}
|
|
14517
|
+
key(peerId, channelName) {
|
|
14518
|
+
return JSON.stringify([peerId, channelName]);
|
|
12995
14519
|
}
|
|
12996
14520
|
};
|
|
12997
|
-
|
|
12998
|
-
|
|
12999
|
-
|
|
14521
|
+
/** `branch: ""` matches every branch. */
|
|
14522
|
+
const DEFAULT_LOCAL_FILTER = {
|
|
14523
|
+
documentId: [],
|
|
14524
|
+
scope: [],
|
|
14525
|
+
branch: ""
|
|
14526
|
+
};
|
|
14527
|
+
/** Session-scoped: no later boot can rebuild a brokered port. */
|
|
14528
|
+
const LOCAL_REMOTE_OPTIONS = {
|
|
14529
|
+
sinceTimestampUtcMs: "0",
|
|
14530
|
+
persistence: "session"
|
|
14531
|
+
};
|
|
14532
|
+
function localChannelConfig(peerId, channelName) {
|
|
14533
|
+
return {
|
|
14534
|
+
type: LOCAL_CHANNEL_TYPE,
|
|
14535
|
+
parameters: {
|
|
14536
|
+
peerId,
|
|
14537
|
+
channelName
|
|
14538
|
+
}
|
|
14539
|
+
};
|
|
13000
14540
|
}
|
|
13001
|
-
/**
|
|
13002
|
-
|
|
13003
|
-
|
|
14541
|
+
/** `fromKey` splits on the last dot, so neither part may hold one. */
|
|
14542
|
+
function assertCollectionIdParts(driveId, branch) {
|
|
14543
|
+
if (driveId.includes(".")) throw new Error(`Drive id ${JSON.stringify(driveId)} contains a "." which the collection id format cannot carry; local sync needs a dot-free drive id`);
|
|
14544
|
+
if (branch.includes(".")) throw new Error(`Branch ${JSON.stringify(branch)} contains a "." which the collection id format cannot carry; local sync needs a dot-free branch`);
|
|
14545
|
+
}
|
|
14546
|
+
/** A dotted drive id parses as a dotted branch, so that is refused here. */
|
|
14547
|
+
function collectionIdFromKey(key) {
|
|
14548
|
+
const collectionId = DriveCollectionId.fromKey(key);
|
|
14549
|
+
assertCollectionIdParts(collectionId.driveId, collectionId.branch);
|
|
14550
|
+
return collectionId;
|
|
14551
|
+
}
|
|
14552
|
+
/** Registers before `add`, because `add` resolves the port through the factory. */
|
|
14553
|
+
async function registerLocalPeer(syncManager, registry, spec, port) {
|
|
14554
|
+
if (registry.has(spec.peerId, spec.channelName)) throw new Error(`This reactor already holds a local sync port for peer '${spec.peerId}' channel '${spec.channelName}'; unlink the existing link first`);
|
|
14555
|
+
if (syncManager.list().some((remote) => remote.meta.name === spec.remoteName)) throw new Error(`This reactor already has a remote named '${spec.remoteName}'; unlink the existing link first`);
|
|
14556
|
+
registry.register(spec.peerId, spec.channelName, port);
|
|
14557
|
+
try {
|
|
14558
|
+
return await syncManager.add(spec.remoteName, spec.collectionId, localChannelConfig(spec.peerId, spec.channelName), spec.filter, LOCAL_REMOTE_OPTIONS);
|
|
14559
|
+
} catch (error) {
|
|
14560
|
+
registry.unregister(spec.peerId, spec.channelName);
|
|
14561
|
+
throw error;
|
|
14562
|
+
}
|
|
14563
|
+
}
|
|
14564
|
+
/**
|
|
14565
|
+
* Waits out a running reset, then removes. The port is closed only once the
|
|
14566
|
+
* remote is gone, so a failed remove (a reset that started meanwhile) leaves
|
|
14567
|
+
* the link intact for a retry.
|
|
14568
|
+
*/
|
|
14569
|
+
async function removeLocalPeer(syncManager, registry, spec) {
|
|
14570
|
+
await syncManager.resetSettled?.(spec.remoteName);
|
|
14571
|
+
try {
|
|
14572
|
+
await syncManager.remove(spec.remoteName);
|
|
14573
|
+
} catch (error) {
|
|
14574
|
+
if (!syncManager.list().some((remote) => remote.meta.name === spec.remoteName)) registry.unregister(spec.peerId, spec.channelName);
|
|
14575
|
+
throw error;
|
|
14576
|
+
}
|
|
14577
|
+
registry.unregister(spec.peerId, spec.channelName);
|
|
13004
14578
|
}
|
|
13005
14579
|
//#endregion
|
|
13006
|
-
//#region src/
|
|
13007
|
-
|
|
13008
|
-
|
|
14580
|
+
//#region src/sync/channels/local-channel-transport.ts
|
|
14581
|
+
/**
|
|
14582
|
+
* Wraps a real MessagePort as a {@link LocalChannelPort}.
|
|
14583
|
+
*
|
|
14584
|
+
* A `node:worker_threads` port is an EventEmitter, so the EventEmitter surface
|
|
14585
|
+
* (`on`/`off`) is preferred when present. A browser port is driven through
|
|
14586
|
+
* `addEventListener` and an explicit `start()`, whose event carries the payload
|
|
14587
|
+
* on `.data`; once started it drops what arrives with no listener attached.
|
|
14588
|
+
* Register the result with a `LocalChannelPortRegistry`, which holds one
|
|
14589
|
+
* listener for the port's life and queues frames between channels.
|
|
14590
|
+
*/
|
|
14591
|
+
function messagePortTransport(port) {
|
|
14592
|
+
return {
|
|
14593
|
+
postMessage(data) {
|
|
14594
|
+
port.postMessage(data);
|
|
14595
|
+
},
|
|
14596
|
+
onMessage(callback) {
|
|
14597
|
+
if (typeof port.on === "function" && typeof port.off === "function") {
|
|
14598
|
+
const listener = (value) => callback(value);
|
|
14599
|
+
port.on("message", listener);
|
|
14600
|
+
return () => {
|
|
14601
|
+
port.off?.("message", listener);
|
|
14602
|
+
};
|
|
14603
|
+
}
|
|
14604
|
+
const listener = (event) => {
|
|
14605
|
+
callback(event.data);
|
|
14606
|
+
};
|
|
14607
|
+
port.addEventListener?.("message", listener);
|
|
14608
|
+
port.start?.();
|
|
14609
|
+
return () => {
|
|
14610
|
+
port.removeEventListener?.("message", listener);
|
|
14611
|
+
};
|
|
14612
|
+
},
|
|
14613
|
+
close() {
|
|
14614
|
+
port.close();
|
|
14615
|
+
}
|
|
14616
|
+
};
|
|
13009
14617
|
}
|
|
13010
14618
|
//#endregion
|
|
13011
14619
|
//#region src/processors/host-module.ts
|
|
@@ -13156,6 +14764,732 @@ var DocumentIntegrityService = class {
|
|
|
13156
14764
|
}
|
|
13157
14765
|
};
|
|
13158
14766
|
//#endregion
|
|
13159
|
-
|
|
14767
|
+
//#region src/inspector/drive-inspection.ts
|
|
14768
|
+
const DRIVE_DOCUMENT_TYPE = "powerhouse/document-drive";
|
|
14769
|
+
const INTEGRITY_FIND_BATCH_SIZE = 500;
|
|
14770
|
+
const DEFAULT_DRIVE_PAGE_SIZE = 100;
|
|
14771
|
+
function readDriveState(doc) {
|
|
14772
|
+
const global = doc.state?.global;
|
|
14773
|
+
const rawNodes = Array.isArray(global?.nodes) ? global.nodes : [];
|
|
14774
|
+
const nodes = [];
|
|
14775
|
+
let folderCount = 0;
|
|
14776
|
+
let otherNodeCount = 0;
|
|
14777
|
+
let unreadableNodeCount = 0;
|
|
14778
|
+
for (const raw of rawNodes) {
|
|
14779
|
+
if (typeof raw !== "object" || raw === null) {
|
|
14780
|
+
unreadableNodeCount += 1;
|
|
14781
|
+
continue;
|
|
14782
|
+
}
|
|
14783
|
+
const node = raw;
|
|
14784
|
+
if (typeof node.id !== "string" || typeof node.kind !== "string") {
|
|
14785
|
+
unreadableNodeCount += 1;
|
|
14786
|
+
continue;
|
|
14787
|
+
}
|
|
14788
|
+
nodes.push({
|
|
14789
|
+
id: node.id,
|
|
14790
|
+
kind: node.kind,
|
|
14791
|
+
documentType: typeof node.documentType === "string" ? node.documentType : void 0
|
|
14792
|
+
});
|
|
14793
|
+
if (node.kind === "folder") folderCount += 1;
|
|
14794
|
+
else if (node.kind !== "file") otherNodeCount += 1;
|
|
14795
|
+
}
|
|
14796
|
+
return {
|
|
14797
|
+
name: typeof global?.name === "string" ? global.name : doc.header.name,
|
|
14798
|
+
icon: typeof global?.icon === "string" ? global.icon : void 0,
|
|
14799
|
+
nodes,
|
|
14800
|
+
totalNodeCount: rawNodes.length,
|
|
14801
|
+
folderCount,
|
|
14802
|
+
otherNodeCount,
|
|
14803
|
+
unreadableNodeCount
|
|
14804
|
+
};
|
|
14805
|
+
}
|
|
14806
|
+
function fileNodesOf(nodes) {
|
|
14807
|
+
return nodes.filter((node) => node.kind === "file");
|
|
14808
|
+
}
|
|
14809
|
+
function toDriveInfo(doc) {
|
|
14810
|
+
const driveId = doc.header.id;
|
|
14811
|
+
const branch = doc.header.branch;
|
|
14812
|
+
const state = readDriveState(doc);
|
|
14813
|
+
return {
|
|
14814
|
+
driveId,
|
|
14815
|
+
name: state.name,
|
|
14816
|
+
branch,
|
|
14817
|
+
collectionId: DriveCollectionId.forDrive(driveId, branch).key,
|
|
14818
|
+
documentType: doc.header.documentType,
|
|
14819
|
+
nodeCount: state.totalNodeCount,
|
|
14820
|
+
fileCount: fileNodesOf(state.nodes).length,
|
|
14821
|
+
folderCount: state.folderCount,
|
|
14822
|
+
otherNodeCount: state.otherNodeCount,
|
|
14823
|
+
unreadableNodeCount: state.unreadableNodeCount,
|
|
14824
|
+
icon: state.icon
|
|
14825
|
+
};
|
|
14826
|
+
}
|
|
14827
|
+
/** Drive listing and integrity, every read made through one reader and view. */
|
|
14828
|
+
var DriveInspection = class {
|
|
14829
|
+
reader;
|
|
14830
|
+
registry;
|
|
14831
|
+
view;
|
|
14832
|
+
constructor(reader, registry, view = {}) {
|
|
14833
|
+
this.reader = reader;
|
|
14834
|
+
this.registry = registry;
|
|
14835
|
+
this.view = view;
|
|
14836
|
+
}
|
|
14837
|
+
async listDrives(cursor, limit) {
|
|
14838
|
+
const page = await this.reader.find({ type: DRIVE_DOCUMENT_TYPE }, this.view, {
|
|
14839
|
+
cursor: cursor ?? "",
|
|
14840
|
+
limit: limit ?? DEFAULT_DRIVE_PAGE_SIZE
|
|
14841
|
+
});
|
|
14842
|
+
return {
|
|
14843
|
+
results: page.results.map(toDriveInfo),
|
|
14844
|
+
nextCursor: page.nextCursor
|
|
14845
|
+
};
|
|
14846
|
+
}
|
|
14847
|
+
async checkDriveIntegrity(driveId, branch) {
|
|
14848
|
+
const fileNodes = fileNodesOf(readDriveState(await this.reader.get(driveId, {
|
|
14849
|
+
...this.view,
|
|
14850
|
+
branch
|
|
14851
|
+
})).nodes);
|
|
14852
|
+
const present = await this.presentDocumentIds(fileNodes.map((node) => node.id), branch);
|
|
14853
|
+
const supported = this.supportedDocumentTypes();
|
|
14854
|
+
const missingDocuments = [];
|
|
14855
|
+
const unsupportedTypes = [];
|
|
14856
|
+
for (const node of fileNodes) {
|
|
14857
|
+
if (!present.has(node.id)) missingDocuments.push({
|
|
14858
|
+
id: node.id,
|
|
14859
|
+
documentType: node.documentType ?? ""
|
|
14860
|
+
});
|
|
14861
|
+
if (supported !== void 0 && node.documentType !== void 0 && !supported.has(node.documentType)) unsupportedTypes.push({
|
|
14862
|
+
id: node.id,
|
|
14863
|
+
documentType: node.documentType
|
|
14864
|
+
});
|
|
14865
|
+
}
|
|
14866
|
+
return {
|
|
14867
|
+
driveId,
|
|
14868
|
+
checkedNodeCount: fileNodes.length,
|
|
14869
|
+
totalFileNodeCount: fileNodes.length,
|
|
14870
|
+
missingDocuments,
|
|
14871
|
+
unsupportedTypes
|
|
14872
|
+
};
|
|
14873
|
+
}
|
|
14874
|
+
async presentDocumentIds(ids, branch) {
|
|
14875
|
+
const present = /* @__PURE__ */ new Set();
|
|
14876
|
+
for (let start = 0; start < ids.length; start += INTEGRITY_FIND_BATCH_SIZE) {
|
|
14877
|
+
const batch = ids.slice(start, start + INTEGRITY_FIND_BATCH_SIZE);
|
|
14878
|
+
let page = await this.reader.find({ ids: batch }, {
|
|
14879
|
+
...this.view,
|
|
14880
|
+
branch
|
|
14881
|
+
}, {
|
|
14882
|
+
cursor: "",
|
|
14883
|
+
limit: batch.length
|
|
14884
|
+
});
|
|
14885
|
+
for (const doc of page.results) present.add(doc.header.id);
|
|
14886
|
+
while (page.nextCursor !== void 0 && page.next) {
|
|
14887
|
+
page = await page.next();
|
|
14888
|
+
for (const doc of page.results) present.add(doc.header.id);
|
|
14889
|
+
}
|
|
14890
|
+
}
|
|
14891
|
+
return present;
|
|
14892
|
+
}
|
|
14893
|
+
supportedDocumentTypes() {
|
|
14894
|
+
if (!this.registry) return;
|
|
14895
|
+
return new Set(this.registry.getAllModules().map((module) => module.documentModel.global.id));
|
|
14896
|
+
}
|
|
14897
|
+
};
|
|
14898
|
+
//#endregion
|
|
14899
|
+
//#region src/inspector/ops.ts
|
|
14900
|
+
/** The `inspector-op` channel, one row per member. */
|
|
14901
|
+
const INSPECTOR_OPS = {
|
|
14902
|
+
info: {
|
|
14903
|
+
rpc: "reactor.info",
|
|
14904
|
+
tier: "read",
|
|
14905
|
+
gql: "info"
|
|
14906
|
+
},
|
|
14907
|
+
listDocumentModels: {
|
|
14908
|
+
rpc: "registry.listDocumentModels",
|
|
14909
|
+
tier: "read",
|
|
14910
|
+
gql: "documentModels"
|
|
14911
|
+
},
|
|
14912
|
+
listDrives: {
|
|
14913
|
+
rpc: "drives.list",
|
|
14914
|
+
tier: "read",
|
|
14915
|
+
gql: "drives"
|
|
14916
|
+
},
|
|
14917
|
+
checkDriveIntegrity: {
|
|
14918
|
+
rpc: "drives.checkIntegrity",
|
|
14919
|
+
tier: "read",
|
|
14920
|
+
gql: "driveIntegrity"
|
|
14921
|
+
},
|
|
14922
|
+
getAttachmentInfo: {
|
|
14923
|
+
rpc: "attachments.info",
|
|
14924
|
+
tier: "read",
|
|
14925
|
+
gql: "attachmentInfo"
|
|
14926
|
+
},
|
|
14927
|
+
getQueueState: {
|
|
14928
|
+
rpc: "queue.getState",
|
|
14929
|
+
tier: "read",
|
|
14930
|
+
gql: "queueState"
|
|
14931
|
+
},
|
|
14932
|
+
getProcessors: {
|
|
14933
|
+
rpc: "processors.getAll",
|
|
14934
|
+
tier: "read",
|
|
14935
|
+
gql: "processors"
|
|
14936
|
+
},
|
|
14937
|
+
getCatchUpStatus: {
|
|
14938
|
+
rpc: "catchUp.status",
|
|
14939
|
+
tier: "read",
|
|
14940
|
+
gql: "catchUpStatus"
|
|
14941
|
+
},
|
|
14942
|
+
getStorageHealth: {
|
|
14943
|
+
rpc: "storage.health",
|
|
14944
|
+
tier: "read",
|
|
14945
|
+
gql: "storageHealth"
|
|
14946
|
+
},
|
|
14947
|
+
validateDocument: {
|
|
14948
|
+
rpc: "integrity.validate",
|
|
14949
|
+
tier: "read",
|
|
14950
|
+
gql: "validateDocument"
|
|
14951
|
+
},
|
|
14952
|
+
pauseQueue: {
|
|
14953
|
+
rpc: "queue.pause",
|
|
14954
|
+
tier: "admin"
|
|
14955
|
+
},
|
|
14956
|
+
resumeQueue: {
|
|
14957
|
+
rpc: "queue.resume",
|
|
14958
|
+
tier: "admin"
|
|
14959
|
+
},
|
|
14960
|
+
retryProcessor: {
|
|
14961
|
+
rpc: "processors.retry",
|
|
14962
|
+
tier: "admin"
|
|
14963
|
+
},
|
|
14964
|
+
sweepCatchUp: {
|
|
14965
|
+
rpc: "catchUp.sweepNow",
|
|
14966
|
+
tier: "admin"
|
|
14967
|
+
},
|
|
14968
|
+
rebuildKeyframes: {
|
|
14969
|
+
rpc: "integrity.rebuildKeyframes",
|
|
14970
|
+
tier: "admin"
|
|
14971
|
+
},
|
|
14972
|
+
rebuildSnapshots: {
|
|
14973
|
+
rpc: "integrity.rebuildSnapshots",
|
|
14974
|
+
tier: "admin"
|
|
14975
|
+
},
|
|
14976
|
+
queryDb: {
|
|
14977
|
+
rpc: "db.query",
|
|
14978
|
+
tier: "sql"
|
|
14979
|
+
}
|
|
14980
|
+
};
|
|
14981
|
+
/** The inspection and repair rows of the `sync-op` channel. */
|
|
14982
|
+
const SYNC_INSPECTION_OPS = {
|
|
14983
|
+
inspectRemote: {
|
|
14984
|
+
rpc: "inspect.remote",
|
|
14985
|
+
tier: "read",
|
|
14986
|
+
gql: "remote"
|
|
14987
|
+
},
|
|
14988
|
+
inspectRemotes: {
|
|
14989
|
+
rpc: "inspect.remotes",
|
|
14990
|
+
tier: "read",
|
|
14991
|
+
gql: "remotes"
|
|
14992
|
+
},
|
|
14993
|
+
listDeadLetters: {
|
|
14994
|
+
rpc: "deadLetters.list",
|
|
14995
|
+
tier: "read",
|
|
14996
|
+
gql: "deadLetters"
|
|
14997
|
+
},
|
|
14998
|
+
resetChannel: {
|
|
14999
|
+
rpc: "repair.resetChannel",
|
|
15000
|
+
tier: "admin"
|
|
15001
|
+
},
|
|
15002
|
+
requeueDeadLetter: {
|
|
15003
|
+
rpc: "deadLetters.requeue",
|
|
15004
|
+
tier: "admin"
|
|
15005
|
+
},
|
|
15006
|
+
clearDeadLetter: {
|
|
15007
|
+
rpc: "deadLetters.clear",
|
|
15008
|
+
tier: "admin"
|
|
15009
|
+
}
|
|
15010
|
+
};
|
|
15011
|
+
/** Reads only, unless a host grants more. */
|
|
15012
|
+
const READ_ONLY_ACCESS = Object.freeze({
|
|
15013
|
+
admin: false,
|
|
15014
|
+
sql: false
|
|
15015
|
+
});
|
|
15016
|
+
function tierAllowed(tier, access) {
|
|
15017
|
+
switch (tier) {
|
|
15018
|
+
case "read": return true;
|
|
15019
|
+
case "admin": return access.admin;
|
|
15020
|
+
case "sql": return access.sql;
|
|
15021
|
+
}
|
|
15022
|
+
}
|
|
15023
|
+
/** The table key whose rpc name is `method`, or undefined. */
|
|
15024
|
+
function opKeyOf(table, method) {
|
|
15025
|
+
for (const key of Object.keys(table)) if (table[key].rpc === method) return key;
|
|
15026
|
+
}
|
|
15027
|
+
/** The read rows' GraphQL root field names, keyed by member. */
|
|
15028
|
+
function readOpFields(table) {
|
|
15029
|
+
const fields = {};
|
|
15030
|
+
for (const key of Object.keys(table)) {
|
|
15031
|
+
const spec = table[key];
|
|
15032
|
+
if (spec.tier === "read") fields[key] = spec.gql;
|
|
15033
|
+
}
|
|
15034
|
+
return fields;
|
|
15035
|
+
}
|
|
15036
|
+
var UnknownInspectorOpError = class extends Error {
|
|
15037
|
+
method;
|
|
15038
|
+
constructor(method) {
|
|
15039
|
+
super(`Unknown inspector op: ${method}`);
|
|
15040
|
+
this.method = method;
|
|
15041
|
+
this.name = "UnknownInspectorOpError";
|
|
15042
|
+
}
|
|
15043
|
+
};
|
|
15044
|
+
var InspectorOpRefusedError = class extends Error {
|
|
15045
|
+
method;
|
|
15046
|
+
tier;
|
|
15047
|
+
constructor(method, tier) {
|
|
15048
|
+
super(`This host does not serve ${tier}-tier inspection ops (${method})`);
|
|
15049
|
+
this.method = method;
|
|
15050
|
+
this.tier = tier;
|
|
15051
|
+
this.name = "InspectorOpRefusedError";
|
|
15052
|
+
}
|
|
15053
|
+
};
|
|
15054
|
+
var InspectorOpUnavailableError = class extends Error {
|
|
15055
|
+
method;
|
|
15056
|
+
constructor(method) {
|
|
15057
|
+
super(`This host has nothing to serve ${method} with`);
|
|
15058
|
+
this.method = method;
|
|
15059
|
+
this.name = "InspectorOpUnavailableError";
|
|
15060
|
+
}
|
|
15061
|
+
};
|
|
15062
|
+
/** Refuses unknown methods and ungranted tiers before resolving a handler. */
|
|
15063
|
+
async function dispatchTableOp(table, resolve, access, method, args) {
|
|
15064
|
+
const key = opKeyOf(table, method);
|
|
15065
|
+
if (key === void 0) throw new UnknownInspectorOpError(method);
|
|
15066
|
+
const tier = table[key].tier;
|
|
15067
|
+
if (!tierAllowed(tier, access)) throw new InspectorOpRefusedError(method, tier);
|
|
15068
|
+
const handler = resolve(key);
|
|
15069
|
+
if (!handler) throw new InspectorOpUnavailableError(method);
|
|
15070
|
+
return await handler(...args);
|
|
15071
|
+
}
|
|
15072
|
+
/** Binds `key` on `target` when it is a method there. */
|
|
15073
|
+
function methodOf(target, key) {
|
|
15074
|
+
const member = target?.[key];
|
|
15075
|
+
return typeof member === "function" ? member.bind(target) : void 0;
|
|
15076
|
+
}
|
|
15077
|
+
//#endregion
|
|
15078
|
+
//#region src/inspector/reactor-inspector.ts
|
|
15079
|
+
const untrackedStorageHealth = {
|
|
15080
|
+
tracked: false,
|
|
15081
|
+
healthy: false,
|
|
15082
|
+
everRecreated: false,
|
|
15083
|
+
recreateCount: 0
|
|
15084
|
+
};
|
|
15085
|
+
const emptyQueueState = {
|
|
15086
|
+
isPaused: false,
|
|
15087
|
+
pendingJobs: [],
|
|
15088
|
+
executingJobs: [],
|
|
15089
|
+
totalPending: 0,
|
|
15090
|
+
totalExecuting: 0
|
|
15091
|
+
};
|
|
15092
|
+
const noAttachmentStore = {
|
|
15093
|
+
present: false,
|
|
15094
|
+
storeKind: "none",
|
|
15095
|
+
hasReplicator: false,
|
|
15096
|
+
replicatorRunning: false,
|
|
15097
|
+
backlogScanned: false,
|
|
15098
|
+
refsSeen: 0,
|
|
15099
|
+
held: 0,
|
|
15100
|
+
bytesHeld: 0,
|
|
15101
|
+
queued: 0,
|
|
15102
|
+
fetching: 0,
|
|
15103
|
+
pendingFetches: 0,
|
|
15104
|
+
waiting: 0,
|
|
15105
|
+
notFound: 0,
|
|
15106
|
+
failed: 0,
|
|
15107
|
+
lastError: void 0
|
|
15108
|
+
};
|
|
15109
|
+
function unsupported(what, why) {
|
|
15110
|
+
return /* @__PURE__ */ new Error(`${what} is unsupported on this host: ${why}`);
|
|
15111
|
+
}
|
|
15112
|
+
const NO_INSPECTABLE_QUEUE = "its queue is not the in-memory one, so it cannot be paused or resumed";
|
|
15113
|
+
const NO_CATCH_UP = "it was built without catch-up";
|
|
15114
|
+
const NO_INTEGRITY = "it has no document integrity service";
|
|
15115
|
+
const NO_PROCESSORS = "it was built without a processor manager";
|
|
15116
|
+
/** In-process inspector over a reactor module's live components. */
|
|
15117
|
+
var ReactorInspector = class {
|
|
15118
|
+
components;
|
|
15119
|
+
storageFacts;
|
|
15120
|
+
syncChannels;
|
|
15121
|
+
access;
|
|
15122
|
+
workflows;
|
|
15123
|
+
constructor(components) {
|
|
15124
|
+
this.components = components;
|
|
15125
|
+
const facts = components.facts ?? {};
|
|
15126
|
+
this.storageFacts = facts.storage ?? UNKNOWN_STORAGE_FACTS;
|
|
15127
|
+
this.syncChannels = Object.freeze([...facts.syncChannels ?? []]);
|
|
15128
|
+
this.access = facts.access ?? READ_ONLY_ACCESS;
|
|
15129
|
+
this.workflows = facts.workflows ?? false;
|
|
15130
|
+
}
|
|
15131
|
+
setWorkflows(composed) {
|
|
15132
|
+
this.workflows = composed;
|
|
15133
|
+
}
|
|
15134
|
+
info() {
|
|
15135
|
+
return Promise.resolve({
|
|
15136
|
+
storage: { ...this.storageFacts },
|
|
15137
|
+
workflows: this.workflows,
|
|
15138
|
+
syncChannels: [...this.syncChannels],
|
|
15139
|
+
access: { ...this.access }
|
|
15140
|
+
});
|
|
15141
|
+
}
|
|
15142
|
+
listDocumentModels() {
|
|
15143
|
+
const registry = this.components.documentModelRegistry;
|
|
15144
|
+
if (!registry) return Promise.resolve([]);
|
|
15145
|
+
const byType = /* @__PURE__ */ new Map();
|
|
15146
|
+
for (const module of registry.getAllModules()) {
|
|
15147
|
+
const documentType = module.documentModel.global.id;
|
|
15148
|
+
if (byType.has(documentType)) continue;
|
|
15149
|
+
const supportedVersions = registry.getSupportedVersions(documentType);
|
|
15150
|
+
byType.set(documentType, {
|
|
15151
|
+
documentType,
|
|
15152
|
+
name: module.documentModel.global.name,
|
|
15153
|
+
version: supportedVersions.at(-1) ?? module.version ?? 1,
|
|
15154
|
+
supportedVersions
|
|
15155
|
+
});
|
|
15156
|
+
}
|
|
15157
|
+
return Promise.resolve([...byType.values()]);
|
|
15158
|
+
}
|
|
15159
|
+
listDrives(cursor, limit) {
|
|
15160
|
+
const drives = this.components.drives;
|
|
15161
|
+
if (!drives) return Promise.resolve({
|
|
15162
|
+
results: [],
|
|
15163
|
+
nextCursor: void 0
|
|
15164
|
+
});
|
|
15165
|
+
return drives.listDrives(cursor, limit);
|
|
15166
|
+
}
|
|
15167
|
+
checkDriveIntegrity(driveId, branch) {
|
|
15168
|
+
const drives = this.components.drives;
|
|
15169
|
+
if (!drives) return Promise.resolve({
|
|
15170
|
+
driveId,
|
|
15171
|
+
checkedNodeCount: 0,
|
|
15172
|
+
totalFileNodeCount: 0,
|
|
15173
|
+
missingDocuments: [],
|
|
15174
|
+
unsupportedTypes: []
|
|
15175
|
+
});
|
|
15176
|
+
return drives.checkDriveIntegrity(driveId, branch);
|
|
15177
|
+
}
|
|
15178
|
+
getAttachmentInfo() {
|
|
15179
|
+
const store = this.components.attachmentStore;
|
|
15180
|
+
if (!store) return Promise.resolve({ ...noAttachmentStore });
|
|
15181
|
+
return store.getAttachmentInfo();
|
|
15182
|
+
}
|
|
15183
|
+
getQueueState() {
|
|
15184
|
+
const queue = this.components.queue;
|
|
15185
|
+
if (!queue) return Promise.resolve({ ...emptyQueueState });
|
|
15186
|
+
const pendingJobs = queue.getPendingJobs();
|
|
15187
|
+
const executingJobs = [];
|
|
15188
|
+
for (const jobIds of queue.getExecutingJobIds().values()) for (const jobId of jobIds) {
|
|
15189
|
+
const job = queue.getJob(jobId);
|
|
15190
|
+
if (job) executingJobs.push(job);
|
|
15191
|
+
}
|
|
15192
|
+
return Promise.resolve({
|
|
15193
|
+
isPaused: queue.paused,
|
|
15194
|
+
pendingJobs,
|
|
15195
|
+
executingJobs,
|
|
15196
|
+
totalPending: pendingJobs.length,
|
|
15197
|
+
totalExecuting: executingJobs.length
|
|
15198
|
+
});
|
|
15199
|
+
}
|
|
15200
|
+
getProcessors() {
|
|
15201
|
+
const tracked = this.components.processorManager?.getAll() ?? [];
|
|
15202
|
+
return Promise.resolve(tracked.map((processor) => ({
|
|
15203
|
+
processorId: processor.processorId,
|
|
15204
|
+
factoryId: processor.factoryId,
|
|
15205
|
+
driveId: processor.driveId,
|
|
15206
|
+
processorIndex: processor.processorIndex,
|
|
15207
|
+
lastOrdinal: processor.lastOrdinal,
|
|
15208
|
+
status: processor.status,
|
|
15209
|
+
lastError: processor.lastError,
|
|
15210
|
+
lastErrorTimestamp: processor.lastErrorTimestamp
|
|
15211
|
+
})));
|
|
15212
|
+
}
|
|
15213
|
+
getCatchUpStatus() {
|
|
15214
|
+
const catchUp = this.components.catchUp;
|
|
15215
|
+
if (!catchUp) return Promise.reject(unsupported("Reading catch-up", NO_CATCH_UP));
|
|
15216
|
+
return Promise.resolve(catchUp.status());
|
|
15217
|
+
}
|
|
15218
|
+
getStorageHealth() {
|
|
15219
|
+
const provider = this.components.storageHealth;
|
|
15220
|
+
if (!provider) return Promise.resolve({ ...untrackedStorageHealth });
|
|
15221
|
+
return Promise.resolve(provider.getStorageHealth());
|
|
15222
|
+
}
|
|
15223
|
+
validateDocument(documentId, branch) {
|
|
15224
|
+
const integrity = this.components.integrity;
|
|
15225
|
+
if (!integrity) return Promise.reject(unsupported("Validating a document", NO_INTEGRITY));
|
|
15226
|
+
return integrity.validateDocument(documentId, branch);
|
|
15227
|
+
}
|
|
15228
|
+
pauseQueue() {
|
|
15229
|
+
const queue = this.components.queue;
|
|
15230
|
+
if (!queue) return Promise.reject(unsupported("Pausing the queue", NO_INSPECTABLE_QUEUE));
|
|
15231
|
+
queue.pause();
|
|
15232
|
+
return Promise.resolve();
|
|
15233
|
+
}
|
|
15234
|
+
resumeQueue() {
|
|
15235
|
+
const queue = this.components.queue;
|
|
15236
|
+
if (!queue) return Promise.reject(unsupported("Resuming the queue", NO_INSPECTABLE_QUEUE));
|
|
15237
|
+
return queue.resume();
|
|
15238
|
+
}
|
|
15239
|
+
retryProcessor(processorId) {
|
|
15240
|
+
const manager = this.components.processorManager;
|
|
15241
|
+
if (!manager) return Promise.reject(unsupported("Retrying a processor", NO_PROCESSORS));
|
|
15242
|
+
const processor = manager.get(processorId);
|
|
15243
|
+
if (!processor) return Promise.reject(/* @__PURE__ */ new Error(`Cannot retry processor ${JSON.stringify(processorId)}: this reactor is not tracking it`));
|
|
15244
|
+
return processor.retry();
|
|
15245
|
+
}
|
|
15246
|
+
sweepCatchUp() {
|
|
15247
|
+
const catchUp = this.components.catchUp;
|
|
15248
|
+
if (!catchUp) return Promise.reject(unsupported("Sweeping catch-up", NO_CATCH_UP));
|
|
15249
|
+
return catchUp.sweepNow();
|
|
15250
|
+
}
|
|
15251
|
+
rebuildKeyframes(documentId, branch) {
|
|
15252
|
+
const integrity = this.components.integrity;
|
|
15253
|
+
if (!integrity) return Promise.reject(unsupported("Rebuilding keyframes", NO_INTEGRITY));
|
|
15254
|
+
return integrity.rebuildKeyframes(documentId, branch);
|
|
15255
|
+
}
|
|
15256
|
+
rebuildSnapshots(documentId, branch) {
|
|
15257
|
+
const integrity = this.components.integrity;
|
|
15258
|
+
if (!integrity) return Promise.reject(unsupported("Rebuilding snapshots", NO_INTEGRITY));
|
|
15259
|
+
return integrity.rebuildSnapshots(documentId, branch);
|
|
15260
|
+
}
|
|
15261
|
+
};
|
|
15262
|
+
//#endregion
|
|
15263
|
+
//#region src/inspector/from-module.ts
|
|
15264
|
+
/** A group-commit store is never recreated in place; a poison needs a restart. */
|
|
15265
|
+
function groupCommitHealth(storage) {
|
|
15266
|
+
if (!storage) return;
|
|
15267
|
+
return { getStorageHealth: () => ({
|
|
15268
|
+
tracked: true,
|
|
15269
|
+
healthy: storage.health.getStorageHealth().healthy,
|
|
15270
|
+
everRecreated: false,
|
|
15271
|
+
recreateCount: 0
|
|
15272
|
+
}) };
|
|
15273
|
+
}
|
|
15274
|
+
/** The in-process inspector over a built module; drive reads run as the host. */
|
|
15275
|
+
function createReactorInspector(module, options = {}) {
|
|
15276
|
+
return new ReactorInspector({
|
|
15277
|
+
queue: module.queue instanceof InMemoryQueue ? module.queue : void 0,
|
|
15278
|
+
processorManager: module.processorManager,
|
|
15279
|
+
catchUp: module.catchUp,
|
|
15280
|
+
integrity: new DocumentIntegrityService(module.keyframeStore, module.operationStore, module.writeCache, module.documentView, module.documentModelRegistry),
|
|
15281
|
+
storageHealth: options.storageHealth ?? groupCommitHealth(module.groupCommitStorage),
|
|
15282
|
+
documentModelRegistry: module.documentModelRegistry,
|
|
15283
|
+
drives: new DriveInspection(module.reactor, module.documentModelRegistry),
|
|
15284
|
+
attachmentStore: options.attachmentStore,
|
|
15285
|
+
facts: {
|
|
15286
|
+
storage: module.storageFacts,
|
|
15287
|
+
syncChannels: module.syncModule ? channelFactoryTypes(module.syncModule.channelFactory) : [],
|
|
15288
|
+
access: options.access,
|
|
15289
|
+
workflows: options.workflows
|
|
15290
|
+
}
|
|
15291
|
+
});
|
|
15292
|
+
}
|
|
15293
|
+
//#endregion
|
|
15294
|
+
//#region src/inspector/wire.ts
|
|
15295
|
+
function toWireQueueJob(job, status) {
|
|
15296
|
+
return {
|
|
15297
|
+
id: job.id,
|
|
15298
|
+
kind: job.kind,
|
|
15299
|
+
documentId: job.documentId,
|
|
15300
|
+
scope: job.scope,
|
|
15301
|
+
branch: job.branch,
|
|
15302
|
+
status,
|
|
15303
|
+
actionCount: job.actions.length,
|
|
15304
|
+
operationCount: job.operations.length,
|
|
15305
|
+
retryCount: job.retryCount ?? 0
|
|
15306
|
+
};
|
|
15307
|
+
}
|
|
15308
|
+
/** The queue as the inspection surface serves it: no document content. */
|
|
15309
|
+
function toWireQueueState(snapshot) {
|
|
15310
|
+
return {
|
|
15311
|
+
isPaused: snapshot.isPaused,
|
|
15312
|
+
totalPending: snapshot.totalPending,
|
|
15313
|
+
totalExecuting: snapshot.totalExecuting,
|
|
15314
|
+
pendingJobs: snapshot.pendingJobs.map((job) => toWireQueueJob(job, "pending")),
|
|
15315
|
+
executingJobs: snapshot.executingJobs.map((job) => toWireQueueJob(job, "executing"))
|
|
15316
|
+
};
|
|
15317
|
+
}
|
|
15318
|
+
/** A dead letter as the inspection surface serves it: no document content. */
|
|
15319
|
+
function toWireDeadLetter(record) {
|
|
15320
|
+
return {
|
|
15321
|
+
id: record.id,
|
|
15322
|
+
jobId: record.jobId,
|
|
15323
|
+
documentId: record.documentId,
|
|
15324
|
+
branch: record.branch,
|
|
15325
|
+
scopes: [...record.scopes],
|
|
15326
|
+
errorType: record.errorType,
|
|
15327
|
+
errorMessage: record.errorMessage,
|
|
15328
|
+
operationCount: record.operations.length
|
|
15329
|
+
};
|
|
15330
|
+
}
|
|
15331
|
+
/** Compile error unless `names` lists every key of `T` and nothing else. */
|
|
15332
|
+
function fieldsOf() {
|
|
15333
|
+
return (names) => names;
|
|
15334
|
+
}
|
|
15335
|
+
/** Every wire record's fields, by the GraphQL type that serves it. */
|
|
15336
|
+
const INSPECTION_WIRE_FIELDS = {
|
|
15337
|
+
ReactorInfo: fieldsOf()([
|
|
15338
|
+
"storage",
|
|
15339
|
+
"workflows",
|
|
15340
|
+
"syncChannels",
|
|
15341
|
+
"access"
|
|
15342
|
+
]),
|
|
15343
|
+
ReactorStorageFacts: fieldsOf()([
|
|
15344
|
+
"engine",
|
|
15345
|
+
"persistence",
|
|
15346
|
+
"durable",
|
|
15347
|
+
"selfHeal"
|
|
15348
|
+
]),
|
|
15349
|
+
InspectorAccess: fieldsOf()(["admin", "sql"]),
|
|
15350
|
+
InspectionDocumentModel: fieldsOf()([
|
|
15351
|
+
"documentType",
|
|
15352
|
+
"name",
|
|
15353
|
+
"version",
|
|
15354
|
+
"supportedVersions"
|
|
15355
|
+
]),
|
|
15356
|
+
InspectionDrive: fieldsOf()([
|
|
15357
|
+
"driveId",
|
|
15358
|
+
"name",
|
|
15359
|
+
"branch",
|
|
15360
|
+
"collectionId",
|
|
15361
|
+
"documentType",
|
|
15362
|
+
"nodeCount",
|
|
15363
|
+
"fileCount",
|
|
15364
|
+
"folderCount",
|
|
15365
|
+
"otherNodeCount",
|
|
15366
|
+
"unreadableNodeCount",
|
|
15367
|
+
"icon"
|
|
15368
|
+
]),
|
|
15369
|
+
InspectionDrivePage: fieldsOf()(["results", "nextCursor"]),
|
|
15370
|
+
InspectionDriveIntegrityRef: fieldsOf()(["id", "documentType"]),
|
|
15371
|
+
InspectionDriveIntegrity: fieldsOf()([
|
|
15372
|
+
"driveId",
|
|
15373
|
+
"checkedNodeCount",
|
|
15374
|
+
"totalFileNodeCount",
|
|
15375
|
+
"missingDocuments",
|
|
15376
|
+
"unsupportedTypes"
|
|
15377
|
+
]),
|
|
15378
|
+
InspectionAttachmentInfo: fieldsOf()([
|
|
15379
|
+
"present",
|
|
15380
|
+
"storeKind",
|
|
15381
|
+
"hasReplicator",
|
|
15382
|
+
"replicatorRunning",
|
|
15383
|
+
"backlogScanned",
|
|
15384
|
+
"refsSeen",
|
|
15385
|
+
"held",
|
|
15386
|
+
"bytesHeld",
|
|
15387
|
+
"queued",
|
|
15388
|
+
"fetching",
|
|
15389
|
+
"pendingFetches",
|
|
15390
|
+
"waiting",
|
|
15391
|
+
"notFound",
|
|
15392
|
+
"failed",
|
|
15393
|
+
"lastError"
|
|
15394
|
+
]),
|
|
15395
|
+
InspectionQueueJob: fieldsOf()([
|
|
15396
|
+
"id",
|
|
15397
|
+
"kind",
|
|
15398
|
+
"documentId",
|
|
15399
|
+
"scope",
|
|
15400
|
+
"branch",
|
|
15401
|
+
"status",
|
|
15402
|
+
"actionCount",
|
|
15403
|
+
"operationCount",
|
|
15404
|
+
"retryCount"
|
|
15405
|
+
]),
|
|
15406
|
+
InspectionQueueState: fieldsOf()([
|
|
15407
|
+
"isPaused",
|
|
15408
|
+
"totalPending",
|
|
15409
|
+
"totalExecuting",
|
|
15410
|
+
"pendingJobs",
|
|
15411
|
+
"executingJobs"
|
|
15412
|
+
]),
|
|
15413
|
+
InspectionProcessor: fieldsOf()([
|
|
15414
|
+
"processorId",
|
|
15415
|
+
"factoryId",
|
|
15416
|
+
"driveId",
|
|
15417
|
+
"processorIndex",
|
|
15418
|
+
"lastOrdinal",
|
|
15419
|
+
"status",
|
|
15420
|
+
"lastError",
|
|
15421
|
+
"lastErrorTimestampUtcMs"
|
|
15422
|
+
]),
|
|
15423
|
+
InspectionStorageHealth: fieldsOf()([
|
|
15424
|
+
"tracked",
|
|
15425
|
+
"healthy",
|
|
15426
|
+
"everRecreated",
|
|
15427
|
+
"recreateCount"
|
|
15428
|
+
]),
|
|
15429
|
+
InspectionValidation: fieldsOf()([
|
|
15430
|
+
"documentId",
|
|
15431
|
+
"isConsistent",
|
|
15432
|
+
"keyframeIssues",
|
|
15433
|
+
"snapshotIssues",
|
|
15434
|
+
"streamOrderIssues"
|
|
15435
|
+
]),
|
|
15436
|
+
InspectionCursor: fieldsOf()([
|
|
15437
|
+
"cursorType",
|
|
15438
|
+
"cursorOrdinal",
|
|
15439
|
+
"lastSyncedAtUtcMs",
|
|
15440
|
+
"liveAckOrdinal",
|
|
15441
|
+
"liveLatestOrdinal"
|
|
15442
|
+
]),
|
|
15443
|
+
InspectionMailboxDepths: fieldsOf()([
|
|
15444
|
+
"inbox",
|
|
15445
|
+
"outbox",
|
|
15446
|
+
"deadLetter"
|
|
15447
|
+
]),
|
|
15448
|
+
InspectionConnectionHealth: fieldsOf()([
|
|
15449
|
+
"snapshot",
|
|
15450
|
+
"neverSucceeded",
|
|
15451
|
+
"stalenessMs"
|
|
15452
|
+
]),
|
|
15453
|
+
InspectionRemote: fieldsOf()([
|
|
15454
|
+
"remoteName",
|
|
15455
|
+
"remoteId",
|
|
15456
|
+
"inboxCursor",
|
|
15457
|
+
"outboxCursor",
|
|
15458
|
+
"mailboxDepths",
|
|
15459
|
+
"connection",
|
|
15460
|
+
"meta"
|
|
15461
|
+
]),
|
|
15462
|
+
InspectionDeadLetter: fieldsOf()([
|
|
15463
|
+
"id",
|
|
15464
|
+
"jobId",
|
|
15465
|
+
"documentId",
|
|
15466
|
+
"branch",
|
|
15467
|
+
"scopes",
|
|
15468
|
+
"errorType",
|
|
15469
|
+
"errorMessage",
|
|
15470
|
+
"operationCount"
|
|
15471
|
+
]),
|
|
15472
|
+
InspectionDeadLetterPage: fieldsOf()([
|
|
15473
|
+
"remoteName",
|
|
15474
|
+
"results",
|
|
15475
|
+
"nextCursor"
|
|
15476
|
+
])
|
|
15477
|
+
};
|
|
15478
|
+
function rootFieldsOf(...tables) {
|
|
15479
|
+
return tables.flatMap((table) => Object.values(readOpFields(table)).filter((field) => field !== void 0));
|
|
15480
|
+
}
|
|
15481
|
+
/** The root fields the inspection subgraph serves: the read rows only. */
|
|
15482
|
+
const INSPECTION_ROOT_FIELDS = Object.freeze({ ReactorInspection: Object.freeze(rootFieldsOf(INSPECTOR_OPS, SYNC_INSPECTION_OPS)) });
|
|
15483
|
+
/** Ordinals pass 2^31, so these are `Float` rather than `Int`. */
|
|
15484
|
+
const INSPECTION_ORDINAL_FIELDS = {
|
|
15485
|
+
InspectionProcessor: ["lastOrdinal"],
|
|
15486
|
+
InspectionCursor: [
|
|
15487
|
+
"cursorOrdinal",
|
|
15488
|
+
"liveAckOrdinal",
|
|
15489
|
+
"liveLatestOrdinal"
|
|
15490
|
+
]
|
|
15491
|
+
};
|
|
15492
|
+
//#endregion
|
|
15493
|
+
export { ALWAYS_READABLE_SCOPES, APPEND_CONDITION_FAILED_PREFIX, AppendConditionFailedError, AuthEnforcementDisabledError, BareReadGate, BaseReadModel, BatchJobFailedError, CatchUpScheduler, ChannelError, ChannelErrorSource, ChannelScheme, CompositeChannelFactory, ConsistencyTracker, ContiguousCursor, DEFAULT_COMMIT_CHUNK_SIZE, DEFAULT_DRIVE_CONTAINER_TYPES, DEFAULT_LOCAL_FILTER, DEFAULT_LONG_STATEMENT_TIMEOUT_MS, DEFAULT_MAX_PURGE_OPERATIONS, DEFAULT_PURGE_DELETE_BATCH, DEFAULT_READ_MODEL_YIELD_DEADLINE_MS, DEFAULT_RECOVERY_TIMEOUT_MS, DEFAULT_STATEMENT_TIMEOUT_MS, DOCUMENT_INDEXER_READ_MODEL, DOCUMENT_VIEW_READ_MODEL, DRIVE_AUTH_ERROR_MESSAGES, DefaultSubscriptionErrorHandler, DeletedDocumentRead, DocumentAlreadyExistsError, DocumentChangeType, DocumentExistence, DocumentIntegrityService, DocumentModelRegistry, DocumentModelResolver, DocumentNotDeletedError, DocumentPurgeService, DocumentPurgedError, DriveClient, DriveCollectionId, DriveInspection, DriveRequestError, DuplicateManifestError, DuplicateModuleError, DuplicateOperationError, EventBus, EventBusAggregateError, GQL_CHANNEL_TYPE, GqlRequestChannel, GqlRequestChannelFactory, GqlResponseChannel, GqlResponseChannelFactory, GroupInUseError, HardenedPGliteDialect, HybridProjectionCoordinator, INSPECTION_ORDINAL_FIELDS, INSPECTION_ROOT_FIELDS, INSPECTION_WIRE_FIELDS, INSPECTOR_OPS, IN_MEMORY_PGLITE_STORAGE_FACTS, SimpleJobExecutor as InMemoryJobExecutor, SimpleJobExecutor, InMemoryJobTracker, InMemoryQueue, InMemorySyncHoldStorage, InMemorySyncPurgeRefusalStorage, InMemorySyncReceivedMarkerStorage, InspectorOpRefusedError, InspectorOpUnavailableError, IntervalPollTimer, InvalidDeadLetterCursorError, InvalidModuleError, InvalidSignatureError, JOB_NOT_FOUND_ERROR_NAME, JobAwaiter, JobExecutorEventTypes, JobStatus, KyselyDocumentIndexer, KyselyDocumentPurger, KyselyDocumentView, KyselyKeyframeStore, KyselyOperationStore, KyselySyncCursorStorage, KyselySyncHoldStorage, KyselySyncPurgeRefusalStorage, KyselySyncReceivedMarkerStorage, KyselySyncRemoteStorage, KyselyWriteCache, LOCAL_CHANNEL_TYPE, LOCAL_REMOTE_OPTIONS, LocalChannel, LocalChannelFactory, LocalChannelPortRegistry, MAX_POLLED_REFUSALS, Mailbox, ModelNotWorkerImportableError, ModelReadGate, ModuleNotFoundError, NullDocumentModelResolver, OptimisticLockError, PGLITE_IDB_STORAGE_FACTS, PGLITE_PATH_STORAGE_FACTS, PGliteAbortedTransactionError, PGliteAcquireTimeoutError, PGliteFlushSyncTimeoutError, PGliteSessionError, PGliteSessionPoisonedError, PGliteStatementTimeoutError, POLLING_CHANNEL_TYPE, POSTGRES_STORAGE_FACTS, PURGE_DOCUMENT, PURGE_LOCK_BUCKETS, PURGE_NS, PollBehavior, PollingChannelError, ProbeSettler, ProcessorManager, PropagationMode, PurgeTooLargeError, QueueEventTypes, REACTOR_SCHEMA, READ_ONLY_ACCESS, RECOVERABLE_GRAPHQL_ERROR_CODES, Reactor, ReactorBuilder, ReactorClient, ReactorClientBuilder, ReactorEventTypes, ReactorInspector, ReactorSubscriptionManager, ReadModelCoordinator, RelationalDbProcessor, RelationshipChangeType, RelationshipNotFoundError, RemotePersistence, ReservedActionError, RetryAccounting, RevisionMismatchError, SIGNATURE_REFUSAL_CODES, SYNC_INSPECTION_OPS, SeededStateReader, SettledWatermark, SimpleJobExecutorManager, StoragePoisonedError, SyncBuilder, SyncEventTypes, SyncOperation, SyncOperationAggregateError, SyncOperationStatus, SyncRepairRefusedError, SyncScopeGate, SyncStatus, SyncStatusTracker, UNKNOWN_STORAGE_FACTS, UnknownInspectorOpError, UnsupportedStoredProtocolError, acquirePurgeLocks, addRelationshipAction, appliedDelete, assertCollectionIdParts, authDecisionModel, batchOperationsByDocument, bucketFor, buildCreateJobs, buildDecisionModel, channelFactoryTypes, classifyJobFailure, collectionIdFromKey, consolidateSyncOperations, createDocumentAction, createEmptyDocument, createForwardingLogger, createHybridProjectionCoordinatorFactory, createKyselyWatermarkProbe, createMutableShutdownStatus, createPeerAgreement, createReactorHostModuleBase, createReactorInspector, createRelationalDb, decideAtHead, defaultCatchUpConfig, defaultReadModelIndexingConfig, deleteDocumentAction, deriveConnectionHealth, dispatchTableOp, documentActions, documentDecisionModel, driveIdFromUrl, envelopesToSyncOperations, errorToInfo, findPurged, firstOutOfOrderPair, getMigrationStatus, hashDocumentId, instrumentPgPool, isCatchUpConsumer, isDriveAuthError, isLocalWireMessage, isLongRunningStatement, isPurgeMarker, isRecoverableGraphQLError, listPurged, loadDocumentModelSpec, localChannelConfig, makeConsistencyKey, messagePortTransport, methodOf, opKeyOf, parseDriveUrl, parsePagingOptions, parseSnapshot, quarantinesDocument, queryThroughDialect, readDecisionModel, readOpFields, registerLocalPeer, removeLocalPeer, removeRelationshipAction, runMigrations, sanitizeArg, selectDecisionModel, selectDocumentModelModule, snapshotFunctionsFor, supportsDeliveryTracking, supportsLiveReadModelRegistration, supportsPurgeRefusals, syncOperationErrorType, tierAllowed, toWireDeadLetter, toWireQueueState, trimMailboxFromAckOrdinal, unchunkedReadModelIndexingConfig, updateRelationshipAction, upgradeDocumentAction, upgradeDocumentWith, verifyActionSignature, workerEntryPath };
|
|
13160
15494
|
|
|
13161
15495
|
//# sourceMappingURL=index.js.map
|