@procedures/store 0.17.3 → 0.18.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/CHANGELOG.md +28 -0
- package/README.md +58 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/browser.d.ts +4 -0
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +3 -0
- package/dist/browser.js.map +1 -1
- package/dist/storage/hybrid.d.ts +9 -0
- package/dist/storage/hybrid.d.ts.map +1 -1
- package/dist/storage/hybrid.js +14 -0
- package/dist/storage/hybrid.js.map +1 -1
- package/dist/storage/index.d.ts +2 -0
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/index.js +2 -0
- package/dist/storage/index.js.map +1 -1
- package/dist/storage/tab-coordinator.d.ts +284 -0
- package/dist/storage/tab-coordinator.d.ts.map +1 -0
- package/dist/storage/tab-coordinator.js +584 -0
- package/dist/storage/tab-coordinator.js.map +1 -0
- package/package.json +5 -5
- package/src/browser.ts +11 -0
- package/src/storage/hybrid.ts +14 -0
- package/src/storage/index.ts +10 -0
- package/src/storage/tab-coordinator.ts +692 -0
|
@@ -0,0 +1,584 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The coordinator of the tabs of a browser (roadmap CLI-25, server stack D9).
|
|
3
|
+
*
|
|
4
|
+
* Each tab of a page has a local copy of a collection. Without a coordinator, each tab runs its own
|
|
5
|
+
* `HybridStorage`: each tab pulls, each tab pushes, and the tabs share one queue store. A
|
|
6
|
+
* `TabCoordinator` lets one leader tab run the sync, and the other tabs copy its changes.
|
|
7
|
+
*
|
|
8
|
+
* - The election uses the Web Locks API: the tab that holds the lock of the coordinator is the
|
|
9
|
+
* leader. When the leader closes, or the browser closes its tab, the lock goes to the next tab.
|
|
10
|
+
* - Only the leader makes a `HybridStorage`, with `capture: "observe"` on the local copy. The
|
|
11
|
+
* followers make no remote call, and they run no timer of a sync.
|
|
12
|
+
* - The tabs send the change batches of their local copies on a `BroadcastChannel`. A tab gives the
|
|
13
|
+
* batches of the other tabs to its observers, with their origins.
|
|
14
|
+
* - The leader captures the batches of the followers: its `HybridStorage` queues them and pushes
|
|
15
|
+
* them. Only the leader writes to the queue store, so the tabs can share it by its name.
|
|
16
|
+
* - A follower keeps each own batch until the leader acknowledges it. The leader acknowledges a
|
|
17
|
+
* batch when its queue store holds it. A new leader gets the batches that no leader
|
|
18
|
+
* acknowledged.
|
|
19
|
+
* - Without the Web Locks API or `BroadcastChannel`, the coordinator is in the single mode: the tab
|
|
20
|
+
* is the leader at once, and it opens no channel.
|
|
21
|
+
*
|
|
22
|
+
* The limits:
|
|
23
|
+
* - When the leader stops during a push, the next leader sends the operation again: the delivery to
|
|
24
|
+
* the remote is at least once.
|
|
25
|
+
* - With a shared local copy, a pull of the leader can come between the write of a follower and its
|
|
26
|
+
* capture. The tabs agree again after the push and the next pull.
|
|
27
|
+
* - The load of the queue in a new leader writes the queued operations to the local copy again, and
|
|
28
|
+
* the observers get them again with the origin `syncOrigin`.
|
|
29
|
+
*
|
|
30
|
+
* Nothing in this module reads a global value when it loads: Node can import it. The coordinator
|
|
31
|
+
* reads `navigator.locks` and `BroadcastChannel` when it starts, and a test gives its own.
|
|
32
|
+
*/
|
|
33
|
+
import { StorageListeners, reportListenerError } from "@procedures/collections";
|
|
34
|
+
import { HybridStorage } from "./hybrid.js";
|
|
35
|
+
/** The version of the messages. A tab ignores a message of another version. */
|
|
36
|
+
const PROTOCOL = 1;
|
|
37
|
+
/**
|
|
38
|
+
* One leader tab runs the sync of a `HybridStorage`, and the other tabs copy its changes.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```typescript
|
|
42
|
+
* import { ApiStorage, IndexedDbQueueStore, IndexedDbStorage, IndexedDbSyncStateStore, TabCoordinator } from "@procedures/store/browser";
|
|
43
|
+
*
|
|
44
|
+
* const local = new IndexedDbStorage<User>("users");
|
|
45
|
+
* const users = new TabCoordinator<User>({
|
|
46
|
+
* name: "users",
|
|
47
|
+
* local,
|
|
48
|
+
* copy: "shared",
|
|
49
|
+
* remote: () => new ApiStorage(client, { collection: "users" }),
|
|
50
|
+
* hybrid: {
|
|
51
|
+
* mode: "write-back",
|
|
52
|
+
* queueStore: new IndexedDbQueueStore("users-queue"),
|
|
53
|
+
* syncState: new IndexedDbSyncStateStore("users-sync"),
|
|
54
|
+
* },
|
|
55
|
+
* });
|
|
56
|
+
*
|
|
57
|
+
* users.observe((batch) => render(batch)); // The changes of this tab and of the other tabs
|
|
58
|
+
* await local.set("1", { name: "Ada" }); // The leader pushes the write
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
export class TabCoordinator {
|
|
62
|
+
/** The id of this tab: a new random id for each coordinator. */
|
|
63
|
+
id;
|
|
64
|
+
/** The name of the lock and of the channel. */
|
|
65
|
+
lockName;
|
|
66
|
+
local;
|
|
67
|
+
shared;
|
|
68
|
+
makeRemote;
|
|
69
|
+
hybridOptions;
|
|
70
|
+
/** The origin of the `HybridStorage` and of the writes of the coordinator to the local copy. */
|
|
71
|
+
tag;
|
|
72
|
+
writeOptions;
|
|
73
|
+
onError;
|
|
74
|
+
observers;
|
|
75
|
+
roleListeners;
|
|
76
|
+
/** The observers that the `HybridStorage` of the leader adds to its local copy: the capture */
|
|
77
|
+
captures;
|
|
78
|
+
currentRole = "follower";
|
|
79
|
+
channel;
|
|
80
|
+
hybrid;
|
|
81
|
+
/** The stop of the observer of the changes of this tab: on the local copy, or on the `HybridStorage` of the leader */
|
|
82
|
+
stopTap;
|
|
83
|
+
/** True while the leader gives a batch of another tab to the capture of its `HybridStorage` */
|
|
84
|
+
capturing = false;
|
|
85
|
+
/** The number of the last batch of this tab */
|
|
86
|
+
seq = 0;
|
|
87
|
+
/** The batches of this tab that no leader acknowledged, the oldest first */
|
|
88
|
+
outbox = [];
|
|
89
|
+
/** The number of the last batch of each other tab that this tab copied */
|
|
90
|
+
seen = new Map();
|
|
91
|
+
/** In the leader: the number of the last batch of each tab that the leader captured. A tab without a hello is not here. */
|
|
92
|
+
captured = new Map();
|
|
93
|
+
/** The batches of the other tabs: they reach the local copy and the observers in their order */
|
|
94
|
+
incoming = Promise.resolve();
|
|
95
|
+
closed = false;
|
|
96
|
+
closing;
|
|
97
|
+
/** The abort of the lock request that waits */
|
|
98
|
+
abortRequest;
|
|
99
|
+
/** The release of the lock */
|
|
100
|
+
releaseLock;
|
|
101
|
+
constructor(options) {
|
|
102
|
+
if (typeof options?.name !== "string" || options.name === "")
|
|
103
|
+
throw new TypeError("TabCoordinator needs a name");
|
|
104
|
+
if (typeof options.local?.observe !== "function")
|
|
105
|
+
throw new TypeError("TabCoordinator needs a local copy with observe()");
|
|
106
|
+
if (options.copy !== "shared" && options.copy !== "tab") {
|
|
107
|
+
throw new TypeError(`Unknown copy ${JSON.stringify(options.copy)}: use shared or tab`);
|
|
108
|
+
}
|
|
109
|
+
if (typeof options.remote !== "function")
|
|
110
|
+
throw new TypeError("TabCoordinator needs the function remote");
|
|
111
|
+
this.id = randomId();
|
|
112
|
+
this.lockName = `@procedures/store/tab:${options.name}`;
|
|
113
|
+
this.local = options.local;
|
|
114
|
+
this.shared = options.copy === "shared";
|
|
115
|
+
this.makeRemote = options.remote;
|
|
116
|
+
this.hybridOptions = options.hybrid ?? {};
|
|
117
|
+
this.tag = this.hybridOptions.origin ?? `tab-${this.id}`;
|
|
118
|
+
this.writeOptions = { origin: this.tag };
|
|
119
|
+
this.onError = options.onError ?? reportListenerError;
|
|
120
|
+
this.observers = new StorageListeners(this.onError);
|
|
121
|
+
this.roleListeners = new StorageListeners(this.onError);
|
|
122
|
+
this.captures = new StorageListeners(this.onError);
|
|
123
|
+
const locks = options.locks === undefined ? globalLocks() : (options.locks ?? undefined);
|
|
124
|
+
const openChannel = options.channel === undefined ? globalChannelFactory() : (options.channel ?? undefined);
|
|
125
|
+
if (!locks || !openChannel) {
|
|
126
|
+
this.lead("single");
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
this.channel = openChannel(this.lockName);
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
this.onError(error);
|
|
134
|
+
this.lead("single");
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
this.channel.onmessage = (event) => this.receive(event?.data);
|
|
138
|
+
this.stopTap = this.local.observe((batch) => this.onFollowerBatch(batch));
|
|
139
|
+
this.hello();
|
|
140
|
+
this.elect(locks);
|
|
141
|
+
}
|
|
142
|
+
/** The role of this tab now. */
|
|
143
|
+
get role() {
|
|
144
|
+
return this.currentRole;
|
|
145
|
+
}
|
|
146
|
+
/** True when this tab runs the sync: the leader, or a tab in the single mode. */
|
|
147
|
+
get isLeader() {
|
|
148
|
+
return this.currentRole === "leader" || this.currentRole === "single";
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* The `HybridStorage` of the sync while this tab runs it, or else undefined. Its `getStatus()`
|
|
152
|
+
* gives the state of the sync. Write to the local copy, not to this storage: each tab then writes
|
|
153
|
+
* in the same way.
|
|
154
|
+
*/
|
|
155
|
+
get syncStorage() {
|
|
156
|
+
return this.hybrid;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* This method starts to send the change batches to an observer: the batches of the local copy of
|
|
160
|
+
* this tab, and the batches of the other tabs. Each batch comes one time, with the origin of each
|
|
161
|
+
* change. In the leader, the batches of the sync have the origin `syncOrigin` of the
|
|
162
|
+
* `HybridStorage`. A batch of another tab comes after the local copy of this tab has it.
|
|
163
|
+
*
|
|
164
|
+
* @returns The function that stops the batches. A second use of it does nothing.
|
|
165
|
+
* @throws Error after `close()`
|
|
166
|
+
*/
|
|
167
|
+
observe(observer) {
|
|
168
|
+
if (this.closed)
|
|
169
|
+
throw new Error("The TabCoordinator is closed");
|
|
170
|
+
return this.observers.add(observer);
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* This method starts to send the role of this tab to a listener at each change.
|
|
174
|
+
*
|
|
175
|
+
* @returns The function that stops the roles. A second use of it does nothing.
|
|
176
|
+
* @throws Error after `close()`
|
|
177
|
+
*/
|
|
178
|
+
onRole(listener) {
|
|
179
|
+
if (this.closed)
|
|
180
|
+
throw new Error("The TabCoordinator is closed");
|
|
181
|
+
return this.roleListeners.add(listener);
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* This method closes the coordinator.
|
|
185
|
+
*
|
|
186
|
+
* - A follower stops its lock request. It does not become the leader.
|
|
187
|
+
* - The leader closes its `HybridStorage`: it sends the queue, and the queue store keeps the
|
|
188
|
+
* operations that the remote did not take. Then the leader releases the lock, and the next
|
|
189
|
+
* tab becomes the leader.
|
|
190
|
+
* - The coordinator closes its channel. It does not close the local copy: the application owns it.
|
|
191
|
+
* - After it, the coordinator sends and copies nothing. A second `close()` gives the promise of
|
|
192
|
+
* the first one.
|
|
193
|
+
*/
|
|
194
|
+
close() {
|
|
195
|
+
if (!this.closing)
|
|
196
|
+
this.closing = this.shutDown();
|
|
197
|
+
return this.closing;
|
|
198
|
+
}
|
|
199
|
+
async shutDown() {
|
|
200
|
+
this.closed = true;
|
|
201
|
+
this.stopTap?.();
|
|
202
|
+
this.stopTap = undefined;
|
|
203
|
+
this.abortRequest?.();
|
|
204
|
+
this.abortRequest = undefined;
|
|
205
|
+
this.setRole("closed");
|
|
206
|
+
const hybrid = this.hybrid;
|
|
207
|
+
this.hybrid = undefined;
|
|
208
|
+
try {
|
|
209
|
+
// The leader holds the lock until its queue reached the remote or its queue store
|
|
210
|
+
if (hybrid)
|
|
211
|
+
await hybrid.close();
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
this.onError(error);
|
|
215
|
+
}
|
|
216
|
+
finally {
|
|
217
|
+
this.releaseLock?.();
|
|
218
|
+
this.releaseLock = undefined;
|
|
219
|
+
// The acknowledgements of the close of the HybridStorage went out before
|
|
220
|
+
try {
|
|
221
|
+
this.channel?.close();
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
// The channel is closed already
|
|
225
|
+
}
|
|
226
|
+
this.channel = undefined;
|
|
227
|
+
this.observers.clear();
|
|
228
|
+
this.roleListeners.clear();
|
|
229
|
+
this.captures.clear();
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
//
|
|
233
|
+
// ═══ The election ═══
|
|
234
|
+
//
|
|
235
|
+
/** This method asks for the lock. The tab becomes the leader when it gets the lock. */
|
|
236
|
+
elect(locks) {
|
|
237
|
+
const Controller = globalAbortController();
|
|
238
|
+
const controller = Controller ? new Controller() : undefined;
|
|
239
|
+
this.abortRequest = controller ? () => controller.abort() : undefined;
|
|
240
|
+
const held = new Promise((resolve) => (this.releaseLock = resolve));
|
|
241
|
+
let request;
|
|
242
|
+
try {
|
|
243
|
+
request = locks.request(this.lockName, controller ? { signal: controller.signal } : {}, async () => {
|
|
244
|
+
// A lock after close(): the tab releases it at once
|
|
245
|
+
if (this.closed)
|
|
246
|
+
return;
|
|
247
|
+
this.abortRequest = undefined;
|
|
248
|
+
// A HybridStorage that cannot start releases the lock: another tab tries
|
|
249
|
+
if (!this.lead("leader"))
|
|
250
|
+
return;
|
|
251
|
+
await held;
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
catch (error) {
|
|
255
|
+
request = Promise.reject(error);
|
|
256
|
+
}
|
|
257
|
+
request.then(undefined, (error) => {
|
|
258
|
+
// The abort of close(), or a tab that leads already
|
|
259
|
+
if (this.closed || this.hybrid)
|
|
260
|
+
return;
|
|
261
|
+
// The lock manager refused the request: this tab runs the sync alone
|
|
262
|
+
this.onError(error);
|
|
263
|
+
this.stopChannel();
|
|
264
|
+
this.lead("single");
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* This method makes the `HybridStorage` of the sync: the tab becomes the leader, or the single
|
|
269
|
+
* tab. The capture of the storage gets the batches of this tab that no leader acknowledged.
|
|
270
|
+
*
|
|
271
|
+
* @returns False when the storage could not start
|
|
272
|
+
*/
|
|
273
|
+
lead(role) {
|
|
274
|
+
if (this.closed)
|
|
275
|
+
return false;
|
|
276
|
+
let hybrid;
|
|
277
|
+
try {
|
|
278
|
+
hybrid = new HybridStorage(this.makeRemote(), {
|
|
279
|
+
...this.hybridOptions,
|
|
280
|
+
local: this.captureCopy(),
|
|
281
|
+
capture: "observe",
|
|
282
|
+
origin: this.tag,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
catch (error) {
|
|
286
|
+
this.onError(error);
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
this.hybrid = hybrid;
|
|
290
|
+
// The new observer starts before the old one stops: no change of the local copy goes between them
|
|
291
|
+
const stopFollower = this.stopTap;
|
|
292
|
+
this.stopTap = hybrid.observe((batch) => this.onLeaderBatch(batch));
|
|
293
|
+
stopFollower?.();
|
|
294
|
+
for (const batch of this.outbox.splice(0))
|
|
295
|
+
this.capture(batch.changes);
|
|
296
|
+
this.setRole(role);
|
|
297
|
+
if (role === "leader")
|
|
298
|
+
this.post({ v: PROTOCOL, type: "leader", from: this.id });
|
|
299
|
+
return true;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* The local copy for the `HybridStorage` of the leader. Its `observe()` gives the changes of the
|
|
303
|
+
* local copy and the batches of the other tabs, so the capture queues both. Its `close()` does
|
|
304
|
+
* nothing: the application owns the local copy.
|
|
305
|
+
*/
|
|
306
|
+
captureCopy() {
|
|
307
|
+
const captures = this.captures;
|
|
308
|
+
const observe = (observer) => {
|
|
309
|
+
const stopLocal = this.local.observe(observer);
|
|
310
|
+
const stopCapture = captures.add(observer);
|
|
311
|
+
return () => {
|
|
312
|
+
stopLocal();
|
|
313
|
+
stopCapture();
|
|
314
|
+
};
|
|
315
|
+
};
|
|
316
|
+
return new Proxy(this.local, {
|
|
317
|
+
get(target, property) {
|
|
318
|
+
if (property === "observe")
|
|
319
|
+
return observe;
|
|
320
|
+
if (property === "close")
|
|
321
|
+
return () => Promise.resolve();
|
|
322
|
+
const value = Reflect.get(target, property, target);
|
|
323
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
324
|
+
},
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* This method gives a batch of another tab to the capture of the `HybridStorage` of the leader.
|
|
329
|
+
* The storage queues it. Its observers get it at once, and the coordinator ignores it there: the
|
|
330
|
+
* observers of the coordinator get it from the channel.
|
|
331
|
+
*/
|
|
332
|
+
capture(changes) {
|
|
333
|
+
if (changes.length === 0)
|
|
334
|
+
return;
|
|
335
|
+
this.capturing = true;
|
|
336
|
+
try {
|
|
337
|
+
this.captures.emit(changes);
|
|
338
|
+
}
|
|
339
|
+
finally {
|
|
340
|
+
this.capturing = false;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
setRole(role) {
|
|
344
|
+
if (this.currentRole === role)
|
|
345
|
+
return;
|
|
346
|
+
this.currentRole = role;
|
|
347
|
+
this.roleListeners.emit(role);
|
|
348
|
+
}
|
|
349
|
+
//
|
|
350
|
+
// ═══ The batches of this tab ═══
|
|
351
|
+
//
|
|
352
|
+
/**
|
|
353
|
+
* A batch of the local copy of a follower. The writes of the coordinator itself (the batches of
|
|
354
|
+
* the other tabs in a copy of each tab) have the tag of the coordinator, and do not go out again.
|
|
355
|
+
*/
|
|
356
|
+
onFollowerBatch(batch) {
|
|
357
|
+
if (this.closed)
|
|
358
|
+
return;
|
|
359
|
+
const own = batch.filter((change) => change.origin !== this.tag);
|
|
360
|
+
if (own.length > 0)
|
|
361
|
+
this.share(own, true);
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* A batch of the `HybridStorage` of the leader: a write of the application to the local copy,
|
|
365
|
+
* a write to the storage, or a change of the sync.
|
|
366
|
+
*/
|
|
367
|
+
onLeaderBatch(batch) {
|
|
368
|
+
if (this.closed || this.capturing)
|
|
369
|
+
return;
|
|
370
|
+
this.share(batch, false);
|
|
371
|
+
}
|
|
372
|
+
/** This method gives a batch of this tab to its observers and to the other tabs. A follower keeps it for the leader. */
|
|
373
|
+
share(changes, keep) {
|
|
374
|
+
const seq = ++this.seq;
|
|
375
|
+
this.observers.emit(changes);
|
|
376
|
+
const sent = this.post({ v: PROTOCOL, type: "changes", from: this.id, seq, changes });
|
|
377
|
+
if (sent && keep)
|
|
378
|
+
this.outbox.push({ seq, changes });
|
|
379
|
+
}
|
|
380
|
+
/** This method tells the leader the batches of this tab that no leader acknowledged. */
|
|
381
|
+
hello() {
|
|
382
|
+
this.post({ v: PROTOCOL, type: "hello", from: this.id, seq: this.seq, pending: this.outbox });
|
|
383
|
+
}
|
|
384
|
+
/** @returns False when the message did not go out: no channel, or a value that the channel cannot clone */
|
|
385
|
+
post(message) {
|
|
386
|
+
const channel = this.channel;
|
|
387
|
+
if (!channel)
|
|
388
|
+
return false;
|
|
389
|
+
try {
|
|
390
|
+
channel.postMessage(message);
|
|
391
|
+
return true;
|
|
392
|
+
}
|
|
393
|
+
catch (error) {
|
|
394
|
+
this.onError(error);
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
stopChannel() {
|
|
399
|
+
const channel = this.channel;
|
|
400
|
+
this.channel = undefined;
|
|
401
|
+
if (!channel)
|
|
402
|
+
return;
|
|
403
|
+
channel.onmessage = null;
|
|
404
|
+
try {
|
|
405
|
+
channel.close();
|
|
406
|
+
}
|
|
407
|
+
catch {
|
|
408
|
+
// The channel is closed already
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
//
|
|
412
|
+
// ═══ The batches of the other tabs ═══
|
|
413
|
+
//
|
|
414
|
+
receive(data) {
|
|
415
|
+
if (this.closed || !isTabMessage(data) || data.from === this.id)
|
|
416
|
+
return;
|
|
417
|
+
switch (data.type) {
|
|
418
|
+
case "changes":
|
|
419
|
+
this.onChanges(data.from, { seq: data.seq, changes: data.changes });
|
|
420
|
+
break;
|
|
421
|
+
case "hello":
|
|
422
|
+
this.onHello(data.from, data.seq, data.pending);
|
|
423
|
+
break;
|
|
424
|
+
case "leader":
|
|
425
|
+
// The new leader gets the batches that no leader acknowledged
|
|
426
|
+
if (this.currentRole === "follower")
|
|
427
|
+
this.hello();
|
|
428
|
+
break;
|
|
429
|
+
case "ack":
|
|
430
|
+
if (data.to === this.id)
|
|
431
|
+
this.outbox = this.outbox.filter((batch) => batch.seq > data.seq);
|
|
432
|
+
break;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* A batch of another tab. The leader captures it when the tab said hello to it. Each tab copies
|
|
437
|
+
* it one time.
|
|
438
|
+
*/
|
|
439
|
+
onChanges(from, batch) {
|
|
440
|
+
const known = this.captured.get(from);
|
|
441
|
+
if (this.hybrid && this.currentRole === "leader" && known !== undefined && batch.seq > known) {
|
|
442
|
+
this.capture(batch.changes);
|
|
443
|
+
this.captured.set(from, batch.seq);
|
|
444
|
+
this.acknowledge(from, batch.seq);
|
|
445
|
+
}
|
|
446
|
+
this.copy(from, batch);
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* The hello of another tab. The leader captures the batches that it did not capture, and then each
|
|
450
|
+
* batch of the tab after `seq`. Each tab copies the batches that it did not see.
|
|
451
|
+
*/
|
|
452
|
+
onHello(from, seq, pending) {
|
|
453
|
+
if (this.hybrid && this.currentRole === "leader") {
|
|
454
|
+
const known = this.captured.get(from);
|
|
455
|
+
for (const batch of pending)
|
|
456
|
+
if (known === undefined || batch.seq > known)
|
|
457
|
+
this.capture(batch.changes);
|
|
458
|
+
this.captured.set(from, Math.max(known ?? 0, seq));
|
|
459
|
+
if (pending.length > 0)
|
|
460
|
+
this.acknowledge(from, seq);
|
|
461
|
+
}
|
|
462
|
+
for (const batch of pending)
|
|
463
|
+
this.copy(from, batch);
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* This method copies a batch of another tab that this tab did not see: to the local copy of a
|
|
467
|
+
* tab, and then to the observers. The batches go in their order.
|
|
468
|
+
*/
|
|
469
|
+
copy(from, batch) {
|
|
470
|
+
if (batch.seq <= (this.seen.get(from) ?? 0))
|
|
471
|
+
return;
|
|
472
|
+
this.seen.set(from, batch.seq);
|
|
473
|
+
this.incoming = this.incoming
|
|
474
|
+
.then(async () => {
|
|
475
|
+
if (this.closed)
|
|
476
|
+
return;
|
|
477
|
+
if (!this.shared)
|
|
478
|
+
await this.write(batch.changes);
|
|
479
|
+
if (!this.closed)
|
|
480
|
+
this.observers.emit(batch.changes);
|
|
481
|
+
})
|
|
482
|
+
.catch((error) => this.onError(error));
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* This method writes a batch of another tab to the local copy of this tab, with the tag of the
|
|
486
|
+
* coordinator: the observer of this tab and the capture of the leader ignore the writes. The sets
|
|
487
|
+
* in a row go in one `setBatch`, and the deletes in a row in one `deleteBatch`.
|
|
488
|
+
*/
|
|
489
|
+
async write(changes) {
|
|
490
|
+
let index = 0;
|
|
491
|
+
while (index < changes.length) {
|
|
492
|
+
const change = changes[index];
|
|
493
|
+
if (change.type === "clear") {
|
|
494
|
+
await this.local.clear(this.writeOptions);
|
|
495
|
+
index++;
|
|
496
|
+
}
|
|
497
|
+
else if (change.type === "set") {
|
|
498
|
+
const items = [];
|
|
499
|
+
for (let next = changes[index]; next?.type === "set"; next = changes[++index])
|
|
500
|
+
items.push([next.id, next.value]);
|
|
501
|
+
await this.local.setBatch(items, this.writeOptions);
|
|
502
|
+
}
|
|
503
|
+
else {
|
|
504
|
+
const ids = [];
|
|
505
|
+
for (let next = changes[index]; next?.type === "delete"; next = changes[++index])
|
|
506
|
+
ids.push(next.id);
|
|
507
|
+
await this.local.deleteBatch(ids, this.writeOptions);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
/** This method tells a tab that the queue store of the leader holds its batches up to `seq`. */
|
|
512
|
+
acknowledge(to, seq) {
|
|
513
|
+
const hybrid = this.hybrid;
|
|
514
|
+
if (!hybrid)
|
|
515
|
+
return;
|
|
516
|
+
hybrid.whenQueued().then(() => void this.post({ v: PROTOCOL, type: "ack", from: this.id, to, seq }), (error) => this.onError(error));
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
/** A new random id. */
|
|
520
|
+
function randomId() {
|
|
521
|
+
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
522
|
+
}
|
|
523
|
+
/** `navigator.locks` of the global scope, or undefined. */
|
|
524
|
+
function globalLocks() {
|
|
525
|
+
return globalThis.navigator?.locks ?? undefined;
|
|
526
|
+
}
|
|
527
|
+
/** A function that opens a `BroadcastChannel` of the global scope, or undefined. */
|
|
528
|
+
function globalChannelFactory() {
|
|
529
|
+
const Channel = globalThis.BroadcastChannel;
|
|
530
|
+
return typeof Channel === "function" ? (name) => new Channel(name) : undefined;
|
|
531
|
+
}
|
|
532
|
+
/** The `AbortController` of the global scope, or undefined. */
|
|
533
|
+
function globalAbortController() {
|
|
534
|
+
const Controller = globalThis.AbortController;
|
|
535
|
+
return typeof Controller === "function" ? Controller : undefined;
|
|
536
|
+
}
|
|
537
|
+
function isSeq(value) {
|
|
538
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
539
|
+
}
|
|
540
|
+
/** True when a value is a change batch. The value of a message of another tab can be anything. */
|
|
541
|
+
function isBatch(value) {
|
|
542
|
+
if (!Array.isArray(value))
|
|
543
|
+
return false;
|
|
544
|
+
return value.every((change) => {
|
|
545
|
+
if (typeof change !== "object" || change === null)
|
|
546
|
+
return false;
|
|
547
|
+
const { type, id, origin } = change;
|
|
548
|
+
if (origin !== undefined && typeof origin !== "string")
|
|
549
|
+
return false;
|
|
550
|
+
if (type === "clear")
|
|
551
|
+
return true;
|
|
552
|
+
if (typeof id !== "string")
|
|
553
|
+
return false;
|
|
554
|
+
return type === "delete" || (type === "set" && "value" in change);
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
/** True when a value is a message of the channel, of this version. */
|
|
558
|
+
function isTabMessage(value) {
|
|
559
|
+
if (typeof value !== "object" || value === null)
|
|
560
|
+
return false;
|
|
561
|
+
const message = value;
|
|
562
|
+
if (message.v !== PROTOCOL || typeof message.from !== "string")
|
|
563
|
+
return false;
|
|
564
|
+
switch (message.type) {
|
|
565
|
+
case "changes":
|
|
566
|
+
return isSeq(message.seq) && isBatch(message.changes);
|
|
567
|
+
case "hello":
|
|
568
|
+
return (isSeq(message.seq) &&
|
|
569
|
+
Array.isArray(message.pending) &&
|
|
570
|
+
message.pending.every((batch) => {
|
|
571
|
+
if (typeof batch !== "object" || batch === null)
|
|
572
|
+
return false;
|
|
573
|
+
const { seq, changes } = batch;
|
|
574
|
+
return isSeq(seq) && isBatch(changes);
|
|
575
|
+
}));
|
|
576
|
+
case "leader":
|
|
577
|
+
return true;
|
|
578
|
+
case "ack":
|
|
579
|
+
return typeof message.to === "string" && isSeq(message.seq);
|
|
580
|
+
default:
|
|
581
|
+
return false;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
//# sourceMappingURL=tab-coordinator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tab-coordinator.js","sourceRoot":"","sources":["../../src/storage/tab-coordinator.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package",
|
|
3
3
|
"name": "@procedures/store",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.18.0",
|
|
5
5
|
"description": "A program store for procedure-as-data: save the programs that procedure.define makes, load them back after a restart, in memory, a JSON file, IndexedDB or a remote collection storage.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"program-store",
|
|
@@ -77,13 +77,13 @@
|
|
|
77
77
|
]
|
|
78
78
|
},
|
|
79
79
|
"dependencies": {
|
|
80
|
-
"@mark1russell7/service": "^0.5.
|
|
81
|
-
"@procedures/client": "0.
|
|
82
|
-
"@procedures/collections": "0.
|
|
80
|
+
"@mark1russell7/service": "^0.5.8",
|
|
81
|
+
"@procedures/client": "0.18.0",
|
|
82
|
+
"@procedures/collections": "0.18.0",
|
|
83
83
|
"zod": "^4.6.5"
|
|
84
84
|
},
|
|
85
85
|
"devDependencies": {
|
|
86
|
-
"@procedures/fs": "0.
|
|
86
|
+
"@procedures/fs": "0.18.0",
|
|
87
87
|
"@mark1russell7/cue": "^1.0.0",
|
|
88
88
|
"@types/node": "^22.0.0",
|
|
89
89
|
"fake-indexeddb": "^6.2.5",
|
package/src/browser.ts
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* - `HybridStorage` keeps a local copy in front of a remote storage, with its errors and its types.
|
|
6
6
|
* - `ApiStorage` is the remote storage over the collection procedures of a server.
|
|
7
7
|
* - `EventsChangeFeed` pushes the remote changes to a `HybridStorage`.
|
|
8
|
+
* - `TabCoordinator` lets one leader tab run the sync of a `HybridStorage`, and the other tabs
|
|
9
|
+
* copy its changes (roadmap CLI-25). It uses the Web Locks API and `BroadcastChannel`.
|
|
8
10
|
* - The entry also has all of `@procedures/store/indexeddb`: `IndexedDbStorage`, the IndexedDB
|
|
9
11
|
* stores and their fallbacks in memory.
|
|
10
12
|
*
|
|
@@ -28,6 +30,15 @@ export {
|
|
|
28
30
|
SyncStateError,
|
|
29
31
|
} from "./storage/hybrid.js";
|
|
30
32
|
export type { HybridStorageOptions, HybridMode, LocalCopyStorage } from "./storage/hybrid.js";
|
|
33
|
+
export { TabCoordinator } from "./storage/tab-coordinator.js";
|
|
34
|
+
export type {
|
|
35
|
+
TabAbortSignalLike,
|
|
36
|
+
TabChannelFactory,
|
|
37
|
+
TabChannelLike,
|
|
38
|
+
TabCoordinatorOptions,
|
|
39
|
+
TabLockManagerLike,
|
|
40
|
+
TabRole,
|
|
41
|
+
} from "./storage/tab-coordinator.js";
|
|
31
42
|
export type { ConflictResolution, SyncError, SyncStatus } from "./storage/types.js";
|
|
32
43
|
export { ApiStorage } from "./storage/api.js";
|
|
33
44
|
export type { ApiStorageOptions } from "./storage/api.js";
|