@procedures/store 0.14.0 → 0.15.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,63 @@
1
1
  # @procedures/store
2
2
 
3
+ ## 0.15.0
4
+
5
+ ### Minor Changes
6
+
7
+ - c7198ca: Breaking (roadmap CLI-16, decision D11): the collection writes take an optional expected version. This is its own MINOR of the client group (D13).
8
+
9
+ New:
10
+
11
+ - `collections.<name>.set` and `delete` take `expectedVersion`, and each item of `setBatch` takes `expectedVersion`. The value is the version of the current item that the write expects. `null` expects no item.
12
+ - A conflict changes nothing. The call fails with the new protocol code `VERSION_CONFLICT` (HTTP 409, not retryable). The metadata key `conflicts` (and the field `conflicts` of the HTTP body) has the current item and its version for each id.
13
+ - `StorageItemWriteOptions.expectedVersion` (`set`, `delete`) and `StorageBatchWriteOptions.expectedVersions` (`setBatch`) in both copies of `CollectionStorage`. `VersionConflictError`, `versionConflictsOf()` and the other helpers come from `@procedures/client/query`, `@procedures/client` and `@procedures/collections`.
14
+ - The option `versionOf` of `InMemoryStorage`, `SqliteStorage` and `MongoDocumentStorage` gives the version of an item. The test and the write are one step: synchronous in memory, one transaction in SQLite, the transaction or a compare-and-set on `_seq` in MongoDB. `MongoStorage` refuses a precondition.
15
+ - `ApiStorage` sends the expected versions to the collection procedures, and gives a `VERSION_CONFLICT` back as a `VersionConflictError`.
16
+ - `HybridStorage` sends the base version of each queued write as its expected version. A conflict of the remote goes to `conflictResolution`, as a conflict of a pull. The new option `sendBaseVersions: false` stops it.
17
+ - `ClientError.metadata` gives the metadata of the error response.
18
+ - The contract suite `@procedures/collections/testing` has the cases of the preconditions, and the option `createWithVersions`.
19
+
20
+ The changes of behavior:
21
+
22
+ - A storage must test a precondition, or refuse the write. A storage without `versionOf` refuses a write with an expected version with a `TypeError` whose code is `PRECONDITION_NOT_SUPPORTED`. The contract suite tests it for each storage.
23
+ - A `HybridStorage` with `versionOf` now sends the base version of each queued write. A queued write over a change of another writer got through before. Now the remote refuses it with `VERSION_CONFLICT`, and `conflictResolution` decides: `remote` drops the queued write, `merge` sends the merged item, `local` sends the write again over the current version, and `error` throws `ConflictError` and keeps the queue. The remote must test the precondition: a server storage without `versionOf` refuses each push.
24
+ - `ApiStorage`: a write without an expected version writes as before, and gets no `VERSION_CONFLICT`. Only a caller that gives an expected version, or a `HybridStorage` with `versionOf` over it, can get one.
25
+ - `HybridStorage` with `versionOf`: a queued write that won before can now get the conflict path. The opt-out is `new HybridStorage(remote, { versionOf, sendBaseVersions: false })`: the push sends no expected version, and the last write wins, as before. The default is the new behavior.
26
+ - A write without an expected version writes as before: the last write wins.
27
+
28
+ Migration:
29
+
30
+ - **Site.** No change. The site does not write collections.
31
+ - **MiniMongo.** No change for a storage or a `HybridStorage` without `versionOf`. A `HybridStorage` with `versionOf` over `ApiStorage` needs a server storage with the same `versionOf` (for example `new InMemoryStorage({ versionOf })`), or the option `sendBaseVersions: false`.
32
+ - **Graph.** Graph keeps last-write-wins first (D11): it gives no expected version, so nothing changes. `GraphDocsStorage` must refuse a write with an expected version until it tests one. Add the guard to its `set`, `delete` and `setBatch`, and run the contract suite: its first case tests the refusal. To adopt the version later, stamp a version field on the server, give the storage a `versionOf` that tests it in the write, and give the same `versionOf` to the `HybridStorage` of the client. Then add `createWithVersions` to the contract run.
33
+ - **jarvis.** No change: jarvis gives no expected version. A storage of jarvis that implements `CollectionStorage` must refuse a write with an expected version, as for Graph.
34
+ - **Other storages.** Each class that implements `CollectionStorage` reads the new options of `set`, `delete` and `setBatch`. It tests them in the write, or it rejects the write with `preconditionNotSupported()`. It never ignores them.
35
+
36
+ ### Patch Changes
37
+
38
+ - Updated dependencies [c7198ca]
39
+ - @procedures/client@0.15.0
40
+ - @procedures/collections@0.15.0
41
+
42
+ ## 0.14.1
43
+
44
+ ### Patch Changes
45
+
46
+ - fc968f6: Fixed: `@procedures/geo`, `@procedures/ics` and `@procedures/splay` import `@procedures/client/browser`, not the root of `@procedures/client` (CLI-39). The root also exports `HttpServerTransport`, which imports `http`. Thus a browser build with Vite found `http` and gave a warning. The exports of the packages do not change.
47
+
48
+ New: each package that operates in a browser names its browser entries in the field `client.browser` of its `package.json`. A CI check bundles each of these entries for a browser with esbuild. The check fails when an import reaches a Node built-in, with or without the prefix `node:`. The root of `@procedures/client` joins the check after CLI-6. `@procedures/splay` joins it when `splay.bridge.info` reads its version without `node:fs/promises`.
49
+ - 26a145d: The new entry `@procedures/store/browser` has the parts of the store that run in a browser (roadmap CLI-19). It exports `HybridStorage` with its errors, `ApiStorage`, `EventsChangeFeed` and all of `@procedures/store/indexeddb`. No module of the entry imports a module of Node. The entry does not have `FileProgramStore` or `FileQueueStore`, because they use `node:fs`.
50
+
51
+ `HybridStorage`, `ApiStorage` and `EventsChangeFeed` import `@procedures/client/browser` and not the root of the client, because the root imports `http` and `ws`. The classes are the same as the classes of the root. PATCH: the change adds an entry, and nothing changes for the existing imports.
52
+ - Updated dependencies [85999c1]
53
+ - Updated dependencies [fc968f6]
54
+ - Updated dependencies [dc7a462]
55
+ - Updated dependencies [e091108]
56
+ - Updated dependencies [073e4c3]
57
+ - Updated dependencies [6969637]
58
+ - @procedures/client@0.14.1
59
+ - @procedures/collections@0.14.1
60
+
3
61
  ## 0.14.0
4
62
 
5
63
  ### Patch Changes
package/README.md CHANGED
@@ -94,10 +94,13 @@ The contract suite of `@procedures/collections/testing` tests `ApiStorage`, and
94
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
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, $remove, $insert }`. An index of `$remove` or `$insert` is a position at the time of the patch. A client that works from an older read uses the version check of the collection, where the collection has one.
96
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
+ - **The preconditions.** `set`, `delete` and `setBatch` take an expected version of the current item (roadmap CLI-16). `ApiStorage` sends it to the collection procedures, and the storage of the server tests it. A conflict rejects with `VersionConflictError`, with the current items. A storage that cannot test it rejects the write.
97
98
 
98
99
  Notes:
99
100
 
100
101
  - A custom `service` or a custom `get` operation gets the input `{ id }`, as before. Its `null` reads back as `undefined`.
102
+ - A custom `service` gets no expected version. `ApiStorage` rejects a write with one, so the precondition is not lost.
103
+ - `HybridStorage` gives the expected version of a caller to its local copy. The default local copy gets the option `versionOf` of the storage. In `write-through` mode, the remote write gets it too. In `remote-only` mode, only the remote tests it.
101
104
  - 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
105
  - `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
106
  - `HybridStorage.query()` reads the local copy, or the remote in `remote-only` mode. Its option `queryPolicy` goes to the storage under it.
@@ -192,6 +195,21 @@ const users = new HybridStorage(new ApiStorage(client, { collection: "users" }),
192
195
  - Without versions, a pull reads each local item without a queued write. It compares the JSON of the local item and the remote item.
193
196
  - The option `pullInterval` starts a periodic pull in the modes `write-through` and `write-back`. `close()` stops it.
194
197
 
198
+ ### The base versions on the server
199
+
200
+ With `versionOf`, the push of a queued write sends its base version as the expected version (roadmap CLI-16, decision D11). The base version is the remote version of the id when the local write went in the queue. The server then refuses a write over a change of another writer.
201
+
202
+ - The remote answers `VERSION_CONFLICT` with the current item. The storage gives the conflict to `conflictResolution`, as a conflict of a pull.
203
+ - `remote`: the remote item replaces the local item, and the queued writes of the id go.
204
+ - `merge`: `mergeFn` gives the item. A new queued write sends it, with the current version as its base.
205
+ - `local`: the current version becomes the base, and the push sends the queued write again. It replaces the remote item.
206
+ - `error`: the push throws `ConflictError` with the ids. The queue and the remote keep their items, and the next push tries again.
207
+ - A remote item that is the value of the queued write is not a conflict. An earlier send reached the remote, and its answer was lost.
208
+ - An id without a known base version gets no expected version. Its queued write wins, as before.
209
+ - 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.
210
+ - The option `sendBaseVersions: false` stops the expected versions. Then a queued write replaces the remote item, as before CLI-16.
211
+ - A write of the mode `write-through` goes to the remote directly. It sends only the expected version of its caller.
212
+
195
213
  ### The clock, the backoff and the status
196
214
 
197
215
  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.
@@ -286,6 +304,34 @@ const users = new HybridStorage(new ApiStorage(client, { collection: "users" }),
286
304
 
287
305
  `createSyncedRegistry({ type: "hybrid", client, mode })` takes the first mode. `setMode()` of the registry changes the mode of its storage.
288
306
 
307
+ ## The browser entry
308
+
309
+ The entry `@procedures/store/browser` has the parts of the store that run in a browser (roadmap CLI-19). A page imports this entry and not the package root.
310
+
311
+ - `HybridStorage`, its errors and its types.
312
+ - `ApiStorage`, the remote storage over the collection procedures of a server.
313
+ - `EventsChangeFeed`, the push feed of the remote changes.
314
+ - All of `@procedures/store/indexeddb`: the IndexedDB stores and their fallbacks in memory.
315
+
316
+ 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
+
318
+ The entry does not have these parts:
319
+
320
+ - `FileProgramStore` and `FileQueueStore`. They use `node:fs`.
321
+ - The procedures, the synced registry and the serialization. They import the root of the client. They are in the package root, for Node.
322
+
323
+ An import of the entry registers nothing. Its classes are the same as the classes of the package root.
324
+
325
+ ```ts
326
+ import { ApiStorage, EventsChangeFeed, HybridStorage, IndexedDbQueueStore, IndexedDbSyncStateStore } from "@procedures/store/browser";
327
+
328
+ const users = new HybridStorage(new ApiStorage(client, { collection: "users" }), {
329
+ queueStore: new IndexedDbQueueStore("users-queue"),
330
+ syncState: new IndexedDbSyncStateStore("users-sync"),
331
+ feed: new EventsChangeFeed(client, { collection: "users" }),
332
+ });
333
+ ```
334
+
289
335
  ## The root of the files
290
336
 
291
337
  No input of `procedure.store`, `procedure.load` and `procedure.sync` names a file. Under the root of a host (`mark --fs-root`, `procedure-server --fs-root`, `MCP_FS_ROOT`), they use the program store of the host. The host chooses the file of a file store, also outside the root. A loaded program is data-driven, so its nested calls get their own rules.