@tanstack/db-sqlite-persistence-core 0.2.21 → 0.4.0
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 +258 -7
- package/dist/cjs/broadcast-coordinator.cjs +1689 -0
- package/dist/cjs/broadcast-coordinator.cjs.map +1 -0
- package/dist/cjs/broadcast-coordinator.d.cts +116 -0
- package/dist/cjs/errors.cjs +73 -0
- package/dist/cjs/errors.cjs.map +1 -1
- package/dist/cjs/errors.d.cts +44 -0
- package/dist/cjs/index.cjs +19 -0
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +2 -0
- package/dist/cjs/persisted.cjs +1840 -399
- package/dist/cjs/persisted.cjs.map +1 -1
- package/dist/cjs/persisted.d.cts +177 -11
- package/dist/cjs/remote-subset-owner.cjs +20 -0
- package/dist/cjs/remote-subset-owner.cjs.map +1 -0
- package/dist/cjs/remote-subset-owner.d.cts +7 -0
- package/dist/cjs/remote-subset-wire.cjs +681 -0
- package/dist/cjs/remote-subset-wire.cjs.map +1 -0
- package/dist/cjs/remote-subset-wire.d.cts +59 -0
- package/dist/cjs/sqlite-core-adapter.cjs +969 -364
- package/dist/cjs/sqlite-core-adapter.cjs.map +1 -1
- package/dist/cjs/sqlite-core-adapter.d.cts +45 -1
- package/dist/cjs/sqlite-value.cjs +31 -0
- package/dist/cjs/sqlite-value.cjs.map +1 -0
- package/dist/cjs/sqlite-value.d.cts +10 -0
- package/dist/esm/broadcast-coordinator.d.ts +116 -0
- package/dist/esm/broadcast-coordinator.js +1689 -0
- package/dist/esm/broadcast-coordinator.js.map +1 -0
- package/dist/esm/errors.d.ts +44 -0
- package/dist/esm/errors.js +74 -1
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/index.js +21 -2
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/persisted.d.ts +177 -11
- package/dist/esm/persisted.js +1842 -401
- package/dist/esm/persisted.js.map +1 -1
- package/dist/esm/remote-subset-owner.d.ts +7 -0
- package/dist/esm/remote-subset-owner.js +20 -0
- package/dist/esm/remote-subset-owner.js.map +1 -0
- package/dist/esm/remote-subset-wire.d.ts +59 -0
- package/dist/esm/remote-subset-wire.js +681 -0
- package/dist/esm/remote-subset-wire.js.map +1 -0
- package/dist/esm/sqlite-core-adapter.d.ts +45 -1
- package/dist/esm/sqlite-core-adapter.js +955 -350
- package/dist/esm/sqlite-core-adapter.js.map +1 -1
- package/dist/esm/sqlite-value.d.ts +10 -0
- package/dist/esm/sqlite-value.js +31 -0
- package/dist/esm/sqlite-value.js.map +1 -0
- package/package.json +12 -2
- package/src/broadcast-coordinator.ts +2456 -0
- package/src/errors.ts +120 -0
- package/src/index.ts +2 -0
- package/src/persisted.ts +2690 -444
- package/src/remote-subset-owner.ts +36 -0
- package/src/remote-subset-wire.ts +946 -0
- package/src/sqlite-core-adapter.ts +1297 -410
- package/src/sqlite-value.ts +30 -0
|
@@ -0,0 +1,1689 @@
|
|
|
1
|
+
import { safeRandomUUID } from "@tanstack/db";
|
|
2
|
+
import { DuplicateRemoteSubsetOwnerError, RetryableRemoteSubsetAcquisitionError, IndeterminateCommitError, toPersistedCollectionDurabilityError, PersistedCollectionDurabilityError } from "./errors.js";
|
|
3
|
+
import { toTransportedLoadSubsetOptions, toProcessLocalLoadSubsetOptions } from "./remote-subset-wire.js";
|
|
4
|
+
import { unloadRemoteSubsetOwner } from "./remote-subset-owner.js";
|
|
5
|
+
const HEARTBEAT_INTERVAL_MS = 3e3;
|
|
6
|
+
const LEADERSHIP_RETRY_DELAY_MS = 200;
|
|
7
|
+
const RPC_TIMEOUT_MS = 1e4;
|
|
8
|
+
const RPC_RETRY_ATTEMPTS = 2;
|
|
9
|
+
const RPC_RETRY_DELAY_MS = 200;
|
|
10
|
+
const REMOTE_SUBSET_REPLAY_RETRY_ATTEMPTS = 2;
|
|
11
|
+
const RPC_DEDUPE_RETENTION_MS = 6e4;
|
|
12
|
+
const WRITER_LOCK_BUSY_RETRY_MS = 50;
|
|
13
|
+
const WRITER_LOCK_MAX_RETRIES = 20;
|
|
14
|
+
class BroadcastCollectionCoordinator {
|
|
15
|
+
constructor(options) {
|
|
16
|
+
this.nodeId = safeRandomUUID();
|
|
17
|
+
this.collectionAdapters = /* @__PURE__ */ new Map();
|
|
18
|
+
this.remoteSubsetOwners = /* @__PURE__ */ new Map();
|
|
19
|
+
this.remoteSubsetIds = /* @__PURE__ */ new Map();
|
|
20
|
+
this.outboundRemoteSubsetAcquisitions = /* @__PURE__ */ new Map();
|
|
21
|
+
this.inboundRemoteSubsetAcquisitions = /* @__PURE__ */ new Map();
|
|
22
|
+
this.releasedRemoteSubsetAcquisitionTimes = /* @__PURE__ */ new Map();
|
|
23
|
+
this.collections = /* @__PURE__ */ new Map();
|
|
24
|
+
this.pendingRPCs = /* @__PURE__ */ new Map();
|
|
25
|
+
this.appliedEnvelopes = /* @__PURE__ */ new Map();
|
|
26
|
+
this.inFlightEnvelopes = /* @__PURE__ */ new Map();
|
|
27
|
+
this.appliedEnvelopePruneTimer = null;
|
|
28
|
+
this.rejectDisposed = null;
|
|
29
|
+
this.disposed = false;
|
|
30
|
+
this.dbName = options.dbName;
|
|
31
|
+
this.coordinatorName = options.coordinatorName;
|
|
32
|
+
this.defaultAdapter = options.adapter ?? null;
|
|
33
|
+
this.channel = new BroadcastChannel(`tsdb:coord:${this.dbName}`);
|
|
34
|
+
this.channel.onmessage = (event) => {
|
|
35
|
+
this.onChannelMessage(event.data);
|
|
36
|
+
};
|
|
37
|
+
this.disposedPromise = new Promise((_resolve, reject) => {
|
|
38
|
+
this.rejectDisposed = reject;
|
|
39
|
+
});
|
|
40
|
+
void this.disposedPromise.catch(() => void 0);
|
|
41
|
+
}
|
|
42
|
+
/** Method indirection to prevent TypeScript from narrowing `disposed` across awaits */
|
|
43
|
+
isDisposed() {
|
|
44
|
+
return this.disposed;
|
|
45
|
+
}
|
|
46
|
+
/** Method indirection to re-read leadership participation across awaits. */
|
|
47
|
+
participatesInLeadership(state) {
|
|
48
|
+
return state.participatesInLeadership;
|
|
49
|
+
}
|
|
50
|
+
requireAdapter(collectionId) {
|
|
51
|
+
let adapter = this.collectionAdapters.get(collectionId);
|
|
52
|
+
if (!adapter && this.defaultAdapter) {
|
|
53
|
+
adapter = this.defaultAdapter;
|
|
54
|
+
this.collectionAdapters.set(collectionId, adapter);
|
|
55
|
+
}
|
|
56
|
+
if (!adapter) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
`${this.coordinatorName}: adapter not set for collection "${collectionId}". Call setAdapterForCollection() before using leader-side operations.`
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
return adapter;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Set or replace the persistence adapter used for leader-side RPC handling.
|
|
65
|
+
* Called by a host persistence factory to wire the internally-created adapter.
|
|
66
|
+
*/
|
|
67
|
+
setAdapter(adapter) {
|
|
68
|
+
this.defaultAdapter = adapter;
|
|
69
|
+
}
|
|
70
|
+
/** Register the persistence adapter that owns one collection. */
|
|
71
|
+
setAdapterForCollection(collectionId, adapter) {
|
|
72
|
+
this.collectionAdapters.set(collectionId, adapter);
|
|
73
|
+
}
|
|
74
|
+
registerRemoteSubsetOwner(collectionId, owner) {
|
|
75
|
+
if (this.remoteSubsetOwners.has(collectionId)) {
|
|
76
|
+
throw new DuplicateRemoteSubsetOwnerError(collectionId);
|
|
77
|
+
}
|
|
78
|
+
this.remoteSubsetOwners.set(collectionId, owner);
|
|
79
|
+
for (const acquisition of this.outboundRemoteSubsetAcquisitions.values()) {
|
|
80
|
+
if (acquisition.collectionId !== collectionId) continue;
|
|
81
|
+
acquisition.acquiredLeaderId = null;
|
|
82
|
+
acquisition.forceReplay = true;
|
|
83
|
+
}
|
|
84
|
+
void this.replayRemoteSubsetAcquisitions(collectionId);
|
|
85
|
+
this.rebindRemoteInboundSubsetAcquisitions(collectionId, owner);
|
|
86
|
+
return () => {
|
|
87
|
+
if (this.remoteSubsetOwners.get(collectionId) !== owner) return;
|
|
88
|
+
this.remoteSubsetOwners.delete(collectionId);
|
|
89
|
+
this.releaseInboundRemoteSubsetAcquisitions(collectionId, owner);
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
// -----------------------------------------------------------------------
|
|
93
|
+
// PersistedCollectionCoordinator interface
|
|
94
|
+
// -----------------------------------------------------------------------
|
|
95
|
+
getNodeId() {
|
|
96
|
+
return this.nodeId;
|
|
97
|
+
}
|
|
98
|
+
subscribe(collectionId, onMessage) {
|
|
99
|
+
const state = this.ensureCollectionState(collectionId);
|
|
100
|
+
state.subscribers.add(onMessage);
|
|
101
|
+
return () => {
|
|
102
|
+
state.subscribers.delete(onMessage);
|
|
103
|
+
if (state.subscribers.size === 0) {
|
|
104
|
+
state.participatesInLeadership = false;
|
|
105
|
+
this.releaseCollectionIfUnused(collectionId, state);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
publish(_collectionId, message) {
|
|
110
|
+
this.channel.postMessage(message);
|
|
111
|
+
}
|
|
112
|
+
isLeader(collectionId) {
|
|
113
|
+
return this.collections.get(collectionId)?.isLeader ?? false;
|
|
114
|
+
}
|
|
115
|
+
async ensureLeadership(collectionId) {
|
|
116
|
+
const state = this.ensureCollectionState(collectionId);
|
|
117
|
+
if (state.isLeader) return;
|
|
118
|
+
await this.acquireLeadership(collectionId, state);
|
|
119
|
+
}
|
|
120
|
+
async requestEnsureRemoteSubset(collectionId, options) {
|
|
121
|
+
const transportedOptions = toTransportedLoadSubsetOptions(options);
|
|
122
|
+
const localOptions = toProcessLocalLoadSubsetOptions(
|
|
123
|
+
options,
|
|
124
|
+
transportedOptions
|
|
125
|
+
);
|
|
126
|
+
let collectionIds = this.remoteSubsetIds.get(collectionId);
|
|
127
|
+
if (!collectionIds) {
|
|
128
|
+
collectionIds = /* @__PURE__ */ new WeakMap();
|
|
129
|
+
this.remoteSubsetIds.set(collectionId, collectionIds);
|
|
130
|
+
}
|
|
131
|
+
let acquisitionId = collectionIds.get(options);
|
|
132
|
+
let acquisition = acquisitionId ? this.outboundRemoteSubsetAcquisitions.get(
|
|
133
|
+
remoteSubsetAcquisitionKey(collectionId, acquisitionId)
|
|
134
|
+
) : void 0;
|
|
135
|
+
if (!acquisition) {
|
|
136
|
+
acquisitionId = safeRandomUUID();
|
|
137
|
+
collectionIds.set(options, acquisitionId);
|
|
138
|
+
acquisition = {
|
|
139
|
+
collectionId,
|
|
140
|
+
acquisitionId,
|
|
141
|
+
options: transportedOptions,
|
|
142
|
+
localOptions,
|
|
143
|
+
acquiredLeaderId: null,
|
|
144
|
+
inFlight: null,
|
|
145
|
+
release: null,
|
|
146
|
+
releaseRequested: false,
|
|
147
|
+
forceReplay: false,
|
|
148
|
+
retryTimer: null,
|
|
149
|
+
retryAttempts: 0
|
|
150
|
+
};
|
|
151
|
+
this.outboundRemoteSubsetAcquisitions.set(
|
|
152
|
+
remoteSubsetAcquisitionKey(collectionId, acquisitionId),
|
|
153
|
+
acquisition
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
try {
|
|
157
|
+
await this.acquireRemoteSubset(acquisition);
|
|
158
|
+
} catch (error) {
|
|
159
|
+
if (options.signal?.aborted) {
|
|
160
|
+
acquisition.releaseRequested = true;
|
|
161
|
+
this.cancelRemoteSubsetReplayRetry(acquisition);
|
|
162
|
+
this.postRemoteSubsetRelease(acquisition);
|
|
163
|
+
const key = remoteSubsetAcquisitionKey(
|
|
164
|
+
collectionId,
|
|
165
|
+
acquisition.acquisitionId
|
|
166
|
+
);
|
|
167
|
+
if (this.outboundRemoteSubsetAcquisitions.get(key) === acquisition) {
|
|
168
|
+
this.outboundRemoteSubsetAcquisitions.delete(key);
|
|
169
|
+
collectionIds.delete(options);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
async requestReleaseRemoteSubset(collectionId, options) {
|
|
176
|
+
const collectionIds = this.remoteSubsetIds.get(collectionId);
|
|
177
|
+
const acquisitionId = collectionIds?.get(options);
|
|
178
|
+
if (!acquisitionId) return;
|
|
179
|
+
const key = remoteSubsetAcquisitionKey(collectionId, acquisitionId);
|
|
180
|
+
const acquisition = this.outboundRemoteSubsetAcquisitions.get(key);
|
|
181
|
+
if (!acquisition) return;
|
|
182
|
+
acquisition.releaseRequested = true;
|
|
183
|
+
this.cancelRemoteSubsetReplayRetry(acquisition);
|
|
184
|
+
if (!acquisition.release) {
|
|
185
|
+
acquisition.release = (async () => {
|
|
186
|
+
const request = {
|
|
187
|
+
type: `rpc:releaseRemoteSubset:req`,
|
|
188
|
+
rpcId: safeRandomUUID(),
|
|
189
|
+
acquisitionId
|
|
190
|
+
};
|
|
191
|
+
const localLeader = this.isLeader(collectionId);
|
|
192
|
+
let releaseReachedLeader = localLeader;
|
|
193
|
+
try {
|
|
194
|
+
const response = localLeader ? await this.handleReleaseRemoteSubset(
|
|
195
|
+
collectionId,
|
|
196
|
+
request,
|
|
197
|
+
this.nodeId
|
|
198
|
+
) : await this.sendRPC(
|
|
199
|
+
collectionId,
|
|
200
|
+
request
|
|
201
|
+
);
|
|
202
|
+
releaseReachedLeader = true;
|
|
203
|
+
if (!response.ok) {
|
|
204
|
+
throw new Error(`releaseRemoteSubset failed: ${response.error}`);
|
|
205
|
+
}
|
|
206
|
+
} finally {
|
|
207
|
+
if (releaseReachedLeader && this.outboundRemoteSubsetAcquisitions.get(key) === acquisition) {
|
|
208
|
+
this.outboundRemoteSubsetAcquisitions.delete(key);
|
|
209
|
+
collectionIds.delete(options);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
})();
|
|
213
|
+
}
|
|
214
|
+
try {
|
|
215
|
+
await acquisition.release;
|
|
216
|
+
} finally {
|
|
217
|
+
acquisition.release = null;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
async acquireRemoteSubset(acquisition) {
|
|
221
|
+
if (acquisition.inFlight) return acquisition.inFlight;
|
|
222
|
+
const route = { localOwner: false };
|
|
223
|
+
let resolveWork;
|
|
224
|
+
let rejectWork;
|
|
225
|
+
const work = new Promise((resolve, reject) => {
|
|
226
|
+
resolveWork = resolve;
|
|
227
|
+
rejectWork = reject;
|
|
228
|
+
});
|
|
229
|
+
acquisition.inFlight = work;
|
|
230
|
+
const run = async () => {
|
|
231
|
+
const request = {
|
|
232
|
+
type: `rpc:ensureRemoteSubset:req`,
|
|
233
|
+
rpcId: safeRandomUUID(),
|
|
234
|
+
acquisitionId: acquisition.acquisitionId,
|
|
235
|
+
options: acquisition.options
|
|
236
|
+
};
|
|
237
|
+
const state = this.collections.get(acquisition.collectionId);
|
|
238
|
+
if (state?.participatesInLeadership && !state.isLeader && state.leaderId === null) {
|
|
239
|
+
await this.waitForLeadershipRoute(
|
|
240
|
+
acquisition.collectionId,
|
|
241
|
+
state,
|
|
242
|
+
acquisition.localOptions.signal
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
route.localOwner = this.isLeader(acquisition.collectionId);
|
|
246
|
+
let response;
|
|
247
|
+
try {
|
|
248
|
+
response = route.localOwner ? await this.handleEnsureRemoteSubset(
|
|
249
|
+
acquisition.collectionId,
|
|
250
|
+
request,
|
|
251
|
+
this.nodeId,
|
|
252
|
+
acquisition.localOptions
|
|
253
|
+
) : await this.sendRPC(
|
|
254
|
+
acquisition.collectionId,
|
|
255
|
+
request
|
|
256
|
+
);
|
|
257
|
+
} catch (error) {
|
|
258
|
+
if (route.localOwner || error instanceof RetryableRemoteSubsetAcquisitionError) {
|
|
259
|
+
throw error;
|
|
260
|
+
}
|
|
261
|
+
throw new RetryableRemoteSubsetAcquisitionError(
|
|
262
|
+
`Remote subset transport failed`,
|
|
263
|
+
error
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
if (!response.ok) {
|
|
267
|
+
if (response.retryable) {
|
|
268
|
+
throw new RetryableRemoteSubsetAcquisitionError(response.error);
|
|
269
|
+
}
|
|
270
|
+
throw new Error(`ensureRemoteSubset failed: ${response.error}`);
|
|
271
|
+
}
|
|
272
|
+
acquisition.acquiredLeaderId = response.leaderId;
|
|
273
|
+
};
|
|
274
|
+
void run().then(resolveWork, rejectWork);
|
|
275
|
+
let acquired = false;
|
|
276
|
+
try {
|
|
277
|
+
await work;
|
|
278
|
+
acquired = true;
|
|
279
|
+
this.cancelRemoteSubsetReplayRetry(acquisition);
|
|
280
|
+
} finally {
|
|
281
|
+
if (acquisition.inFlight === work) acquisition.inFlight = null;
|
|
282
|
+
const current = this.collections.get(acquisition.collectionId);
|
|
283
|
+
const currentLeaderId = current?.isLeader ? this.nodeId : current?.leaderId ?? null;
|
|
284
|
+
const key = remoteSubsetAcquisitionKey(
|
|
285
|
+
acquisition.collectionId,
|
|
286
|
+
acquisition.acquisitionId
|
|
287
|
+
);
|
|
288
|
+
if (acquired && !acquisition.releaseRequested && this.outboundRemoteSubsetAcquisitions.get(key) === acquisition && (acquisition.forceReplay || currentLeaderId !== null && acquisition.acquiredLeaderId !== currentLeaderId)) {
|
|
289
|
+
acquisition.forceReplay = false;
|
|
290
|
+
void this.acquireRemoteSubset(acquisition).catch((error) => {
|
|
291
|
+
this.scheduleRemoteSubsetReplayRetry(acquisition, error);
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
async requestEnsurePersistedIndex(collectionId, signature, spec, scopedAdapter, localEnsureCompleted = false) {
|
|
297
|
+
if (this.isLeader(collectionId)) {
|
|
298
|
+
if (localEnsureCompleted) return;
|
|
299
|
+
await (scopedAdapter ?? this.requireAdapter(collectionId)).ensureIndex(
|
|
300
|
+
collectionId,
|
|
301
|
+
signature,
|
|
302
|
+
spec
|
|
303
|
+
);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
const response = await this.sendRPC(collectionId, {
|
|
307
|
+
type: `rpc:ensurePersistedIndex:req`,
|
|
308
|
+
rpcId: safeRandomUUID(),
|
|
309
|
+
signature,
|
|
310
|
+
spec
|
|
311
|
+
});
|
|
312
|
+
if (!response.ok) {
|
|
313
|
+
throw new Error(
|
|
314
|
+
`ensurePersistedIndex failed: ${response.error ?? `unknown error`}`
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
async requestApplyLocalMutations(collectionId, mutations) {
|
|
319
|
+
const initialRoute = this.waitForInitialRoute(collectionId);
|
|
320
|
+
if (initialRoute) await initialRoute;
|
|
321
|
+
if (this.isLeader(collectionId)) {
|
|
322
|
+
return this.handleApplyLocalMutations(collectionId, {
|
|
323
|
+
type: `rpc:applyLocalMutations:req`,
|
|
324
|
+
rpcId: safeRandomUUID(),
|
|
325
|
+
envelopeId: safeRandomUUID(),
|
|
326
|
+
mutations
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
return this.sendRPC(collectionId, {
|
|
330
|
+
type: `rpc:applyLocalMutations:req`,
|
|
331
|
+
rpcId: safeRandomUUID(),
|
|
332
|
+
envelopeId: safeRandomUUID(),
|
|
333
|
+
mutations
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
async requestApplyCommittedTx(collectionId, tx, scopedAdapter) {
|
|
337
|
+
const initialRoute = this.waitForInitialRoute(collectionId);
|
|
338
|
+
if (initialRoute) await initialRoute;
|
|
339
|
+
const request = {
|
|
340
|
+
type: `rpc:applyCommittedTx:req`,
|
|
341
|
+
rpcId: safeRandomUUID(),
|
|
342
|
+
envelopeId: safeRandomUUID(),
|
|
343
|
+
tx
|
|
344
|
+
};
|
|
345
|
+
if (this.isLeader(collectionId)) {
|
|
346
|
+
return this.handleApplyCommittedTx(collectionId, request, scopedAdapter);
|
|
347
|
+
}
|
|
348
|
+
return this.sendRPC(collectionId, request);
|
|
349
|
+
}
|
|
350
|
+
async pullSince(collectionId, fromRowVersion, scopedAdapter) {
|
|
351
|
+
if (this.isLeader(collectionId)) {
|
|
352
|
+
return this.handlePullSince(
|
|
353
|
+
collectionId,
|
|
354
|
+
{
|
|
355
|
+
type: `rpc:pullSince:req`,
|
|
356
|
+
rpcId: safeRandomUUID(),
|
|
357
|
+
fromRowVersion
|
|
358
|
+
},
|
|
359
|
+
scopedAdapter
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
return this.sendRPC(collectionId, {
|
|
363
|
+
type: `rpc:pullSince:req`,
|
|
364
|
+
rpcId: safeRandomUUID(),
|
|
365
|
+
fromRowVersion
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
// -----------------------------------------------------------------------
|
|
369
|
+
// Lifecycle
|
|
370
|
+
// -----------------------------------------------------------------------
|
|
371
|
+
dispose() {
|
|
372
|
+
if (this.disposed) return;
|
|
373
|
+
this.disposed = true;
|
|
374
|
+
const disposedError = new Error(`coordinator disposed`);
|
|
375
|
+
this.rejectDisposed?.(disposedError);
|
|
376
|
+
this.rejectDisposed = null;
|
|
377
|
+
for (const acquisition of this.outboundRemoteSubsetAcquisitions.values()) {
|
|
378
|
+
this.cancelRemoteSubsetReplayRetry(acquisition);
|
|
379
|
+
this.postRemoteSubsetRelease(acquisition);
|
|
380
|
+
}
|
|
381
|
+
this.outboundRemoteSubsetAcquisitions.clear();
|
|
382
|
+
this.remoteSubsetIds.clear();
|
|
383
|
+
for (const [collectionId, state] of this.collections) {
|
|
384
|
+
this.releaseLeadership(collectionId, state);
|
|
385
|
+
}
|
|
386
|
+
for (const [, pending] of this.pendingRPCs) {
|
|
387
|
+
clearTimeout(pending.timer);
|
|
388
|
+
pending.reject(disposedError);
|
|
389
|
+
}
|
|
390
|
+
this.pendingRPCs.clear();
|
|
391
|
+
if (this.appliedEnvelopePruneTimer !== null) {
|
|
392
|
+
clearTimeout(this.appliedEnvelopePruneTimer);
|
|
393
|
+
this.appliedEnvelopePruneTimer = null;
|
|
394
|
+
}
|
|
395
|
+
this.channel.close();
|
|
396
|
+
this.collections.clear();
|
|
397
|
+
this.collectionAdapters.clear();
|
|
398
|
+
for (const collectionId of this.remoteSubsetOwners.keys()) {
|
|
399
|
+
this.releaseInboundRemoteSubsetAcquisitions(collectionId);
|
|
400
|
+
}
|
|
401
|
+
this.remoteSubsetOwners.clear();
|
|
402
|
+
this.inboundRemoteSubsetAcquisitions.clear();
|
|
403
|
+
this.releasedRemoteSubsetAcquisitionTimes.clear();
|
|
404
|
+
this.appliedEnvelopes.clear();
|
|
405
|
+
this.inFlightEnvelopes.clear();
|
|
406
|
+
}
|
|
407
|
+
// -----------------------------------------------------------------------
|
|
408
|
+
// Leadership via Web Locks
|
|
409
|
+
// -----------------------------------------------------------------------
|
|
410
|
+
getOrCreateCollectionState(collectionId) {
|
|
411
|
+
let state = this.collections.get(collectionId);
|
|
412
|
+
if (!state) {
|
|
413
|
+
state = {
|
|
414
|
+
participatesInLeadership: false,
|
|
415
|
+
isLeader: false,
|
|
416
|
+
leaderId: null,
|
|
417
|
+
lockAbortController: null,
|
|
418
|
+
heartbeatTimer: null,
|
|
419
|
+
latestTerm: 0,
|
|
420
|
+
latestSeq: 0,
|
|
421
|
+
latestRowVersion: 0,
|
|
422
|
+
subscribers: /* @__PURE__ */ new Set(),
|
|
423
|
+
routeWaiters: /* @__PURE__ */ new Set()
|
|
424
|
+
};
|
|
425
|
+
this.collections.set(collectionId, state);
|
|
426
|
+
}
|
|
427
|
+
return state;
|
|
428
|
+
}
|
|
429
|
+
ensureCollectionState(collectionId) {
|
|
430
|
+
const state = this.getOrCreateCollectionState(collectionId);
|
|
431
|
+
if (!state.participatesInLeadership) {
|
|
432
|
+
state.participatesInLeadership = true;
|
|
433
|
+
void this.acquireLeadership(collectionId, state);
|
|
434
|
+
}
|
|
435
|
+
return state;
|
|
436
|
+
}
|
|
437
|
+
async acquireLeadership(collectionId, state) {
|
|
438
|
+
if (this.disposed || !state.participatesInLeadership || state.isLeader) {
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
const lockName = `tsdb:leader:${this.dbName}:${collectionId}`;
|
|
442
|
+
const abortController = new AbortController();
|
|
443
|
+
state.lockAbortController?.abort();
|
|
444
|
+
state.lockAbortController = abortController;
|
|
445
|
+
try {
|
|
446
|
+
await navigator.locks.request(
|
|
447
|
+
lockName,
|
|
448
|
+
{ signal: abortController.signal },
|
|
449
|
+
async () => {
|
|
450
|
+
if (this.isDisposed() || !this.participatesInLeadership(state) || this.collections.get(collectionId) !== state) {
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
try {
|
|
454
|
+
const adapter = this.requireAdapter(collectionId);
|
|
455
|
+
if (adapter.getStreamPosition) {
|
|
456
|
+
const pos = await adapter.getStreamPosition(collectionId);
|
|
457
|
+
state.latestTerm = pos.latestTerm;
|
|
458
|
+
state.latestSeq = pos.latestSeq;
|
|
459
|
+
state.latestRowVersion = pos.latestRowVersion;
|
|
460
|
+
}
|
|
461
|
+
if (this.isDisposed() || !this.participatesInLeadership(state) || this.collections.get(collectionId) !== state || abortController.signal.aborted || state.lockAbortController !== abortController) {
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
state.latestTerm++;
|
|
465
|
+
state.isLeader = true;
|
|
466
|
+
state.leaderId = this.nodeId;
|
|
467
|
+
this.notifyRouteWaiters(state);
|
|
468
|
+
this.emitHeartbeat(collectionId, state);
|
|
469
|
+
void this.replayRemoteSubsetAcquisitions(collectionId);
|
|
470
|
+
state.heartbeatTimer = setInterval(() => {
|
|
471
|
+
this.emitHeartbeat(collectionId, state);
|
|
472
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
473
|
+
await new Promise((resolve) => {
|
|
474
|
+
const onAbort = () => {
|
|
475
|
+
abortController.signal.removeEventListener(`abort`, onAbort);
|
|
476
|
+
resolve();
|
|
477
|
+
};
|
|
478
|
+
if (abortController.signal.aborted) {
|
|
479
|
+
resolve();
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
abortController.signal.addEventListener(`abort`, onAbort);
|
|
483
|
+
});
|
|
484
|
+
} finally {
|
|
485
|
+
this.releaseInboundRemoteSubsetAcquisitions(collectionId);
|
|
486
|
+
state.isLeader = false;
|
|
487
|
+
state.leaderId = null;
|
|
488
|
+
if (state.heartbeatTimer) {
|
|
489
|
+
clearInterval(state.heartbeatTimer);
|
|
490
|
+
state.heartbeatTimer = null;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
);
|
|
495
|
+
} catch (error) {
|
|
496
|
+
if (error instanceof DOMException && error.name === `AbortError`) {
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
console.warn(`Failed to acquire leadership for ${collectionId}:`, error);
|
|
500
|
+
if (!this.isDisposed() && this.participatesInLeadership(state) && this.collections.get(collectionId) === state) {
|
|
501
|
+
await sleep(LEADERSHIP_RETRY_DELAY_MS);
|
|
502
|
+
}
|
|
503
|
+
} finally {
|
|
504
|
+
if (state.lockAbortController === abortController) {
|
|
505
|
+
state.lockAbortController = null;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
if (!this.isDisposed() && this.participatesInLeadership(state) && this.collections.get(collectionId) === state && state.lockAbortController === null) {
|
|
509
|
+
void this.acquireLeadership(collectionId, state);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
waitForInitialRoute(collectionId) {
|
|
513
|
+
const state = this.collections.get(collectionId);
|
|
514
|
+
if (state?.participatesInLeadership && !state.isLeader && state.leaderId === null) {
|
|
515
|
+
this.channel.postMessage({
|
|
516
|
+
v: 1,
|
|
517
|
+
dbName: this.dbName,
|
|
518
|
+
collectionId,
|
|
519
|
+
senderId: this.nodeId,
|
|
520
|
+
ts: Date.now(),
|
|
521
|
+
payload: { type: `leader:routeRequest` }
|
|
522
|
+
});
|
|
523
|
+
return this.waitForLeadershipRoute(
|
|
524
|
+
collectionId,
|
|
525
|
+
state,
|
|
526
|
+
void 0,
|
|
527
|
+
RPC_TIMEOUT_MS
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
return void 0;
|
|
531
|
+
}
|
|
532
|
+
waitForLeadershipRoute(collectionId, state, signal, timeoutMs) {
|
|
533
|
+
if (state.isLeader || state.leaderId !== null) return Promise.resolve();
|
|
534
|
+
return new Promise((resolve, reject) => {
|
|
535
|
+
let timer;
|
|
536
|
+
const finish = () => {
|
|
537
|
+
state.routeWaiters?.delete(onRouteChange);
|
|
538
|
+
signal?.removeEventListener(`abort`, onAbort);
|
|
539
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
540
|
+
};
|
|
541
|
+
const onAbort = () => {
|
|
542
|
+
finish();
|
|
543
|
+
reject(
|
|
544
|
+
new DOMException(`Remote subset acquisition aborted`, `AbortError`)
|
|
545
|
+
);
|
|
546
|
+
};
|
|
547
|
+
const onRouteChange = () => {
|
|
548
|
+
if (this.isDisposed() || !state.participatesInLeadership) {
|
|
549
|
+
finish();
|
|
550
|
+
reject(
|
|
551
|
+
new Error(
|
|
552
|
+
`${this.coordinatorName}: leadership route closed for collection "${collectionId}"`
|
|
553
|
+
)
|
|
554
|
+
);
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
if (state.isLeader || state.leaderId !== null) {
|
|
558
|
+
finish();
|
|
559
|
+
resolve();
|
|
560
|
+
}
|
|
561
|
+
};
|
|
562
|
+
(state.routeWaiters ??= /* @__PURE__ */ new Set()).add(onRouteChange);
|
|
563
|
+
if (timeoutMs !== void 0) {
|
|
564
|
+
timer = setTimeout(() => {
|
|
565
|
+
finish();
|
|
566
|
+
reject(
|
|
567
|
+
new Error(
|
|
568
|
+
`${this.coordinatorName}: leadership route for collection "${collectionId}" timed out after ${timeoutMs}ms`
|
|
569
|
+
)
|
|
570
|
+
);
|
|
571
|
+
}, timeoutMs);
|
|
572
|
+
}
|
|
573
|
+
signal?.addEventListener(`abort`, onAbort, { once: true });
|
|
574
|
+
if (signal?.aborted) {
|
|
575
|
+
onAbort();
|
|
576
|
+
} else {
|
|
577
|
+
onRouteChange();
|
|
578
|
+
}
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
notifyRouteWaiters(state) {
|
|
582
|
+
for (const notify of Array.from(state.routeWaiters ?? [])) notify();
|
|
583
|
+
}
|
|
584
|
+
releaseLeadership(collectionId, state) {
|
|
585
|
+
this.releaseInboundRemoteSubsetAcquisitions(collectionId);
|
|
586
|
+
if (state.lockAbortController) {
|
|
587
|
+
state.lockAbortController.abort();
|
|
588
|
+
state.lockAbortController = null;
|
|
589
|
+
}
|
|
590
|
+
if (state.heartbeatTimer) {
|
|
591
|
+
clearInterval(state.heartbeatTimer);
|
|
592
|
+
state.heartbeatTimer = null;
|
|
593
|
+
}
|
|
594
|
+
state.isLeader = false;
|
|
595
|
+
state.leaderId = null;
|
|
596
|
+
this.notifyRouteWaiters(state);
|
|
597
|
+
}
|
|
598
|
+
releaseCollectionIfUnused(collectionId, state) {
|
|
599
|
+
if (state.subscribers.size > 0 || this.collections.get(collectionId) !== state) {
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
this.releaseLeadership(collectionId, state);
|
|
603
|
+
this.collections.delete(collectionId);
|
|
604
|
+
this.collectionAdapters.delete(collectionId);
|
|
605
|
+
const prefix = `${JSON.stringify([collectionId]).slice(0, -1)},`;
|
|
606
|
+
for (const key of this.appliedEnvelopes.keys()) {
|
|
607
|
+
if (key.startsWith(prefix)) this.appliedEnvelopes.delete(key);
|
|
608
|
+
}
|
|
609
|
+
this.pruneAppliedEnvelopes();
|
|
610
|
+
}
|
|
611
|
+
postRemoteSubsetRelease(acquisition) {
|
|
612
|
+
if (acquisition.release) return;
|
|
613
|
+
const request = {
|
|
614
|
+
type: `rpc:releaseRemoteSubset:req`,
|
|
615
|
+
rpcId: safeRandomUUID(),
|
|
616
|
+
acquisitionId: acquisition.acquisitionId
|
|
617
|
+
};
|
|
618
|
+
if (this.isLeader(acquisition.collectionId)) {
|
|
619
|
+
void this.handleReleaseRemoteSubset(
|
|
620
|
+
acquisition.collectionId,
|
|
621
|
+
request,
|
|
622
|
+
this.nodeId
|
|
623
|
+
).catch(() => {
|
|
624
|
+
});
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
this.channel.postMessage({
|
|
628
|
+
v: 1,
|
|
629
|
+
dbName: this.dbName,
|
|
630
|
+
collectionId: acquisition.collectionId,
|
|
631
|
+
senderId: this.nodeId,
|
|
632
|
+
ts: Date.now(),
|
|
633
|
+
payload: request
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
cancelRemoteSubsetReplayRetry(acquisition) {
|
|
637
|
+
if (acquisition.retryTimer !== null) {
|
|
638
|
+
clearTimeout(acquisition.retryTimer);
|
|
639
|
+
acquisition.retryTimer = null;
|
|
640
|
+
}
|
|
641
|
+
acquisition.retryAttempts = 0;
|
|
642
|
+
}
|
|
643
|
+
scheduleRemoteSubsetReplayRetry(acquisition, error) {
|
|
644
|
+
const key = remoteSubsetAcquisitionKey(
|
|
645
|
+
acquisition.collectionId,
|
|
646
|
+
acquisition.acquisitionId
|
|
647
|
+
);
|
|
648
|
+
if (!(error instanceof RetryableRemoteSubsetAcquisitionError) || this.isDisposed() || this.outboundRemoteSubsetAcquisitions.get(key) !== acquisition || acquisition.releaseRequested || acquisition.retryTimer !== null || acquisition.retryAttempts >= REMOTE_SUBSET_REPLAY_RETRY_ATTEMPTS) {
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
acquisition.retryAttempts++;
|
|
652
|
+
acquisition.retryTimer = setTimeout(() => {
|
|
653
|
+
acquisition.retryTimer = null;
|
|
654
|
+
if (this.isDisposed() || this.outboundRemoteSubsetAcquisitions.get(key) !== acquisition || acquisition.releaseRequested) {
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
void this.acquireRemoteSubset(acquisition).catch((retryError) => {
|
|
658
|
+
this.scheduleRemoteSubsetReplayRetry(acquisition, retryError);
|
|
659
|
+
});
|
|
660
|
+
}, RPC_RETRY_DELAY_MS);
|
|
661
|
+
}
|
|
662
|
+
async replayRemoteSubsetAcquisitions(collectionId) {
|
|
663
|
+
if (this.isDisposed()) return;
|
|
664
|
+
const state = this.collections.get(collectionId);
|
|
665
|
+
const leaderId = state?.isLeader ? this.nodeId : state?.leaderId;
|
|
666
|
+
if (!leaderId) return;
|
|
667
|
+
const replays = [];
|
|
668
|
+
for (const acquisition of this.outboundRemoteSubsetAcquisitions.values()) {
|
|
669
|
+
if (acquisition.collectionId !== collectionId || acquisition.releaseRequested || !acquisition.forceReplay && acquisition.acquiredLeaderId === leaderId) {
|
|
670
|
+
continue;
|
|
671
|
+
}
|
|
672
|
+
if (acquisition.inFlight) {
|
|
673
|
+
acquisition.forceReplay = true;
|
|
674
|
+
continue;
|
|
675
|
+
}
|
|
676
|
+
acquisition.forceReplay = false;
|
|
677
|
+
replays.push(
|
|
678
|
+
this.acquireRemoteSubset(acquisition).catch((error) => {
|
|
679
|
+
this.scheduleRemoteSubsetReplayRetry(acquisition, error);
|
|
680
|
+
})
|
|
681
|
+
);
|
|
682
|
+
}
|
|
683
|
+
await Promise.all(replays);
|
|
684
|
+
}
|
|
685
|
+
emitHeartbeat(collectionId, state) {
|
|
686
|
+
const envelope = {
|
|
687
|
+
v: 1,
|
|
688
|
+
dbName: this.dbName,
|
|
689
|
+
collectionId,
|
|
690
|
+
senderId: this.nodeId,
|
|
691
|
+
ts: Date.now(),
|
|
692
|
+
payload: {
|
|
693
|
+
type: `leader:heartbeat`,
|
|
694
|
+
term: state.latestTerm,
|
|
695
|
+
leaderId: this.nodeId,
|
|
696
|
+
latestSeq: state.latestSeq,
|
|
697
|
+
latestRowVersion: state.latestRowVersion
|
|
698
|
+
}
|
|
699
|
+
};
|
|
700
|
+
this.channel.postMessage(envelope);
|
|
701
|
+
}
|
|
702
|
+
// -----------------------------------------------------------------------
|
|
703
|
+
// BroadcastChannel message handling
|
|
704
|
+
// -----------------------------------------------------------------------
|
|
705
|
+
onChannelMessage(data) {
|
|
706
|
+
if (!isProtocolEnvelope(data)) return;
|
|
707
|
+
const envelope = data;
|
|
708
|
+
if (envelope.senderId === this.nodeId) return;
|
|
709
|
+
const payload = envelope.payload;
|
|
710
|
+
if (!payload || typeof payload !== `object`) return;
|
|
711
|
+
const type = payload.type;
|
|
712
|
+
if (type === `leader:routeRequest`) {
|
|
713
|
+
const state2 = this.collections.get(envelope.collectionId);
|
|
714
|
+
if (state2?.isLeader) this.emitHeartbeat(envelope.collectionId, state2);
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
if (type === `leader:heartbeat`) {
|
|
718
|
+
const heartbeat = payload;
|
|
719
|
+
if (typeof heartbeat.leaderId === `string` && typeof heartbeat.term === `number` && typeof heartbeat.latestSeq === `number` && typeof heartbeat.latestRowVersion === `number`) {
|
|
720
|
+
const state2 = this.getOrCreateCollectionState(envelope.collectionId);
|
|
721
|
+
if (heartbeat.term < state2.latestTerm) return;
|
|
722
|
+
const changedLeader = state2.leaderId !== heartbeat.leaderId;
|
|
723
|
+
state2.leaderId = heartbeat.leaderId;
|
|
724
|
+
state2.latestTerm = Math.max(state2.latestTerm, heartbeat.term);
|
|
725
|
+
state2.latestSeq = Math.max(state2.latestSeq, heartbeat.latestSeq);
|
|
726
|
+
state2.latestRowVersion = Math.max(
|
|
727
|
+
state2.latestRowVersion,
|
|
728
|
+
heartbeat.latestRowVersion
|
|
729
|
+
);
|
|
730
|
+
this.notifyRouteWaiters(state2);
|
|
731
|
+
if (changedLeader) {
|
|
732
|
+
void this.replayRemoteSubsetAcquisitions(envelope.collectionId);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
if (type && type.endsWith(`:res`)) {
|
|
737
|
+
const rpcId = payload.rpcId;
|
|
738
|
+
if (rpcId && this.pendingRPCs.has(rpcId)) {
|
|
739
|
+
const pending = this.pendingRPCs.get(rpcId);
|
|
740
|
+
this.pendingRPCs.delete(rpcId);
|
|
741
|
+
clearTimeout(pending.timer);
|
|
742
|
+
pending.resolve(payload);
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
if (type && type.endsWith(`:req`)) {
|
|
747
|
+
if (!isRPCRequest(payload)) return;
|
|
748
|
+
const collectionId = envelope.collectionId;
|
|
749
|
+
if (this.isLeader(collectionId)) {
|
|
750
|
+
void this.handleRPCRequest(collectionId, payload, envelope.senderId);
|
|
751
|
+
}
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
const state = this.collections.get(envelope.collectionId);
|
|
755
|
+
if (state) {
|
|
756
|
+
for (const subscriber of state.subscribers) {
|
|
757
|
+
subscriber(envelope);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
// -----------------------------------------------------------------------
|
|
762
|
+
// RPC - Outbound (follower side)
|
|
763
|
+
// -----------------------------------------------------------------------
|
|
764
|
+
async sendRPC(collectionId, request) {
|
|
765
|
+
if (this.isDisposed()) throw new Error(`coordinator disposed`);
|
|
766
|
+
let lastError;
|
|
767
|
+
let firstTransportCause;
|
|
768
|
+
const mutationRequestType = isMutatingRPCRequest(request) ? request.type : void 0;
|
|
769
|
+
const mutationRoute = mutationRequestType ? this.captureMutationRoute(collectionId) : void 0;
|
|
770
|
+
for (let attempt = 0; attempt <= RPC_RETRY_ATTEMPTS; attempt++) {
|
|
771
|
+
if (attempt > 0) {
|
|
772
|
+
await Promise.race([
|
|
773
|
+
sleep(RPC_RETRY_DELAY_MS * attempt),
|
|
774
|
+
this.disposedPromise
|
|
775
|
+
]);
|
|
776
|
+
}
|
|
777
|
+
if (this.isDisposed()) throw new Error(`coordinator disposed`);
|
|
778
|
+
if (mutationRoute && mutationRequestType && firstTransportCause !== void 0) {
|
|
779
|
+
this.assertMutationRouteUnchanged(
|
|
780
|
+
collectionId,
|
|
781
|
+
mutationRequestType,
|
|
782
|
+
mutationRoute,
|
|
783
|
+
firstTransportCause
|
|
784
|
+
);
|
|
785
|
+
}
|
|
786
|
+
if (this.isLeader(collectionId)) {
|
|
787
|
+
return await this.dispatchRPCRequest(
|
|
788
|
+
collectionId,
|
|
789
|
+
request,
|
|
790
|
+
this.nodeId
|
|
791
|
+
);
|
|
792
|
+
}
|
|
793
|
+
try {
|
|
794
|
+
return await this.sendRPCOnce(collectionId, request);
|
|
795
|
+
} catch (error) {
|
|
796
|
+
if (this.isDisposed()) throw error;
|
|
797
|
+
firstTransportCause ??= error;
|
|
798
|
+
if (mutationRoute && mutationRequestType) {
|
|
799
|
+
this.assertMutationRouteUnchanged(
|
|
800
|
+
collectionId,
|
|
801
|
+
mutationRequestType,
|
|
802
|
+
mutationRoute,
|
|
803
|
+
firstTransportCause
|
|
804
|
+
);
|
|
805
|
+
}
|
|
806
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
throw lastError ?? new Error(`RPC failed after retries`);
|
|
810
|
+
}
|
|
811
|
+
captureMutationRoute(collectionId) {
|
|
812
|
+
const state = this.collections.get(collectionId);
|
|
813
|
+
return {
|
|
814
|
+
leaderId: state?.isLeader ? this.nodeId : state?.leaderId ?? null,
|
|
815
|
+
term: state?.latestTerm ?? null
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
assertMutationRouteUnchanged(collectionId, requestType, previous, cause) {
|
|
819
|
+
const current = this.captureMutationRoute(collectionId);
|
|
820
|
+
if (previous.leaderId !== null && previous.term !== null && current.leaderId === previous.leaderId && current.term === previous.term) {
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
throw new IndeterminateCommitError({
|
|
824
|
+
collectionId,
|
|
825
|
+
requestType,
|
|
826
|
+
previousLeaderId: previous.leaderId,
|
|
827
|
+
previousTerm: previous.term,
|
|
828
|
+
currentLeaderId: current.leaderId,
|
|
829
|
+
currentTerm: current.term,
|
|
830
|
+
cause
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
sendRPCOnce(collectionId, request) {
|
|
834
|
+
return new Promise((resolve, reject) => {
|
|
835
|
+
const rpcId = request.rpcId;
|
|
836
|
+
const timer = setTimeout(() => {
|
|
837
|
+
this.pendingRPCs.delete(rpcId);
|
|
838
|
+
reject(
|
|
839
|
+
new Error(`RPC ${request.type} timed out after ${RPC_TIMEOUT_MS}ms`)
|
|
840
|
+
);
|
|
841
|
+
}, RPC_TIMEOUT_MS);
|
|
842
|
+
this.pendingRPCs.set(rpcId, {
|
|
843
|
+
resolve,
|
|
844
|
+
reject,
|
|
845
|
+
timer
|
|
846
|
+
});
|
|
847
|
+
const envelope = {
|
|
848
|
+
v: 1,
|
|
849
|
+
dbName: this.dbName,
|
|
850
|
+
collectionId,
|
|
851
|
+
senderId: this.nodeId,
|
|
852
|
+
ts: Date.now(),
|
|
853
|
+
payload: request
|
|
854
|
+
};
|
|
855
|
+
this.channel.postMessage(envelope);
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
// -----------------------------------------------------------------------
|
|
859
|
+
// RPC - Inbound (leader side)
|
|
860
|
+
// -----------------------------------------------------------------------
|
|
861
|
+
async handleRPCRequest(collectionId, request, requesterId) {
|
|
862
|
+
let response;
|
|
863
|
+
try {
|
|
864
|
+
response = await this.dispatchRPCRequest(
|
|
865
|
+
collectionId,
|
|
866
|
+
request,
|
|
867
|
+
requesterId
|
|
868
|
+
);
|
|
869
|
+
} catch (error) {
|
|
870
|
+
response = createRPCErrorResponse(request, error);
|
|
871
|
+
}
|
|
872
|
+
if (this.isDisposed()) {
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
const envelope = {
|
|
876
|
+
v: 1,
|
|
877
|
+
dbName: this.dbName,
|
|
878
|
+
collectionId,
|
|
879
|
+
senderId: this.nodeId,
|
|
880
|
+
ts: Date.now(),
|
|
881
|
+
payload: response
|
|
882
|
+
};
|
|
883
|
+
this.channel.postMessage(envelope);
|
|
884
|
+
}
|
|
885
|
+
dispatchRPCRequest(collectionId, request, requesterId) {
|
|
886
|
+
switch (request.type) {
|
|
887
|
+
case `rpc:ensureRemoteSubset:req`:
|
|
888
|
+
return this.handleEnsureRemoteSubset(collectionId, request, requesterId);
|
|
889
|
+
case `rpc:releaseRemoteSubset:req`:
|
|
890
|
+
return this.handleReleaseRemoteSubset(
|
|
891
|
+
collectionId,
|
|
892
|
+
request,
|
|
893
|
+
requesterId
|
|
894
|
+
);
|
|
895
|
+
case `rpc:ensurePersistedIndex:req`:
|
|
896
|
+
return this.handleEnsurePersistedIndex(collectionId, request);
|
|
897
|
+
case `rpc:applyLocalMutations:req`:
|
|
898
|
+
return this.handleApplyLocalMutations(collectionId, request);
|
|
899
|
+
case `rpc:applyCommittedTx:req`:
|
|
900
|
+
return this.handleApplyCommittedTx(collectionId, request);
|
|
901
|
+
case `rpc:pullSince:req`:
|
|
902
|
+
return this.handlePullSince(collectionId, request);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
async handleEnsureRemoteSubset(collectionId, request, requesterId, localOptions) {
|
|
906
|
+
this.pruneReleasedRemoteSubsetAcquisitions();
|
|
907
|
+
const key = inboundRemoteSubsetAcquisitionKey(
|
|
908
|
+
collectionId,
|
|
909
|
+
requesterId,
|
|
910
|
+
request.acquisitionId
|
|
911
|
+
);
|
|
912
|
+
const existing = this.inboundRemoteSubsetAcquisitions.get(key);
|
|
913
|
+
const awaitingOwner = existing && `awaitingOwner` in existing ? existing : void 0;
|
|
914
|
+
if (existing) {
|
|
915
|
+
if (`owner` in existing) {
|
|
916
|
+
if (!existing.released) {
|
|
917
|
+
await existing.load;
|
|
918
|
+
return {
|
|
919
|
+
type: `rpc:ensureRemoteSubset:res`,
|
|
920
|
+
rpcId: request.rpcId,
|
|
921
|
+
ok: true,
|
|
922
|
+
leaderId: this.nodeId
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
await existing.release;
|
|
926
|
+
if (existing.terminalRelease) {
|
|
927
|
+
return {
|
|
928
|
+
type: `rpc:ensureRemoteSubset:res`,
|
|
929
|
+
rpcId: request.rpcId,
|
|
930
|
+
ok: true,
|
|
931
|
+
leaderId: this.nodeId
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
if (this.inboundRemoteSubsetAcquisitions.get(key) === existing) {
|
|
935
|
+
this.inboundRemoteSubsetAcquisitions.delete(key);
|
|
936
|
+
}
|
|
937
|
+
} else if (!(`awaitingOwner` in existing)) {
|
|
938
|
+
return {
|
|
939
|
+
type: `rpc:ensureRemoteSubset:res`,
|
|
940
|
+
rpcId: request.rpcId,
|
|
941
|
+
ok: true,
|
|
942
|
+
leaderId: this.nodeId
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
const owner = this.remoteSubsetOwners.get(collectionId);
|
|
947
|
+
if (!owner) {
|
|
948
|
+
throw new RetryableRemoteSubsetAcquisitionError(
|
|
949
|
+
`${this.coordinatorName}: no remote subset owner registered for collection "${collectionId}"`
|
|
950
|
+
);
|
|
951
|
+
}
|
|
952
|
+
await this.startRemoteSubsetOwnerAcquisition({
|
|
953
|
+
collectionId,
|
|
954
|
+
requesterId,
|
|
955
|
+
acquisitionId: request.acquisitionId,
|
|
956
|
+
owner,
|
|
957
|
+
options: awaitingOwner?.options ?? localOptions ?? request.options,
|
|
958
|
+
restoreOnSynchronousFailure: awaitingOwner
|
|
959
|
+
});
|
|
960
|
+
return {
|
|
961
|
+
type: `rpc:ensureRemoteSubset:res`,
|
|
962
|
+
rpcId: request.rpcId,
|
|
963
|
+
ok: true,
|
|
964
|
+
leaderId: this.nodeId
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
async handleReleaseRemoteSubset(collectionId, request, requesterId) {
|
|
968
|
+
this.pruneReleasedRemoteSubsetAcquisitions();
|
|
969
|
+
const key = inboundRemoteSubsetAcquisitionKey(
|
|
970
|
+
collectionId,
|
|
971
|
+
requesterId,
|
|
972
|
+
request.acquisitionId
|
|
973
|
+
);
|
|
974
|
+
const acquisition = this.inboundRemoteSubsetAcquisitions.get(key);
|
|
975
|
+
if (!acquisition) {
|
|
976
|
+
this.setReleasedRemoteSubsetAcquisition(key, {
|
|
977
|
+
collectionId,
|
|
978
|
+
requesterId,
|
|
979
|
+
acquisitionId: request.acquisitionId,
|
|
980
|
+
released: true
|
|
981
|
+
});
|
|
982
|
+
} else if (`owner` in acquisition) {
|
|
983
|
+
acquisition.terminalRelease = true;
|
|
984
|
+
try {
|
|
985
|
+
await this.releaseRemoteSubsetAcquisition(acquisition);
|
|
986
|
+
} finally {
|
|
987
|
+
if (this.inboundRemoteSubsetAcquisitions.get(key) === acquisition) {
|
|
988
|
+
this.setReleasedRemoteSubsetAcquisition(key, {
|
|
989
|
+
collectionId,
|
|
990
|
+
requesterId,
|
|
991
|
+
acquisitionId: request.acquisitionId,
|
|
992
|
+
released: true
|
|
993
|
+
});
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
} else if (`awaitingOwner` in acquisition) {
|
|
997
|
+
this.setReleasedRemoteSubsetAcquisition(key, {
|
|
998
|
+
collectionId,
|
|
999
|
+
requesterId,
|
|
1000
|
+
acquisitionId: request.acquisitionId,
|
|
1001
|
+
released: true
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
return {
|
|
1005
|
+
type: `rpc:releaseRemoteSubset:res`,
|
|
1006
|
+
rpcId: request.rpcId,
|
|
1007
|
+
ok: true
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
1010
|
+
releaseRemoteSubsetAcquisition(acquisition) {
|
|
1011
|
+
if (acquisition.release) return acquisition.release;
|
|
1012
|
+
acquisition.released = true;
|
|
1013
|
+
acquisition.release = (async () => {
|
|
1014
|
+
try {
|
|
1015
|
+
await acquisition.load;
|
|
1016
|
+
} catch {
|
|
1017
|
+
}
|
|
1018
|
+
try {
|
|
1019
|
+
if (acquisition.transferred) {
|
|
1020
|
+
await unloadRemoteSubsetOwner(acquisition.owner, acquisition.options);
|
|
1021
|
+
}
|
|
1022
|
+
} finally {
|
|
1023
|
+
if (!acquisition.terminalRelease) {
|
|
1024
|
+
const key = inboundRemoteSubsetAcquisitionKey(
|
|
1025
|
+
acquisition.collectionId,
|
|
1026
|
+
acquisition.requesterId,
|
|
1027
|
+
acquisition.acquisitionId
|
|
1028
|
+
);
|
|
1029
|
+
if (this.inboundRemoteSubsetAcquisitions.get(key) === acquisition) {
|
|
1030
|
+
this.inboundRemoteSubsetAcquisitions.set(key, {
|
|
1031
|
+
collectionId: acquisition.collectionId,
|
|
1032
|
+
requesterId: acquisition.requesterId,
|
|
1033
|
+
acquisitionId: acquisition.acquisitionId,
|
|
1034
|
+
options: acquisition.options,
|
|
1035
|
+
released: true,
|
|
1036
|
+
awaitingOwner: true
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
})();
|
|
1042
|
+
return acquisition.release;
|
|
1043
|
+
}
|
|
1044
|
+
releaseInboundRemoteSubsetAcquisitions(collectionId, owner) {
|
|
1045
|
+
for (const acquisition of this.inboundRemoteSubsetAcquisitions.values()) {
|
|
1046
|
+
if (!(`owner` in acquisition) || acquisition.collectionId !== collectionId || owner && acquisition.owner !== owner) {
|
|
1047
|
+
continue;
|
|
1048
|
+
}
|
|
1049
|
+
void this.releaseRemoteSubsetAcquisition(acquisition).catch(
|
|
1050
|
+
() => void 0
|
|
1051
|
+
);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
rebindRemoteInboundSubsetAcquisitions(collectionId, owner) {
|
|
1055
|
+
for (const acquisition of this.inboundRemoteSubsetAcquisitions.values()) {
|
|
1056
|
+
if (`awaitingOwner` in acquisition) {
|
|
1057
|
+
if (acquisition.collectionId === collectionId && acquisition.requesterId !== this.nodeId) {
|
|
1058
|
+
void this.bindAwaitingRemoteSubsetAcquisition(
|
|
1059
|
+
acquisition,
|
|
1060
|
+
owner
|
|
1061
|
+
).catch(() => {
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
continue;
|
|
1065
|
+
}
|
|
1066
|
+
if (!(`owner` in acquisition) || acquisition.collectionId !== collectionId || acquisition.requesterId === this.nodeId || !acquisition.released || acquisition.terminalRelease) {
|
|
1067
|
+
continue;
|
|
1068
|
+
}
|
|
1069
|
+
void this.rebindRemoteInboundSubsetAcquisition(acquisition, owner).catch(
|
|
1070
|
+
() => void 0
|
|
1071
|
+
);
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
async rebindRemoteInboundSubsetAcquisition(previous, owner) {
|
|
1075
|
+
await previous.release;
|
|
1076
|
+
if (previous.terminalRelease || this.remoteSubsetOwners.get(previous.collectionId) !== owner) {
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
const key = inboundRemoteSubsetAcquisitionKey(
|
|
1080
|
+
previous.collectionId,
|
|
1081
|
+
previous.requesterId,
|
|
1082
|
+
previous.acquisitionId
|
|
1083
|
+
);
|
|
1084
|
+
const current = this.inboundRemoteSubsetAcquisitions.get(key);
|
|
1085
|
+
if (current && current !== previous) {
|
|
1086
|
+
if (`awaitingOwner` in current) {
|
|
1087
|
+
await this.bindAwaitingRemoteSubsetAcquisition(current, owner);
|
|
1088
|
+
}
|
|
1089
|
+
return;
|
|
1090
|
+
}
|
|
1091
|
+
const awaitingOwner = {
|
|
1092
|
+
collectionId: previous.collectionId,
|
|
1093
|
+
requesterId: previous.requesterId,
|
|
1094
|
+
acquisitionId: previous.acquisitionId,
|
|
1095
|
+
options: previous.options,
|
|
1096
|
+
released: true,
|
|
1097
|
+
awaitingOwner: true
|
|
1098
|
+
};
|
|
1099
|
+
this.inboundRemoteSubsetAcquisitions.set(key, awaitingOwner);
|
|
1100
|
+
await this.bindAwaitingRemoteSubsetAcquisition(awaitingOwner, owner);
|
|
1101
|
+
}
|
|
1102
|
+
async bindAwaitingRemoteSubsetAcquisition(awaitingOwner, owner) {
|
|
1103
|
+
if (this.remoteSubsetOwners.get(awaitingOwner.collectionId) !== owner) {
|
|
1104
|
+
return;
|
|
1105
|
+
}
|
|
1106
|
+
const key = inboundRemoteSubsetAcquisitionKey(
|
|
1107
|
+
awaitingOwner.collectionId,
|
|
1108
|
+
awaitingOwner.requesterId,
|
|
1109
|
+
awaitingOwner.acquisitionId
|
|
1110
|
+
);
|
|
1111
|
+
if (this.inboundRemoteSubsetAcquisitions.get(key) !== awaitingOwner) return;
|
|
1112
|
+
await this.startRemoteSubsetOwnerAcquisition({
|
|
1113
|
+
collectionId: awaitingOwner.collectionId,
|
|
1114
|
+
requesterId: awaitingOwner.requesterId,
|
|
1115
|
+
acquisitionId: awaitingOwner.acquisitionId,
|
|
1116
|
+
owner,
|
|
1117
|
+
options: awaitingOwner.options,
|
|
1118
|
+
restoreOnSynchronousFailure: awaitingOwner
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
async startRemoteSubsetOwnerAcquisition(options) {
|
|
1122
|
+
const key = inboundRemoteSubsetAcquisitionKey(
|
|
1123
|
+
options.collectionId,
|
|
1124
|
+
options.requesterId,
|
|
1125
|
+
options.acquisitionId
|
|
1126
|
+
);
|
|
1127
|
+
const acquisition = {
|
|
1128
|
+
collectionId: options.collectionId,
|
|
1129
|
+
requesterId: options.requesterId,
|
|
1130
|
+
acquisitionId: options.acquisitionId,
|
|
1131
|
+
owner: options.owner,
|
|
1132
|
+
options: options.options,
|
|
1133
|
+
load: Promise.resolve(),
|
|
1134
|
+
transferred: false,
|
|
1135
|
+
released: false,
|
|
1136
|
+
terminalRelease: false,
|
|
1137
|
+
release: null
|
|
1138
|
+
};
|
|
1139
|
+
this.releasedRemoteSubsetAcquisitionTimes.delete(key);
|
|
1140
|
+
this.inboundRemoteSubsetAcquisitions.set(key, acquisition);
|
|
1141
|
+
let resolveLoad;
|
|
1142
|
+
let rejectLoad;
|
|
1143
|
+
acquisition.load = new Promise((resolve, reject) => {
|
|
1144
|
+
resolveLoad = resolve;
|
|
1145
|
+
rejectLoad = reject;
|
|
1146
|
+
});
|
|
1147
|
+
try {
|
|
1148
|
+
const load = options.owner(acquisition.options);
|
|
1149
|
+
acquisition.transferred = true;
|
|
1150
|
+
void Promise.resolve(load).then(resolveLoad, rejectLoad);
|
|
1151
|
+
} catch (error) {
|
|
1152
|
+
rejectLoad(error);
|
|
1153
|
+
}
|
|
1154
|
+
try {
|
|
1155
|
+
await acquisition.load;
|
|
1156
|
+
} catch (error) {
|
|
1157
|
+
if (!acquisition.transferred) {
|
|
1158
|
+
if (this.inboundRemoteSubsetAcquisitions.get(key) === acquisition) {
|
|
1159
|
+
if (options.restoreOnSynchronousFailure) {
|
|
1160
|
+
this.inboundRemoteSubsetAcquisitions.set(
|
|
1161
|
+
key,
|
|
1162
|
+
options.restoreOnSynchronousFailure
|
|
1163
|
+
);
|
|
1164
|
+
} else {
|
|
1165
|
+
this.inboundRemoteSubsetAcquisitions.delete(key);
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
} else {
|
|
1169
|
+
await this.releaseRemoteSubsetAcquisition(acquisition).catch(
|
|
1170
|
+
() => void 0
|
|
1171
|
+
);
|
|
1172
|
+
}
|
|
1173
|
+
throw error;
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
async handleEnsurePersistedIndex(collectionId, request) {
|
|
1177
|
+
await this.withScheduledWriterLock(
|
|
1178
|
+
collectionId,
|
|
1179
|
+
(adapter) => adapter.ensureIndex(collectionId, request.signature, request.spec)
|
|
1180
|
+
);
|
|
1181
|
+
return {
|
|
1182
|
+
type: `rpc:ensurePersistedIndex:res`,
|
|
1183
|
+
rpcId: request.rpcId,
|
|
1184
|
+
ok: true
|
|
1185
|
+
};
|
|
1186
|
+
}
|
|
1187
|
+
async handleApplyLocalMutations(collectionId, request) {
|
|
1188
|
+
const envelopeKey = appliedEnvelopeKey(collectionId, request.envelopeId);
|
|
1189
|
+
return this.runDeduplicatedEnvelope({
|
|
1190
|
+
envelopeKey,
|
|
1191
|
+
rpcId: request.rpcId,
|
|
1192
|
+
selectApplied: (envelope) => envelope.requestType === `rpc:applyLocalMutations:req` ? envelope.response : void 0,
|
|
1193
|
+
selectInFlight: (envelope) => envelope.requestType === `rpc:applyLocalMutations:req` ? envelope.response : void 0,
|
|
1194
|
+
conflict: (phase) => ({
|
|
1195
|
+
type: `rpc:applyLocalMutations:res`,
|
|
1196
|
+
rpcId: request.rpcId,
|
|
1197
|
+
ok: false,
|
|
1198
|
+
code: `CONFLICT`,
|
|
1199
|
+
error: `envelope ${request.envelopeId} ${phase}`
|
|
1200
|
+
}),
|
|
1201
|
+
apply: () => this.applyLocalMutationsOnce(collectionId, request),
|
|
1202
|
+
createPending: (response) => ({
|
|
1203
|
+
requestType: request.type,
|
|
1204
|
+
response
|
|
1205
|
+
})
|
|
1206
|
+
});
|
|
1207
|
+
}
|
|
1208
|
+
async applyLocalMutationsOnce(collectionId, request) {
|
|
1209
|
+
const state = this.collections.get(collectionId);
|
|
1210
|
+
if (!state || !state.isLeader) {
|
|
1211
|
+
return {
|
|
1212
|
+
type: `rpc:applyLocalMutations:res`,
|
|
1213
|
+
rpcId: request.rpcId,
|
|
1214
|
+
ok: false,
|
|
1215
|
+
code: `NOT_LEADER`,
|
|
1216
|
+
error: `not the leader for ${collectionId}`
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
const rowMetadataMutations = [];
|
|
1220
|
+
for (const mutation of request.mutations) {
|
|
1221
|
+
if (!(`metadataChanged` in mutation) || !mutation.metadataChanged) {
|
|
1222
|
+
continue;
|
|
1223
|
+
}
|
|
1224
|
+
rowMetadataMutations.push(
|
|
1225
|
+
mutation.metadata === void 0 ? { type: `delete`, key: mutation.key } : {
|
|
1226
|
+
type: `set`,
|
|
1227
|
+
key: mutation.key,
|
|
1228
|
+
value: mutation.metadata
|
|
1229
|
+
}
|
|
1230
|
+
);
|
|
1231
|
+
}
|
|
1232
|
+
const pendingTx = {
|
|
1233
|
+
txId: safeRandomUUID(),
|
|
1234
|
+
mutations: request.mutations.map((m) => ({
|
|
1235
|
+
type: m.type,
|
|
1236
|
+
key: m.key,
|
|
1237
|
+
value: m.value,
|
|
1238
|
+
...`metadataChanged` in m ? { metadata: m.metadata, metadataChanged: m.metadataChanged } : {}
|
|
1239
|
+
})),
|
|
1240
|
+
rowMetadataMutations
|
|
1241
|
+
};
|
|
1242
|
+
const tx = await this.applyDurablyAtNextStreamPosition(
|
|
1243
|
+
collectionId,
|
|
1244
|
+
state,
|
|
1245
|
+
pendingTx
|
|
1246
|
+
);
|
|
1247
|
+
if (tx === null) {
|
|
1248
|
+
return {
|
|
1249
|
+
type: `rpc:applyLocalMutations:res`,
|
|
1250
|
+
rpcId: request.rpcId,
|
|
1251
|
+
ok: false,
|
|
1252
|
+
code: `NOT_LEADER`,
|
|
1253
|
+
error: `not the leader for ${collectionId}`
|
|
1254
|
+
};
|
|
1255
|
+
}
|
|
1256
|
+
const response = {
|
|
1257
|
+
type: `rpc:applyLocalMutations:res`,
|
|
1258
|
+
rpcId: request.rpcId,
|
|
1259
|
+
ok: true,
|
|
1260
|
+
term: tx.term,
|
|
1261
|
+
seq: tx.seq,
|
|
1262
|
+
latestRowVersion: tx.rowVersion,
|
|
1263
|
+
acceptedMutationIds: request.mutations.map((m) => m.mutationId)
|
|
1264
|
+
};
|
|
1265
|
+
if (this.isDisposed() || this.collections.get(collectionId) !== state) {
|
|
1266
|
+
return response;
|
|
1267
|
+
}
|
|
1268
|
+
this.appliedEnvelopes.set(
|
|
1269
|
+
appliedEnvelopeKey(collectionId, request.envelopeId),
|
|
1270
|
+
{
|
|
1271
|
+
appliedAt: Date.now(),
|
|
1272
|
+
requestType: request.type,
|
|
1273
|
+
response
|
|
1274
|
+
}
|
|
1275
|
+
);
|
|
1276
|
+
this.pruneAppliedEnvelopes();
|
|
1277
|
+
const changedRows = request.mutations.filter((m) => m.type !== `delete`).map((m) => ({ key: m.key, value: m.value }));
|
|
1278
|
+
const deletedKeys = request.mutations.filter((m) => m.type === `delete`).map((m) => m.key);
|
|
1279
|
+
const txCommitted = {
|
|
1280
|
+
v: 1,
|
|
1281
|
+
dbName: this.dbName,
|
|
1282
|
+
collectionId,
|
|
1283
|
+
senderId: this.nodeId,
|
|
1284
|
+
ts: Date.now(),
|
|
1285
|
+
payload: {
|
|
1286
|
+
type: `tx:committed`,
|
|
1287
|
+
term: tx.term,
|
|
1288
|
+
seq: tx.seq,
|
|
1289
|
+
txId: tx.txId,
|
|
1290
|
+
latestRowVersion: tx.rowVersion,
|
|
1291
|
+
requiresFullReload: false,
|
|
1292
|
+
changedRows,
|
|
1293
|
+
deletedKeys,
|
|
1294
|
+
rowMetadataMutations
|
|
1295
|
+
}
|
|
1296
|
+
};
|
|
1297
|
+
this.channel.postMessage(txCommitted);
|
|
1298
|
+
for (const subscriber of state.subscribers) {
|
|
1299
|
+
subscriber(txCommitted);
|
|
1300
|
+
}
|
|
1301
|
+
return response;
|
|
1302
|
+
}
|
|
1303
|
+
async handleApplyCommittedTx(collectionId, request, scopedAdapter) {
|
|
1304
|
+
const envelopeKey = appliedEnvelopeKey(collectionId, request.envelopeId);
|
|
1305
|
+
return this.runDeduplicatedEnvelope({
|
|
1306
|
+
envelopeKey,
|
|
1307
|
+
rpcId: request.rpcId,
|
|
1308
|
+
selectApplied: (envelope) => envelope.requestType === `rpc:applyCommittedTx:req` ? envelope.response : void 0,
|
|
1309
|
+
selectInFlight: (envelope) => envelope.requestType === `rpc:applyCommittedTx:req` ? envelope.response : void 0,
|
|
1310
|
+
conflict: (phase) => ({
|
|
1311
|
+
type: `rpc:applyCommittedTx:res`,
|
|
1312
|
+
rpcId: request.rpcId,
|
|
1313
|
+
ok: false,
|
|
1314
|
+
code: `CONFLICT`,
|
|
1315
|
+
error: `envelope ${request.envelopeId} ${phase}`
|
|
1316
|
+
}),
|
|
1317
|
+
apply: () => this.applyCommittedTxOnce(collectionId, request, scopedAdapter),
|
|
1318
|
+
createPending: (response) => ({
|
|
1319
|
+
requestType: request.type,
|
|
1320
|
+
response
|
|
1321
|
+
})
|
|
1322
|
+
});
|
|
1323
|
+
}
|
|
1324
|
+
async runDeduplicatedEnvelope(options) {
|
|
1325
|
+
const appliedEnvelope = this.appliedEnvelopes.get(options.envelopeKey);
|
|
1326
|
+
if (appliedEnvelope) {
|
|
1327
|
+
const response2 = options.selectApplied(appliedEnvelope);
|
|
1328
|
+
return response2 ? { ...response2, rpcId: options.rpcId } : options.conflict(`already applied`);
|
|
1329
|
+
}
|
|
1330
|
+
const inFlightEnvelope = this.inFlightEnvelopes.get(options.envelopeKey);
|
|
1331
|
+
if (inFlightEnvelope) {
|
|
1332
|
+
const pending = options.selectInFlight(inFlightEnvelope);
|
|
1333
|
+
if (!pending) return options.conflict(`is already in flight`);
|
|
1334
|
+
const response2 = await pending;
|
|
1335
|
+
return { ...response2, rpcId: options.rpcId };
|
|
1336
|
+
}
|
|
1337
|
+
const response = options.apply();
|
|
1338
|
+
const pendingEnvelope = options.createPending(response);
|
|
1339
|
+
this.inFlightEnvelopes.set(options.envelopeKey, pendingEnvelope);
|
|
1340
|
+
try {
|
|
1341
|
+
return await response;
|
|
1342
|
+
} finally {
|
|
1343
|
+
if (this.inFlightEnvelopes.get(options.envelopeKey) === pendingEnvelope) {
|
|
1344
|
+
this.inFlightEnvelopes.delete(options.envelopeKey);
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
async applyCommittedTxOnce(collectionId, request, scopedAdapter) {
|
|
1349
|
+
const state = this.collections.get(collectionId);
|
|
1350
|
+
if (!state || !state.isLeader) {
|
|
1351
|
+
return {
|
|
1352
|
+
type: `rpc:applyCommittedTx:res`,
|
|
1353
|
+
rpcId: request.rpcId,
|
|
1354
|
+
ok: false,
|
|
1355
|
+
code: `NOT_LEADER`,
|
|
1356
|
+
error: `not the leader for ${collectionId}`
|
|
1357
|
+
};
|
|
1358
|
+
}
|
|
1359
|
+
const tx = await this.applyDurablyAtNextStreamPosition(
|
|
1360
|
+
collectionId,
|
|
1361
|
+
state,
|
|
1362
|
+
request.tx,
|
|
1363
|
+
scopedAdapter
|
|
1364
|
+
);
|
|
1365
|
+
if (tx === null) {
|
|
1366
|
+
return {
|
|
1367
|
+
type: `rpc:applyCommittedTx:res`,
|
|
1368
|
+
rpcId: request.rpcId,
|
|
1369
|
+
ok: false,
|
|
1370
|
+
code: `NOT_LEADER`,
|
|
1371
|
+
error: `not the leader for ${collectionId}`
|
|
1372
|
+
};
|
|
1373
|
+
}
|
|
1374
|
+
const response = {
|
|
1375
|
+
type: `rpc:applyCommittedTx:res`,
|
|
1376
|
+
rpcId: request.rpcId,
|
|
1377
|
+
ok: true,
|
|
1378
|
+
term: tx.term,
|
|
1379
|
+
seq: tx.seq,
|
|
1380
|
+
latestRowVersion: tx.rowVersion
|
|
1381
|
+
};
|
|
1382
|
+
if (this.isDisposed() || this.collections.get(collectionId) !== state) {
|
|
1383
|
+
return response;
|
|
1384
|
+
}
|
|
1385
|
+
this.appliedEnvelopes.set(
|
|
1386
|
+
appliedEnvelopeKey(collectionId, request.envelopeId),
|
|
1387
|
+
{
|
|
1388
|
+
appliedAt: Date.now(),
|
|
1389
|
+
requestType: request.type,
|
|
1390
|
+
response
|
|
1391
|
+
}
|
|
1392
|
+
);
|
|
1393
|
+
this.pruneAppliedEnvelopes();
|
|
1394
|
+
const committedBase = {
|
|
1395
|
+
type: `tx:committed`,
|
|
1396
|
+
term: tx.term,
|
|
1397
|
+
seq: tx.seq,
|
|
1398
|
+
txId: tx.txId,
|
|
1399
|
+
latestRowVersion: tx.rowVersion
|
|
1400
|
+
};
|
|
1401
|
+
const committedPayload = tx.truncate ? {
|
|
1402
|
+
...committedBase,
|
|
1403
|
+
requiresFullReload: true
|
|
1404
|
+
} : {
|
|
1405
|
+
...committedBase,
|
|
1406
|
+
requiresFullReload: false,
|
|
1407
|
+
changedRows: tx.mutations.filter((mutation) => mutation.type !== `delete`).map((mutation) => ({
|
|
1408
|
+
key: mutation.key,
|
|
1409
|
+
value: mutation.value
|
|
1410
|
+
})),
|
|
1411
|
+
deletedKeys: tx.mutations.filter((mutation) => mutation.type === `delete`).map((mutation) => mutation.key),
|
|
1412
|
+
rowMetadataMutations: tx.rowMetadataMutations,
|
|
1413
|
+
collectionMetadataMutations: tx.collectionMetadataMutations
|
|
1414
|
+
};
|
|
1415
|
+
const committed = {
|
|
1416
|
+
v: 1,
|
|
1417
|
+
dbName: this.dbName,
|
|
1418
|
+
collectionId,
|
|
1419
|
+
senderId: this.nodeId,
|
|
1420
|
+
ts: Date.now(),
|
|
1421
|
+
payload: committedPayload
|
|
1422
|
+
};
|
|
1423
|
+
this.channel.postMessage(committed);
|
|
1424
|
+
for (const subscriber of state.subscribers) {
|
|
1425
|
+
subscriber(committed);
|
|
1426
|
+
}
|
|
1427
|
+
return response;
|
|
1428
|
+
}
|
|
1429
|
+
async applyDurablyAtNextStreamPosition(collectionId, state, pendingTx, scopedAdapter) {
|
|
1430
|
+
const expectedTerm = state.latestTerm;
|
|
1431
|
+
return this.withScheduledWriterLock(
|
|
1432
|
+
collectionId,
|
|
1433
|
+
async (adapter) => {
|
|
1434
|
+
if (this.isDisposed() || this.collections.get(collectionId) !== state || !state.isLeader || state.leaderId !== this.nodeId || state.latestTerm !== expectedTerm) {
|
|
1435
|
+
return null;
|
|
1436
|
+
}
|
|
1437
|
+
const tx = {
|
|
1438
|
+
...pendingTx,
|
|
1439
|
+
term: state.latestTerm,
|
|
1440
|
+
seq: state.latestSeq + 1,
|
|
1441
|
+
rowVersion: state.latestRowVersion + 1
|
|
1442
|
+
};
|
|
1443
|
+
try {
|
|
1444
|
+
await adapter.applyCommittedTx(collectionId, tx);
|
|
1445
|
+
} catch (error) {
|
|
1446
|
+
throw toPersistedCollectionDurabilityError(collectionId, error);
|
|
1447
|
+
}
|
|
1448
|
+
state.latestSeq = tx.seq;
|
|
1449
|
+
state.latestRowVersion = tx.rowVersion;
|
|
1450
|
+
return tx;
|
|
1451
|
+
},
|
|
1452
|
+
scopedAdapter
|
|
1453
|
+
);
|
|
1454
|
+
}
|
|
1455
|
+
async handlePullSince(collectionId, request, scopedAdapter) {
|
|
1456
|
+
const state = this.collections.get(collectionId);
|
|
1457
|
+
const adapter = scopedAdapter ?? this.requireAdapter(collectionId);
|
|
1458
|
+
if (!adapter.pullSince) {
|
|
1459
|
+
return {
|
|
1460
|
+
type: `rpc:pullSince:res`,
|
|
1461
|
+
rpcId: request.rpcId,
|
|
1462
|
+
ok: true,
|
|
1463
|
+
latestTerm: state?.latestTerm ?? 0,
|
|
1464
|
+
latestSeq: state?.latestSeq ?? 0,
|
|
1465
|
+
latestRowVersion: state?.latestRowVersion ?? 0,
|
|
1466
|
+
requiresFullReload: true
|
|
1467
|
+
};
|
|
1468
|
+
}
|
|
1469
|
+
const result = await adapter.pullSince(collectionId, request.fromRowVersion);
|
|
1470
|
+
if (result.requiresFullReload) {
|
|
1471
|
+
return {
|
|
1472
|
+
type: `rpc:pullSince:res`,
|
|
1473
|
+
rpcId: request.rpcId,
|
|
1474
|
+
ok: true,
|
|
1475
|
+
latestTerm: state?.latestTerm ?? 0,
|
|
1476
|
+
latestSeq: state?.latestSeq ?? 0,
|
|
1477
|
+
latestRowVersion: result.latestRowVersion,
|
|
1478
|
+
requiresFullReload: true
|
|
1479
|
+
};
|
|
1480
|
+
}
|
|
1481
|
+
return {
|
|
1482
|
+
type: `rpc:pullSince:res`,
|
|
1483
|
+
rpcId: request.rpcId,
|
|
1484
|
+
ok: true,
|
|
1485
|
+
latestTerm: state?.latestTerm ?? 0,
|
|
1486
|
+
latestSeq: state?.latestSeq ?? 0,
|
|
1487
|
+
latestRowVersion: result.latestRowVersion,
|
|
1488
|
+
requiresFullReload: false,
|
|
1489
|
+
changedKeys: result.changedKeys,
|
|
1490
|
+
deletedKeys: result.deletedKeys
|
|
1491
|
+
};
|
|
1492
|
+
}
|
|
1493
|
+
// -----------------------------------------------------------------------
|
|
1494
|
+
// DB Writer Lock
|
|
1495
|
+
// -----------------------------------------------------------------------
|
|
1496
|
+
withScheduledWriterLock(collectionId, task, scopedAdapter) {
|
|
1497
|
+
if (scopedAdapter) {
|
|
1498
|
+
return this.withWriterLock(() => task(scopedAdapter));
|
|
1499
|
+
}
|
|
1500
|
+
const adapter = this.requireAdapter(collectionId);
|
|
1501
|
+
return adapter.runInRegularScope ? adapter.runInRegularScope(
|
|
1502
|
+
(unscheduledAdapter) => this.withWriterLock(() => task(unscheduledAdapter))
|
|
1503
|
+
) : this.withWriterLock(() => task(adapter));
|
|
1504
|
+
}
|
|
1505
|
+
async withWriterLock(fn) {
|
|
1506
|
+
const lockName = `tsdb:writer:${this.dbName}`;
|
|
1507
|
+
for (let attempt = 0; attempt <= WRITER_LOCK_MAX_RETRIES; attempt++) {
|
|
1508
|
+
const callbackState = { entered: false };
|
|
1509
|
+
try {
|
|
1510
|
+
return await navigator.locks.request(lockName, async () => {
|
|
1511
|
+
callbackState.entered = true;
|
|
1512
|
+
return fn();
|
|
1513
|
+
});
|
|
1514
|
+
} catch (error) {
|
|
1515
|
+
if (callbackState.entered) {
|
|
1516
|
+
throw error;
|
|
1517
|
+
}
|
|
1518
|
+
if (error instanceof DOMException && error.name === `AbortError`) {
|
|
1519
|
+
throw error;
|
|
1520
|
+
}
|
|
1521
|
+
if (attempt < WRITER_LOCK_MAX_RETRIES) {
|
|
1522
|
+
await sleep(WRITER_LOCK_BUSY_RETRY_MS * Math.min(attempt + 1, 5));
|
|
1523
|
+
continue;
|
|
1524
|
+
}
|
|
1525
|
+
throw error;
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
throw new Error(`writer lock acquisition failed`);
|
|
1529
|
+
}
|
|
1530
|
+
// -----------------------------------------------------------------------
|
|
1531
|
+
// Helpers
|
|
1532
|
+
// -----------------------------------------------------------------------
|
|
1533
|
+
pruneAppliedEnvelopes() {
|
|
1534
|
+
if (this.appliedEnvelopePruneTimer !== null) {
|
|
1535
|
+
clearTimeout(this.appliedEnvelopePruneTimer);
|
|
1536
|
+
this.appliedEnvelopePruneTimer = null;
|
|
1537
|
+
}
|
|
1538
|
+
const cutoff = Date.now() - RPC_DEDUPE_RETENTION_MS;
|
|
1539
|
+
for (const [key, envelope] of this.appliedEnvelopes) {
|
|
1540
|
+
if (envelope.appliedAt <= cutoff) {
|
|
1541
|
+
this.appliedEnvelopes.delete(key);
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
if (this.disposed || this.appliedEnvelopes.size === 0) return;
|
|
1545
|
+
let earliestAppliedAt = Number.POSITIVE_INFINITY;
|
|
1546
|
+
for (const envelope of this.appliedEnvelopes.values()) {
|
|
1547
|
+
earliestAppliedAt = Math.min(earliestAppliedAt, envelope.appliedAt);
|
|
1548
|
+
}
|
|
1549
|
+
const delay = Math.max(
|
|
1550
|
+
0,
|
|
1551
|
+
earliestAppliedAt + RPC_DEDUPE_RETENTION_MS - Date.now()
|
|
1552
|
+
);
|
|
1553
|
+
this.appliedEnvelopePruneTimer = setTimeout(() => {
|
|
1554
|
+
this.appliedEnvelopePruneTimer = null;
|
|
1555
|
+
this.pruneAppliedEnvelopes();
|
|
1556
|
+
}, delay);
|
|
1557
|
+
}
|
|
1558
|
+
setReleasedRemoteSubsetAcquisition(key, acquisition) {
|
|
1559
|
+
this.inboundRemoteSubsetAcquisitions.set(key, acquisition);
|
|
1560
|
+
this.releasedRemoteSubsetAcquisitionTimes.set(key, Date.now());
|
|
1561
|
+
}
|
|
1562
|
+
pruneReleasedRemoteSubsetAcquisitions() {
|
|
1563
|
+
const cutoff = Date.now() - RPC_DEDUPE_RETENTION_MS;
|
|
1564
|
+
for (const [key, releasedAt] of this.releasedRemoteSubsetAcquisitionTimes) {
|
|
1565
|
+
if (releasedAt < cutoff) {
|
|
1566
|
+
this.releasedRemoteSubsetAcquisitionTimes.delete(key);
|
|
1567
|
+
this.inboundRemoteSubsetAcquisitions.delete(key);
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
function isProtocolEnvelope(data) {
|
|
1573
|
+
if (!data || typeof data !== `object`) return false;
|
|
1574
|
+
const record = data;
|
|
1575
|
+
return record.v === 1 && typeof record.dbName === `string` && typeof record.collectionId === `string` && typeof record.senderId === `string` && typeof record.ts === `number`;
|
|
1576
|
+
}
|
|
1577
|
+
function isRPCRequest(payload) {
|
|
1578
|
+
if (!payload || typeof payload !== `object`) return false;
|
|
1579
|
+
switch (payload.type) {
|
|
1580
|
+
case `rpc:ensureRemoteSubset:req`:
|
|
1581
|
+
case `rpc:releaseRemoteSubset:req`:
|
|
1582
|
+
case `rpc:ensurePersistedIndex:req`:
|
|
1583
|
+
case `rpc:applyLocalMutations:req`:
|
|
1584
|
+
case `rpc:applyCommittedTx:req`:
|
|
1585
|
+
case `rpc:pullSince:req`:
|
|
1586
|
+
return true;
|
|
1587
|
+
default:
|
|
1588
|
+
return false;
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
function sleep(ms) {
|
|
1592
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1593
|
+
}
|
|
1594
|
+
function appliedEnvelopeKey(collectionId, envelopeId) {
|
|
1595
|
+
return JSON.stringify([collectionId, envelopeId]);
|
|
1596
|
+
}
|
|
1597
|
+
function remoteSubsetAcquisitionKey(collectionId, acquisitionId) {
|
|
1598
|
+
return JSON.stringify([collectionId, acquisitionId]);
|
|
1599
|
+
}
|
|
1600
|
+
function inboundRemoteSubsetAcquisitionKey(collectionId, requesterId, acquisitionId) {
|
|
1601
|
+
return JSON.stringify([collectionId, requesterId, acquisitionId]);
|
|
1602
|
+
}
|
|
1603
|
+
function createRPCErrorResponse(request, cause) {
|
|
1604
|
+
const error = cause instanceof Error ? cause.message : String(cause);
|
|
1605
|
+
switch (request.type) {
|
|
1606
|
+
case `rpc:ensureRemoteSubset:req`:
|
|
1607
|
+
return {
|
|
1608
|
+
type: `rpc:ensureRemoteSubset:res`,
|
|
1609
|
+
rpcId: request.rpcId,
|
|
1610
|
+
ok: false,
|
|
1611
|
+
error,
|
|
1612
|
+
...cause instanceof RetryableRemoteSubsetAcquisitionError ? { retryable: true } : {}
|
|
1613
|
+
};
|
|
1614
|
+
case `rpc:releaseRemoteSubset:req`:
|
|
1615
|
+
return {
|
|
1616
|
+
type: `rpc:releaseRemoteSubset:res`,
|
|
1617
|
+
rpcId: request.rpcId,
|
|
1618
|
+
ok: false,
|
|
1619
|
+
error
|
|
1620
|
+
};
|
|
1621
|
+
case `rpc:ensurePersistedIndex:req`:
|
|
1622
|
+
return {
|
|
1623
|
+
type: `rpc:ensurePersistedIndex:res`,
|
|
1624
|
+
rpcId: request.rpcId,
|
|
1625
|
+
ok: false,
|
|
1626
|
+
error
|
|
1627
|
+
};
|
|
1628
|
+
case `rpc:applyLocalMutations:req`:
|
|
1629
|
+
if (cause instanceof PersistedCollectionDurabilityError) {
|
|
1630
|
+
return {
|
|
1631
|
+
type: `rpc:applyLocalMutations:res`,
|
|
1632
|
+
rpcId: request.rpcId,
|
|
1633
|
+
ok: false,
|
|
1634
|
+
code: `PERSISTENCE_ERROR`,
|
|
1635
|
+
error,
|
|
1636
|
+
...toSafeDurabilityDetails(cause)
|
|
1637
|
+
};
|
|
1638
|
+
}
|
|
1639
|
+
return {
|
|
1640
|
+
type: `rpc:applyLocalMutations:res`,
|
|
1641
|
+
rpcId: request.rpcId,
|
|
1642
|
+
ok: false,
|
|
1643
|
+
code: `CONFLICT`,
|
|
1644
|
+
error
|
|
1645
|
+
};
|
|
1646
|
+
case `rpc:applyCommittedTx:req`:
|
|
1647
|
+
if (cause instanceof PersistedCollectionDurabilityError) {
|
|
1648
|
+
return {
|
|
1649
|
+
type: `rpc:applyCommittedTx:res`,
|
|
1650
|
+
rpcId: request.rpcId,
|
|
1651
|
+
ok: false,
|
|
1652
|
+
code: `PERSISTENCE_ERROR`,
|
|
1653
|
+
error,
|
|
1654
|
+
...toSafeDurabilityDetails(cause)
|
|
1655
|
+
};
|
|
1656
|
+
}
|
|
1657
|
+
return {
|
|
1658
|
+
type: `rpc:applyCommittedTx:res`,
|
|
1659
|
+
rpcId: request.rpcId,
|
|
1660
|
+
ok: false,
|
|
1661
|
+
code: `CONFLICT`,
|
|
1662
|
+
error
|
|
1663
|
+
};
|
|
1664
|
+
case `rpc:pullSince:req`:
|
|
1665
|
+
return {
|
|
1666
|
+
type: `rpc:pullSince:res`,
|
|
1667
|
+
rpcId: request.rpcId,
|
|
1668
|
+
ok: false,
|
|
1669
|
+
error
|
|
1670
|
+
};
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
function isMutatingRPCRequest(request) {
|
|
1674
|
+
return request.type === `rpc:applyLocalMutations:req` || request.type === `rpc:applyCommittedTx:req`;
|
|
1675
|
+
}
|
|
1676
|
+
function toSafeDurabilityDetails(error) {
|
|
1677
|
+
const sourceCode = typeof error.code === `string` || typeof error.code === `number` ? error.code : void 0;
|
|
1678
|
+
const path = typeof error.path === `string` || Array.isArray(error.path) && error.path.every(
|
|
1679
|
+
(part) => typeof part === `string` || typeof part === `number`
|
|
1680
|
+
) ? error.path : void 0;
|
|
1681
|
+
return {
|
|
1682
|
+
...sourceCode === void 0 ? {} : { sourceCode },
|
|
1683
|
+
...path === void 0 ? {} : { path }
|
|
1684
|
+
};
|
|
1685
|
+
}
|
|
1686
|
+
export {
|
|
1687
|
+
BroadcastCollectionCoordinator
|
|
1688
|
+
};
|
|
1689
|
+
//# sourceMappingURL=broadcast-coordinator.js.map
|