@tanstack/db-sqlite-persistence-core 0.4.7 → 0.4.8
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/README.md +22 -2
- package/dist/cjs/broadcast-coordinator.cjs +169 -18
- package/dist/cjs/broadcast-coordinator.cjs.map +1 -1
- package/dist/cjs/broadcast-coordinator.d.cts +11 -2
- package/dist/cjs/committed-tx-anchor.cjs +9 -0
- package/dist/cjs/committed-tx-anchor.cjs.map +1 -0
- package/dist/cjs/committed-tx-anchor.d.cts +1 -0
- package/dist/cjs/persisted.cjs +199 -43
- package/dist/cjs/persisted.cjs.map +1 -1
- package/dist/cjs/persisted.d.cts +46 -0
- package/dist/cjs/sqlite-core-adapter.cjs +94 -3
- package/dist/cjs/sqlite-core-adapter.cjs.map +1 -1
- package/dist/cjs/sqlite-core-adapter.d.cts +10 -1
- package/dist/esm/broadcast-coordinator.d.ts +11 -2
- package/dist/esm/broadcast-coordinator.js +170 -19
- package/dist/esm/broadcast-coordinator.js.map +1 -1
- package/dist/esm/committed-tx-anchor.d.ts +1 -0
- package/dist/esm/committed-tx-anchor.js +9 -0
- package/dist/esm/committed-tx-anchor.js.map +1 -0
- package/dist/esm/persisted.d.ts +46 -0
- package/dist/esm/persisted.js +201 -45
- package/dist/esm/persisted.js.map +1 -1
- package/dist/esm/sqlite-core-adapter.d.ts +10 -1
- package/dist/esm/sqlite-core-adapter.js +94 -3
- package/dist/esm/sqlite-core-adapter.js.map +1 -1
- package/package.json +2 -2
- package/src/broadcast-coordinator.ts +267 -44
- package/src/committed-tx-anchor.ts +13 -0
- package/src/persisted.ts +315 -53
- package/src/sqlite-core-adapter.ts +143 -3
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { LoadSubsetOptions } from '@tanstack/db';
|
|
2
|
-
import { HydrationPersistenceAdapter, PersistedIndexSpec, PersistedKeySetEvidence, PersistedRowScanOptions, PersistedScannedRow, PersistedTx, PersistenceAdapter, ReplayableTxDelta, SQLiteDriver } from './persisted.cjs';
|
|
2
|
+
import { CommittedTxAnchor, HydrationPersistenceAdapter, PersistedIndexSpec, PersistedKeySetEvidence, PersistedRowScanOptions, PersistedScannedRow, PersistedTx, PersistenceAdapter, ReconciledCommittedTx, ReplayableTxDelta, SQLiteDriver } from './persisted.cjs';
|
|
3
3
|
type SQLiteCoreAdapterSchemaMismatchPolicy = `sync-present-reset` | `sync-absent-error` | `reset`;
|
|
4
4
|
export type SQLiteCoreAdapterOptions = {
|
|
5
5
|
driver: SQLiteDriver;
|
|
@@ -83,7 +83,10 @@ export declare class SQLiteCorePersistenceAdapter implements PersistenceAdapter
|
|
|
83
83
|
}>;
|
|
84
84
|
private loadResumeSnapshotUnscheduled;
|
|
85
85
|
applyCommittedTx(collectionId: string, tx: PersistedTx): Promise<void>;
|
|
86
|
+
reconcileCommittedTx(collectionId: string, tx: PersistedTx, anchor: CommittedTxAnchor): Promise<ReconciledCommittedTx>;
|
|
86
87
|
private applyCommittedTxUnscheduled;
|
|
88
|
+
private reconcileCommittedTxUnscheduled;
|
|
89
|
+
private applyCommittedTxInternal;
|
|
87
90
|
loadCollectionMetadata(collectionId: string): Promise<Array<{
|
|
88
91
|
key: string;
|
|
89
92
|
value: unknown;
|
|
@@ -95,6 +98,12 @@ export declare class SQLiteCorePersistenceAdapter implements PersistenceAdapter
|
|
|
95
98
|
private ensureIndexUnscheduled;
|
|
96
99
|
markIndexRemoved(collectionId: string, signature: string): Promise<void>;
|
|
97
100
|
private markIndexRemovedUnscheduled;
|
|
101
|
+
reserveLeadershipTerm(collectionId: string, observedTerm: number): Promise<{
|
|
102
|
+
latestTerm: number;
|
|
103
|
+
latestSeq: number;
|
|
104
|
+
latestRowVersion: number;
|
|
105
|
+
}>;
|
|
106
|
+
private reserveLeadershipTermUnscheduled;
|
|
98
107
|
getStreamPosition(collectionId: string): Promise<{
|
|
99
108
|
latestTerm: number;
|
|
100
109
|
latestSeq: number;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { LoadSubsetOptions } from '@tanstack/db';
|
|
2
2
|
import { RemoteSubsetOwner } from './remote-subset-owner.js';
|
|
3
|
-
import { ApplyCommittedTxResponse, ApplyLocalMutationsResponse, HydrationPersistenceAdapter, PersistedCollectionCoordinator, PersistedIndexSpec, PersistedMutationEnvelope, PersistedTx, PersistenceAdapter, ProtocolEnvelope, PullSinceResponse } from './persisted.js';
|
|
4
|
-
type CoordinatorAdapter = Pick<PersistenceAdapter, `loadSubset` | `applyCommittedTx` | `ensureIndex` | `runInRegularScope`> & {
|
|
3
|
+
import { ApplyCommittedTxResponse, ApplyLocalMutationsResponse, CommittedTxAnchor, HydrationPersistenceAdapter, PersistedCollectionCoordinator, PersistedIndexSpec, PersistedMutationEnvelope, PersistedTx, PersistenceAdapter, ProtocolEnvelope, PullSinceResponse, ReconcileCommittedTxResponse } from './persisted.js';
|
|
4
|
+
type CoordinatorAdapter = Pick<PersistenceAdapter, `loadSubset` | `applyCommittedTx` | `reconcileCommittedTx` | `ensureIndex` | `runInRegularScope`> & {
|
|
5
5
|
pullSince?: (collectionId: string, fromRowVersion: number) => Promise<{
|
|
6
6
|
latestRowVersion: number;
|
|
7
7
|
requiresFullReload: true;
|
|
@@ -16,6 +16,11 @@ type CoordinatorAdapter = Pick<PersistenceAdapter, `loadSubset` | `applyCommitte
|
|
|
16
16
|
latestSeq: number;
|
|
17
17
|
latestRowVersion: number;
|
|
18
18
|
}>;
|
|
19
|
+
reserveLeadershipTerm?: (collectionId: string, observedTerm: number) => Promise<{
|
|
20
|
+
latestTerm: number;
|
|
21
|
+
latestSeq: number;
|
|
22
|
+
latestRowVersion: number;
|
|
23
|
+
}>;
|
|
19
24
|
};
|
|
20
25
|
/** @internal Shared by the Browser and Electron persistence hosts. */
|
|
21
26
|
export type BroadcastCollectionCoordinatorOptions = {
|
|
@@ -69,6 +74,7 @@ export declare class BroadcastCollectionCoordinator implements PersistedCollecti
|
|
|
69
74
|
requestEnsurePersistedIndex(collectionId: string, signature: string, spec: PersistedIndexSpec, scopedAdapter?: HydrationPersistenceAdapter, localEnsureCompleted?: boolean): Promise<void>;
|
|
70
75
|
requestApplyLocalMutations(collectionId: string, mutations: Array<PersistedMutationEnvelope>): Promise<ApplyLocalMutationsResponse>;
|
|
71
76
|
requestApplyCommittedTx(collectionId: string, tx: PersistedTx, scopedAdapter?: HydrationPersistenceAdapter): Promise<ApplyCommittedTxResponse>;
|
|
77
|
+
reconcileCommittedTx(collectionId: string, tx: PersistedTx, anchor: CommittedTxAnchor, scopedAdapter?: HydrationPersistenceAdapter): Promise<ReconcileCommittedTxResponse>;
|
|
72
78
|
pullSince(collectionId: string, fromRowVersion: number, scopedAdapter?: HydrationPersistenceAdapter): Promise<PullSinceResponse>;
|
|
73
79
|
dispose(): void;
|
|
74
80
|
private getOrCreateCollectionState;
|
|
@@ -105,6 +111,9 @@ export declare class BroadcastCollectionCoordinator implements PersistedCollecti
|
|
|
105
111
|
private handleApplyCommittedTx;
|
|
106
112
|
private runDeduplicatedEnvelope;
|
|
107
113
|
private applyCommittedTxOnce;
|
|
114
|
+
private publishCommittedTx;
|
|
115
|
+
private handleReconcileCommittedTx;
|
|
116
|
+
private nextPersistedTxIfLeader;
|
|
108
117
|
private applyDurablyAtNextStreamPosition;
|
|
109
118
|
private handlePullSince;
|
|
110
119
|
private withScheduledWriterLock;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { safeRandomUUID } from "@tanstack/db";
|
|
2
|
-
import {
|
|
2
|
+
import { isValidCommittedTxAnchor } from "./committed-tx-anchor.js";
|
|
3
|
+
import { DuplicateRemoteSubsetOwnerError, InvalidPersistenceAdapterError, RetryableRemoteSubsetAcquisitionError, IndeterminateCommitError, toPersistedCollectionDurabilityError, PersistedCollectionDurabilityError } from "./errors.js";
|
|
3
4
|
import { toTransportedLoadSubsetOptions, toProcessLocalLoadSubsetOptions } from "./remote-subset-wire.js";
|
|
4
5
|
import { unloadRemoteSubsetOwner } from "./remote-subset-owner.js";
|
|
5
6
|
const HEARTBEAT_INTERVAL_MS = 3e3;
|
|
@@ -11,6 +12,11 @@ const REMOTE_SUBSET_REPLAY_RETRY_ATTEMPTS = 2;
|
|
|
11
12
|
const RPC_DEDUPE_RETENTION_MS = 6e4;
|
|
12
13
|
const WRITER_LOCK_BUSY_RETRY_MS = 50;
|
|
13
14
|
const WRITER_LOCK_MAX_RETRIES = 20;
|
|
15
|
+
function requireDurableElection(adapter) {
|
|
16
|
+
if (typeof adapter.reserveLeadershipTerm !== `function`) {
|
|
17
|
+
throw new InvalidPersistenceAdapterError(`reserveLeadershipTerm`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
14
20
|
class BroadcastCollectionCoordinator {
|
|
15
21
|
constructor(options) {
|
|
16
22
|
this.nodeId = safeRandomUUID();
|
|
@@ -27,6 +33,7 @@ class BroadcastCollectionCoordinator {
|
|
|
27
33
|
this.appliedEnvelopePruneTimer = null;
|
|
28
34
|
this.rejectDisposed = null;
|
|
29
35
|
this.disposed = false;
|
|
36
|
+
if (options.adapter) requireDurableElection(options.adapter);
|
|
30
37
|
this.dbName = options.dbName;
|
|
31
38
|
this.coordinatorName = options.coordinatorName;
|
|
32
39
|
this.defaultAdapter = options.adapter ?? null;
|
|
@@ -65,10 +72,12 @@ class BroadcastCollectionCoordinator {
|
|
|
65
72
|
* Called by a host persistence factory to wire the internally-created adapter.
|
|
66
73
|
*/
|
|
67
74
|
setAdapter(adapter) {
|
|
75
|
+
requireDurableElection(adapter);
|
|
68
76
|
this.defaultAdapter = adapter;
|
|
69
77
|
}
|
|
70
78
|
/** Register the persistence adapter that owns one collection. */
|
|
71
79
|
setAdapterForCollection(collectionId, adapter) {
|
|
80
|
+
requireDurableElection(adapter);
|
|
72
81
|
this.collectionAdapters.set(collectionId, adapter);
|
|
73
82
|
}
|
|
74
83
|
registerRemoteSubsetOwner(collectionId, owner) {
|
|
@@ -347,6 +356,30 @@ class BroadcastCollectionCoordinator {
|
|
|
347
356
|
}
|
|
348
357
|
return this.sendRPC(collectionId, request);
|
|
349
358
|
}
|
|
359
|
+
async reconcileCommittedTx(collectionId, tx, anchor, scopedAdapter) {
|
|
360
|
+
const initialRoute = this.waitForInitialRoute(collectionId);
|
|
361
|
+
if (initialRoute) await initialRoute;
|
|
362
|
+
let response;
|
|
363
|
+
for (let attempt = 0; attempt <= RPC_RETRY_ATTEMPTS; attempt++) {
|
|
364
|
+
if (attempt > 0) await sleep(RPC_RETRY_DELAY_MS * attempt);
|
|
365
|
+
const request = {
|
|
366
|
+
type: `rpc:reconcileCommittedTx:req`,
|
|
367
|
+
rpcId: safeRandomUUID(),
|
|
368
|
+
tx,
|
|
369
|
+
anchor
|
|
370
|
+
};
|
|
371
|
+
response = this.isLeader(collectionId) ? await this.handleReconcileCommittedTx(
|
|
372
|
+
collectionId,
|
|
373
|
+
request,
|
|
374
|
+
scopedAdapter
|
|
375
|
+
) : await this.sendRPC(
|
|
376
|
+
collectionId,
|
|
377
|
+
request
|
|
378
|
+
);
|
|
379
|
+
if (response.ok || response.code !== `NOT_LEADER`) return response;
|
|
380
|
+
}
|
|
381
|
+
return response;
|
|
382
|
+
}
|
|
350
383
|
async pullSince(collectionId, fromRowVersion, scopedAdapter) {
|
|
351
384
|
if (this.isLeader(collectionId)) {
|
|
352
385
|
return this.handlePullSince(
|
|
@@ -452,16 +485,18 @@ class BroadcastCollectionCoordinator {
|
|
|
452
485
|
}
|
|
453
486
|
try {
|
|
454
487
|
const adapter = this.requireAdapter(collectionId);
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
488
|
+
const reserveTerm = adapter.reserveLeadershipTerm;
|
|
489
|
+
if (!reserveTerm)
|
|
490
|
+
throw new InvalidPersistenceAdapterError(`reserveLeadershipTerm`);
|
|
491
|
+
const pos = await this.withWriterLock(
|
|
492
|
+
() => reserveTerm.call(adapter, collectionId, state.latestTerm)
|
|
493
|
+
);
|
|
494
|
+
state.latestTerm = pos.latestTerm;
|
|
495
|
+
state.latestSeq = pos.latestSeq;
|
|
496
|
+
state.latestRowVersion = pos.latestRowVersion;
|
|
461
497
|
if (this.isDisposed() || !this.participatesInLeadership(state) || this.collections.get(collectionId) !== state || abortController.signal.aborted || state.lockAbortController !== abortController) {
|
|
462
498
|
return;
|
|
463
499
|
}
|
|
464
|
-
state.latestTerm++;
|
|
465
500
|
state.isLeader = true;
|
|
466
501
|
state.leaderId = this.nodeId;
|
|
467
502
|
this.notifyRouteWaiters(state);
|
|
@@ -718,6 +753,7 @@ class BroadcastCollectionCoordinator {
|
|
|
718
753
|
const heartbeat = payload;
|
|
719
754
|
if (typeof heartbeat.leaderId === `string` && typeof heartbeat.term === `number` && typeof heartbeat.latestSeq === `number` && typeof heartbeat.latestRowVersion === `number`) {
|
|
720
755
|
const state2 = this.getOrCreateCollectionState(envelope.collectionId);
|
|
756
|
+
if (state2.isLeader) return;
|
|
721
757
|
if (heartbeat.term < state2.latestTerm) return;
|
|
722
758
|
const changedLeader = state2.leaderId !== heartbeat.leaderId;
|
|
723
759
|
state2.leaderId = heartbeat.leaderId;
|
|
@@ -898,6 +934,8 @@ class BroadcastCollectionCoordinator {
|
|
|
898
934
|
return this.handleApplyLocalMutations(collectionId, request);
|
|
899
935
|
case `rpc:applyCommittedTx:req`:
|
|
900
936
|
return this.handleApplyCommittedTx(collectionId, request);
|
|
937
|
+
case `rpc:reconcileCommittedTx:req`:
|
|
938
|
+
return this.handleReconcileCommittedTx(collectionId, request);
|
|
901
939
|
case `rpc:pullSince:req`:
|
|
902
940
|
return this.handlePullSince(collectionId, request);
|
|
903
941
|
}
|
|
@@ -1391,6 +1429,10 @@ class BroadcastCollectionCoordinator {
|
|
|
1391
1429
|
}
|
|
1392
1430
|
);
|
|
1393
1431
|
this.pruneAppliedEnvelopes();
|
|
1432
|
+
this.publishCommittedTx(collectionId, state, tx);
|
|
1433
|
+
return response;
|
|
1434
|
+
}
|
|
1435
|
+
publishCommittedTx(collectionId, state, tx, forceReload = false) {
|
|
1394
1436
|
const committedBase = {
|
|
1395
1437
|
type: `tx:committed`,
|
|
1396
1438
|
term: tx.term,
|
|
@@ -1398,9 +1440,10 @@ class BroadcastCollectionCoordinator {
|
|
|
1398
1440
|
txId: tx.txId,
|
|
1399
1441
|
latestRowVersion: tx.rowVersion
|
|
1400
1442
|
};
|
|
1401
|
-
const committedPayload = tx.truncate ? {
|
|
1443
|
+
const committedPayload = tx.truncate || forceReload ? {
|
|
1402
1444
|
...committedBase,
|
|
1403
|
-
requiresFullReload: true
|
|
1445
|
+
requiresFullReload: true,
|
|
1446
|
+
...forceReload ? { reconciliationReload: true } : {}
|
|
1404
1447
|
} : {
|
|
1405
1448
|
...committedBase,
|
|
1406
1449
|
requiresFullReload: false,
|
|
@@ -1424,22 +1467,115 @@ class BroadcastCollectionCoordinator {
|
|
|
1424
1467
|
for (const subscriber of state.subscribers) {
|
|
1425
1468
|
subscriber(committed);
|
|
1426
1469
|
}
|
|
1470
|
+
}
|
|
1471
|
+
async handleReconcileCommittedTx(collectionId, request, scopedAdapter) {
|
|
1472
|
+
const state = this.collections.get(collectionId);
|
|
1473
|
+
const notLeader = () => ({
|
|
1474
|
+
type: `rpc:reconcileCommittedTx:res`,
|
|
1475
|
+
rpcId: request.rpcId,
|
|
1476
|
+
ok: false,
|
|
1477
|
+
code: `NOT_LEADER`,
|
|
1478
|
+
error: `not the leader for ${collectionId}`
|
|
1479
|
+
});
|
|
1480
|
+
if (!state?.isLeader) return notLeader();
|
|
1481
|
+
if (!isValidCommittedTxAnchor(request.anchor) || !hasReconciliationTxPayload(request.tx)) {
|
|
1482
|
+
return {
|
|
1483
|
+
type: `rpc:reconcileCommittedTx:res`,
|
|
1484
|
+
rpcId: request.rpcId,
|
|
1485
|
+
ok: false,
|
|
1486
|
+
code: `UNDETERMINED`,
|
|
1487
|
+
error: `the source transaction has no valid durable proof`
|
|
1488
|
+
};
|
|
1489
|
+
}
|
|
1490
|
+
const expectedTerm = state.latestTerm;
|
|
1491
|
+
const result = await this.withScheduledWriterLock(
|
|
1492
|
+
collectionId,
|
|
1493
|
+
async (adapter) => {
|
|
1494
|
+
const candidate = this.nextPersistedTxIfLeader(
|
|
1495
|
+
collectionId,
|
|
1496
|
+
state,
|
|
1497
|
+
expectedTerm,
|
|
1498
|
+
request.tx
|
|
1499
|
+
);
|
|
1500
|
+
if (candidate === null) return null;
|
|
1501
|
+
if (!adapter.reconcileCommittedTx) return { kind: `unknown` };
|
|
1502
|
+
try {
|
|
1503
|
+
const reconciled = await adapter.reconcileCommittedTx(
|
|
1504
|
+
collectionId,
|
|
1505
|
+
candidate,
|
|
1506
|
+
request.anchor
|
|
1507
|
+
);
|
|
1508
|
+
if (reconciled.kind !== `unknown`) {
|
|
1509
|
+
state.latestRowVersion = Math.max(
|
|
1510
|
+
state.latestRowVersion,
|
|
1511
|
+
reconciled.latestRowVersion
|
|
1512
|
+
);
|
|
1513
|
+
if (reconciled.kind === `applied-now`) {
|
|
1514
|
+
state.latestSeq = reconciled.committed.seq;
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
return reconciled;
|
|
1518
|
+
} catch (error) {
|
|
1519
|
+
throw toPersistedCollectionDurabilityError(collectionId, error);
|
|
1520
|
+
}
|
|
1521
|
+
},
|
|
1522
|
+
scopedAdapter
|
|
1523
|
+
);
|
|
1524
|
+
if (result === null) return notLeader();
|
|
1525
|
+
if (result.kind === `unknown`) {
|
|
1526
|
+
return {
|
|
1527
|
+
type: `rpc:reconcileCommittedTx:res`,
|
|
1528
|
+
rpcId: request.rpcId,
|
|
1529
|
+
ok: false,
|
|
1530
|
+
code: `UNDETERMINED`,
|
|
1531
|
+
error: `the durable outcome cannot be certified`
|
|
1532
|
+
};
|
|
1533
|
+
}
|
|
1534
|
+
const current = {
|
|
1535
|
+
term: state.latestTerm,
|
|
1536
|
+
seq: state.latestSeq,
|
|
1537
|
+
rowVersion: state.latestRowVersion
|
|
1538
|
+
};
|
|
1539
|
+
const response = {
|
|
1540
|
+
type: `rpc:reconcileCommittedTx:res`,
|
|
1541
|
+
rpcId: request.rpcId,
|
|
1542
|
+
ok: true,
|
|
1543
|
+
alreadyApplied: result.kind === `already-applied`,
|
|
1544
|
+
committed: result.committed
|
|
1545
|
+
};
|
|
1546
|
+
if (!this.isDisposed() && this.collections.get(collectionId) === state) {
|
|
1547
|
+
this.publishCommittedTx(
|
|
1548
|
+
collectionId,
|
|
1549
|
+
state,
|
|
1550
|
+
result.kind === `already-applied` ? { ...request.tx, ...current } : { ...request.tx, ...result.committed },
|
|
1551
|
+
result.kind === `already-applied`
|
|
1552
|
+
);
|
|
1553
|
+
}
|
|
1427
1554
|
return response;
|
|
1428
1555
|
}
|
|
1556
|
+
nextPersistedTxIfLeader(collectionId, state, expectedTerm, pendingTx) {
|
|
1557
|
+
if (this.isDisposed() || this.collections.get(collectionId) !== state || !state.isLeader || state.leaderId !== this.nodeId || state.latestTerm !== expectedTerm) {
|
|
1558
|
+
return null;
|
|
1559
|
+
}
|
|
1560
|
+
return {
|
|
1561
|
+
...pendingTx,
|
|
1562
|
+
term: state.latestTerm,
|
|
1563
|
+
seq: state.latestSeq + 1,
|
|
1564
|
+
rowVersion: state.latestRowVersion + 1
|
|
1565
|
+
};
|
|
1566
|
+
}
|
|
1429
1567
|
async applyDurablyAtNextStreamPosition(collectionId, state, pendingTx, scopedAdapter) {
|
|
1430
1568
|
const expectedTerm = state.latestTerm;
|
|
1431
1569
|
return this.withScheduledWriterLock(
|
|
1432
1570
|
collectionId,
|
|
1433
1571
|
async (adapter) => {
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
rowVersion: state.latestRowVersion + 1
|
|
1442
|
-
};
|
|
1572
|
+
const tx = this.nextPersistedTxIfLeader(
|
|
1573
|
+
collectionId,
|
|
1574
|
+
state,
|
|
1575
|
+
expectedTerm,
|
|
1576
|
+
pendingTx
|
|
1577
|
+
);
|
|
1578
|
+
if (tx === null) return null;
|
|
1443
1579
|
try {
|
|
1444
1580
|
await adapter.applyCommittedTx(collectionId, tx);
|
|
1445
1581
|
} catch (error) {
|
|
@@ -1582,12 +1718,18 @@ function isRPCRequest(payload) {
|
|
|
1582
1718
|
case `rpc:ensurePersistedIndex:req`:
|
|
1583
1719
|
case `rpc:applyLocalMutations:req`:
|
|
1584
1720
|
case `rpc:applyCommittedTx:req`:
|
|
1721
|
+
case `rpc:reconcileCommittedTx:req`:
|
|
1585
1722
|
case `rpc:pullSince:req`:
|
|
1586
1723
|
return true;
|
|
1587
1724
|
default:
|
|
1588
1725
|
return false;
|
|
1589
1726
|
}
|
|
1590
1727
|
}
|
|
1728
|
+
function hasReconciliationTxPayload(value) {
|
|
1729
|
+
if (!value || typeof value !== `object`) return false;
|
|
1730
|
+
const tx = value;
|
|
1731
|
+
return typeof tx.txId === `string` && Array.isArray(tx.mutations);
|
|
1732
|
+
}
|
|
1591
1733
|
function sleep(ms) {
|
|
1592
1734
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1593
1735
|
}
|
|
@@ -1661,6 +1803,15 @@ function createRPCErrorResponse(request, cause) {
|
|
|
1661
1803
|
code: `CONFLICT`,
|
|
1662
1804
|
error
|
|
1663
1805
|
};
|
|
1806
|
+
case `rpc:reconcileCommittedTx:req`:
|
|
1807
|
+
return {
|
|
1808
|
+
type: `rpc:reconcileCommittedTx:res`,
|
|
1809
|
+
rpcId: request.rpcId,
|
|
1810
|
+
ok: false,
|
|
1811
|
+
code: cause instanceof PersistedCollectionDurabilityError ? `PERSISTENCE_ERROR` : `UNDETERMINED`,
|
|
1812
|
+
error,
|
|
1813
|
+
...cause instanceof PersistedCollectionDurabilityError ? toSafeDurabilityDetails(cause) : {}
|
|
1814
|
+
};
|
|
1664
1815
|
case `rpc:pullSince:req`:
|
|
1665
1816
|
return {
|
|
1666
1817
|
type: `rpc:pullSince:res`,
|