@procedures/store 0.7.0 → 0.8.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/CHANGELOG.md +77 -0
- package/README.md +98 -1
- package/dist/.tsbuildinfo +1 -1
- package/dist/indexeddb/sync-state-store.d.ts +40 -0
- package/dist/indexeddb/sync-state-store.d.ts.map +1 -0
- package/dist/indexeddb/sync-state-store.js +94 -0
- package/dist/indexeddb/sync-state-store.js.map +1 -0
- package/dist/indexeddb.d.ts +7 -2
- package/dist/indexeddb.d.ts.map +1 -1
- package/dist/indexeddb.js +6 -2
- package/dist/indexeddb.js.map +1 -1
- package/dist/storage/api.d.ts +94 -11
- package/dist/storage/api.d.ts.map +1 -1
- package/dist/storage/api.js +177 -24
- package/dist/storage/api.js.map +1 -1
- package/dist/storage/feed.d.ts +133 -0
- package/dist/storage/feed.d.ts.map +1 -0
- package/dist/storage/feed.js +159 -0
- package/dist/storage/feed.js.map +1 -0
- package/dist/storage/file-queue.d.ts.map +1 -1
- package/dist/storage/file-queue.js +2 -1
- package/dist/storage/file-queue.js.map +1 -1
- package/dist/storage/hybrid.d.ts +415 -20
- package/dist/storage/hybrid.d.ts.map +1 -1
- package/dist/storage/hybrid.js +1405 -196
- package/dist/storage/hybrid.js.map +1 -1
- package/dist/storage/index.d.ts +6 -2
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/index.js +6 -1
- package/dist/storage/index.js.map +1 -1
- package/dist/storage/sync-state.d.ts +56 -0
- package/dist/storage/sync-state.d.ts.map +1 -0
- package/dist/storage/sync-state.js +58 -0
- package/dist/storage/sync-state.js.map +1 -0
- package/dist/storage/synced-registry.d.ts +22 -4
- package/dist/storage/synced-registry.d.ts.map +1 -1
- package/dist/storage/synced-registry.js +63 -31
- package/dist/storage/synced-registry.js.map +1 -1
- package/dist/storage/types.d.ts +22 -1
- package/dist/storage/types.d.ts.map +1 -1
- package/package.json +4 -3
- package/src/indexeddb/sync-state-store.ts +101 -0
- package/src/indexeddb.ts +7 -2
- package/src/storage/api.ts +240 -28
- package/src/storage/feed.ts +251 -0
- package/src/storage/file-queue.ts +2 -1
- package/src/storage/hybrid.ts +1526 -203
- package/src/storage/index.ts +17 -0
- package/src/storage/sync-state.ts +89 -0
- package/src/storage/synced-registry.ts +76 -31
- package/src/storage/types.ts +25 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,82 @@
|
|
|
1
1
|
# @procedures/store
|
|
2
2
|
|
|
3
|
+
## 0.8.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 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).
|
|
8
|
+
|
|
9
|
+
- 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.
|
|
10
|
+
- 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.
|
|
11
|
+
- 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.
|
|
12
|
+
- `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.
|
|
13
|
+
- `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.
|
|
14
|
+
- `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.
|
|
15
|
+
- `InMemoryStorage` has the three methods, and the options `queryPolicy` and `onObserverError`. `StorageListeners` is the set of listeners that the storages share.
|
|
16
|
+
- 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.
|
|
17
|
+
- 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.
|
|
18
|
+
- `createCollectionProcedures(name, { queryPolicy })` and `ProcedureServer.registerCollectionProcedures(name, { queryPolicy })` take the policy of the host.
|
|
19
|
+
- 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`.
|
|
20
|
+
- `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.
|
|
21
|
+
- `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.
|
|
22
|
+
- `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.
|
|
23
|
+
- `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.
|
|
24
|
+
- `SqliteStorage` has `query()`, `patch()` and the option `queryPolicy`. A patch runs in one transaction.
|
|
25
|
+
- `@procedures/store` depends on `@mark1russell7/service`, and `@procedures/collections` depends on `@procedures/client`.
|
|
26
|
+
- 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).
|
|
27
|
+
|
|
28
|
+
- `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.
|
|
29
|
+
- 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 }`.
|
|
30
|
+
- `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.
|
|
31
|
+
- 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.
|
|
32
|
+
- 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).
|
|
33
|
+
- 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.
|
|
34
|
+
- 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).
|
|
35
|
+
- `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()`.
|
|
36
|
+
- 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.
|
|
37
|
+
- 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.
|
|
38
|
+
- `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.
|
|
39
|
+
- 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).
|
|
40
|
+
- 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.
|
|
41
|
+
- `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.
|
|
42
|
+
- 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`.
|
|
43
|
+
- Updated dependencies [2d3f235]
|
|
44
|
+
- Updated dependencies [1aa9ef7]
|
|
45
|
+
- Updated dependencies [2523b1b]
|
|
46
|
+
- Updated dependencies [c559b00]
|
|
47
|
+
- @procedures/client@0.8.1
|
|
48
|
+
- @procedures/collections@0.8.1
|
|
49
|
+
|
|
50
|
+
## 0.8.0
|
|
51
|
+
|
|
52
|
+
### Patch Changes
|
|
53
|
+
|
|
54
|
+
- aa6e3aa: `ApiStorage` has `getIds()`. A full pull through it does not read each entry for the ids.
|
|
55
|
+
|
|
56
|
+
- The collections have the procedure `collections.<name>.ids`. It gives the ids in the order of the entries, without the values. Its effects read the collection and write nothing. A storage without `getIds()` gives the ids of its entries.
|
|
57
|
+
- The `CollectionStorage` of the core has the optional method `getIds()`, as the `CollectionStorage` of `@procedures/collections` has.
|
|
58
|
+
- `ApiStorage.getIds()` uses the procedure `ids`. An older server without it answers `NOT_FOUND`. Then the storage reads the ids from the entries, and it does not ask for `ids` again.
|
|
59
|
+
- `ApiStorageOptions.operations` takes `ids`, the name of the operation. The default is `ids`.
|
|
60
|
+
- 9380ba0: `FileQueueStore.load` names the right line in its "not JSON (line N)" error when blank lines come before the bad line. Before, it counted the lines after it had removed the blank ones.
|
|
61
|
+
- 276b93c: A pull of `HybridStorage` reads a local item only when the result depends on it. Before, the pull read each local item. The results do not change.
|
|
62
|
+
|
|
63
|
+
- A pull does not read the item of a queued write that wins: without versions, or when the remote version did not change after the last sync.
|
|
64
|
+
- With `versionOf`, the storage keeps the version of each local item that it wrote or read. A pull then reads a local item only for a conflict, or when it does not know the version. A full pull of a collection without a change reads no local item.
|
|
65
|
+
- Without versions, a pull reads each local item without a queued write. It compares the JSON of the two items, as before.
|
|
66
|
+
- Only the storage writes to its local copy: with `versionOf`, a pull does not see a write from outside.
|
|
67
|
+
- Updated dependencies [4b09a03]
|
|
68
|
+
- Updated dependencies [4b09a03]
|
|
69
|
+
- Updated dependencies [aa6e3aa]
|
|
70
|
+
- Updated dependencies [698e24d]
|
|
71
|
+
- Updated dependencies [4b09a03]
|
|
72
|
+
- Updated dependencies [600c497]
|
|
73
|
+
- Updated dependencies [7c2e7f7]
|
|
74
|
+
- Updated dependencies [90ea741]
|
|
75
|
+
- Updated dependencies [c4cfa34]
|
|
76
|
+
- Updated dependencies [d25805a]
|
|
77
|
+
- @procedures/client@0.8.0
|
|
78
|
+
- @procedures/collections@0.8.0
|
|
79
|
+
|
|
3
80
|
## 0.7.0
|
|
4
81
|
|
|
5
82
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -88,13 +88,23 @@ The contract suite of `@procedures/collections/testing` tests `ApiStorage`, and
|
|
|
88
88
|
- **Copies.** A change of a written or a read value does not change the stored item. `HybridStorage` takes a copy of each written value for the local copy, the queue and the remote. The values that `ApiStorage` sends and reads go through the transport.
|
|
89
89
|
- **`null`.** A stored `null` reads back as `null`. `ApiStorage.get()` sends `{ id, found: true }` to the procedure `collections.<name>.get`. The procedure then gives `{ found, value }` in one call.
|
|
90
90
|
- **The order of a delta.** `getChangesSince()` follows the order of the last change of each id.
|
|
91
|
-
- **The ids.** `
|
|
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.
|
|
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.
|
|
98
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.
|
|
99
109
|
- `SyncedProcedureRegistry.close()` closes its storage. After it, `register()` and `unregister()` change only the registry in memory.
|
|
100
110
|
|
|
@@ -177,12 +187,99 @@ const users = new HybridStorage(new ApiStorage(client, { collection: "users" }),
|
|
|
177
187
|
- With versions, a conflict is a queued local write of an id whose remote version changed. The option `conflictResolution` gives the result.
|
|
178
188
|
- The values of `conflictResolution` are `local`, `remote`, `merge` with `mergeFn`, and `error`. With `error`, the sync throws `ConflictError`.
|
|
179
189
|
- Without versions, a queued write always wins. A local value that differs from the remote value is a conflict.
|
|
190
|
+
- A pull reads a local item only when the result depends on it. A pull does not read the item of a queued write that wins.
|
|
191
|
+
- With versions, the storage knows the version of each local item that it wrote or read. Then a pull reads a local item only for a conflict, or when the version is not known.
|
|
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.
|
|
180
193
|
- The option `pullInterval` starts a periodic pull in the modes `write-through` and `write-back`. `close()` stops it.
|
|
181
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
|
+
|
|
182
274
|
### The change of a storage in a server
|
|
183
275
|
|
|
184
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.
|
|
185
277
|
|
|
278
|
+
- The new calls use the new storage at once.
|
|
279
|
+
- Before the close, the server waits for the calls that took the old storage. A stream holds its storage until its end.
|
|
280
|
+
- The option `grace` gives the longest wait, for example `seconds(5)`. The default is 30 seconds. A plain number is a `TypeError`.
|
|
281
|
+
- When the time ends, the server closes the old storage. A call that still uses it then fails with the error of a closed storage.
|
|
282
|
+
|
|
186
283
|
`ProcedureServer.stop()` closes each storage one time and removes the storages from the server. A closed storage rejects each operation. Before the server starts again, give it new storages with `useStorage()`.
|
|
187
284
|
|
|
188
285
|
### The mode of a synced registry
|