@procedures/store 0.17.2 → 0.17.4

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.
Files changed (39) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +92 -4
  3. package/dist/.tsbuildinfo +1 -1
  4. package/dist/browser.d.ts +6 -2
  5. package/dist/browser.d.ts.map +1 -1
  6. package/dist/browser.js +5 -2
  7. package/dist/browser.js.map +1 -1
  8. package/dist/indexeddb/document-storage.d.ts +186 -0
  9. package/dist/indexeddb/document-storage.d.ts.map +1 -0
  10. package/dist/indexeddb/document-storage.js +598 -0
  11. package/dist/indexeddb/document-storage.js.map +1 -0
  12. package/dist/indexeddb/idb.d.ts +82 -9
  13. package/dist/indexeddb/idb.d.ts.map +1 -1
  14. package/dist/indexeddb/idb.js +100 -22
  15. package/dist/indexeddb/idb.js.map +1 -1
  16. package/dist/indexeddb.d.ts +8 -3
  17. package/dist/indexeddb.d.ts.map +1 -1
  18. package/dist/indexeddb.js +8 -3
  19. package/dist/indexeddb.js.map +1 -1
  20. package/dist/storage/hybrid.d.ts +9 -0
  21. package/dist/storage/hybrid.d.ts.map +1 -1
  22. package/dist/storage/hybrid.js +14 -0
  23. package/dist/storage/hybrid.js.map +1 -1
  24. package/dist/storage/index.d.ts +2 -0
  25. package/dist/storage/index.d.ts.map +1 -1
  26. package/dist/storage/index.js +2 -0
  27. package/dist/storage/index.js.map +1 -1
  28. package/dist/storage/tab-coordinator.d.ts +284 -0
  29. package/dist/storage/tab-coordinator.d.ts.map +1 -0
  30. package/dist/storage/tab-coordinator.js +584 -0
  31. package/dist/storage/tab-coordinator.js.map +1 -0
  32. package/package.json +5 -5
  33. package/src/browser.ts +13 -2
  34. package/src/indexeddb/document-storage.ts +689 -0
  35. package/src/indexeddb/idb.ts +136 -21
  36. package/src/indexeddb.ts +8 -3
  37. package/src/storage/hybrid.ts +14 -0
  38. package/src/storage/index.ts +10 -0
  39. package/src/storage/tab-coordinator.ts +692 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,42 @@
1
1
  # @procedures/store
2
2
 
3
+ ## 0.17.4
4
+
5
+ ### Patch Changes
6
+
7
+ - b0ccc96: Added: `TabCoordinator`, so one leader tab of a browser does the sync of a `HybridStorage`, and the other tabs copy its changes (roadmap CLI-25). It is in `@procedures/store/browser` and in the package root.
8
+
9
+ - The election uses the Web Locks API. When the leader closes, or the browser closes its tab, the next tab becomes the leader.
10
+ - Only the leader makes the `HybridStorage`, with `capture: "observe"`. The followers make no remote call and start no timer.
11
+ - The tabs send the change batches of their local copies on a `BroadcastChannel`. `observe()` gives each batch of each tab one time, with its origin. A tab does not send again a batch that it got.
12
+ - The leader captures the batches of the followers, and its queue store keeps them. Only the leader writes to the shared queue store.
13
+ - A follower keeps each own batch until the leader acknowledges it. A new leader gets the batches that no leader acknowledged.
14
+ - The option `copy` is `"shared"` for one durable local copy of all tabs, or `"tab"` for a local copy in each tab, for example a SignalDB collection.
15
+ - Without the Web Locks API or `BroadcastChannel`, the tab does its own sync in the single mode.
16
+
17
+ Added: `HybridStorage.whenQueued()`. It resolves when the queue store holds the queue, and it sends nothing.
18
+
19
+ PATCH: new exports and a new method that are backward compatible, before 1.0.0.
20
+ - @procedures/client@0.17.4
21
+ - @procedures/collections@0.17.4
22
+
23
+ ## 0.17.3
24
+
25
+ ### Patch Changes
26
+
27
+ - d552672: Added: `IndexedDbStorage`, a collection storage in the IndexedDB of a browser (roadmap CLI-22). It is in `@procedures/store/indexeddb` and `@procedures/store/browser`. It is the durable local copy of a `HybridStorage` in a browser app without SignalDB.
28
+
29
+ - Each collection has its own database. The items, their order and the versions of `getChangesSince()` stay after a reload of the page.
30
+ - The entries keep the order of the first set of each id, as a `Map` does.
31
+ - Each write is one transaction of IndexedDB. A batch that fails changes nothing, and two patches at the same time keep each field.
32
+ - The storage has `getEntries()`, `getIds()`, `getChangesSince()`, `query()`, `observe()` and `patch()`. With the option `versionOf`, a write can expect the version of the current item.
33
+ - The storage checks each record that it reads. The contract suite of `@procedures/collections/testing` tests it.
34
+ - The entry `@procedures/store/indexeddb` still does not import the root of the client or zod.
35
+
36
+ PATCH: a new export that is backward compatible, before 1.0.0.
37
+ - @procedures/client@0.17.3
38
+ - @procedures/collections@0.17.3
39
+
3
40
  ## 0.17.2
4
41
 
5
42
  ### Patch Changes
package/README.md CHANGED
@@ -41,8 +41,9 @@ console.log(await client.exec(["math", "addTen"], { x: 5 })); // 15
41
41
 
42
42
  ### The stores of a browser
43
43
 
44
- The entry `@procedures/store/indexeddb` has the stores of a browser. The package root does not import it, and it does not import the client or zod.
44
+ The entry `@procedures/store/indexeddb` has the stores of a browser. The package root does not import it. It does not import the root of the client or zod.
45
45
 
46
+ - `IndexedDbStorage` is a collection storage in IndexedDB. Refer to "The collection storage of a browser".
46
47
  - `IndexedDbProgramStore` keeps the programs in one database, `procedures-programs` by default.
47
48
  - `IndexedDbQueueStore` keeps the queue of a `HybridStorage`. Give each storage its own database.
48
49
  - `hasIndexedDb()` tells if the browser has IndexedDB. `MemoryProgramStore` and `MemoryQueueStore` are the fallbacks.
@@ -111,6 +112,34 @@ Notes:
111
112
  - `HybridStorage.close()` waits for the operations, the switch and the syncs that run. Then it sends the queue, and it closes the remote and the local copy.
112
113
  - `SyncedProcedureRegistry.close()` closes its storage. After it, `register()` and `unregister()` change only the registry in memory.
113
114
 
115
+ ### The collection storage of a browser
116
+
117
+ `IndexedDbStorage` of `@procedures/store/indexeddb` keeps the items of one collection in IndexedDB (roadmap CLI-22). It is the durable local copy of a `HybridStorage` in a browser app without SignalDB.
118
+
119
+ - Give each collection its own database. The storage makes the database at the first operation.
120
+ - The items stay after `close()` and after a reload of the page. A new storage of the same database reads them.
121
+ - The entries keep the order of the first set of each id, as a `Map` does. Each record keeps the number of its first set.
122
+ - `getChangesSince()` gives the changes after a version. A version stays valid after a reload.
123
+ - Each write is one transaction of IndexedDB. A batch that fails changes nothing. Two patches at the same time keep each field.
124
+ - `query()` applies a selector. `patch()` changes an item in one transaction.
125
+ - `observe()` tells each write of this storage before the write resolves. It does not tell the writes of another tab.
126
+ - With the option `versionOf`, a write can expect the version of the current item. The test and the write are one transaction.
127
+ - The storage checks each record that it reads. A record of another application or of another version is an error.
128
+ - The contract suite of `@procedures/collections/testing` tests the storage, and a `HybridStorage` with the storage as its local copy.
129
+ - `InMemoryStorage` of `@procedures/collections` is the fallback for a browser without IndexedDB.
130
+
131
+ ```ts
132
+ import { ApiStorage, HybridStorage, IndexedDbQueueStore, IndexedDbStorage, IndexedDbSyncStateStore } from "@procedures/store/browser";
133
+
134
+ const versionOf = (user: User) => user.rev;
135
+ const users = new HybridStorage(new ApiStorage(client, { collection: "users" }), {
136
+ local: new IndexedDbStorage<User>("users", { versionOf }),
137
+ versionOf,
138
+ queueStore: new IndexedDbQueueStore("users-queue"),
139
+ syncState: new IndexedDbSyncStateStore("users-sync"),
140
+ });
141
+ ```
142
+
114
143
  ## The modes of `HybridStorage`
115
144
 
116
145
  `HybridStorage` keeps a local copy of a remote collection storage. It has four modes, and `setMode()` changes the mode while the storage operates.
@@ -163,10 +192,11 @@ await users.setMode("write-through"); // The switch sends the queue, then syncs
163
192
 
164
193
  - The option `local` gives the storage of the local copy. The default is a new `InMemoryStorage`.
165
194
  - A durable local copy keeps the items after a restart. `SqliteStorage` of `@procedures/sqlite` keeps them in a SQLite file.
195
+ - `IndexedDbStorage` of `@procedures/store/indexeddb` keeps them in the IndexedDB of a browser.
166
196
  - With a durable local copy, a new storage in `local-only` mode reads the items of the last run. It makes no remote call.
167
197
  - Give a durable `queueStore` with a durable local copy. A restart applies the loaded queue to the local copy again, and the result does not change.
168
198
  - The first sync after a restart reads all remote items. It removes each local item that the remote deleted and that no queued write changes.
169
- - A full sync reads the local ids with `getIds()` of the local copy. `InMemoryStorage` and `SqliteStorage` give the ids without a copy of the values. A local copy without `getIds()` gives its entries.
199
+ - A full sync reads the local ids with `getIds()` of the local copy. `InMemoryStorage`, `SqliteStorage` and `IndexedDbStorage` give the ids without a copy of the values. A local copy without `getIds()` gives its entries.
170
200
  - `HybridStorage.getIds()` gives the ids of the local copy, or of the remote in `remote-only` mode.
171
201
  - `close()` closes the local copy and the remote. A durable local copy keeps its items. A remote stays open when its own `close()` leaves it open, as `ApiStorage.close()` leaves the server.
172
202
 
@@ -244,7 +274,7 @@ const stop = users.onStatus((status) => {
244
274
 
245
275
  ### The changes, the capture, the sync state and the push feed
246
276
 
247
- `HybridStorage` is a sync engine for each local copy: `InMemoryStorage`, `SqliteStorage`, IndexedDB or a SignalDB collection (server stack D9, CLI-21, CLI-15).
277
+ `HybridStorage` is a sync engine for each local copy: `InMemoryStorage`, `SqliteStorage`, `IndexedDbStorage` or a SignalDB collection (server stack D9, CLI-21, CLI-15).
248
278
 
249
279
  - `observe(observer)` gives one batch of `set`, `delete` and `clear` changes for each change of the storage.
250
280
  - A write of the storage gives its batch with the `origin` of its options. A pull and a delta of the feed give their changes with the origin `syncOrigin`, `"sync"` by default.
@@ -289,6 +319,63 @@ const users = new HybridStorage(new ApiStorage(client, { collection: "users" }),
289
319
  - The server serves the events with the option `events` of `ProcedureServer`, for example `{ topics: ["collections.*"] }`. Without a subscriber, the procedures publish nothing.
290
320
  - `ApiStorage` sends the `origin` of a write to the procedure. Thus the storage of the server and the event have it.
291
321
 
322
+ ### The tabs of a browser
323
+
324
+ `TabCoordinator` of `@procedures/store/browser` lets one leader tab run the sync of a `HybridStorage` (roadmap CLI-25). The other tabs copy its changes. Without it, each tab pulls and pushes, and each tab writes over the queue of the other tabs.
325
+
326
+ - The election uses the Web Locks API. The tab that holds the lock of the coordinator is the leader.
327
+ - Only the leader makes the `HybridStorage`, with `capture: "observe"` on its local copy. The followers make no remote call and start no timer.
328
+ - The application writes to the local copy in each tab. A tab sends the change batches of its local copy to the other tabs on a `BroadcastChannel`.
329
+ - `observe()` gives the batches of this tab and of the other tabs, each batch one time, with the origin of each change.
330
+ - The leader also sends the changes of its sync. They have the origin `syncOrigin`, `"sync"` by default.
331
+ - A tab does not send again a batch of another tab.
332
+ - The leader captures the batches of the followers: its `HybridStorage` queues them and pushes them. Thus a write of a follower reaches the remote one time.
333
+ - Only the leader writes to the queue store and to the store of the sync state. Give the same `IndexedDbQueueStore` and `IndexedDbSyncStateStore` in each tab.
334
+
335
+ The option `copy` tells the kind of the local copy:
336
+
337
+ - `"shared"`: the tabs open the same durable local copy, for example an `IndexedDbStorage` of one database. A tab gives the batches of the other tabs only to its observers.
338
+ - `"tab"`: each tab has its own local copy, for example a SignalDB collection. A tab writes the batches of the other tabs to its copy, and then gives them to its observers.
339
+ - In the mode `"tab"`, a new tab gets only the changes after its start. Its local copy loads the older items itself, for example from its persistence.
340
+ - In the mode `"shared"`, a pull of the leader can come between the write of a follower and its capture. The tabs agree again after the push and the next pull.
341
+
342
+ A new leader and a closed tab:
343
+
344
+ - A follower keeps each own batch until the leader acknowledges it. The leader acknowledges a batch when its queue store holds it.
345
+ - A new leader tells the other tabs. Each follower then sends the batches that no leader acknowledged. Thus a write during the change of the leader reaches the new leader.
346
+ - `close()` of the leader sends the queue, and then it releases the lock. The queue store keeps the operations that the remote did not take.
347
+ - When the browser closes the tab of the leader, the browser releases the lock. The next leader loads the shared queue store and pushes the queue.
348
+ - When the leader stops during a push, the next leader sends the operation again. A set or a delete gives the same result two times.
349
+ - The load of the queue writes the queued operations to the local copy again. Thus the observers can get them again, with the origin `syncOrigin`.
350
+ - `close()` of a follower stops its lock request. `close()` does not close the local copy, because the application owns it.
351
+
352
+ Without the Web Locks API or `BroadcastChannel`, the coordinator is in the single mode. The tab does its own sync, and it opens no channel. A browser has the Web Locks API only in a secure context, for example HTTPS or `localhost`.
353
+
354
+ ```ts
355
+ import { ApiStorage, IndexedDbQueueStore, IndexedDbStorage, IndexedDbSyncStateStore, TabCoordinator } from "@procedures/store/browser";
356
+
357
+ const local = new IndexedDbStorage<User>("users");
358
+ const users = new TabCoordinator<User>({
359
+ name: "users",
360
+ local,
361
+ copy: "shared",
362
+ remote: () => new ApiStorage(client, { collection: "users" }),
363
+ hybrid: {
364
+ mode: "write-back",
365
+ queueStore: new IndexedDbQueueStore("users-queue"),
366
+ syncState: new IndexedDbSyncStateStore("users-sync"),
367
+ },
368
+ });
369
+
370
+ users.observe((batch) => render(batch));
371
+ await local.set("1", { name: "Ada" });
372
+ users.syncStorage?.getStatus(); // In the leader only
373
+ ```
374
+
375
+ - `role` is `follower`, `leader`, `single` or `closed`. `onRole()` tells each change.
376
+ - The options `locks` and `channel` take a lock manager and a channel factory, for example the ones of a test. `null` sets the single mode.
377
+ - `HybridStorage.whenQueued()` resolves when the queue store holds the queue. The leader acknowledges the batches of the followers after it.
378
+
292
379
  ### The change of a storage in a server
293
380
 
294
381
  `ProcedureServer.useStorage(name, storage, { drain: true, close: true })` first starts `flush()` of the old storage. Then it uses the new storage and closes the old one. When the flush fails, the old storage stays.
@@ -311,7 +398,8 @@ The entry `@procedures/store/browser` has the parts of the store that run in a b
311
398
  - `HybridStorage`, its errors and its types.
312
399
  - `ApiStorage`, the remote storage over the collection procedures of a server.
313
400
  - `EventsChangeFeed`, the push feed of the remote changes.
314
- - All of `@procedures/store/indexeddb`: the IndexedDB stores and their fallbacks in memory.
401
+ - `TabCoordinator`: one leader tab does the sync of a `HybridStorage`, and the other tabs copy its changes.
402
+ - All of `@procedures/store/indexeddb`: `IndexedDbStorage`, the IndexedDB stores and their fallbacks in memory.
315
403
 
316
404
  No module of the entry imports a module of Node. The modules import `@procedures/client/browser` and not the root of the client, because the root imports `http` and `ws`. The test `src/browser.test.ts` examines the imports of each module of the entry. The entry is in `client.browser` of `package.json`, so the CI check of the browser entries (`scripts/check-browser-entries.mjs`) bundles it for a browser too.
317
405