@procedures/store 0.48.1 → 0.49.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 CHANGED
@@ -1,5 +1,42 @@
1
1
  # @procedures/store
2
2
 
3
+ ## 0.49.0
4
+
5
+ ### Breaking changes
6
+
7
+ - `onDropped` of `HybridStorage` (CLI-108) gets the new reason `too-large`. The type `DroppedWrite["reason"]` has the new member `"too-large"`. To migrate, add a case for `"too-large"` to each exhaustive `switch` over `reason`.
8
+
9
+ ### Minor Changes
10
+
11
+ - bf0aa79: A write over the size limit of the remote no longer stops the offline queue of `HybridStorage` (CLI-108).
12
+
13
+ - A batch that the remote refuses, or that is too large, goes again as one write for each id. Only the bad write leaves the queue, and its siblings reach the remote.
14
+ - A write of one id that is too large leaves the queue at once, and the writes behind it go on. The hook `onDropped` gets its id and value with the new reason `too-large`.
15
+ - The new option `isTooLarge` tells which error is too large. The default is the code `PAYLOAD_TOO_LARGE`.
16
+ - The `hello` of the WebSocket server tells its limit `maxPayload`. The client refuses a larger call alone with `PAYLOAD_TOO_LARGE`, and the other calls of the connection go on. Before, the server closed the connection, and each pending call failed.
17
+ - The HTTP server reads and drops up to 64 MiB of a body over the limit. So the client gets the answer 413, not a reset of the connection.
18
+
19
+ Breaking change: the type `DroppedWrite["reason"]` has the new member `"too-large"`. To migrate, add a case for `"too-large"` to each `switch` over the reason. A refused batch no longer drops its good siblings.
20
+
21
+ ### Patch Changes
22
+
23
+ - 5da1994: With `versionOf` and `conflictResolution: "merge"`, `HybridStorage` keeps the remote value that each queued write changed (roadmap CLI-109). `mergeFn(local, remote, base)` gets it as its third argument, for a three-way merge. `base` is `null` when the remote had no item, and undefined when the storage does not know it. The queue store keeps the value, so a restart does not lose it. Old queued operations load with no base value. A merge function with two parameters works as before.
24
+ - Updated dependencies [bf0aa79]
25
+ - Updated dependencies [a1aedc5]
26
+ - @procedures/client@0.49.0
27
+ - @procedures/collections@0.49.0
28
+
29
+ ## 0.48.2
30
+
31
+ ### Patch Changes
32
+
33
+ - bc7359f: `HybridStorage` gets the optional hook `onDropped`. The hook gets each queued write that leaves the queue with no send that the remote took. The causes are a conflict that the policy `remote` decides, a delete of the remote, and a refusal. Before this change, a conflict gave only a count, and a refusal with no `onRefused` gave nothing. The hook gets the id, the value, the reason and the sync, once for each id. `getMetadata()` gives the count as `stats.dropped`. The default policy does not change.
34
+ - Updated dependencies [08beec5]
35
+ - Updated dependencies [48b7127]
36
+ - Updated dependencies [9541575]
37
+ - @procedures/client@0.48.2
38
+ - @procedures/collections@0.48.2
39
+
3
40
  ## 0.48.1
4
41
 
5
42
  ### Patch Changes
package/README.md CHANGED
@@ -255,6 +255,8 @@ await users.setMode("write-through"); // The switch sends the queue, then syncs
255
255
  - Another error, for example `TIMEOUT` or `UNAUTHORIZED`, keeps the write at the head of the queue. The next push sends it again.
256
256
  - After a refusal, the local copy gets the remote values of its ids again. An id that a later queued write changes keeps its local value.
257
257
  - The option `onRefused` gets each refusal: the write, its ids, the error, and `restored`. An error of the hook does not stop the push.
258
+ - The option `onDropped` gets each queued write that leaves the queue with no send that the remote took. The causes are a conflict that the policy `remote` decides, a delete of the remote, and a refusal. It gets one call for each id, with the value, the reason and the sync. `getMetadata()` gives the count as `stats.dropped`.
259
+ - The hook can write the value again. The sync waits for the hook, and an error of the hook does not stop the sync. After a crash, the hook can get a write again.
258
260
  - The option `queueStore` keeps the queue. The storage writes each change to the store before the write ends.
259
261
  - A new storage with the same store loads the queue, shows its writes, and sends them.
260
262
  - `MemoryQueueStore` is the default. `FileQueueStore` keeps the queue in a JSON Lines file, for Node.
@@ -292,6 +294,33 @@ const users = new HybridStorage(new ApiStorage(client, { collection: "users" }),
292
294
  - The option `versionOf` gives the version of an item, for example its ETag.
293
295
  - With versions, a conflict is a queued local write of an id whose remote version changed. The option `conflictResolution` gives the result.
294
296
  - The values of `conflictResolution` are `local`, `remote`, `merge` with `mergeFn`, and `error`. With `error`, the sync throws `ConflictError`.
297
+
298
+ #### The conflict policies
299
+
300
+ The default policy is `remote`. A change of the default changes the behavior of each caller, so the default stays `remote` (roadmap CLI-109).
301
+
302
+ Each example below starts from the same conflict. The remote holds `{ n: 1, rev: "1" }`. The storage queues the local write `{ n: 5 }` with the base version `"1"`. Before the push, another writer writes `{ n: 9, rev: "2" }`.
303
+
304
+ | Policy | Result | Example |
305
+ | --- | --- | --- |
306
+ | `remote` (the default) | The remote item replaces the local item. `onDropped` gets the local value and the remote value. | The local copy and the remote hold `{ n: 9 }`. The hook gets `{ value: { n: 5 }, remote: { n: 9 }, reason: "conflict" }`. |
307
+ | `local` | The queued write replaces the remote item. The write of the other writer goes. | The remote holds `{ n: 5 }`. |
308
+ | `merge` | `mergeFn(local, remote, base)` gives the item, and a new queued write sends it. | A merge that adds the changes of both sides gives `{ n: 13 }`: 1 + (5 - 1) + (9 - 1). |
309
+ | `error` | The sync throws `ConflictError` with the ids. The queue and the remote keep their items. | The application reads `error.ids`, which is `["a"]`, and decides. |
310
+
311
+ ```ts
312
+ type Counter = { n: number; rev: string };
313
+ const mergeCounters = (local: Counter, remote: Counter, base: Counter | null | undefined): Counter => ({
314
+ ...remote,
315
+ n: local.n + remote.n - (base?.n ?? 0),
316
+ });
317
+ const storage = new HybridStorage<Counter>(remote, {
318
+ versionOf: (item) => item.rev,
319
+ conflictResolution: "merge",
320
+ // The option type is generic over the item: a merge of one item type needs the cast
321
+ mergeFn: mergeCounters as NonNullable<HybridStorageOptions["mergeFn"]>,
322
+ });
323
+ ```
295
324
  - Without versions, a queued write always wins. A local value that differs from the remote value is a conflict.
296
325
  - 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.
297
326
  - 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.
@@ -312,6 +341,20 @@ With `versionOf`, the push of a queued write sends its base version as the expec
312
341
  - When only some ids of a batch are echoes, the batch does not leave the queue. The policy decides for the other ids, as before.
313
342
  - An id without a known base version gets no expected version. Its queued write wins, as before.
314
343
  - The remote must test the precondition: an `ApiStorage` of the collection procedures over a storage with the same `versionOf`. A remote that cannot test it refuses each push.
344
+
345
+ ### The base value of a three-way merge
346
+
347
+ With `versionOf` and the policy `merge`, a queued write also keeps its base value (roadmap CLI-109). The base value is the remote item at the base version.
348
+
349
+ - `mergeFn(local, remote, base)` gets the base value as its third argument. Thus a merge can keep the changes of both sides.
350
+ - `base` is `null` when the remote had no item at the base version.
351
+ - `base` is undefined when the storage does not know the item at the base version. An example is a write after a restart, before a sync reads the item.
352
+ - The queue store keeps the base values with the queued writes. A restart does not lose them.
353
+ - An operation that an older version queued has no base value. It loads, and its merge gets undefined.
354
+ - A merge function with two parameters works as before.
355
+ - The other policies keep no base value.
356
+ - A write of the storage reads its base value from the local copy before the write.
357
+ - A captured change of `capture: "observe"` has no value before it. Thus with capture, the storage keeps in memory the remote value of each id that a sync reads.
315
358
  - The option `sendBaseVersions: false` stops the expected versions. Then a queued write replaces the remote item, as before CLI-16.
316
359
  - A write of the mode `write-through` goes to the remote directly. It sends only the expected version of its caller.
317
360