@tanstack/electron-db-sqlite-persistence 0.1.33 → 0.2.1

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 CHANGED
@@ -2,10 +2,13 @@
2
2
 
3
3
  Thin Electron bridge for TanStack DB SQLite persistence.
4
4
 
5
+ Start with the [SQLite Persistence guide](../../docs/guides/sqlite-persistence.md) for Collection setup and lifecycle behavior.
6
+
5
7
  ## Public API
6
8
 
7
9
  - `exposeElectronSQLitePersistence(...)` (main process)
8
10
  - `createElectronSQLitePersistence(...)` (renderer process)
11
+ - `ElectronCollectionCoordinator`
9
12
  - `persistedCollectionOptions(...)` (re-exported from core)
10
13
 
11
14
  Use `@tanstack/electron-db-sqlite-persistence/main` and
@@ -64,8 +67,77 @@ export const todosCollection = createCollection(
64
67
  )
65
68
  ```
66
69
 
70
+ ## Multi-renderer ownership
71
+
72
+ Pass an `ElectronCollectionCoordinator` when multiple renderers share the same
73
+ database. Coordinators using the same `dbName` elect a writer independently for
74
+ each collection.
75
+
76
+ ```ts
77
+ import {
78
+ ElectronCollectionCoordinator,
79
+ createElectronSQLitePersistence,
80
+ } from '@tanstack/electron-db-sqlite-persistence'
81
+
82
+ const coordinator = new ElectronCollectionCoordinator({
83
+ dbName: `tanstack-db`,
84
+ })
85
+
86
+ const persistence = createElectronSQLitePersistence({
87
+ ipcRenderer,
88
+ coordinator,
89
+ })
90
+
91
+ // On teardown:
92
+ // coordinator.dispose()
93
+ ```
94
+
95
+ The persisted sync wrapper routes every source transaction with durable effects
96
+ through the coordinator's required
97
+ `requestApplyCommittedTx(collectionId, tx)` method. The elected renderer applies
98
+ the complete `PersistedTx` through its resolved per-collection renderer adapter,
99
+ which forwards it over IPC to the main-process persistence owner. Resolution
100
+ remains specific to the collection id, mode, and `schemaVersion`.
101
+
102
+ Truncation, row changes, row metadata, collection metadata, and stream position
103
+ stay in the same transaction. Electron does not feature-detect a partial route
104
+ or fall back to row-only mutation RPC. A custom coordinator that omits
105
+ `requestApplyCommittedTx` is rejected while the collection is configured,
106
+ before its sync source can publish rows. An effect-free source commit neither
107
+ publishes nor consumes a coordinator sequence.
108
+
109
+ If a mutating RPC loses its response, Electron coordination replays it only
110
+ while the requester still knows the same non-null leader id and term. An
111
+ unknown initial route or any leader/term change rejects with
112
+ `IndeterminateCommitError`; the application must reconcile the outcome. The
113
+ coordinator does not retry that mutation against an unknown or replacement
114
+ leader.
115
+
116
+ Remote-subset requests use the same crash-only boundary. The coordinator
117
+ projects a live `LoadSubsetOptions` input to the exported
118
+ `TransportedLoadSubsetOptions` type before either local completion or renderer
119
+ transport. Live `signal` and `subscription` fields do not cross the boundary.
120
+ Unsupported nested values throw `RemoteSubsetWireValueError` with the exact
121
+ value path before publication; Electron does not add codecs, coercion, or a
122
+ fallback representation.
123
+
124
+ Electron uses the same explicit lease lifecycle as Browser coordination. The
125
+ same request object reuses one acquisition identity, distinct equal objects
126
+ remain independent, and release unloads the exact transported options once.
127
+ Leadership loss unloads the retiring renderer's leases; still-live requester
128
+ leases replay against the next leader. A second owner registration for one
129
+ collection throws `DuplicateRemoteSubsetOwnerError`, and there is no adapter
130
+ fallback for a missing owner. Remote follower transport/admission failures
131
+ reject and retain demand for its normal retry without entering the follower's
132
+ local owner lifecycle. Terminal release identities expire after the existing
133
+ RPC dedupe horizon while delayed duplicates inside that horizon remain
134
+ idempotent.
135
+
67
136
  ## Notes
68
137
 
69
138
  - The renderer API mirrors other runtimes: one shared `create...Persistence`.
70
139
  - Collection mode (`sync-present` vs `sync-absent`) and `schemaVersion` are
71
140
  resolved per collection and forwarded across IPC automatically.
141
+ - Without an `ElectronCollectionCoordinator`, single-renderer mode uses
142
+ `SingleProcessCoordinator`: it is always leader, but still routes complete
143
+ transactions to the collection's resolved IPC adapter.
@@ -1,542 +1,13 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
- const dbSqlitePersistenceCore = require("@tanstack/db-sqlite-persistence-core");
4
- const HEARTBEAT_INTERVAL_MS = 3e3;
5
- const RPC_TIMEOUT_MS = 1e4;
6
- const RPC_RETRY_ATTEMPTS = 2;
7
- const RPC_RETRY_DELAY_MS = 200;
8
- const WRITER_LOCK_BUSY_RETRY_MS = 50;
9
- const WRITER_LOCK_MAX_RETRIES = 20;
10
- class ElectronCollectionCoordinator {
3
+ const broadcastCoordinator = require("@tanstack/db-sqlite-persistence-core/broadcast-coordinator");
4
+ class ElectronCollectionCoordinator extends broadcastCoordinator.BroadcastCollectionCoordinator {
11
5
  constructor(options) {
12
- this.nodeId = dbSqlitePersistenceCore.safeRandomUUID();
13
- this.collections = /* @__PURE__ */ new Map();
14
- this.pendingRPCs = /* @__PURE__ */ new Map();
15
- this.appliedEnvelopeIds = /* @__PURE__ */ new Map();
16
- this.disposed = false;
17
- this.dbName = options.dbName;
18
- this.adapter = options.adapter ?? null;
19
- this.channel = new BroadcastChannel(`tsdb:coord:${this.dbName}`);
20
- this.channel.onmessage = (event) => {
21
- this.onChannelMessage(event.data);
22
- };
23
- }
24
- /** Method indirection to prevent TypeScript from narrowing `disposed` across awaits */
25
- isDisposed() {
26
- return this.disposed;
27
- }
28
- requireAdapter() {
29
- if (!this.adapter) {
30
- throw new Error(
31
- `ElectronCollectionCoordinator: adapter not set. Call setAdapter() before using leader-side operations.`
32
- );
33
- }
34
- return this.adapter;
35
- }
36
- /**
37
- * Set or replace the persistence adapter used for leader-side RPC handling.
38
- * Called by `createElectronSQLitePersistence` to wire the internally-created
39
- * adapter into the coordinator.
40
- */
41
- setAdapter(adapter) {
42
- this.adapter = adapter;
43
- }
44
- // -----------------------------------------------------------------------
45
- // PersistedCollectionCoordinator interface
46
- // -----------------------------------------------------------------------
47
- getNodeId() {
48
- return this.nodeId;
49
- }
50
- subscribe(collectionId, onMessage) {
51
- const state = this.ensureCollectionState(collectionId);
52
- state.subscribers.add(onMessage);
53
- return () => {
54
- state.subscribers.delete(onMessage);
55
- };
56
- }
57
- publish(_collectionId, message) {
58
- this.channel.postMessage(message);
59
- }
60
- isLeader(collectionId) {
61
- return this.collections.get(collectionId)?.isLeader ?? false;
62
- }
63
- async ensureLeadership(collectionId) {
64
- const state = this.ensureCollectionState(collectionId);
65
- if (state.isLeader) return;
66
- await this.acquireLeadership(collectionId, state);
67
- }
68
- async requestEnsureRemoteSubset(collectionId, options) {
69
- if (this.isLeader(collectionId)) return;
70
- const response = await this.sendRPC(collectionId, {
71
- type: `rpc:ensureRemoteSubset:req`,
72
- rpcId: dbSqlitePersistenceCore.safeRandomUUID(),
73
- options
6
+ super({
7
+ ...options,
8
+ coordinatorName: `ElectronCollectionCoordinator`
74
9
  });
75
- if (!response.ok) {
76
- throw new Error(
77
- `ensureRemoteSubset failed: ${response.error ?? `unknown error`}`
78
- );
79
- }
80
- }
81
- async requestEnsurePersistedIndex(collectionId, signature, spec) {
82
- if (this.isLeader(collectionId)) {
83
- await this.requireAdapter().ensureIndex(collectionId, signature, spec);
84
- return;
85
- }
86
- const response = await this.sendRPC(collectionId, {
87
- type: `rpc:ensurePersistedIndex:req`,
88
- rpcId: dbSqlitePersistenceCore.safeRandomUUID(),
89
- signature,
90
- spec
91
- });
92
- if (!response.ok) {
93
- throw new Error(
94
- `ensurePersistedIndex failed: ${response.error ?? `unknown error`}`
95
- );
96
- }
97
- }
98
- async requestApplyLocalMutations(collectionId, mutations) {
99
- if (this.isLeader(collectionId)) {
100
- return this.handleApplyLocalMutations(collectionId, {
101
- type: `rpc:applyLocalMutations:req`,
102
- rpcId: dbSqlitePersistenceCore.safeRandomUUID(),
103
- envelopeId: dbSqlitePersistenceCore.safeRandomUUID(),
104
- mutations
105
- });
106
- }
107
- return this.sendRPC(collectionId, {
108
- type: `rpc:applyLocalMutations:req`,
109
- rpcId: dbSqlitePersistenceCore.safeRandomUUID(),
110
- envelopeId: dbSqlitePersistenceCore.safeRandomUUID(),
111
- mutations
112
- });
113
- }
114
- async pullSince(collectionId, fromRowVersion) {
115
- if (this.isLeader(collectionId)) {
116
- return this.handlePullSince(collectionId, {
117
- type: `rpc:pullSince:req`,
118
- rpcId: dbSqlitePersistenceCore.safeRandomUUID(),
119
- fromRowVersion
120
- });
121
- }
122
- return this.sendRPC(collectionId, {
123
- type: `rpc:pullSince:req`,
124
- rpcId: dbSqlitePersistenceCore.safeRandomUUID(),
125
- fromRowVersion
126
- });
127
- }
128
- // -----------------------------------------------------------------------
129
- // Lifecycle
130
- // -----------------------------------------------------------------------
131
- dispose() {
132
- this.disposed = true;
133
- for (const [collectionId, state] of this.collections) {
134
- this.releaseLeadership(collectionId, state);
135
- }
136
- for (const [, pending] of this.pendingRPCs) {
137
- clearTimeout(pending.timer);
138
- pending.reject(new Error(`coordinator disposed`));
139
- }
140
- this.pendingRPCs.clear();
141
- this.channel.close();
142
- this.collections.clear();
143
- }
144
- // -----------------------------------------------------------------------
145
- // Leadership via Web Locks
146
- // -----------------------------------------------------------------------
147
- ensureCollectionState(collectionId) {
148
- let state = this.collections.get(collectionId);
149
- if (!state) {
150
- state = {
151
- isLeader: false,
152
- lockAbortController: null,
153
- heartbeatTimer: null,
154
- latestTerm: 0,
155
- latestSeq: 0,
156
- latestRowVersion: 0,
157
- subscribers: /* @__PURE__ */ new Set()
158
- };
159
- this.collections.set(collectionId, state);
160
- void this.acquireLeadership(collectionId, state);
161
- }
162
- return state;
163
10
  }
164
- async acquireLeadership(collectionId, state) {
165
- if (this.disposed || state.isLeader) return;
166
- const lockName = `tsdb:leader:${this.dbName}:${collectionId}`;
167
- const abortController = new AbortController();
168
- state.lockAbortController = abortController;
169
- try {
170
- await navigator.locks.request(
171
- lockName,
172
- { signal: abortController.signal },
173
- async () => {
174
- if (this.isDisposed()) return;
175
- try {
176
- const adapter = this.requireAdapter();
177
- if (adapter.getStreamPosition) {
178
- const pos = await adapter.getStreamPosition(collectionId);
179
- state.latestTerm = pos.latestTerm;
180
- state.latestSeq = pos.latestSeq;
181
- state.latestRowVersion = pos.latestRowVersion;
182
- }
183
- state.latestTerm++;
184
- state.isLeader = true;
185
- this.emitHeartbeat(collectionId, state);
186
- state.heartbeatTimer = setInterval(() => {
187
- this.emitHeartbeat(collectionId, state);
188
- }, HEARTBEAT_INTERVAL_MS);
189
- await new Promise((resolve) => {
190
- const onAbort = () => {
191
- abortController.signal.removeEventListener(`abort`, onAbort);
192
- resolve();
193
- };
194
- if (abortController.signal.aborted) {
195
- resolve();
196
- return;
197
- }
198
- abortController.signal.addEventListener(`abort`, onAbort);
199
- });
200
- } finally {
201
- state.isLeader = false;
202
- if (state.heartbeatTimer) {
203
- clearInterval(state.heartbeatTimer);
204
- state.heartbeatTimer = null;
205
- }
206
- }
207
- }
208
- );
209
- } catch (error) {
210
- if (error instanceof DOMException && error.name === `AbortError`) {
211
- return;
212
- }
213
- console.warn(`Failed to acquire leadership for ${collectionId}:`, error);
214
- }
215
- if (!this.isDisposed()) {
216
- void this.acquireLeadership(collectionId, state);
217
- }
218
- }
219
- releaseLeadership(_collectionId, state) {
220
- if (state.lockAbortController) {
221
- state.lockAbortController.abort();
222
- state.lockAbortController = null;
223
- }
224
- if (state.heartbeatTimer) {
225
- clearInterval(state.heartbeatTimer);
226
- state.heartbeatTimer = null;
227
- }
228
- state.isLeader = false;
229
- }
230
- emitHeartbeat(collectionId, state) {
231
- const envelope = {
232
- v: 1,
233
- dbName: this.dbName,
234
- collectionId,
235
- senderId: this.nodeId,
236
- ts: Date.now(),
237
- payload: {
238
- type: `leader:heartbeat`,
239
- term: state.latestTerm,
240
- leaderId: this.nodeId,
241
- latestSeq: state.latestSeq,
242
- latestRowVersion: state.latestRowVersion
243
- }
244
- };
245
- this.channel.postMessage(envelope);
246
- }
247
- // -----------------------------------------------------------------------
248
- // BroadcastChannel message handling
249
- // -----------------------------------------------------------------------
250
- onChannelMessage(data) {
251
- if (!isProtocolEnvelope(data)) return;
252
- const envelope = data;
253
- if (envelope.senderId === this.nodeId) return;
254
- const payload = envelope.payload;
255
- if (!payload || typeof payload !== `object`) return;
256
- const type = payload.type;
257
- if (type && type.endsWith(`:res`)) {
258
- const rpcId = payload.rpcId;
259
- if (rpcId && this.pendingRPCs.has(rpcId)) {
260
- const pending = this.pendingRPCs.get(rpcId);
261
- this.pendingRPCs.delete(rpcId);
262
- clearTimeout(pending.timer);
263
- pending.resolve(payload);
264
- return;
265
- }
266
- }
267
- if (type && type.endsWith(`:req`)) {
268
- const collectionId = envelope.collectionId;
269
- if (this.isLeader(collectionId)) {
270
- void this.handleRPCRequest(collectionId, payload);
271
- }
272
- return;
273
- }
274
- const state = this.collections.get(envelope.collectionId);
275
- if (state) {
276
- for (const subscriber of state.subscribers) {
277
- subscriber(envelope);
278
- }
279
- }
280
- }
281
- // -----------------------------------------------------------------------
282
- // RPC - Outbound (follower side)
283
- // -----------------------------------------------------------------------
284
- async sendRPC(collectionId, request) {
285
- let lastError;
286
- for (let attempt = 0; attempt <= RPC_RETRY_ATTEMPTS; attempt++) {
287
- if (attempt > 0) {
288
- await sleep(RPC_RETRY_DELAY_MS * attempt);
289
- }
290
- try {
291
- return await this.sendRPCOnce(collectionId, request);
292
- } catch (error) {
293
- lastError = error instanceof Error ? error : new Error(String(error));
294
- }
295
- }
296
- throw lastError ?? new Error(`RPC failed after retries`);
297
- }
298
- sendRPCOnce(collectionId, request) {
299
- return new Promise((resolve, reject) => {
300
- const rpcId = request.rpcId;
301
- const timer = setTimeout(() => {
302
- this.pendingRPCs.delete(rpcId);
303
- reject(
304
- new Error(`RPC ${request.type} timed out after ${RPC_TIMEOUT_MS}ms`)
305
- );
306
- }, RPC_TIMEOUT_MS);
307
- this.pendingRPCs.set(rpcId, {
308
- resolve,
309
- reject,
310
- timer
311
- });
312
- const envelope = {
313
- v: 1,
314
- dbName: this.dbName,
315
- collectionId,
316
- senderId: this.nodeId,
317
- ts: Date.now(),
318
- payload: request
319
- };
320
- this.channel.postMessage(envelope);
321
- });
322
- }
323
- // -----------------------------------------------------------------------
324
- // RPC - Inbound (leader side)
325
- // -----------------------------------------------------------------------
326
- async handleRPCRequest(collectionId, request) {
327
- let response;
328
- try {
329
- switch (request.type) {
330
- case `rpc:ensureRemoteSubset:req`:
331
- response = await this.handleEnsureRemoteSubset(collectionId, request);
332
- break;
333
- case `rpc:ensurePersistedIndex:req`:
334
- response = await this.handleEnsurePersistedIndex(
335
- collectionId,
336
- request
337
- );
338
- break;
339
- case `rpc:applyLocalMutations:req`:
340
- response = await this.handleApplyLocalMutations(collectionId, request);
341
- break;
342
- case `rpc:pullSince:req`:
343
- response = await this.handlePullSince(collectionId, request);
344
- break;
345
- default:
346
- return;
347
- }
348
- } catch (error) {
349
- const errorMessage = error instanceof Error ? error.message : String(error);
350
- response = {
351
- type: request.type.replace(`:req`, `:res`),
352
- rpcId: request.rpcId,
353
- ok: false,
354
- error: errorMessage
355
- };
356
- }
357
- const envelope = {
358
- v: 1,
359
- dbName: this.dbName,
360
- collectionId,
361
- senderId: this.nodeId,
362
- ts: Date.now(),
363
- payload: response
364
- };
365
- this.channel.postMessage(envelope);
366
- }
367
- handleEnsureRemoteSubset(_collectionId, request) {
368
- return {
369
- type: `rpc:ensureRemoteSubset:res`,
370
- rpcId: request.rpcId,
371
- ok: true
372
- };
373
- }
374
- async handleEnsurePersistedIndex(collectionId, request) {
375
- await this.withWriterLock(
376
- () => this.requireAdapter().ensureIndex(
377
- collectionId,
378
- request.signature,
379
- request.spec
380
- )
381
- );
382
- return {
383
- type: `rpc:ensurePersistedIndex:res`,
384
- rpcId: request.rpcId,
385
- ok: true
386
- };
387
- }
388
- async handleApplyLocalMutations(collectionId, request) {
389
- if (this.appliedEnvelopeIds.has(request.envelopeId)) {
390
- return {
391
- type: `rpc:applyLocalMutations:res`,
392
- rpcId: request.rpcId,
393
- ok: false,
394
- code: `CONFLICT`,
395
- error: `envelope ${request.envelopeId} already applied`
396
- };
397
- }
398
- const state = this.collections.get(collectionId);
399
- if (!state || !state.isLeader) {
400
- return {
401
- type: `rpc:applyLocalMutations:res`,
402
- rpcId: request.rpcId,
403
- ok: false,
404
- code: `NOT_LEADER`,
405
- error: `not the leader for ${collectionId}`
406
- };
407
- }
408
- state.latestSeq++;
409
- state.latestRowVersion++;
410
- const term = state.latestTerm;
411
- const seq = state.latestSeq;
412
- const rowVersion = state.latestRowVersion;
413
- const tx = {
414
- txId: dbSqlitePersistenceCore.safeRandomUUID(),
415
- term,
416
- seq,
417
- rowVersion,
418
- mutations: request.mutations.map((m) => ({
419
- type: m.type,
420
- key: m.key,
421
- value: m.value
422
- }))
423
- };
424
- await this.withWriterLock(
425
- () => this.requireAdapter().applyCommittedTx(collectionId, tx)
426
- );
427
- this.appliedEnvelopeIds.set(request.envelopeId, Date.now());
428
- this.pruneAppliedEnvelopeIds();
429
- const changedRows = request.mutations.filter((m) => m.type !== `delete`).map((m) => ({ key: m.key, value: m.value }));
430
- const deletedKeys = request.mutations.filter((m) => m.type === `delete`).map((m) => m.key);
431
- const txCommitted = {
432
- v: 1,
433
- dbName: this.dbName,
434
- collectionId,
435
- senderId: this.nodeId,
436
- ts: Date.now(),
437
- payload: {
438
- type: `tx:committed`,
439
- term,
440
- seq,
441
- txId: tx.txId,
442
- latestRowVersion: rowVersion,
443
- requiresFullReload: false,
444
- changedRows,
445
- deletedKeys
446
- }
447
- };
448
- this.channel.postMessage(txCommitted);
449
- for (const subscriber of state.subscribers) {
450
- subscriber(txCommitted);
451
- }
452
- return {
453
- type: `rpc:applyLocalMutations:res`,
454
- rpcId: request.rpcId,
455
- ok: true,
456
- term,
457
- seq,
458
- latestRowVersion: rowVersion,
459
- acceptedMutationIds: request.mutations.map((m) => m.mutationId)
460
- };
461
- }
462
- async handlePullSince(collectionId, request) {
463
- const state = this.collections.get(collectionId);
464
- const adapter = this.requireAdapter();
465
- if (!adapter.pullSince) {
466
- return {
467
- type: `rpc:pullSince:res`,
468
- rpcId: request.rpcId,
469
- ok: true,
470
- latestTerm: state?.latestTerm ?? 0,
471
- latestSeq: state?.latestSeq ?? 0,
472
- latestRowVersion: state?.latestRowVersion ?? 0,
473
- requiresFullReload: true
474
- };
475
- }
476
- const result = await adapter.pullSince(collectionId, request.fromRowVersion);
477
- if (result.requiresFullReload) {
478
- return {
479
- type: `rpc:pullSince:res`,
480
- rpcId: request.rpcId,
481
- ok: true,
482
- latestTerm: state?.latestTerm ?? 0,
483
- latestSeq: state?.latestSeq ?? 0,
484
- latestRowVersion: result.latestRowVersion,
485
- requiresFullReload: true
486
- };
487
- }
488
- return {
489
- type: `rpc:pullSince:res`,
490
- rpcId: request.rpcId,
491
- ok: true,
492
- latestTerm: state?.latestTerm ?? 0,
493
- latestSeq: state?.latestSeq ?? 0,
494
- latestRowVersion: result.latestRowVersion,
495
- requiresFullReload: false,
496
- changedKeys: result.changedKeys,
497
- deletedKeys: result.deletedKeys
498
- };
499
- }
500
- // -----------------------------------------------------------------------
501
- // DB Writer Lock
502
- // -----------------------------------------------------------------------
503
- async withWriterLock(fn) {
504
- const lockName = `tsdb:writer:${this.dbName}`;
505
- for (let attempt = 0; attempt <= WRITER_LOCK_MAX_RETRIES; attempt++) {
506
- try {
507
- return await navigator.locks.request(lockName, async () => fn());
508
- } catch (error) {
509
- if (error instanceof DOMException && error.name === `AbortError`) {
510
- throw error;
511
- }
512
- if (attempt < WRITER_LOCK_MAX_RETRIES) {
513
- await sleep(WRITER_LOCK_BUSY_RETRY_MS * Math.min(attempt + 1, 5));
514
- continue;
515
- }
516
- throw error;
517
- }
518
- }
519
- throw new Error(`writer lock acquisition failed`);
520
- }
521
- // -----------------------------------------------------------------------
522
- // Helpers
523
- // -----------------------------------------------------------------------
524
- pruneAppliedEnvelopeIds() {
525
- const cutoff = Date.now() - 6e4;
526
- for (const [id, ts] of this.appliedEnvelopeIds) {
527
- if (ts < cutoff) {
528
- this.appliedEnvelopeIds.delete(id);
529
- }
530
- }
531
- }
532
- }
533
- function isProtocolEnvelope(data) {
534
- if (!data || typeof data !== `object`) return false;
535
- const record = data;
536
- return record.v === 1 && typeof record.dbName === `string` && typeof record.collectionId === `string` && typeof record.senderId === `string` && typeof record.ts === `number`;
537
- }
538
- function sleep(ms) {
539
- return new Promise((resolve) => setTimeout(resolve, ms));
540
11
  }
541
12
  exports.ElectronCollectionCoordinator = ElectronCollectionCoordinator;
542
13
  //# sourceMappingURL=electron-coordinator.cjs.map