@procedures/store 0.8.0 → 0.9.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.
Files changed (47) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/README.md +88 -0
  3. package/dist/.tsbuildinfo +1 -1
  4. package/dist/indexeddb/sync-state-store.d.ts +40 -0
  5. package/dist/indexeddb/sync-state-store.d.ts.map +1 -0
  6. package/dist/indexeddb/sync-state-store.js +94 -0
  7. package/dist/indexeddb/sync-state-store.js.map +1 -0
  8. package/dist/indexeddb.d.ts +7 -2
  9. package/dist/indexeddb.d.ts.map +1 -1
  10. package/dist/indexeddb.js +6 -2
  11. package/dist/indexeddb.js.map +1 -1
  12. package/dist/storage/api.d.ts +84 -11
  13. package/dist/storage/api.d.ts.map +1 -1
  14. package/dist/storage/api.js +148 -25
  15. package/dist/storage/api.js.map +1 -1
  16. package/dist/storage/feed.d.ts +133 -0
  17. package/dist/storage/feed.d.ts.map +1 -0
  18. package/dist/storage/feed.js +159 -0
  19. package/dist/storage/feed.js.map +1 -0
  20. package/dist/storage/hybrid.d.ts +367 -20
  21. package/dist/storage/hybrid.d.ts.map +1 -1
  22. package/dist/storage/hybrid.js +1295 -206
  23. package/dist/storage/hybrid.js.map +1 -1
  24. package/dist/storage/index.d.ts +6 -2
  25. package/dist/storage/index.d.ts.map +1 -1
  26. package/dist/storage/index.js +6 -1
  27. package/dist/storage/index.js.map +1 -1
  28. package/dist/storage/sync-state.d.ts +56 -0
  29. package/dist/storage/sync-state.d.ts.map +1 -0
  30. package/dist/storage/sync-state.js +58 -0
  31. package/dist/storage/sync-state.js.map +1 -0
  32. package/dist/storage/synced-registry.d.ts +22 -4
  33. package/dist/storage/synced-registry.d.ts.map +1 -1
  34. package/dist/storage/synced-registry.js +63 -31
  35. package/dist/storage/synced-registry.js.map +1 -1
  36. package/dist/storage/types.d.ts +22 -1
  37. package/dist/storage/types.d.ts.map +1 -1
  38. package/package.json +4 -3
  39. package/src/indexeddb/sync-state-store.ts +101 -0
  40. package/src/indexeddb.ts +7 -2
  41. package/src/storage/api.ts +210 -29
  42. package/src/storage/feed.ts +251 -0
  43. package/src/storage/hybrid.ts +1404 -211
  44. package/src/storage/index.ts +17 -0
  45. package/src/storage/sync-state.ts +89 -0
  46. package/src/storage/synced-registry.ts +76 -31
  47. package/src/storage/types.ts +25 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,67 @@
1
1
  # @procedures/store
2
2
 
3
+ ## 0.9.0
4
+
5
+ ### Patch Changes
6
+
7
+ - dca28c1: The packages accept `@mark1russell7/service` 0.5.1 and later 0.5 versions, and `@procedures/client` accepts `@mark1russell7/algebra` 0.7.1. Before, the range `^0.4.1` stopped below 0.5. An application on service 0.5 then got a second copy of service inside the client.
8
+
9
+ - The client uses only the `/backoff`, `/heartbeat` and `/testing` entries of service. The code of `/backoff`, `/heartbeat` and the clock is the same in 0.4.1 and 0.5.1. `/testing` only gets new exports.
10
+ - The public API of the client does not change.
11
+ - Updated dependencies [8bb469c]
12
+ - Updated dependencies [0dd1c86]
13
+ - Updated dependencies [dc03903]
14
+ - Updated dependencies [dca28c1]
15
+ - @procedures/client@0.9.0
16
+ - @procedures/collections@0.9.0
17
+
18
+ ## 0.8.1
19
+
20
+ ### Patch Changes
21
+
22
+ - 1aa9ef7: The collection storages get a JSON query language with a query policy, change batches and patches in one step, and the sync of `@procedures/store` runs on the service clock and backoff (server stack CLI-20, CLI-14, CLI-13, CLI-34).
23
+
24
+ - The query language is in the core, in the new entry `@procedures/client/query`, which imports nothing else. It is the one copy (CLI-34). `@procedures/collections` now depends on `@procedures/client` for it, and exports the same names again.
25
+ - The selector is JSON: a subset of the query selectors of MongoDB. `queryEntries()` is the reference, and `selectorProblems()`, `queryOptionsProblems()`, `compileSelector()`, `matchesSelector()` and `compareStorageValues()` are exported.
26
+ - A query policy (`StorageQueryPolicy`) limits the operators, the length and the flags of a `$regex`, the clauses and the depth of a selector, the lists of `$in`, `$nin` and `$all`, and the limit. The default policy does not permit `$regex`. A pattern that repeats a repetition, for example `(a+)+`, is always an error.
27
+ - `CollectionStorage` of `@procedures/collections` has the optional `query(selector, { sort, skip, limit }, policy)`, `observe(observer)` and `patch(id, fields)`. A caller tests for each one. A storage without them still follows the contract.
28
+ - `observe()` gives one batch of `set`, `delete` and `clear` changes for each write that changes the storage, before the write resolves. The write methods take `{ origin }`, and each change has it.
29
+ - `patch()` takes a JSON Merge Patch (RFC 7386), or the operations `{ $set, $unset }`, which can set `null` and nested field paths. A patch that mixes the two forms is an error. `applyPatch()`, `mergePatch()` and `patchProblems()` are exported.
30
+ - `InMemoryStorage` has the three methods, and the options `queryPolicy` and `onObserverError`. `StorageListeners` is the set of listeners that the storages share.
31
+ - The contract suite of `@procedures/collections/testing` tests the selectors against `queryEntries()`, with fixed and random selectors, the refusal of `$regex` by default, and, with `createWithQueryPolicy`, the opt-in of `$regex` and each limit of a policy. It also tests the two forms of a patch, the patches at the same time, and the batches with their origins.
32
+ - The core collection procedures have `collections.<name>.find` (it reads) and `collections.<name>.patch` (it reads and writes, idempotent). `find` refuses a selector or a limit that the policy does not permit with `VALIDATION_ERROR` and the reason, and a `find` without a limit gets at most `DEFAULT_FIND_LIMIT` (1000) items. It gives the selector and the policy to `query()` of the storage, or runs it on the entries. `patch` uses `patch()` of the storage, or a read and a write, one patch of an id at a time.
33
+ - `createCollectionProcedures(name, { queryPolicy })` and `ProcedureServer.registerCollectionProcedures(name, { queryPolicy })` take the policy of the host.
34
+ - The `CollectionStorage` of the core has the same optional methods and the write options. Its new types are exported from the root, `./browser` and `./procedures`.
35
+ - `ApiStorage` has `query()` (the procedure `find`, or the entries of an older server) and `patch()` (the procedure `patch`). Its option `queryPolicy` adds limits of the client.
36
+ - `ApiStorage` waits `backoffDelay` of `@mark1russell7/service/backoff` between its tries, with full jitter, on the option `clock`. The timeout uses the clock too. The new options are `retryFactor`, `maxRetryDelay`, `retryJitter`, `clock` and `random`. An error with `retryAfterMs` gets at least that wait.
37
+ - `HybridStorage` has `query()` and the option `queryPolicy`. Its timers run on the option `clock`, and the delay after a failure grows with the option `backoff`. `getStatus()` and `onStatus()` give the state of the sync and its failures. The storage writes nothing to the console.
38
+ - `SyncedProcedureRegistry` takes the option `clock` for its auto-sync. Its status has the failures of the auto-sync and of the write-through writes, and `onStatus()` sends them.
39
+ - `SqliteStorage` has `query()`, `patch()` and the option `queryPolicy`. A patch runs in one transaction.
40
+ - `@procedures/store` depends on `@mark1russell7/service`, and `@procedures/collections` depends on `@procedures/client`.
41
+ - c559b00: `HybridStorage` captures the writes of the application to its local copy, keeps its sync state after a reload and takes a push feed of the remote changes. Each write of the core collection procedures publishes a change event (server stack CLI-21, CLI-15, decision D9).
42
+
43
+ - `HybridStorage.observe(observer)` gives one batch of changes for each change of the storage. 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 of the option `syncOrigin` (`"sync"` by default). In `remote-only` mode, the batches tell the writes of the storage to the remote.
44
+ - The option `origin` of `HybridStorage` is the tag of the storage (a random `hybrid-…` tag by default). Each write of the storage to the local copy and to the remote has it. The write methods of `HybridStorage` and `ApiStorage` take `{ origin }`.
45
+ - `capture: "observe"`: the storage observes the local copy and queues each change with another origin. Thus an application can write to the local copy directly, for example with the synchronous writes of a SignalDB collection. In `write-through` and `remote-only` mode, the storage sends a captured change at once.
46
+ - The option `syncState` takes a `SyncStateStore`: `MemorySyncStateStore`, or `IndexedDbSyncStateStore` of `@procedures/store/indexeddb`. The store keeps the cursor and the base versions. A new storage with the same store and a durable local copy asks only for the changes since the cursor, and it finds the conflicts of the base versions. A failure of the store goes to the status (`operation: "store"`, `SyncStateError`), and the storage goes on.
47
+ - The option `feed` takes a `RemoteChangeFeed`. A delta whose `since` is the cursor applies without a read of the remote. A gap, a lost event, a new subscription or another delta makes one pull. A delta that comes during a pull applies after the pull, so the pull does not erase it (the LiveLog rule of Graph).
48
+ - In `write-through` mode, a pull or a delta does not write over an own write that waits for the remote, a set or a delete. The storage keeps the remote value of the id, and after the write it reads the id again: then the rules of a pull decide. A read of a missing id does not bring back an item whose own delete waits.
49
+ - With `versionOf`, the storage keeps the versions of its own writes until their echoes come (option `echoWindow`, 30 seconds by default). The echo of an older own write does not write over a newer local write (the EchoLedger rule of Graph).
50
+ - `EventsChangeFeed` of `@procedures/store` is the feed of the change events. Its source is a `Client`, a `WebSocketTransport` or another transport. It treats the events as hints: a gap of the numbers, a lost event, a hint and each confirmation of the subscription call `onGap()`.
51
+ - The core collection procedures publish `CollectionChangeEvent` `{ since, version, changed, deleted, origin }` on `ctx.bus`, under the topic `collections.<name>` (`collectionChangeTopic()`), while a subscriber listens. The changes come from `getChangesSince()` of the storage, and the events of one storage make a chain of versions. The option `changeEvents` of `createCollectionProcedures()` turns them off, or gives the topic of each call: the partition of CLI-17 goes there.
52
+ - A value reaches a subscriber only through the checks of a read. By default, an event has the ids and the versions only, and `EventsChangeFeed` gives it to `onHint()`: `HybridStorage` reads the values with `changes`, and a hint with only deletes applies without a read. With `changeEvents: { values: true }`, an event also has `values`: each row `{ id, value }` passes the output filter of the host (`admitItem`) for each subscriber, and a refused id goes in `withheld`, so the storage pulls it.
53
+ - `EventPublisher` refuses a topic whose reads the capability of the call does not permit (`FORBIDDEN`, `effects-exceeded`). Its new option `reads` gives the reads of a topic. The default, `collectionTopicReads()`, gives `collection:<name>` for `collections.<name>` and `collections.<name>.<partition>`. `ctx.admit` gives a procedure the output filter of its host for the data that it sends later.
54
+ - The syncs of one `HybridStorage` run one at a time: the deltas and the hints of the feed, the pulls, the pushes, the reads after an own write and the mode switches. A remote delta and a full sync never run at the same time (Graph R18).
55
+ - The write inputs of the collection procedures take an optional `origin` (1 to 256 characters). The procedure gives it to the storage and to the event. A server before this change ignores the field.
56
+ - `Client.subscribe()` and `Client.events()` subscribe to the topics of the bus of the server through the middleware and the context of the client. A subscription has no deadline: the timeouts of the client do not apply to it, and only `metadata: { timeout }` in its options gives one.
57
+ - New exports: `collectionChangeTopic`, `CollectionChangeEvent` and `CollectionChangeEventsOptions` from `@procedures/client`, `./browser` and `./procedures`. `EventsChangeFeed`, `MemorySyncStateStore`, `SyncStateError` and the types `RemoteChangeFeed`, `RemoteChangeListener`, `RemoteChanges`, `EventSubscriber`, `ChangeEventSource`, `EventsChangeFeedOptions`, `SyncState`, `SyncStateChange` and `SyncStateStore` from `@procedures/store`. `IndexedDbSyncStateStore` and `MemorySyncStateStore` from `@procedures/store/indexeddb`.
58
+ - Updated dependencies [2d3f235]
59
+ - Updated dependencies [1aa9ef7]
60
+ - Updated dependencies [2523b1b]
61
+ - Updated dependencies [c559b00]
62
+ - @procedures/client@0.8.1
63
+ - @procedures/collections@0.8.1
64
+
3
65
  ## 0.8.0
4
66
 
5
67
  ### Patch Changes
package/README.md CHANGED
@@ -91,9 +91,18 @@ The contract suite of `@procedures/collections/testing` tests `ApiStorage`, and
91
91
  - **The ids.** `getIds()` gives the ids in the order of the entries. `ApiStorage.getIds()` uses the procedure `collections.<name>.ids`, so the values stay on the server.
92
92
  - **After `close()`.** Each operation rejects with an error whose message says "closed". A second `close()` does nothing.
93
93
 
94
+ - **The queries.** `query(selector, { sort, skip, limit })` gives the result of `queryEntries()` of `@procedures/collections`. `ApiStorage.query()` sends the JSON selector to the procedure `collections.<name>.find`, so the server applies it.
95
+ - **The patches.** `ApiStorage.patch(id, fields)` sends a patch to the procedure `collections.<name>.patch`. The server changes the item in one step. A patch is a JSON Merge Patch, or the operations `{ $set, $unset }`.
96
+ - **The query policy.** The server applies the policy of its host to each `find`. The default policy does not permit `$regex`, and a `find` gets at most 1000 items.
97
+
94
98
  Notes:
95
99
 
96
100
  - A custom `service` or a custom `get` operation gets the input `{ id }`, as before. Its `null` reads back as `undefined`.
101
+ - A server before the procedure `find` answers `NOT_FOUND`. Then `ApiStorage.query()` reads the entries and applies the query in the client, with the same result.
102
+ - `ApiStorage.find(predicate)` reads each item, because a function cannot go over the network. Use `query()` with a selector to filter on the server.
103
+ - `HybridStorage.query()` reads the local copy, or the remote in `remote-only` mode. Its option `queryPolicy` goes to the storage under it.
104
+ - `ApiStorage.query()` examines the form of a selector before it sends it. Its option `queryPolicy` adds the limits of the client.
105
+ - A host turns `$regex` on with `server.registerCollectionProcedures(name, { queryPolicy })` or `createCollectionProcedures(name, { queryPolicy })`.
97
106
  - A server before `found` ignores it and gives the item. Then a stored `null` reads back as `undefined`, as before.
98
107
  - A server before the procedure `ids` answers `NOT_FOUND`. Then `ApiStorage.getIds()` reads the ids from the entries, and it does not ask for `ids` again.
99
108
  - `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.
@@ -183,6 +192,85 @@ const users = new HybridStorage(new ApiStorage(client, { collection: "users" }),
183
192
  - Without versions, a pull reads each local item without a queued write. It compares the JSON of the local item and the remote item.
184
193
  - The option `pullInterval` starts a periodic pull in the modes `write-through` and `write-back`. `close()` stops it.
185
194
 
195
+ ### The clock, the backoff and the status
196
+
197
+ The timers and the retries run on a clock: `Clock` of `@mark1russell7/service/clock`. A test gives `fakeClock` of `@mark1russell7/service/testing`, and the times are then exact.
198
+
199
+ - The option `clock` of `HybridStorage` and of `ApiStorage` gives the clock. The default is `realClock` of `@procedures/client`.
200
+ - The timers of `HybridStorage` do not keep a Node process alive. These timers are the push of `write-back`, the periodic pull, and the send of the queue after a failure.
201
+ - After a failed run of a timer, the next delay grows with `backoffDelay` of `@mark1russell7/service/backoff`. The option `backoff` gives the `factor`, the `maxDelay` and the `jitter`.
202
+ - A run that succeeds sets the delay back to its interval.
203
+ - `ApiStorage` waits `backoffDelay` between its tries, with full jitter. The options are `retryDelay`, `retryFactor`, `maxRetryDelay` and `retryJitter`.
204
+ - An error with `retryAfterMs` gets at least that wait.
205
+ - The timeout of each call of `ApiStorage` uses the clock too.
206
+
207
+ `HybridStorage.getStatus()` gives the state of the sync. `onStatus(listener)` sends the status at each change. The storages of this package do not write to the console.
208
+
209
+ | Field | Value |
210
+ |---|---|
211
+ | `connected` | False after a remote call failed, until a remote call succeeds |
212
+ | `pendingChanges` | The number of operations in the queue |
213
+ | `syncing` | True while a push or a pull runs |
214
+ | `lastSync` | The time of the last pull that succeeded |
215
+ | `mode` | The mode of the storage |
216
+ | `error` | The newest failure of a load, a push, a pull, the queue store or the sync state store, with its `operation` and its `error`. A later success of the same work clears it. |
217
+
218
+ ```ts
219
+ const stop = users.onStatus((status) => {
220
+ if (status.error) showBanner(`The sync failed: ${String(status.error.error)}`);
221
+ else if (status.pendingChanges > 0) showBanner(`${status.pendingChanges} changes wait for the server`);
222
+ });
223
+ ```
224
+
225
+ `SyncedProcedureRegistry` takes the option `clock` for its auto-sync. Its `getStatus()` and `onStatus()` give the failures of the auto-sync and of the write-through writes.
226
+
227
+ ### The changes, the capture, the sync state and the push feed
228
+
229
+ `HybridStorage` is a sync engine for each local copy: `InMemoryStorage`, `SqliteStorage`, IndexedDB or a SignalDB collection (server stack D9, CLI-21, CLI-15).
230
+
231
+ - `observe(observer)` gives one batch of `set`, `delete` and `clear` changes for each change of the storage.
232
+ - 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.
233
+ - The option `origin` is the tag of the storage. Each write of the storage to the local copy and to the remote has it.
234
+ - With `capture: "observe"`, the storage observes the local copy. It puts each change with another origin in the queue. Thus the application can write to the local copy directly.
235
+ - A captured change always goes in the queue, because the application made it already. In `write-through` mode, the storage sends it at once.
236
+ - The option `syncState` keeps the cursor and the base versions. A new storage with the same store asks only for the changes since the cursor.
237
+ - `MemorySyncStateStore` keeps the state in memory. `IndexedDbSyncStateStore` of `@procedures/store/indexeddb` keeps it in a browser.
238
+ - The storage uses a loaded cursor only with the option `local` and a local copy that has items. Give the state store with a durable local copy.
239
+ - The option `feed` takes a push feed of the remote changes, for example `EventsChangeFeed`. The storage subscribes in the modes `write-through` and `write-back`.
240
+ - A delta whose `since` is the cursor of the storage applies without a read of the remote.
241
+ - A gap, a lost event, a new subscription or another delta makes the storage pull the changes since its cursor.
242
+ - A delta that comes during a pull applies after the pull. Thus a pull does not erase a change that came during it.
243
+ - In `write-through` mode, a pull or a delta does not write over an own write that waits for the remote. After the write, the storage reads the id again, and the rules of a pull decide.
244
+ - With `versionOf`, the storage keeps the versions of its own writes until their echoes come. The echo of an older own write does not write over a newer local write.
245
+ - The option `echoWindow` gives the time that the storage waits for an echo. The default is 30 seconds.
246
+
247
+ `EventsChangeFeed` subscribes to the change events of the core collection procedures. Each write of `collections.<name>.*` publishes `{ since, version, changed, deleted, origin }` on the topic `collections.<name>`.
248
+
249
+ - An event has the ids and the versions, and no value. The storage reads the values with `changes`, so the checks of a read apply. A hint with only deletes applies without a read.
250
+ - A subscriber must have a capability that reads `collection:<name>`, as for a read.
251
+ - With `changeEvents: { values: true }` on the server, the events also have the values. Each row passes the output filter of the host for each subscriber, and a refused id goes in `withheld`.
252
+ - An event with a value for each changed id applies without a read. An event with a withheld id makes the storage pull.
253
+ - The syncs of one storage run one at a time: the deltas, the hints, the pulls, the pushes and the mode switches. Thus a remote delta and a full sync do not run at the same time.
254
+
255
+ ```ts
256
+ import { ApiStorage, EventsChangeFeed, HybridStorage } from "@procedures/store";
257
+ import { IndexedDbQueueStore, IndexedDbSyncStateStore } from "@procedures/store/indexeddb";
258
+
259
+ const users = new HybridStorage(new ApiStorage(client, { collection: "users" }), {
260
+ local: signalDbStorage,
261
+ capture: "observe",
262
+ versionOf: (user) => user.rev,
263
+ queueStore: new IndexedDbQueueStore("users-queue"),
264
+ syncState: new IndexedDbSyncStateStore("users-sync"),
265
+ feed: new EventsChangeFeed(client, { collection: "users" }),
266
+ });
267
+ ```
268
+
269
+ - The source of an `EventsChangeFeed` is a `Client`, a `WebSocketTransport`, or another transport of `@procedures/client`. `Client.subscribe()` sends the subscription through the middleware of the client.
270
+ - The delivery of the events is at most once. Thus the feed gives each loss to the storage, and the storage pulls.
271
+ - The server serves the events with the option `events` of `ProcedureServer`, for example `{ topics: ["collections.*"] }`. Without a subscriber, the procedures publish nothing.
272
+ - `ApiStorage` sends the `origin` of a write to the procedure. Thus the storage of the server and the event have it.
273
+
186
274
  ### The change of a storage in a server
187
275
 
188
276
  `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.