@procedures/store 0.2.2 → 0.3.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 +63 -0
- package/README.md +109 -2
- package/dist/.tsbuildinfo +1 -1
- package/dist/indexeddb/idb.d.ts +100 -0
- package/dist/indexeddb/idb.d.ts.map +1 -0
- package/dist/indexeddb/idb.js +137 -0
- package/dist/indexeddb/idb.js.map +1 -0
- package/dist/indexeddb/program-store.d.ts +42 -0
- package/dist/indexeddb/program-store.d.ts.map +1 -0
- package/dist/indexeddb/program-store.js +71 -0
- package/dist/indexeddb/program-store.js.map +1 -0
- package/dist/indexeddb/queue-store.d.ts +38 -0
- package/dist/indexeddb/queue-store.d.ts.map +1 -0
- package/dist/indexeddb/queue-store.js +73 -0
- package/dist/indexeddb/queue-store.js.map +1 -0
- package/dist/indexeddb.d.ts +19 -0
- package/dist/indexeddb.d.ts.map +1 -0
- package/dist/indexeddb.js +17 -0
- package/dist/indexeddb.js.map +1 -0
- package/dist/program-store/file.d.ts +5 -0
- package/dist/program-store/file.d.ts.map +1 -1
- package/dist/program-store/file.js +5 -2
- package/dist/program-store/file.js.map +1 -1
- package/dist/program-store/procedures.d.ts.map +1 -1
- package/dist/program-store/procedures.js +5 -2
- package/dist/program-store/procedures.js.map +1 -1
- package/dist/storage/api.d.ts +18 -7
- package/dist/storage/api.d.ts.map +1 -1
- package/dist/storage/api.js +34 -3
- package/dist/storage/api.js.map +1 -1
- package/dist/storage/factory.d.ts +13 -2
- package/dist/storage/factory.d.ts.map +1 -1
- package/dist/storage/factory.js +12 -4
- package/dist/storage/factory.js.map +1 -1
- package/dist/storage/file-queue.d.ts +32 -0
- package/dist/storage/file-queue.d.ts.map +1 -0
- package/dist/storage/file-queue.js +101 -0
- package/dist/storage/file-queue.js.map +1 -0
- package/dist/storage/hybrid.d.ts +355 -42
- package/dist/storage/hybrid.d.ts.map +1 -1
- package/dist/storage/hybrid.js +1093 -250
- package/dist/storage/hybrid.js.map +1 -1
- package/dist/storage/index.d.ts +4 -2
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/index.js +5 -1
- package/dist/storage/index.js.map +1 -1
- package/dist/storage/queue.d.ts +67 -0
- package/dist/storage/queue.d.ts.map +1 -0
- package/dist/storage/queue.js +85 -0
- package/dist/storage/queue.js.map +1 -0
- package/dist/storage/synced-registry.d.ts +18 -2
- package/dist/storage/synced-registry.d.ts.map +1 -1
- package/dist/storage/synced-registry.js +37 -3
- package/dist/storage/synced-registry.js.map +1 -1
- package/dist/storage/types.d.ts +5 -2
- package/dist/storage/types.d.ts.map +1 -1
- package/package.json +16 -6
- package/src/indexeddb/idb.ts +190 -0
- package/src/indexeddb/program-store.ts +80 -0
- package/src/indexeddb/queue-store.ts +80 -0
- package/src/indexeddb.ts +19 -0
- package/src/program-store/file.ts +5 -2
- package/src/program-store/procedures.ts +5 -2
- package/src/storage/api.ts +50 -10
- package/src/storage/factory.ts +21 -4
- package/src/storage/file-queue.ts +103 -0
- package/src/storage/hybrid.ts +1142 -274
- package/src/storage/index.ts +15 -2
- package/src/storage/queue.ts +128 -0
- package/src/storage/synced-registry.ts +49 -4
- package/src/storage/types.ts +6 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,68 @@
|
|
|
1
1
|
# @procedures/store
|
|
2
2
|
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 7873940: The collection mode switch (roadmap R28). `HybridStorage` has four modes: `local-only` (no remote call, each write waits in the queue), `write-through`, `write-back` and `remote-only` (no local copy). The option `mode` gives the first mode, and `writeStrategy` stays as a deprecated name of it. `setMode()` changes the mode while the storage operates. The switch waits for the operations that run, sends the queue to the remote first, and syncs the local copy when it leaves `local-only` or `remote-only`. A switch to `remote-only` drops the local copy only after the flush. When the remote fails, the mode stays and `setMode()` throws `ModeSwitchError`. A switch to `local-only` makes no remote call.
|
|
8
|
+
|
|
9
|
+
The queue is durable: the option `queueStore` takes a `QueueStore` (`load`, `append`, `replace`, `clear`). `MemoryQueueStore` is the default, and `FileQueueStore` of the new entry `@procedures/store/file-queue` keeps the queue in a JSON Lines file. A write ends only after the store kept it (`QueueStoreError` when it cannot). A new storage with the same store loads the queue, shows its writes and sends them. The modes `write-back` and `local-only` always use the queue: before, `write-back` with `offlineQueue: false` never sent a write.
|
|
10
|
+
|
|
11
|
+
A sync asks for the changes since the version of the last sync. Storages get the optional `getChangesSince(version)` (`InMemoryStorage` keeps a change log with tombstones, `ApiStorage` calls the new collection procedure `collections.<name>.changes`), and a remote without it gives all its entries. The option `versionOf` gives the version of an item: with versions, a conflict is a queued local write of an id whose remote version changed, and `conflictResolution` decides (`error` throws `ConflictError`). The option `pullInterval` starts a periodic pull. Storages get the optional `flush()`, and `ProcedureServer.useStorage(name, storage, { drain, close })` flushes the old storage before the change. `createSyncedRegistry({ type: "hybrid", mode })` takes the mode, and the synced registry has `setMode()` and `getMode()`.
|
|
12
|
+
- c799cdd: `HybridStorage` cannot lose data (roadmap R2). A sync gets the ids of the remote items from the new `getEntries()` of a storage (the collection procedure `collections.<name>.entries`), or else from `keyOf`. A remote item without a key stops the sync with `MissingKeysError` before any change: before, such a list emptied the local cache. A write-through that the remote cannot take waits in the queue and throws `PendingWriteError`, and a timer sends the queue. A write that the remote refuses is undone in the local cache. A full queue refuses a write with `QueueFullError`: before, it dropped the oldest operation. A second try of a write replaces its queued operation. A write-through sends the older queued operations first. The reads wait for the first sync.
|
|
13
|
+
- 4ef0d44: The new entry `@procedures/store/indexeddb` has the stores of a browser (roadmap R45). `IndexedDbProgramStore` keeps programs in IndexedDB, and it follows the contract of the other program stores. `IndexedDbQueueStore` keeps the queue of a `HybridStorage` in IndexedDB (the open point of R28). `hasIndexedDb()` tells if the browser has IndexedDB, and the entry also exports `MemoryProgramStore` and `MemoryQueueStore` as fallbacks. The entry does not import the client or zod, and the package root does not import it.
|
|
14
|
+
|
|
15
|
+
`HybridStorage` has the new option `local`: the storage of the local copy (the type `LocalCopyStorage`). The default is a new `InMemoryStorage`, as before. A durable local copy, for example `SqliteStorage` of `@procedures/sqlite`, keeps the items after a restart. Then a new storage in `local-only` mode reads the items of the last run (the open point "the local copy is not durable" of R28). `close()` now closes the local copy too. A local copy that cannot take the loaded queue makes the storage refuse each operation, as a queue store that cannot load does.
|
|
16
|
+
- fb30718: Typed durations (roadmap R11). This change breaks the API. Each public option that is a time takes a `Duration` of `@mark1russell7/quantity`, made with `ms()`, `seconds()`, `minutes()` or `hours()` of `@procedures/client`. A plain number is a type error, and at run time the option throws a `TypeError` that names it. This covers the timeouts and `resilience()`, the retry delays, the cache, the circuit breaker, the rate limit, the HTTP, WebSocket and mock transports, the WebSocket server heartbeat, `timeoutInterceptor({ limit })` (was `ms`), `hostPolicy({ timeout })` (was `timeoutMs`), the idempotency cache (`ttl`, was `ttlMs`), `ApiStorage`, `HybridStorage`, the synced registry and `connect()` of the peer. The per-call overrides in a context (`timeout`, `retry.delay`, `cache.ttl`) and the `route()` overrides (`timeout.overall`, was `timeout.ms`) take durations too. The wire and the inputs of procedures keep milliseconds.
|
|
17
|
+
|
|
18
|
+
Migration: `createOverallTimeoutMiddleware({ overall: 5000 })` becomes `createOverallTimeoutMiddleware({ overall: seconds(5) })`.
|
|
19
|
+
|
|
20
|
+
### Patch Changes
|
|
21
|
+
|
|
22
|
+
- 43f7de5: Retry and the circuit breaker (roadmap R12). The retry middleware has a retry budget (`budget`, default: 0.2 of the calls and at least 10 retries a second), so a failing service gets a fraction more requests and not `maxRetries` times more. The delay has full jitter by default (`jitter: 1`: a random time from 0 to the backoff, before ±10 percent). A server that asks for a wait gets it: `retryAfterMs` of a thrown error becomes the `Retry-After` header and the `retry-after` metadata, and the retry waits at least that long. `maxAttempts` in a context counts the tries, as its name says (before, it counted the retries). The circuit breaker counts only the failures of availability (`isAvailabilityFailure`: UNAVAILABLE, TIMEOUT, a stopped server, a gateway error, network errors): an abort or a validation error no longer opens it. Its windows use a monotonic clock. `ApiStorage` asks a retry middleware of its client for one try, so the two loops cannot multiply.
|
|
23
|
+
- Updated dependencies [ddea529]
|
|
24
|
+
- Updated dependencies [b2a3e3a]
|
|
25
|
+
- Updated dependencies [a8c47f1]
|
|
26
|
+
- Updated dependencies [381ce3a]
|
|
27
|
+
- Updated dependencies [0c627f8]
|
|
28
|
+
- Updated dependencies [ae7f05f]
|
|
29
|
+
- Updated dependencies [7873940]
|
|
30
|
+
- Updated dependencies [a70d445]
|
|
31
|
+
- Updated dependencies [bc70357]
|
|
32
|
+
- Updated dependencies [07cc998]
|
|
33
|
+
- Updated dependencies [a6b01a9]
|
|
34
|
+
- Updated dependencies [c799cdd]
|
|
35
|
+
- Updated dependencies [540f8d9]
|
|
36
|
+
- Updated dependencies [b6daaec]
|
|
37
|
+
- Updated dependencies [5b9264e]
|
|
38
|
+
- Updated dependencies [956e88b]
|
|
39
|
+
- Updated dependencies [ff62281]
|
|
40
|
+
- Updated dependencies [d81d0ed]
|
|
41
|
+
- Updated dependencies [aa67cc7]
|
|
42
|
+
- Updated dependencies [061fa22]
|
|
43
|
+
- Updated dependencies [cfb9d69]
|
|
44
|
+
- Updated dependencies [43f7de5]
|
|
45
|
+
- Updated dependencies [b07bb50]
|
|
46
|
+
- Updated dependencies [4a3bfa1]
|
|
47
|
+
- Updated dependencies [bd0b626]
|
|
48
|
+
- Updated dependencies [a6be873]
|
|
49
|
+
- Updated dependencies [fb30718]
|
|
50
|
+
- Updated dependencies [c5528e4]
|
|
51
|
+
- Updated dependencies [fee5d9c]
|
|
52
|
+
- Updated dependencies [21f7743]
|
|
53
|
+
- Updated dependencies [31a0b46]
|
|
54
|
+
- @procedures/client@0.3.0
|
|
55
|
+
- @procedures/collections@0.3.0
|
|
56
|
+
|
|
57
|
+
## 0.2.3
|
|
58
|
+
|
|
59
|
+
### Patch Changes
|
|
60
|
+
|
|
61
|
+
- 15e48cf: Three fixes of the architecture review (2026-10-16). A `timeoutMs` that is not a finite number of zero or more asks for nothing: before, `NaN` removed the deadline. A procedure of `procedure.define` declares no effects of its own, so a read-only capability runs it and examines each nested call. `mcp.list-tools`, `mcp.serve`, `_discovery.announce`, `manifest.generate`, `server.create`, `procedure.export` and `lib.audit` read the registry of the call, not the global registry. `getProcedureInfos` and `watchProcedures` take a registry.
|
|
62
|
+
- Updated dependencies [15e48cf]
|
|
63
|
+
- @procedures/client@0.2.3
|
|
64
|
+
- @procedures/collections@0.2.3
|
|
65
|
+
|
|
3
66
|
## 0.2.2
|
|
4
67
|
|
|
5
68
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -37,6 +37,26 @@ console.log(await client.exec(["math", "addTen"], { x: 5 })); // 15
|
|
|
37
37
|
| `MemoryProgramStore` | In the memory of the process |
|
|
38
38
|
| `FileProgramStore` | In one JSON file. A lock file and an atomic rename keep the programs of two processes. |
|
|
39
39
|
| `CollectionProgramStore` | In a collection storage: `InMemoryStorage` of `@procedures/collections`, `ApiStorage` (the collection procedures of a server) or `HybridStorage` (a local cache with a remote copy) |
|
|
40
|
+
| `IndexedDbProgramStore` | In the IndexedDB of a browser. It is in `@procedures/store/indexeddb`. |
|
|
41
|
+
|
|
42
|
+
### The stores of a browser
|
|
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.
|
|
45
|
+
|
|
46
|
+
- `IndexedDbProgramStore` keeps the programs in one database, `procedures-programs` by default.
|
|
47
|
+
- `IndexedDbQueueStore` keeps the queue of a `HybridStorage`. Give each storage its own database.
|
|
48
|
+
- `hasIndexedDb()` tells if the browser has IndexedDB. `MemoryProgramStore` and `MemoryQueueStore` are the fallbacks.
|
|
49
|
+
- A browser can refuse to open a database, for example in a private window. Then the first operation of a store fails.
|
|
50
|
+
- The store checks each record that it reads. A record of an old version or of another application is an error.
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
import { IndexedDbProgramStore, MemoryProgramStore, hasIndexedDb } from "@procedures/store/indexeddb";
|
|
54
|
+
|
|
55
|
+
const programs = hasIndexedDb() ? new IndexedDbProgramStore("my-app-programs") : new MemoryProgramStore();
|
|
56
|
+
await programs.put({ key: "reports.weekly", path: ["reports", "weekly"], program, storedAt: Date.now() });
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The Composer of the site saves programs with `IndexedDbProgramStore`. It loads the store only when the reader opens the saved programs.
|
|
40
60
|
|
|
41
61
|
A host gives a registry its store with `attachProgramStore(registry, store)`. The procedures use the store of the caller's registry:
|
|
42
62
|
|
|
@@ -49,7 +69,7 @@ A host gives a registry its store with `attachProgramStore(registry, store)`. Th
|
|
|
49
69
|
|
|
50
70
|
Without a store, `procedure.store`, `load` and `sync` fail with the code `NOT_CONFIGURED`.
|
|
51
71
|
|
|
52
|
-
A loaded program is a runtime-defined procedure, so it is data-driven: a server with an expose rule lets it call only the exposed procedures. Load programs only from a store that you trust. A host with no expose rule
|
|
72
|
+
A loaded program is a runtime-defined procedure, so it is data-driven: a server with an expose rule lets it call only the exposed procedures. Load programs only from a store that you trust. A host with no expose rule does not limit the calls of a program.
|
|
53
73
|
|
|
54
74
|
## The storage layer
|
|
55
75
|
|
|
@@ -59,7 +79,94 @@ These parts moved here from `@procedures/client` (roadmap 3.2):
|
|
|
59
79
|
- The synced registry (`createSyncedRegistry()`, `SyncedProcedureRegistry`), the serialization of procedures, and the dynamic handler loader with an allowlist (`createDynamicHandlerLoader`).
|
|
60
80
|
- `procedure.register`, which registers a procedure from a handler reference.
|
|
61
81
|
|
|
62
|
-
|
|
82
|
+
An import of `@procedures/store/register` registers its procedures. An import of the package root registers nothing.
|
|
83
|
+
|
|
84
|
+
## The modes of `HybridStorage`
|
|
85
|
+
|
|
86
|
+
`HybridStorage` keeps a local copy of a remote collection storage. It has four modes, and `setMode()` changes the mode while the storage operates.
|
|
87
|
+
|
|
88
|
+
| Mode | Reads | Writes | Remote calls |
|
|
89
|
+
|---|---|---|---|
|
|
90
|
+
| `local-only` | The local copy | The local copy and the queue | None |
|
|
91
|
+
| `write-through` | The local copy, and the remote on a miss | The local copy, then the remote | Yes |
|
|
92
|
+
| `write-back` | The local copy, and the remote on a miss | The local copy and the queue. A timer sends the queue. | Yes |
|
|
93
|
+
| `remote-only` | The remote | The remote | Yes. The storage keeps no local copy. |
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
import { minutes } from "@procedures/client";
|
|
97
|
+
import { ApiStorage, HybridStorage } from "@procedures/store";
|
|
98
|
+
import { FileQueueStore } from "@procedures/store/file-queue";
|
|
99
|
+
|
|
100
|
+
const users = new HybridStorage(new ApiStorage(client, { collection: "users" }), {
|
|
101
|
+
mode: "write-back",
|
|
102
|
+
queueStore: new FileQueueStore("./users.queue.jsonl"),
|
|
103
|
+
versionOf: (user) => user.etag,
|
|
104
|
+
conflictResolution: "remote",
|
|
105
|
+
pullInterval: minutes(1),
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
await users.setMode("local-only"); // No remote call. The writes wait in the queue.
|
|
109
|
+
await users.setMode("write-through"); // The switch sends the queue, then syncs the local copy.
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### The switch
|
|
113
|
+
|
|
114
|
+
- A switch waits for the operations that run. An operation that starts during the switch waits for the switch.
|
|
115
|
+
- A switch to a remote mode sends the queue first. When the remote fails, the mode stays, and `setMode()` throws `ModeSwitchError`.
|
|
116
|
+
- A switch to `local-only` makes no remote call. The queue stays until the next switch to a remote mode.
|
|
117
|
+
- A switch to `remote-only` drops the local copy after the queue arrives at the remote.
|
|
118
|
+
- A switch from `local-only` or `remote-only` to a mode with a local copy syncs the local copy. The reads wait for the sync.
|
|
119
|
+
- The switches and the syncs run one at a time.
|
|
120
|
+
|
|
121
|
+
### The queue
|
|
122
|
+
|
|
123
|
+
- In the modes `local-only` and `write-back`, each write goes in the queue.
|
|
124
|
+
- In the mode `write-through`, a write that the remote cannot take goes in the queue, and the write throws `PendingWriteError`.
|
|
125
|
+
- A full queue refuses the next write with `QueueFullError`. The limit is `maxOfflineOps`, 1000 by default.
|
|
126
|
+
- The option `queueStore` keeps the queue. The storage writes each change to the store before the write ends.
|
|
127
|
+
- A new storage with the same store loads the queue, shows its writes, and sends them.
|
|
128
|
+
- `MemoryQueueStore` is the default. `FileQueueStore` keeps the queue in a JSON Lines file, for Node.
|
|
129
|
+
- `FileQueueStore` is in `@procedures/store/file-queue`, because it uses `node:fs`. The package root does not import it.
|
|
130
|
+
- `IndexedDbQueueStore` keeps the queue in IndexedDB, for a browser. It is in `@procedures/store/indexeddb`.
|
|
131
|
+
|
|
132
|
+
### The local copy
|
|
133
|
+
|
|
134
|
+
- The option `local` gives the storage of the local copy. The default is a new `InMemoryStorage`.
|
|
135
|
+
- A durable local copy keeps the items after a restart. `SqliteStorage` of `@procedures/sqlite` keeps them in a SQLite file.
|
|
136
|
+
- With a durable local copy, a new storage in `local-only` mode reads the items of the last run. It makes no remote call.
|
|
137
|
+
- 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.
|
|
138
|
+
- 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.
|
|
139
|
+
- `close()` closes the local copy. A durable local copy keeps its items.
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
import { ApiStorage, HybridStorage } from "@procedures/store";
|
|
143
|
+
import { FileQueueStore } from "@procedures/store/file-queue";
|
|
144
|
+
import { SqliteStorage } from "@procedures/sqlite/storage";
|
|
145
|
+
|
|
146
|
+
const users = new HybridStorage(new ApiStorage(client, { collection: "users" }), {
|
|
147
|
+
mode: "write-back",
|
|
148
|
+
local: new SqliteStorage({ path: "./users.db", table: "users" }),
|
|
149
|
+
queueStore: new FileQueueStore("./users.queue.jsonl"),
|
|
150
|
+
});
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### The sync, the versions and the conflicts
|
|
154
|
+
|
|
155
|
+
- A sync asks the remote for the changes since the version of the last sync. `ApiStorage` uses the procedure `collections.<name>.changes`.
|
|
156
|
+
- A remote without a change log gives all its entries.
|
|
157
|
+
- The option `versionOf` gives the version of an item, for example its ETag.
|
|
158
|
+
- With versions, a conflict is a queued local write of an id whose remote version changed. The option `conflictResolution` gives the result.
|
|
159
|
+
- The values of `conflictResolution` are `local`, `remote`, `merge` with `mergeFn`, and `error`. With `error`, the sync throws `ConflictError`.
|
|
160
|
+
- Without versions, a queued write always wins. A local value that differs from the remote value is a conflict.
|
|
161
|
+
- The option `pullInterval` starts a periodic pull in the modes `write-through` and `write-back`. `close()` stops it.
|
|
162
|
+
|
|
163
|
+
### The change of a storage in a server
|
|
164
|
+
|
|
165
|
+
`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.
|
|
166
|
+
|
|
167
|
+
### The mode of a synced registry
|
|
168
|
+
|
|
169
|
+
`createSyncedRegistry({ type: "hybrid", client, mode })` takes the first mode. `setMode()` of the registry changes the mode of its storage.
|
|
63
170
|
|
|
64
171
|
## License
|
|
65
172
|
|