@tanstack/db-sqlite-persistence-core 0.4.7 → 0.4.9
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 +32 -3
- package/dist/cjs/broadcast-coordinator.cjs +282 -43
- package/dist/cjs/broadcast-coordinator.cjs.map +1 -1
- package/dist/cjs/broadcast-coordinator.d.cts +25 -8
- 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/index.cjs +4 -0
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/persisted.cjs +1599 -313
- package/dist/cjs/persisted.cjs.map +1 -1
- package/dist/cjs/persisted.d.cts +106 -13
- package/dist/cjs/remote-subset-wire.cjs +11 -0
- package/dist/cjs/remote-subset-wire.cjs.map +1 -1
- package/dist/cjs/remote-subset-wire.d.cts +1 -0
- package/dist/cjs/sqlite-core-adapter.cjs +705 -64
- package/dist/cjs/sqlite-core-adapter.cjs.map +1 -1
- package/dist/cjs/sqlite-core-adapter.d.cts +53 -10
- package/dist/esm/broadcast-coordinator.d.ts +25 -8
- package/dist/esm/broadcast-coordinator.js +283 -44
- 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/index.js +6 -2
- package/dist/esm/persisted.d.ts +106 -13
- package/dist/esm/persisted.js +1601 -315
- package/dist/esm/persisted.js.map +1 -1
- package/dist/esm/remote-subset-wire.d.ts +1 -0
- package/dist/esm/remote-subset-wire.js +11 -0
- package/dist/esm/remote-subset-wire.js.map +1 -1
- package/dist/esm/sqlite-core-adapter.d.ts +53 -10
- package/dist/esm/sqlite-core-adapter.js +708 -67
- package/dist/esm/sqlite-core-adapter.js.map +1 -1
- package/package.json +2 -2
- package/src/broadcast-coordinator.ts +415 -62
- package/src/committed-tx-anchor.ts +13 -0
- package/src/persisted.ts +2283 -370
- package/src/remote-subset-wire.ts +12 -0
- package/src/sqlite-core-adapter.ts +891 -68
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { safeRandomUUID } from '@tanstack/db'
|
|
2
|
+
import { isValidCommittedTxAnchor } from './committed-tx-anchor'
|
|
2
3
|
import {
|
|
3
4
|
DuplicateRemoteSubsetOwnerError,
|
|
4
5
|
IndeterminateCommitError,
|
|
6
|
+
InvalidPersistedCollectionConfigError,
|
|
7
|
+
InvalidPersistenceAdapterError,
|
|
5
8
|
PersistedCollectionDurabilityError,
|
|
6
9
|
RetryableRemoteSubsetAcquisitionError,
|
|
7
10
|
toPersistedCollectionDurabilityError,
|
|
@@ -11,6 +14,7 @@ import {
|
|
|
11
14
|
toTransportedLoadSubsetOptions,
|
|
12
15
|
} from './remote-subset-wire'
|
|
13
16
|
import { unloadRemoteSubsetOwner } from './remote-subset-owner'
|
|
17
|
+
import { eagerStorageTarget, managedStorageTarget } from './persisted'
|
|
14
18
|
import type { LoadSubsetOptions } from '@tanstack/db'
|
|
15
19
|
import type { IndeterminateCommitRequestType } from './errors'
|
|
16
20
|
import type { TransportedLoadSubsetOptions } from './remote-subset-wire'
|
|
@@ -18,6 +22,7 @@ import type { RemoteSubsetOwner } from './remote-subset-owner'
|
|
|
18
22
|
import type {
|
|
19
23
|
ApplyCommittedTxResponse,
|
|
20
24
|
ApplyLocalMutationsResponse,
|
|
25
|
+
CommittedTxAnchor,
|
|
21
26
|
EnsureRemoteSubsetRequest,
|
|
22
27
|
EnsureRemoteSubsetResponse,
|
|
23
28
|
HydrationPersistenceAdapter,
|
|
@@ -25,10 +30,12 @@ import type {
|
|
|
25
30
|
PersistedIndexSpec,
|
|
26
31
|
PersistedMutationEnvelope,
|
|
27
32
|
PersistedRowMetadataMutation,
|
|
33
|
+
PersistedStorageTarget,
|
|
28
34
|
PersistedTx,
|
|
29
35
|
PersistenceAdapter,
|
|
30
36
|
ProtocolEnvelope,
|
|
31
37
|
PullSinceResponse,
|
|
38
|
+
ReconcileCommittedTxResponse,
|
|
32
39
|
ReleaseRemoteSubsetRequest,
|
|
33
40
|
ReleaseRemoteSubsetResponse,
|
|
34
41
|
TxCommitted,
|
|
@@ -60,12 +67,14 @@ type RPCRequest =
|
|
|
60
67
|
rpcId: string
|
|
61
68
|
signature: string
|
|
62
69
|
spec: PersistedIndexSpec
|
|
70
|
+
cacheGenerationClaimId?: string
|
|
63
71
|
}
|
|
64
72
|
| {
|
|
65
73
|
type: `rpc:applyLocalMutations:req`
|
|
66
74
|
rpcId: string
|
|
67
75
|
envelopeId: string
|
|
68
76
|
mutations: Array<PersistedMutationEnvelope>
|
|
77
|
+
cacheGenerationClaimId?: string
|
|
69
78
|
}
|
|
70
79
|
| {
|
|
71
80
|
type: `rpc:applyCommittedTx:req`
|
|
@@ -73,10 +82,17 @@ type RPCRequest =
|
|
|
73
82
|
envelopeId: string
|
|
74
83
|
tx: PersistedTx
|
|
75
84
|
}
|
|
85
|
+
| {
|
|
86
|
+
type: `rpc:reconcileCommittedTx:req`
|
|
87
|
+
rpcId: string
|
|
88
|
+
tx: PersistedTx
|
|
89
|
+
anchor: CommittedTxAnchor
|
|
90
|
+
}
|
|
76
91
|
| {
|
|
77
92
|
type: `rpc:pullSince:req`
|
|
78
93
|
rpcId: string
|
|
79
94
|
fromRowVersion: number
|
|
95
|
+
cacheGenerationClaimId?: string
|
|
80
96
|
}
|
|
81
97
|
|
|
82
98
|
type RPCResponse =
|
|
@@ -90,6 +106,7 @@ type RPCResponse =
|
|
|
90
106
|
}
|
|
91
107
|
| ApplyLocalMutationsResponse
|
|
92
108
|
| ApplyCommittedTxResponse
|
|
109
|
+
| ReconcileCommittedTxResponse
|
|
93
110
|
| PullSinceResponse
|
|
94
111
|
|
|
95
112
|
type PendingRPC = {
|
|
@@ -115,11 +132,16 @@ type CollectionState = {
|
|
|
115
132
|
// belong to the persisted sync wrapper, not the elected writer transport.
|
|
116
133
|
type CoordinatorAdapter = Pick<
|
|
117
134
|
PersistenceAdapter,
|
|
118
|
-
|
|
135
|
+
| `loadSubset`
|
|
136
|
+
| `applyCommittedTx`
|
|
137
|
+
| `reconcileCommittedTx`
|
|
138
|
+
| `ensureIndex`
|
|
139
|
+
| `runInRegularScope`
|
|
119
140
|
> & {
|
|
120
141
|
pullSince?: (
|
|
121
|
-
|
|
142
|
+
target: PersistedStorageTarget,
|
|
122
143
|
fromRowVersion: number,
|
|
144
|
+
ctx?: { cacheGenerationClaimId?: string },
|
|
123
145
|
) => Promise<
|
|
124
146
|
| {
|
|
125
147
|
latestRowVersion: number
|
|
@@ -132,13 +154,31 @@ type CoordinatorAdapter = Pick<
|
|
|
132
154
|
deletedKeys: Array<string | number>
|
|
133
155
|
}
|
|
134
156
|
>
|
|
135
|
-
getStreamPosition?: (
|
|
157
|
+
getStreamPosition?: (
|
|
158
|
+
target: PersistedStorageTarget,
|
|
159
|
+
ctx?: { cacheGenerationClaimId?: string },
|
|
160
|
+
) => Promise<{
|
|
161
|
+
latestTerm: number
|
|
162
|
+
latestSeq: number
|
|
163
|
+
latestRowVersion: number
|
|
164
|
+
}>
|
|
165
|
+
reserveLeadershipTerm?: (
|
|
166
|
+
target: PersistedStorageTarget,
|
|
167
|
+
observedTerm: number,
|
|
168
|
+
ctx?: { cacheGenerationClaimId?: string },
|
|
169
|
+
) => Promise<{
|
|
136
170
|
latestTerm: number
|
|
137
171
|
latestSeq: number
|
|
138
172
|
latestRowVersion: number
|
|
139
173
|
}>
|
|
140
174
|
}
|
|
141
175
|
|
|
176
|
+
function requireDurableElection(adapter: CoordinatorAdapter): void {
|
|
177
|
+
if (typeof adapter.reserveLeadershipTerm !== `function`) {
|
|
178
|
+
throw new InvalidPersistenceAdapterError(`reserveLeadershipTerm`)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
142
182
|
type ActiveRemoteSubsetAcquisition = {
|
|
143
183
|
collectionId: string
|
|
144
184
|
requesterId: string
|
|
@@ -146,6 +186,7 @@ type ActiveRemoteSubsetAcquisition = {
|
|
|
146
186
|
owner: RemoteSubsetOwner
|
|
147
187
|
options: TransportedLoadSubsetOptions
|
|
148
188
|
load: Promise<void>
|
|
189
|
+
admitted: Promise<void>
|
|
149
190
|
transferred: boolean
|
|
150
191
|
released: boolean
|
|
151
192
|
terminalRelease: boolean
|
|
@@ -230,6 +271,7 @@ export class BroadcastCollectionCoordinator implements PersistedCollectionCoordi
|
|
|
230
271
|
private readonly coordinatorName: string
|
|
231
272
|
private defaultAdapter: CoordinatorAdapter | null
|
|
232
273
|
private readonly collectionAdapters = new Map<string, CoordinatorAdapter>()
|
|
274
|
+
private readonly collectionClaimIds = new Map<string, string>()
|
|
233
275
|
private readonly remoteSubsetOwners = new Map<string, RemoteSubsetOwner>()
|
|
234
276
|
private readonly remoteSubsetIds = new Map<
|
|
235
277
|
string,
|
|
@@ -282,6 +324,7 @@ export class BroadcastCollectionCoordinator implements PersistedCollectionCoordi
|
|
|
282
324
|
}
|
|
283
325
|
|
|
284
326
|
constructor(options: BroadcastCollectionCoordinatorOptions) {
|
|
327
|
+
if (options.adapter) requireDurableElection(options.adapter)
|
|
285
328
|
this.dbName = options.dbName
|
|
286
329
|
this.coordinatorName = options.coordinatorName
|
|
287
330
|
this.defaultAdapter = options.adapter ?? null
|
|
@@ -300,6 +343,7 @@ export class BroadcastCollectionCoordinator implements PersistedCollectionCoordi
|
|
|
300
343
|
* Called by a host persistence factory to wire the internally-created adapter.
|
|
301
344
|
*/
|
|
302
345
|
setAdapter(adapter: CoordinatorAdapter): void {
|
|
346
|
+
requireDurableElection(adapter)
|
|
303
347
|
this.defaultAdapter = adapter
|
|
304
348
|
}
|
|
305
349
|
|
|
@@ -307,8 +351,15 @@ export class BroadcastCollectionCoordinator implements PersistedCollectionCoordi
|
|
|
307
351
|
setAdapterForCollection(
|
|
308
352
|
collectionId: string,
|
|
309
353
|
adapter: CoordinatorAdapter,
|
|
354
|
+
cacheGenerationClaimId?: string,
|
|
310
355
|
): void {
|
|
356
|
+
requireDurableElection(adapter)
|
|
311
357
|
this.collectionAdapters.set(collectionId, adapter)
|
|
358
|
+
if (cacheGenerationClaimId) {
|
|
359
|
+
this.collectionClaimIds.set(collectionId, cacheGenerationClaimId)
|
|
360
|
+
} else {
|
|
361
|
+
this.collectionClaimIds.delete(collectionId)
|
|
362
|
+
}
|
|
312
363
|
}
|
|
313
364
|
|
|
314
365
|
registerRemoteSubsetOwner(
|
|
@@ -491,11 +542,38 @@ export class BroadcastCollectionCoordinator implements PersistedCollectionCoordi
|
|
|
491
542
|
}
|
|
492
543
|
try {
|
|
493
544
|
await acquisition.release
|
|
545
|
+
} catch (error) {
|
|
546
|
+
this.scheduleRemoteSubsetReleaseRetry(acquisition, options)
|
|
547
|
+
throw error
|
|
494
548
|
} finally {
|
|
495
549
|
acquisition.release = null
|
|
496
550
|
}
|
|
497
551
|
}
|
|
498
552
|
|
|
553
|
+
private scheduleRemoteSubsetReleaseRetry(
|
|
554
|
+
acquisition: OutboundRemoteSubsetAcquisition,
|
|
555
|
+
options: LoadSubsetOptions,
|
|
556
|
+
): void {
|
|
557
|
+
const key = remoteSubsetAcquisitionKey(
|
|
558
|
+
acquisition.collectionId,
|
|
559
|
+
acquisition.acquisitionId,
|
|
560
|
+
)
|
|
561
|
+
if (
|
|
562
|
+
this.isDisposed() ||
|
|
563
|
+
this.outboundRemoteSubsetAcquisitions.get(key) !== acquisition ||
|
|
564
|
+
acquisition.retryTimer !== null
|
|
565
|
+
) {
|
|
566
|
+
return
|
|
567
|
+
}
|
|
568
|
+
acquisition.retryTimer = setTimeout(() => {
|
|
569
|
+
acquisition.retryTimer = null
|
|
570
|
+
void this.requestReleaseRemoteSubset(
|
|
571
|
+
acquisition.collectionId,
|
|
572
|
+
options,
|
|
573
|
+
).catch(() => undefined)
|
|
574
|
+
}, RPC_RETRY_DELAY_MS)
|
|
575
|
+
}
|
|
576
|
+
|
|
499
577
|
private async acquireRemoteSubset(
|
|
500
578
|
acquisition: OutboundRemoteSubsetAcquisition,
|
|
501
579
|
): Promise<void> {
|
|
@@ -571,7 +649,11 @@ export class BroadcastCollectionCoordinator implements PersistedCollectionCoordi
|
|
|
571
649
|
try {
|
|
572
650
|
await work
|
|
573
651
|
acquired = true
|
|
574
|
-
this.
|
|
652
|
+
// Release may have failed while this acquisition was in flight. In that
|
|
653
|
+
// case the shared timer carries release retry debt, not replay work.
|
|
654
|
+
if (!acquisition.releaseRequested) {
|
|
655
|
+
this.cancelRemoteSubsetReplayRetry(acquisition)
|
|
656
|
+
}
|
|
575
657
|
} finally {
|
|
576
658
|
if (acquisition.inFlight === work) acquisition.inFlight = null
|
|
577
659
|
const current = this.collections.get(acquisition.collectionId)
|
|
@@ -604,14 +686,27 @@ export class BroadcastCollectionCoordinator implements PersistedCollectionCoordi
|
|
|
604
686
|
spec: PersistedIndexSpec,
|
|
605
687
|
scopedAdapter?: HydrationPersistenceAdapter,
|
|
606
688
|
localEnsureCompleted = false,
|
|
689
|
+
cacheGenerationClaimId?: string,
|
|
607
690
|
): Promise<void> {
|
|
608
691
|
if (this.isLeader(collectionId)) {
|
|
609
692
|
if (localEnsureCompleted) return
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
693
|
+
const adapter = scopedAdapter ?? this.requireAdapter(collectionId)
|
|
694
|
+
if (cacheGenerationClaimId) {
|
|
695
|
+
await adapter.ensureIndex(
|
|
696
|
+
managedStorageTarget(collectionId, cacheGenerationClaimId),
|
|
697
|
+
signature,
|
|
698
|
+
spec,
|
|
699
|
+
{
|
|
700
|
+
cacheGenerationClaimId,
|
|
701
|
+
},
|
|
702
|
+
)
|
|
703
|
+
} else {
|
|
704
|
+
await adapter.ensureIndex(
|
|
705
|
+
eagerStorageTarget(collectionId),
|
|
706
|
+
signature,
|
|
707
|
+
spec,
|
|
708
|
+
)
|
|
709
|
+
}
|
|
615
710
|
return
|
|
616
711
|
}
|
|
617
712
|
|
|
@@ -625,6 +720,7 @@ export class BroadcastCollectionCoordinator implements PersistedCollectionCoordi
|
|
|
625
720
|
rpcId: safeRandomUUID(),
|
|
626
721
|
signature,
|
|
627
722
|
spec,
|
|
723
|
+
cacheGenerationClaimId,
|
|
628
724
|
})
|
|
629
725
|
|
|
630
726
|
if (!response.ok) {
|
|
@@ -637,6 +733,7 @@ export class BroadcastCollectionCoordinator implements PersistedCollectionCoordi
|
|
|
637
733
|
async requestApplyLocalMutations(
|
|
638
734
|
collectionId: string,
|
|
639
735
|
mutations: Array<PersistedMutationEnvelope>,
|
|
736
|
+
cacheGenerationClaimId?: string,
|
|
640
737
|
): Promise<ApplyLocalMutationsResponse> {
|
|
641
738
|
const initialRoute = this.waitForInitialRoute(collectionId)
|
|
642
739
|
if (initialRoute) await initialRoute
|
|
@@ -646,6 +743,7 @@ export class BroadcastCollectionCoordinator implements PersistedCollectionCoordi
|
|
|
646
743
|
rpcId: safeRandomUUID(),
|
|
647
744
|
envelopeId: safeRandomUUID(),
|
|
648
745
|
mutations,
|
|
746
|
+
cacheGenerationClaimId,
|
|
649
747
|
})
|
|
650
748
|
}
|
|
651
749
|
|
|
@@ -654,6 +752,7 @@ export class BroadcastCollectionCoordinator implements PersistedCollectionCoordi
|
|
|
654
752
|
rpcId: safeRandomUUID(),
|
|
655
753
|
envelopeId: safeRandomUUID(),
|
|
656
754
|
mutations,
|
|
755
|
+
cacheGenerationClaimId,
|
|
657
756
|
})
|
|
658
757
|
}
|
|
659
758
|
|
|
@@ -677,10 +776,48 @@ export class BroadcastCollectionCoordinator implements PersistedCollectionCoordi
|
|
|
677
776
|
return this.sendRPC<ApplyCommittedTxResponse>(collectionId, request)
|
|
678
777
|
}
|
|
679
778
|
|
|
779
|
+
async reconcileCommittedTx(
|
|
780
|
+
collectionId: string,
|
|
781
|
+
tx: PersistedTx,
|
|
782
|
+
anchor: CommittedTxAnchor,
|
|
783
|
+
scopedAdapter?: HydrationPersistenceAdapter,
|
|
784
|
+
): Promise<ReconcileCommittedTxResponse> {
|
|
785
|
+
const initialRoute = this.waitForInitialRoute(collectionId)
|
|
786
|
+
if (initialRoute) await initialRoute
|
|
787
|
+
// Each attempt checks the exact txId under the elected writer lock, so a
|
|
788
|
+
// stale NOT_LEADER reply can follow the replacement route safely.
|
|
789
|
+
let response: ReconcileCommittedTxResponse | undefined
|
|
790
|
+
for (let attempt = 0; attempt <= RPC_RETRY_ATTEMPTS; attempt++) {
|
|
791
|
+
if (attempt > 0) await sleep(RPC_RETRY_DELAY_MS * attempt)
|
|
792
|
+
const request: Extract<
|
|
793
|
+
RPCRequest,
|
|
794
|
+
{ type: `rpc:reconcileCommittedTx:req` }
|
|
795
|
+
> = {
|
|
796
|
+
type: `rpc:reconcileCommittedTx:req`,
|
|
797
|
+
rpcId: safeRandomUUID(),
|
|
798
|
+
tx,
|
|
799
|
+
anchor,
|
|
800
|
+
}
|
|
801
|
+
response = this.isLeader(collectionId)
|
|
802
|
+
? await this.handleReconcileCommittedTx(
|
|
803
|
+
collectionId,
|
|
804
|
+
request,
|
|
805
|
+
scopedAdapter,
|
|
806
|
+
)
|
|
807
|
+
: await this.sendRPC<ReconcileCommittedTxResponse>(
|
|
808
|
+
collectionId,
|
|
809
|
+
request,
|
|
810
|
+
)
|
|
811
|
+
if (response.ok || response.code !== `NOT_LEADER`) return response
|
|
812
|
+
}
|
|
813
|
+
return response!
|
|
814
|
+
}
|
|
815
|
+
|
|
680
816
|
async pullSince(
|
|
681
817
|
collectionId: string,
|
|
682
818
|
fromRowVersion: number,
|
|
683
819
|
scopedAdapter?: HydrationPersistenceAdapter,
|
|
820
|
+
cacheGenerationClaimId?: string,
|
|
684
821
|
): Promise<PullSinceResponse> {
|
|
685
822
|
if (this.isLeader(collectionId)) {
|
|
686
823
|
return this.handlePullSince(
|
|
@@ -689,6 +826,7 @@ export class BroadcastCollectionCoordinator implements PersistedCollectionCoordi
|
|
|
689
826
|
type: `rpc:pullSince:req`,
|
|
690
827
|
rpcId: safeRandomUUID(),
|
|
691
828
|
fromRowVersion,
|
|
829
|
+
cacheGenerationClaimId,
|
|
692
830
|
},
|
|
693
831
|
scopedAdapter,
|
|
694
832
|
)
|
|
@@ -698,6 +836,7 @@ export class BroadcastCollectionCoordinator implements PersistedCollectionCoordi
|
|
|
698
836
|
type: `rpc:pullSince:req`,
|
|
699
837
|
rpcId: safeRandomUUID(),
|
|
700
838
|
fromRowVersion,
|
|
839
|
+
cacheGenerationClaimId,
|
|
701
840
|
})
|
|
702
841
|
}
|
|
703
842
|
|
|
@@ -736,6 +875,7 @@ export class BroadcastCollectionCoordinator implements PersistedCollectionCoordi
|
|
|
736
875
|
this.channel.close()
|
|
737
876
|
this.collections.clear()
|
|
738
877
|
this.collectionAdapters.clear()
|
|
878
|
+
this.collectionClaimIds.clear()
|
|
739
879
|
for (const collectionId of this.remoteSubsetOwners.keys()) {
|
|
740
880
|
this.releaseInboundRemoteSubsetAcquisitions(collectionId)
|
|
741
881
|
}
|
|
@@ -806,14 +946,28 @@ export class BroadcastCollectionCoordinator implements PersistedCollectionCoordi
|
|
|
806
946
|
}
|
|
807
947
|
|
|
808
948
|
try {
|
|
809
|
-
//
|
|
949
|
+
// Reserve an election term durably before advertising this route.
|
|
950
|
+
// Election bypasses the shared scheduler because a hydrate may be
|
|
951
|
+
// waiting for its first writer route. The writer Web Lock orders
|
|
952
|
+
// reservation after any old owner's in-flight durable write.
|
|
810
953
|
const adapter = this.requireAdapter(collectionId)
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
954
|
+
const reserveTerm = adapter.reserveLeadershipTerm
|
|
955
|
+
if (!reserveTerm)
|
|
956
|
+
throw new InvalidPersistenceAdapterError(`reserveLeadershipTerm`)
|
|
957
|
+
const pos = await this.withWriterLock(() => {
|
|
958
|
+
const claimId = this.collectionClaimIds.get(collectionId)
|
|
959
|
+
return reserveTerm.call(
|
|
960
|
+
adapter,
|
|
961
|
+
claimId
|
|
962
|
+
? managedStorageTarget(collectionId, claimId)
|
|
963
|
+
: eagerStorageTarget(collectionId),
|
|
964
|
+
state.latestTerm,
|
|
965
|
+
{ cacheGenerationClaimId: claimId },
|
|
966
|
+
)
|
|
967
|
+
})
|
|
968
|
+
state.latestTerm = pos.latestTerm
|
|
969
|
+
state.latestSeq = pos.latestSeq
|
|
970
|
+
state.latestRowVersion = pos.latestRowVersion
|
|
817
971
|
|
|
818
972
|
if (
|
|
819
973
|
this.isDisposed() ||
|
|
@@ -825,7 +979,6 @@ export class BroadcastCollectionCoordinator implements PersistedCollectionCoordi
|
|
|
825
979
|
return
|
|
826
980
|
}
|
|
827
981
|
|
|
828
|
-
state.latestTerm++
|
|
829
982
|
state.isLeader = true
|
|
830
983
|
state.leaderId = this.nodeId
|
|
831
984
|
this.notifyRouteWaiters(state)
|
|
@@ -1005,6 +1158,7 @@ export class BroadcastCollectionCoordinator implements PersistedCollectionCoordi
|
|
|
1005
1158
|
this.releaseLeadership(collectionId, state)
|
|
1006
1159
|
this.collections.delete(collectionId)
|
|
1007
1160
|
this.collectionAdapters.delete(collectionId)
|
|
1161
|
+
this.collectionClaimIds.delete(collectionId)
|
|
1008
1162
|
const prefix = `${JSON.stringify([collectionId]).slice(0, -1)},`
|
|
1009
1163
|
for (const key of this.appliedEnvelopes.keys()) {
|
|
1010
1164
|
if (key.startsWith(prefix)) this.appliedEnvelopes.delete(key)
|
|
@@ -1175,6 +1329,9 @@ export class BroadcastCollectionCoordinator implements PersistedCollectionCoordi
|
|
|
1175
1329
|
typeof heartbeat.latestRowVersion === `number`
|
|
1176
1330
|
) {
|
|
1177
1331
|
const state = this.getOrCreateCollectionState(envelope.collectionId)
|
|
1332
|
+
// The local Web Lock owns this route even if an older heartbeat arrives
|
|
1333
|
+
// while leadership changes.
|
|
1334
|
+
if (state.isLeader) return
|
|
1178
1335
|
if (heartbeat.term < state.latestTerm) return
|
|
1179
1336
|
const changedLeader = state.leaderId !== heartbeat.leaderId
|
|
1180
1337
|
state.leaderId = heartbeat.leaderId
|
|
@@ -1415,6 +1572,8 @@ export class BroadcastCollectionCoordinator implements PersistedCollectionCoordi
|
|
|
1415
1572
|
return this.handleApplyLocalMutations(collectionId, request)
|
|
1416
1573
|
case `rpc:applyCommittedTx:req`:
|
|
1417
1574
|
return this.handleApplyCommittedTx(collectionId, request)
|
|
1575
|
+
case `rpc:reconcileCommittedTx:req`:
|
|
1576
|
+
return this.handleReconcileCommittedTx(collectionId, request)
|
|
1418
1577
|
case `rpc:pullSince:req`:
|
|
1419
1578
|
return this.handlePullSince(collectionId, request)
|
|
1420
1579
|
}
|
|
@@ -1546,12 +1705,11 @@ export class BroadcastCollectionCoordinator implements PersistedCollectionCoordi
|
|
|
1546
1705
|
acquisition.released = true
|
|
1547
1706
|
acquisition.release = (async () => {
|
|
1548
1707
|
try {
|
|
1549
|
-
await acquisition.
|
|
1550
|
-
} catch {
|
|
1551
|
-
// A returned promise transfers the lease even when initial loading fails.
|
|
1552
|
-
}
|
|
1553
|
-
try {
|
|
1708
|
+
await acquisition.admitted
|
|
1554
1709
|
if (acquisition.transferred) {
|
|
1710
|
+
// The owner may require unload to settle its load. Invocation of the
|
|
1711
|
+
// owner already transferred this lease; source settlement is not a
|
|
1712
|
+
// prerequisite for giving it the cancellation opportunity.
|
|
1555
1713
|
await unloadRemoteSubsetOwner(acquisition.owner, acquisition.options)
|
|
1556
1714
|
}
|
|
1557
1715
|
} finally {
|
|
@@ -1711,6 +1869,7 @@ export class BroadcastCollectionCoordinator implements PersistedCollectionCoordi
|
|
|
1711
1869
|
owner: options.owner,
|
|
1712
1870
|
options: options.options,
|
|
1713
1871
|
load: Promise.resolve(),
|
|
1872
|
+
admitted: Promise.resolve(),
|
|
1714
1873
|
transferred: false,
|
|
1715
1874
|
released: false,
|
|
1716
1875
|
terminalRelease: false,
|
|
@@ -1718,6 +1877,10 @@ export class BroadcastCollectionCoordinator implements PersistedCollectionCoordi
|
|
|
1718
1877
|
}
|
|
1719
1878
|
this.releasedRemoteSubsetAcquisitionTimes.delete(key)
|
|
1720
1879
|
this.inboundRemoteSubsetAcquisitions.set(key, acquisition)
|
|
1880
|
+
let resolveAdmission!: () => void
|
|
1881
|
+
acquisition.admitted = new Promise<void>((resolve) => {
|
|
1882
|
+
resolveAdmission = resolve
|
|
1883
|
+
})
|
|
1721
1884
|
let resolveLoad!: () => void
|
|
1722
1885
|
let rejectLoad!: (error: unknown) => void
|
|
1723
1886
|
acquisition.load = new Promise<void>((resolve, reject) => {
|
|
@@ -1730,6 +1893,8 @@ export class BroadcastCollectionCoordinator implements PersistedCollectionCoordi
|
|
|
1730
1893
|
void Promise.resolve(load).then(resolveLoad, rejectLoad)
|
|
1731
1894
|
} catch (error) {
|
|
1732
1895
|
rejectLoad(error)
|
|
1896
|
+
} finally {
|
|
1897
|
+
resolveAdmission()
|
|
1733
1898
|
}
|
|
1734
1899
|
|
|
1735
1900
|
try {
|
|
@@ -1765,11 +1930,26 @@ export class BroadcastCollectionCoordinator implements PersistedCollectionCoordi
|
|
|
1765
1930
|
rpcId: string
|
|
1766
1931
|
signature: string
|
|
1767
1932
|
spec: PersistedIndexSpec
|
|
1933
|
+
cacheGenerationClaimId?: string
|
|
1768
1934
|
},
|
|
1769
1935
|
): Promise<RPCResponse> {
|
|
1770
|
-
await this.withScheduledWriterLock(collectionId, (adapter) =>
|
|
1771
|
-
|
|
1772
|
-
|
|
1936
|
+
await this.withScheduledWriterLock(collectionId, (adapter) => {
|
|
1937
|
+
if (request.cacheGenerationClaimId) {
|
|
1938
|
+
return adapter.ensureIndex(
|
|
1939
|
+
managedStorageTarget(collectionId, request.cacheGenerationClaimId),
|
|
1940
|
+
request.signature,
|
|
1941
|
+
request.spec,
|
|
1942
|
+
{
|
|
1943
|
+
cacheGenerationClaimId: request.cacheGenerationClaimId,
|
|
1944
|
+
},
|
|
1945
|
+
)
|
|
1946
|
+
}
|
|
1947
|
+
return adapter.ensureIndex(
|
|
1948
|
+
eagerStorageTarget(collectionId),
|
|
1949
|
+
request.signature,
|
|
1950
|
+
request.spec,
|
|
1951
|
+
)
|
|
1952
|
+
})
|
|
1773
1953
|
return {
|
|
1774
1954
|
type: `rpc:ensurePersistedIndex:res`,
|
|
1775
1955
|
rpcId: request.rpcId,
|
|
@@ -1784,6 +1964,7 @@ export class BroadcastCollectionCoordinator implements PersistedCollectionCoordi
|
|
|
1784
1964
|
rpcId: string
|
|
1785
1965
|
envelopeId: string
|
|
1786
1966
|
mutations: Array<PersistedMutationEnvelope>
|
|
1967
|
+
cacheGenerationClaimId?: string
|
|
1787
1968
|
},
|
|
1788
1969
|
): Promise<ApplyLocalMutationsResponse> {
|
|
1789
1970
|
const envelopeKey = appliedEnvelopeKey(collectionId, request.envelopeId)
|
|
@@ -1817,6 +1998,14 @@ export class BroadcastCollectionCoordinator implements PersistedCollectionCoordi
|
|
|
1817
1998
|
collectionId: string,
|
|
1818
1999
|
request: Extract<RPCRequest, { type: `rpc:applyLocalMutations:req` }>,
|
|
1819
2000
|
): Promise<ApplyLocalMutationsResponse> {
|
|
2001
|
+
if (
|
|
2002
|
+
this.collectionClaimIds.has(collectionId) &&
|
|
2003
|
+
!request.cacheGenerationClaimId
|
|
2004
|
+
) {
|
|
2005
|
+
throw new InvalidPersistedCollectionConfigError(
|
|
2006
|
+
`A managed local mutation requires a persisted cache claim`,
|
|
2007
|
+
)
|
|
2008
|
+
}
|
|
1820
2009
|
const state = this.collections.get(collectionId)
|
|
1821
2010
|
if (!state || !state.isLeader) {
|
|
1822
2011
|
return {
|
|
@@ -1846,6 +2035,7 @@ export class BroadcastCollectionCoordinator implements PersistedCollectionCoordi
|
|
|
1846
2035
|
}
|
|
1847
2036
|
const pendingTx = {
|
|
1848
2037
|
txId: safeRandomUUID(),
|
|
2038
|
+
cacheGenerationClaimId: request.cacheGenerationClaimId,
|
|
1849
2039
|
mutations: request.mutations.map((m) => ({
|
|
1850
2040
|
type: m.type,
|
|
1851
2041
|
key: m.key,
|
|
@@ -2056,6 +2246,17 @@ export class BroadcastCollectionCoordinator implements PersistedCollectionCoordi
|
|
|
2056
2246
|
)
|
|
2057
2247
|
this.pruneAppliedEnvelopes()
|
|
2058
2248
|
|
|
2249
|
+
this.publishCommittedTx(collectionId, state, tx)
|
|
2250
|
+
|
|
2251
|
+
return response
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
private publishCommittedTx(
|
|
2255
|
+
collectionId: string,
|
|
2256
|
+
state: CollectionState,
|
|
2257
|
+
tx: PersistedTx,
|
|
2258
|
+
forceReload = false,
|
|
2259
|
+
): void {
|
|
2059
2260
|
const committedBase = {
|
|
2060
2261
|
type: `tx:committed` as const,
|
|
2061
2262
|
term: tx.term,
|
|
@@ -2063,26 +2264,28 @@ export class BroadcastCollectionCoordinator implements PersistedCollectionCoordi
|
|
|
2063
2264
|
txId: tx.txId,
|
|
2064
2265
|
latestRowVersion: tx.rowVersion,
|
|
2065
2266
|
}
|
|
2066
|
-
const committedPayload: TxCommitted =
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
.
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2267
|
+
const committedPayload: TxCommitted =
|
|
2268
|
+
tx.truncate || forceReload
|
|
2269
|
+
? {
|
|
2270
|
+
...committedBase,
|
|
2271
|
+
requiresFullReload: true,
|
|
2272
|
+
...(forceReload ? { reconciliationReload: true as const } : {}),
|
|
2273
|
+
}
|
|
2274
|
+
: {
|
|
2275
|
+
...committedBase,
|
|
2276
|
+
requiresFullReload: false,
|
|
2277
|
+
changedRows: tx.mutations
|
|
2278
|
+
.filter((mutation) => mutation.type !== `delete`)
|
|
2279
|
+
.map((mutation) => ({
|
|
2280
|
+
key: mutation.key,
|
|
2281
|
+
value: mutation.value,
|
|
2282
|
+
})),
|
|
2283
|
+
deletedKeys: tx.mutations
|
|
2284
|
+
.filter((mutation) => mutation.type === `delete`)
|
|
2285
|
+
.map((mutation) => mutation.key),
|
|
2286
|
+
rowMetadataMutations: tx.rowMetadataMutations,
|
|
2287
|
+
collectionMetadataMutations: tx.collectionMetadataMutations,
|
|
2288
|
+
}
|
|
2086
2289
|
const committed: ProtocolEnvelope<TxCommitted> = {
|
|
2087
2290
|
v: 1,
|
|
2088
2291
|
dbName: this.dbName,
|
|
@@ -2095,10 +2298,133 @@ export class BroadcastCollectionCoordinator implements PersistedCollectionCoordi
|
|
|
2095
2298
|
for (const subscriber of state.subscribers) {
|
|
2096
2299
|
subscriber(committed)
|
|
2097
2300
|
}
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2303
|
+
private async handleReconcileCommittedTx(
|
|
2304
|
+
collectionId: string,
|
|
2305
|
+
request: Extract<RPCRequest, { type: `rpc:reconcileCommittedTx:req` }>,
|
|
2306
|
+
scopedAdapter?: HydrationPersistenceAdapter,
|
|
2307
|
+
): Promise<ReconcileCommittedTxResponse> {
|
|
2308
|
+
const state = this.collections.get(collectionId)
|
|
2309
|
+
const notLeader = (): ReconcileCommittedTxResponse => ({
|
|
2310
|
+
type: `rpc:reconcileCommittedTx:res`,
|
|
2311
|
+
rpcId: request.rpcId,
|
|
2312
|
+
ok: false,
|
|
2313
|
+
code: `NOT_LEADER`,
|
|
2314
|
+
error: `not the leader for ${collectionId}`,
|
|
2315
|
+
})
|
|
2316
|
+
if (!state?.isLeader) return notLeader()
|
|
2317
|
+
if (
|
|
2318
|
+
!isValidCommittedTxAnchor(request.anchor) ||
|
|
2319
|
+
!hasReconciliationTxPayload(request.tx)
|
|
2320
|
+
) {
|
|
2321
|
+
return {
|
|
2322
|
+
type: `rpc:reconcileCommittedTx:res`,
|
|
2323
|
+
rpcId: request.rpcId,
|
|
2324
|
+
ok: false,
|
|
2325
|
+
code: `UNDETERMINED`,
|
|
2326
|
+
error: `the source transaction has no valid durable proof`,
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
|
|
2330
|
+
const expectedTerm = state.latestTerm
|
|
2331
|
+
const result = await this.withScheduledWriterLock(
|
|
2332
|
+
collectionId,
|
|
2333
|
+
async (adapter) => {
|
|
2334
|
+
const candidate = this.nextPersistedTxIfLeader(
|
|
2335
|
+
collectionId,
|
|
2336
|
+
state,
|
|
2337
|
+
expectedTerm,
|
|
2338
|
+
request.tx,
|
|
2339
|
+
)
|
|
2340
|
+
if (candidate === null) return null
|
|
2341
|
+
if (!adapter.reconcileCommittedTx) return { kind: `unknown` as const }
|
|
2342
|
+
try {
|
|
2343
|
+
const reconciled = await adapter.reconcileCommittedTx(
|
|
2344
|
+
candidate.cacheGenerationClaimId
|
|
2345
|
+
? managedStorageTarget(
|
|
2346
|
+
collectionId,
|
|
2347
|
+
candidate.cacheGenerationClaimId,
|
|
2348
|
+
)
|
|
2349
|
+
: eagerStorageTarget(collectionId),
|
|
2350
|
+
candidate,
|
|
2351
|
+
request.anchor,
|
|
2352
|
+
)
|
|
2353
|
+
if (reconciled.kind !== `unknown`) {
|
|
2354
|
+
state.latestRowVersion = Math.max(
|
|
2355
|
+
state.latestRowVersion,
|
|
2356
|
+
reconciled.latestRowVersion,
|
|
2357
|
+
)
|
|
2358
|
+
if (reconciled.kind === `applied-now`) {
|
|
2359
|
+
state.latestSeq = reconciled.committed.seq
|
|
2360
|
+
}
|
|
2361
|
+
}
|
|
2362
|
+
return reconciled
|
|
2363
|
+
} catch (error) {
|
|
2364
|
+
throw toPersistedCollectionDurabilityError(collectionId, error)
|
|
2365
|
+
}
|
|
2366
|
+
},
|
|
2367
|
+
scopedAdapter,
|
|
2368
|
+
)
|
|
2369
|
+
if (result === null) return notLeader()
|
|
2370
|
+
if (result.kind === `unknown`) {
|
|
2371
|
+
return {
|
|
2372
|
+
type: `rpc:reconcileCommittedTx:res`,
|
|
2373
|
+
rpcId: request.rpcId,
|
|
2374
|
+
ok: false,
|
|
2375
|
+
code: `UNDETERMINED`,
|
|
2376
|
+
error: `the durable outcome cannot be certified`,
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2098
2379
|
|
|
2380
|
+
const current = {
|
|
2381
|
+
term: state.latestTerm,
|
|
2382
|
+
seq: state.latestSeq,
|
|
2383
|
+
rowVersion: state.latestRowVersion,
|
|
2384
|
+
}
|
|
2385
|
+
const response: ReconcileCommittedTxResponse = {
|
|
2386
|
+
type: `rpc:reconcileCommittedTx:res`,
|
|
2387
|
+
rpcId: request.rpcId,
|
|
2388
|
+
ok: true,
|
|
2389
|
+
alreadyApplied: result.kind === `already-applied`,
|
|
2390
|
+
committed: result.committed,
|
|
2391
|
+
}
|
|
2392
|
+
if (!this.isDisposed() && this.collections.get(collectionId) === state) {
|
|
2393
|
+
this.publishCommittedTx(
|
|
2394
|
+
collectionId,
|
|
2395
|
+
state,
|
|
2396
|
+
result.kind === `already-applied`
|
|
2397
|
+
? { ...request.tx, ...current }
|
|
2398
|
+
: { ...request.tx, ...result.committed },
|
|
2399
|
+
result.kind === `already-applied`,
|
|
2400
|
+
)
|
|
2401
|
+
}
|
|
2099
2402
|
return response
|
|
2100
2403
|
}
|
|
2101
2404
|
|
|
2405
|
+
private nextPersistedTxIfLeader(
|
|
2406
|
+
collectionId: string,
|
|
2407
|
+
state: CollectionState,
|
|
2408
|
+
expectedTerm: number,
|
|
2409
|
+
pendingTx: Omit<PersistedTx, `term` | `seq` | `rowVersion`>,
|
|
2410
|
+
): PersistedTx | null {
|
|
2411
|
+
if (
|
|
2412
|
+
this.isDisposed() ||
|
|
2413
|
+
this.collections.get(collectionId) !== state ||
|
|
2414
|
+
!state.isLeader ||
|
|
2415
|
+
state.leaderId !== this.nodeId ||
|
|
2416
|
+
state.latestTerm !== expectedTerm
|
|
2417
|
+
) {
|
|
2418
|
+
return null
|
|
2419
|
+
}
|
|
2420
|
+
return {
|
|
2421
|
+
...pendingTx,
|
|
2422
|
+
term: state.latestTerm,
|
|
2423
|
+
seq: state.latestSeq + 1,
|
|
2424
|
+
rowVersion: state.latestRowVersion + 1,
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
|
|
2102
2428
|
private async applyDurablyAtNextStreamPosition(
|
|
2103
2429
|
collectionId: string,
|
|
2104
2430
|
state: CollectionState,
|
|
@@ -2109,23 +2435,20 @@ export class BroadcastCollectionCoordinator implements PersistedCollectionCoordi
|
|
|
2109
2435
|
return this.withScheduledWriterLock(
|
|
2110
2436
|
collectionId,
|
|
2111
2437
|
async (adapter) => {
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
)
|
|
2119
|
-
return null
|
|
2120
|
-
}
|
|
2121
|
-
const tx: PersistedTx = {
|
|
2122
|
-
...pendingTx,
|
|
2123
|
-
term: state.latestTerm,
|
|
2124
|
-
seq: state.latestSeq + 1,
|
|
2125
|
-
rowVersion: state.latestRowVersion + 1,
|
|
2126
|
-
}
|
|
2438
|
+
const tx = this.nextPersistedTxIfLeader(
|
|
2439
|
+
collectionId,
|
|
2440
|
+
state,
|
|
2441
|
+
expectedTerm,
|
|
2442
|
+
pendingTx,
|
|
2443
|
+
)
|
|
2444
|
+
if (tx === null) return null
|
|
2127
2445
|
try {
|
|
2128
|
-
await adapter.applyCommittedTx(
|
|
2446
|
+
await adapter.applyCommittedTx(
|
|
2447
|
+
tx.cacheGenerationClaimId
|
|
2448
|
+
? managedStorageTarget(collectionId, tx.cacheGenerationClaimId)
|
|
2449
|
+
: eagerStorageTarget(collectionId),
|
|
2450
|
+
tx,
|
|
2451
|
+
)
|
|
2129
2452
|
} catch (error) {
|
|
2130
2453
|
throw toPersistedCollectionDurabilityError(collectionId, error)
|
|
2131
2454
|
}
|
|
@@ -2143,6 +2466,7 @@ export class BroadcastCollectionCoordinator implements PersistedCollectionCoordi
|
|
|
2143
2466
|
type: `rpc:pullSince:req`
|
|
2144
2467
|
rpcId: string
|
|
2145
2468
|
fromRowVersion: number
|
|
2469
|
+
cacheGenerationClaimId?: string
|
|
2146
2470
|
},
|
|
2147
2471
|
scopedAdapter?: HydrationPersistenceAdapter,
|
|
2148
2472
|
): Promise<PullSinceResponse> {
|
|
@@ -2161,7 +2485,15 @@ export class BroadcastCollectionCoordinator implements PersistedCollectionCoordi
|
|
|
2161
2485
|
}
|
|
2162
2486
|
}
|
|
2163
2487
|
|
|
2164
|
-
const result = await adapter.pullSince(
|
|
2488
|
+
const result = await adapter.pullSince(
|
|
2489
|
+
request.cacheGenerationClaimId
|
|
2490
|
+
? managedStorageTarget(collectionId, request.cacheGenerationClaimId)
|
|
2491
|
+
: eagerStorageTarget(collectionId),
|
|
2492
|
+
request.fromRowVersion,
|
|
2493
|
+
{
|
|
2494
|
+
cacheGenerationClaimId: request.cacheGenerationClaimId,
|
|
2495
|
+
},
|
|
2496
|
+
)
|
|
2165
2497
|
|
|
2166
2498
|
if (result.requiresFullReload) {
|
|
2167
2499
|
return {
|
|
@@ -2315,6 +2647,7 @@ function isRPCRequest(payload: unknown): payload is RPCRequest {
|
|
|
2315
2647
|
case `rpc:ensurePersistedIndex:req`:
|
|
2316
2648
|
case `rpc:applyLocalMutations:req`:
|
|
2317
2649
|
case `rpc:applyCommittedTx:req`:
|
|
2650
|
+
case `rpc:reconcileCommittedTx:req`:
|
|
2318
2651
|
case `rpc:pullSince:req`:
|
|
2319
2652
|
return true
|
|
2320
2653
|
default:
|
|
@@ -2322,6 +2655,12 @@ function isRPCRequest(payload: unknown): payload is RPCRequest {
|
|
|
2322
2655
|
}
|
|
2323
2656
|
}
|
|
2324
2657
|
|
|
2658
|
+
function hasReconciliationTxPayload(value: unknown): boolean {
|
|
2659
|
+
if (!value || typeof value !== `object`) return false
|
|
2660
|
+
const tx = value as Record<string, unknown>
|
|
2661
|
+
return typeof tx.txId === `string` && Array.isArray(tx.mutations)
|
|
2662
|
+
}
|
|
2663
|
+
|
|
2325
2664
|
function sleep(ms: number): Promise<void> {
|
|
2326
2665
|
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
2327
2666
|
}
|
|
@@ -2411,6 +2750,20 @@ function createRPCErrorResponse(
|
|
|
2411
2750
|
code: `CONFLICT`,
|
|
2412
2751
|
error,
|
|
2413
2752
|
}
|
|
2753
|
+
case `rpc:reconcileCommittedTx:req`:
|
|
2754
|
+
return {
|
|
2755
|
+
type: `rpc:reconcileCommittedTx:res`,
|
|
2756
|
+
rpcId: request.rpcId,
|
|
2757
|
+
ok: false,
|
|
2758
|
+
code:
|
|
2759
|
+
cause instanceof PersistedCollectionDurabilityError
|
|
2760
|
+
? `PERSISTENCE_ERROR`
|
|
2761
|
+
: `UNDETERMINED`,
|
|
2762
|
+
error,
|
|
2763
|
+
...(cause instanceof PersistedCollectionDurabilityError
|
|
2764
|
+
? toSafeDurabilityDetails(cause)
|
|
2765
|
+
: {}),
|
|
2766
|
+
}
|
|
2414
2767
|
case `rpc:pullSince:req`:
|
|
2415
2768
|
return {
|
|
2416
2769
|
type: `rpc:pullSince:res`,
|