@procedures/client 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.
Files changed (234) hide show
  1. package/CHANGELOG.md +107 -0
  2. package/README.md +115 -11
  3. package/dist/.tsbuildinfo +1 -1
  4. package/dist/adapters/http/client/transport.d.ts +4 -0
  5. package/dist/adapters/http/client/transport.d.ts.map +1 -1
  6. package/dist/adapters/http/client/transport.js +11 -1
  7. package/dist/adapters/http/client/transport.js.map +1 -1
  8. package/dist/adapters/http/client/types.d.ts +19 -0
  9. package/dist/adapters/http/client/types.d.ts.map +1 -1
  10. package/dist/adapters/http/server/cors.d.ts +64 -0
  11. package/dist/adapters/http/server/cors.d.ts.map +1 -0
  12. package/dist/adapters/http/server/cors.js +113 -0
  13. package/dist/adapters/http/server/cors.js.map +1 -0
  14. package/dist/adapters/http/server/handler.d.ts +136 -0
  15. package/dist/adapters/http/server/handler.d.ts.map +1 -0
  16. package/dist/adapters/http/server/handler.js +495 -0
  17. package/dist/adapters/http/server/handler.js.map +1 -0
  18. package/dist/adapters/http/server/strategies.d.ts +14 -5
  19. package/dist/adapters/http/server/strategies.d.ts.map +1 -1
  20. package/dist/adapters/http/server/strategies.js.map +1 -1
  21. package/dist/adapters/http/server/transport.d.ts +36 -46
  22. package/dist/adapters/http/server/transport.d.ts.map +1 -1
  23. package/dist/adapters/http/server/transport.js +58 -433
  24. package/dist/adapters/http/server/transport.js.map +1 -1
  25. package/dist/adapters/http/server/types.d.ts +13 -0
  26. package/dist/adapters/http/server/types.d.ts.map +1 -1
  27. package/dist/adapters/websocket/client/transport.d.ts +7 -3
  28. package/dist/adapters/websocket/client/transport.d.ts.map +1 -1
  29. package/dist/adapters/websocket/client/transport.js +9 -3
  30. package/dist/adapters/websocket/client/transport.js.map +1 -1
  31. package/dist/adapters/websocket/server/transport.d.ts +56 -1
  32. package/dist/adapters/websocket/server/transport.d.ts.map +1 -1
  33. package/dist/adapters/websocket/server/transport.js +174 -17
  34. package/dist/adapters/websocket/server/transport.js.map +1 -1
  35. package/dist/adapters/websocket/server/types.d.ts +10 -3
  36. package/dist/adapters/websocket/server/types.d.ts.map +1 -1
  37. package/dist/adapters/websocket/server/upgrade-router.d.ts +28 -0
  38. package/dist/adapters/websocket/server/upgrade-router.d.ts.map +1 -0
  39. package/dist/adapters/websocket/server/upgrade-router.js +62 -0
  40. package/dist/adapters/websocket/server/upgrade-router.js.map +1 -0
  41. package/dist/adapters/websocket/server/upgrade.d.ts +68 -0
  42. package/dist/adapters/websocket/server/upgrade.d.ts.map +1 -0
  43. package/dist/adapters/websocket/server/upgrade.js +49 -0
  44. package/dist/adapters/websocket/server/upgrade.js.map +1 -0
  45. package/dist/adapters/websocket/shared/rpc.d.ts +2 -0
  46. package/dist/adapters/websocket/shared/rpc.d.ts.map +1 -1
  47. package/dist/adapters/websocket/shared/rpc.js +8 -2
  48. package/dist/adapters/websocket/shared/rpc.js.map +1 -1
  49. package/dist/browser.d.ts +14 -6
  50. package/dist/browser.d.ts.map +1 -1
  51. package/dist/browser.js +12 -3
  52. package/dist/browser.js.map +1 -1
  53. package/dist/client/client.d.ts +43 -0
  54. package/dist/client/client.d.ts.map +1 -1
  55. package/dist/client/client.js +69 -1
  56. package/dist/client/client.js.map +1 -1
  57. package/dist/client/errors/registry.d.ts.map +1 -1
  58. package/dist/client/errors/registry.js +1 -0
  59. package/dist/client/errors/registry.js.map +1 -1
  60. package/dist/client/middleware/retry.d.ts +4 -0
  61. package/dist/client/middleware/retry.d.ts.map +1 -1
  62. package/dist/client/middleware/retry.js +86 -78
  63. package/dist/client/middleware/retry.js.map +1 -1
  64. package/dist/events/bus.d.ts +32 -14
  65. package/dist/events/bus.d.ts.map +1 -1
  66. package/dist/events/bus.js +119 -65
  67. package/dist/events/bus.js.map +1 -1
  68. package/dist/events/index.d.ts +3 -2
  69. package/dist/events/index.d.ts.map +1 -1
  70. package/dist/events/index.js.map +1 -1
  71. package/dist/events/publisher.d.ts +14 -0
  72. package/dist/events/publisher.d.ts.map +1 -1
  73. package/dist/events/publisher.js +62 -10
  74. package/dist/events/publisher.js.map +1 -1
  75. package/dist/events/remote.d.ts +80 -18
  76. package/dist/events/remote.d.ts.map +1 -1
  77. package/dist/events/remote.js +448 -58
  78. package/dist/events/remote.js.map +1 -1
  79. package/dist/events/rows.d.ts +31 -0
  80. package/dist/events/rows.d.ts.map +1 -0
  81. package/dist/events/rows.js +39 -0
  82. package/dist/events/rows.js.map +1 -0
  83. package/dist/events/types.d.ts +13 -2
  84. package/dist/events/types.d.ts.map +1 -1
  85. package/dist/index.d.ts +1 -1
  86. package/dist/index.d.ts.map +1 -1
  87. package/dist/index.js +1 -1
  88. package/dist/index.js.map +1 -1
  89. package/dist/node.d.ts +14 -0
  90. package/dist/node.d.ts.map +1 -1
  91. package/dist/node.js +9 -0
  92. package/dist/node.js.map +1 -1
  93. package/dist/procedures/caller.d.ts +33 -0
  94. package/dist/procedures/caller.d.ts.map +1 -0
  95. package/dist/procedures/caller.js +27 -0
  96. package/dist/procedures/caller.js.map +1 -0
  97. package/dist/procedures/capability.d.ts +6 -0
  98. package/dist/procedures/capability.d.ts.map +1 -1
  99. package/dist/procedures/capability.js +25 -0
  100. package/dist/procedures/capability.js.map +1 -1
  101. package/dist/procedures/collection/changes.d.ts +94 -0
  102. package/dist/procedures/collection/changes.d.ts.map +1 -0
  103. package/dist/procedures/collection/changes.js +182 -0
  104. package/dist/procedures/collection/changes.js.map +1 -0
  105. package/dist/procedures/collection/index.d.ts +8 -3
  106. package/dist/procedures/collection/index.d.ts.map +1 -1
  107. package/dist/procedures/collection/index.js +6 -1
  108. package/dist/procedures/collection/index.js.map +1 -1
  109. package/dist/procedures/collection/procedures.d.ts +39 -2
  110. package/dist/procedures/collection/procedures.d.ts.map +1 -1
  111. package/dist/procedures/collection/procedures.js +131 -8
  112. package/dist/procedures/collection/procedures.js.map +1 -1
  113. package/dist/procedures/collection/query.d.ts +246 -0
  114. package/dist/procedures/collection/query.d.ts.map +1 -0
  115. package/dist/procedures/collection/query.js +854 -0
  116. package/dist/procedures/collection/query.js.map +1 -0
  117. package/dist/procedures/collection/schemas.d.ts +59 -0
  118. package/dist/procedures/collection/schemas.d.ts.map +1 -1
  119. package/dist/procedures/collection/schemas.js +172 -6
  120. package/dist/procedures/collection/schemas.js.map +1 -1
  121. package/dist/procedures/collection/storage.d.ts +64 -6
  122. package/dist/procedures/collection/storage.d.ts.map +1 -1
  123. package/dist/procedures/collection/storage.js +23 -0
  124. package/dist/procedures/collection/storage.js.map +1 -1
  125. package/dist/procedures/elicit.d.ts +190 -0
  126. package/dist/procedures/elicit.d.ts.map +1 -0
  127. package/dist/procedures/elicit.js +127 -0
  128. package/dist/procedures/elicit.js.map +1 -0
  129. package/dist/procedures/hints.d.ts +43 -0
  130. package/dist/procedures/hints.d.ts.map +1 -0
  131. package/dist/procedures/hints.js +74 -0
  132. package/dist/procedures/hints.js.map +1 -0
  133. package/dist/procedures/index.d.ts +12 -3
  134. package/dist/procedures/index.d.ts.map +1 -1
  135. package/dist/procedures/index.js +7 -1
  136. package/dist/procedures/index.js.map +1 -1
  137. package/dist/procedures/invoke.d.ts +20 -0
  138. package/dist/procedures/invoke.d.ts.map +1 -1
  139. package/dist/procedures/invoke.js +16 -0
  140. package/dist/procedures/invoke.js.map +1 -1
  141. package/dist/procedures/progress.d.ts +24 -0
  142. package/dist/procedures/progress.d.ts.map +1 -0
  143. package/dist/procedures/progress.js +11 -0
  144. package/dist/procedures/progress.js.map +1 -0
  145. package/dist/procedures/registry.d.ts.map +1 -1
  146. package/dist/procedures/registry.js +5 -0
  147. package/dist/procedures/registry.js.map +1 -1
  148. package/dist/procedures/types.d.ts +92 -0
  149. package/dist/procedures/types.d.ts.map +1 -1
  150. package/dist/procedures/types.js.map +1 -1
  151. package/dist/procedures/untrusted.d.ts +28 -0
  152. package/dist/procedures/untrusted.d.ts.map +1 -0
  153. package/dist/procedures/untrusted.js +32 -0
  154. package/dist/procedures/untrusted.js.map +1 -0
  155. package/dist/protocol.d.ts +1 -1
  156. package/dist/protocol.d.ts.map +1 -1
  157. package/dist/protocol.js +1 -0
  158. package/dist/protocol.js.map +1 -1
  159. package/dist/server/browser-guard.d.ts +12 -2
  160. package/dist/server/browser-guard.d.ts.map +1 -1
  161. package/dist/server/browser-guard.js +43 -6
  162. package/dist/server/browser-guard.js.map +1 -1
  163. package/dist/server/host-policy.d.ts +10 -1
  164. package/dist/server/host-policy.d.ts.map +1 -1
  165. package/dist/server/host-policy.js +5 -0
  166. package/dist/server/host-policy.js.map +1 -1
  167. package/dist/server/index.d.ts +1 -1
  168. package/dist/server/index.d.ts.map +1 -1
  169. package/dist/server/index.js.map +1 -1
  170. package/dist/server/procedure-server.d.ts +75 -9
  171. package/dist/server/procedure-server.d.ts.map +1 -1
  172. package/dist/server/procedure-server.js +151 -17
  173. package/dist/server/procedure-server.js.map +1 -1
  174. package/dist/server/request-auth.d.ts +51 -4
  175. package/dist/server/request-auth.d.ts.map +1 -1
  176. package/dist/server/request-auth.js +116 -5
  177. package/dist/server/request-auth.js.map +1 -1
  178. package/dist/server/types.d.ts +73 -5
  179. package/dist/server/types.d.ts.map +1 -1
  180. package/dist/server/types.js.map +1 -1
  181. package/dist/timers.d.ts +5 -0
  182. package/dist/timers.d.ts.map +1 -1
  183. package/dist/timers.js +3 -1
  184. package/dist/timers.js.map +1 -1
  185. package/package.json +5 -1
  186. package/src/adapters/http/client/transport.ts +11 -1
  187. package/src/adapters/http/client/types.ts +22 -0
  188. package/src/adapters/http/server/cors.ts +144 -0
  189. package/src/adapters/http/server/handler.ts +624 -0
  190. package/src/adapters/http/server/strategies.ts +16 -6
  191. package/src/adapters/http/server/transport.ts +66 -479
  192. package/src/adapters/http/server/types.ts +15 -0
  193. package/src/adapters/websocket/client/transport.ts +9 -3
  194. package/src/adapters/websocket/server/transport.ts +189 -18
  195. package/src/adapters/websocket/server/types.ts +11 -3
  196. package/src/adapters/websocket/server/upgrade-router.ts +77 -0
  197. package/src/adapters/websocket/server/upgrade.ts +88 -0
  198. package/src/adapters/websocket/shared/rpc.ts +9 -2
  199. package/src/browser.ts +85 -2
  200. package/src/client/client.ts +83 -1
  201. package/src/client/errors/registry.ts +1 -0
  202. package/src/client/middleware/retry.ts +89 -78
  203. package/src/events/bus.ts +141 -73
  204. package/src/events/index.ts +3 -0
  205. package/src/events/publisher.ts +71 -10
  206. package/src/events/remote.ts +575 -73
  207. package/src/events/rows.ts +48 -0
  208. package/src/events/types.ts +15 -2
  209. package/src/index.ts +1 -1
  210. package/src/node.ts +21 -0
  211. package/src/procedures/caller.ts +45 -0
  212. package/src/procedures/capability.ts +21 -0
  213. package/src/procedures/collection/changes.ts +245 -0
  214. package/src/procedures/collection/index.ts +56 -1
  215. package/src/procedures/collection/procedures.ts +185 -7
  216. package/src/procedures/collection/query.ts +1009 -0
  217. package/src/procedures/collection/schemas.ts +233 -7
  218. package/src/procedures/collection/storage.ts +84 -6
  219. package/src/procedures/elicit.ts +320 -0
  220. package/src/procedures/hints.ts +91 -0
  221. package/src/procedures/index.ts +82 -2
  222. package/src/procedures/invoke.ts +31 -0
  223. package/src/procedures/progress.ts +25 -0
  224. package/src/procedures/registry.ts +4 -0
  225. package/src/procedures/types.ts +102 -0
  226. package/src/procedures/untrusted.ts +50 -0
  227. package/src/protocol.ts +3 -1
  228. package/src/server/browser-guard.ts +48 -6
  229. package/src/server/host-policy.ts +13 -1
  230. package/src/server/index.ts +3 -0
  231. package/src/server/procedure-server.ts +185 -20
  232. package/src/server/request-auth.ts +128 -6
  233. package/src/server/types.ts +82 -5
  234. package/src/timers.ts +8 -1
@@ -20,6 +20,8 @@ import {
20
20
  getAllOutputSchema,
21
21
  entriesInputSchema,
22
22
  entriesOutputSchema,
23
+ idsInputSchema,
24
+ idsOutputSchema,
23
25
  changesInputSchema,
24
26
  changesOutputSchema,
25
27
  sizeInputSchema,
@@ -32,6 +34,12 @@ import {
32
34
  setBatchOutputSchema,
33
35
  deleteBatchInputSchema,
34
36
  deleteBatchOutputSchema,
37
+ findInputSchemaOf,
38
+ findOutputSchema,
39
+ patchInputSchema,
40
+ patchOutputSchema,
41
+ type FindInput,
42
+ type PatchInput,
35
43
  type GetInput,
36
44
  type GetFoundOutput,
37
45
  type SetInput,
@@ -39,6 +47,7 @@ import {
39
47
  type HasInput,
40
48
  type GetAllInput,
41
49
  type EntriesInput,
50
+ type IdsInput,
42
51
  type ChangesInput,
43
52
  type ChangesOutput,
44
53
  type SizeInput,
@@ -47,6 +56,9 @@ import {
47
56
  type SetBatchInput,
48
57
  type DeleteBatchInput,
49
58
  } from "./schemas.js";
59
+ import { DEFAULT_FIND_LIMIT, applyPatch, queryEntries, type StoragePatch, type StorageQueryOptions, type StorageQueryPolicy } from "./query.js";
60
+ import type { CollectionStorage, StorageWriteOptions } from "./storage.js";
61
+ import { collectionChangeTopic, startChangeChain, writeWithChangeEvent } from "./changes.js";
50
62
 
51
63
  // =============================================================================
52
64
  // Handler Factory
@@ -74,6 +86,93 @@ function getStorage<T>(ctx: ProcedureContext): ReturnType<NonNullable<ProcedureC
74
86
  return ctx.repository.getStorage<T>(collection);
75
87
  }
76
88
 
89
+ /** The options of a query from the input of `find`. */
90
+ function queryOptionsOf(input: FindInput, policy: StorageQueryPolicy): StorageQueryOptions {
91
+ // A find without a limit gets the maxLimit of the policy (roadmap CLI-14)
92
+ const limit = input.limit ?? (Number.isFinite(policy.maxLimit) ? policy.maxLimit : undefined);
93
+ return {
94
+ ...(input.sort !== undefined && { sort: input.sort }),
95
+ ...(input.skip !== undefined && { skip: input.skip }),
96
+ ...(limit !== undefined && { limit }),
97
+ };
98
+ }
99
+
100
+ /** The options of the change events of the collection procedures (roadmap CLI-15). */
101
+ export interface CollectionChangeEventsOptions {
102
+ /**
103
+ * The topic of the change events of a call, or undefined for no event. The partition of a
104
+ * collection (roadmap CLI-17) goes into the topic here, for example from the scope of the call.
105
+ * @default the topic `collections.<name>` of `collectionChangeTopic()`
106
+ */
107
+ topic?: ((collection: string, ctx: ProcedureContext) => string | undefined) | undefined;
108
+ /**
109
+ * True: the events also carry the values of the changed items. Each row `{ id, value }` passes
110
+ * the output filter of the host (`admitItem`) for each subscriber, and a refused row stays out of
111
+ * that event: its id goes to `withheld`. Without it, the events carry only the ids and the
112
+ * versions, and a subscriber reads the values with `changes`.
113
+ * @default false
114
+ */
115
+ values?: boolean | undefined;
116
+ }
117
+
118
+ /** The options of the collection procedures. */
119
+ export interface CollectionProceduresOptions {
120
+ /**
121
+ * The query policy of `find`: the operators, and the limits of a selector and of the result. A
122
+ * selector or a limit that the policy does not permit is a `VALIDATION_ERROR`. The policy also
123
+ * goes to `query()` of the storage. `$regex` is off unless the policy has it in `operators`.
124
+ * @default DEFAULT_QUERY_POLICY, with `maxLimit` DEFAULT_FIND_LIMIT (1000)
125
+ */
126
+ queryPolicy?: StorageQueryPolicy | undefined;
127
+ /**
128
+ * The change events of the writes (roadmap CLI-15). Each write publishes a
129
+ * `CollectionChangeEvent` on the bus of its context, while a subscriber listens to the topic.
130
+ * The event has the ids and the versions, and the values only with `values`. False: the
131
+ * procedures publish no event.
132
+ * @default true, with the topic `collections.<name>` and no values
133
+ */
134
+ changeEvents?: boolean | CollectionChangeEventsOptions | undefined;
135
+ }
136
+
137
+ /** The write options of an input with an origin. */
138
+ function writeOptionsOf(input: { origin?: string }): StorageWriteOptions | undefined {
139
+ return input.origin === undefined ? undefined : { origin: input.origin };
140
+ }
141
+
142
+ /**
143
+ * The patches of a storage without `patch()` that run, for each id. A patch of an id waits for the
144
+ * patch before it, so two patches of one process keep each field. A write of another writer
145
+ * between the read and the write of a patch is lost: give the storage a `patch()` for one step.
146
+ */
147
+ const PATCHES = new WeakMap<object, Map<string, Promise<unknown>>>();
148
+
149
+ /** This function patches an item of a storage without `patch()`: a read, a merge and a write. */
150
+ function patchInParts(storage: CollectionStorage<unknown>, id: string, fields: StoragePatch, options?: StorageWriteOptions): Promise<unknown> {
151
+ let running = PATCHES.get(storage);
152
+ if (!running) {
153
+ running = new Map();
154
+ PATCHES.set(storage, running);
155
+ }
156
+ const queue = running;
157
+ const before = queue.get(id) ?? Promise.resolve();
158
+ const patch = before.then(async () => {
159
+ const current = await storage.get(id);
160
+ if (current === undefined) return undefined;
161
+ const next = applyPatch(current, fields);
162
+ await storage.set(id, next, options);
163
+ return next;
164
+ });
165
+ const settled = patch.then(
166
+ () => undefined,
167
+ () => undefined,
168
+ );
169
+ queue.set(id, settled);
170
+ void settled.then(() => {
171
+ if (queue.get(id) === settled) queue.delete(id);
172
+ });
173
+ return patch;
174
+ }
175
+
77
176
  // =============================================================================
78
177
  // Collection Procedure Definitions
79
178
  // =============================================================================
@@ -82,6 +181,7 @@ function getStorage<T>(ctx: ProcedureContext): ReturnType<NonNullable<ProcedureC
82
181
  * Create collection procedures for a specific collection name.
83
182
  *
84
183
  * @param collectionName - Name of the collection
184
+ * @param options - The query policy of `find`
85
185
  * @returns Array of procedure definitions
86
186
  *
87
187
  * @example
@@ -90,10 +190,21 @@ function getStorage<T>(ctx: ProcedureContext): ReturnType<NonNullable<ProcedureC
90
190
  * registry.registerAll(userProcedures);
91
191
  * ```
92
192
  */
93
- export function createCollectionProcedures(collectionName: string): AnyProcedure[] {
193
+ export function createCollectionProcedures(collectionName: string, options: CollectionProceduresOptions = {}): AnyProcedure[] {
94
194
  const basePath = ["collections", collectionName];
195
+ // A find of a caller gets a limit: DEFAULT_FIND_LIMIT, unless the policy gives its own maxLimit
196
+ const queryPolicy: StorageQueryPolicy = { ...options.queryPolicy, maxLimit: options.queryPolicy?.maxLimit ?? DEFAULT_FIND_LIMIT };
95
197
  // Each collection is a resource of its own (CAPABILITIES-2026-10): a capability can permit one
96
198
  const resource = `collection:${collectionName}`;
199
+ // The topic of the change events of a call (roadmap CLI-15). CLI-17 adds the partition with the option topic.
200
+ const events = options.changeEvents ?? true;
201
+ const topicRule: ((collection: string, ctx: ProcedureContext) => string | undefined) | undefined =
202
+ events === false ? undefined : (events !== true && events.topic) || ((name: string) => collectionChangeTopic(name));
203
+ const topicOf = (ctx: ProcedureContext): string | undefined => topicRule?.(collectionName, ctx);
204
+ const withValues = events !== false && events !== true && events.values === true;
205
+ /** A write, and then its change event on the bus of the context. */
206
+ const changing = <R>(ctx: ProcedureContext, storage: CollectionStorage<unknown>, ids: readonly string[] | "*", origin: string | undefined, write: () => Promise<R>): Promise<R> =>
207
+ writeWithChangeEvent(ctx.bus, storage, topicRule ? topicOf(ctx) : undefined, ids, origin, write, withValues);
97
208
 
98
209
  return [
99
210
  // Get
@@ -128,7 +239,7 @@ export function createCollectionProcedures(collectionName: string): AnyProcedure
128
239
  },
129
240
  handler: async (input: SetInput, ctx: ProcedureContext) => {
130
241
  const storage = getStorage(ctx);
131
- await storage.set(input.id, input.value);
242
+ await changing(ctx, storage, [input.id], input.origin, () => storage.set(input.id, input.value, writeOptionsOf(input)));
132
243
  },
133
244
  }),
134
245
 
@@ -144,7 +255,7 @@ export function createCollectionProcedures(collectionName: string): AnyProcedure
144
255
  },
145
256
  handler: async (input: DeleteInput, ctx: ProcedureContext) => {
146
257
  const storage = getStorage(ctx);
147
- return storage.delete(input.id);
258
+ return changing(ctx, storage, [input.id], input.origin, () => storage.delete(input.id, writeOptionsOf(input)));
148
259
  },
149
260
  }),
150
261
 
@@ -199,6 +310,25 @@ export function createCollectionProcedures(collectionName: string): AnyProcedure
199
310
  },
200
311
  }),
201
312
 
313
+ // Ids: the ids of the items without the values, for a full sync
314
+ defineProcedure({
315
+ path: [...basePath, "ids"],
316
+ input: idsInputSchema,
317
+ output: idsOutputSchema,
318
+ metadata: {
319
+ effects: { reads: [resource], writes: [] },
320
+ description: `Get the ids of the items of ${collectionName}, without the values`,
321
+ tags: ["collections", collectionName, "read"],
322
+ },
323
+ handler: async (_input: IdsInput, ctx: ProcedureContext) => {
324
+ const storage = getStorage(ctx);
325
+ if (storage.getIds) return storage.getIds();
326
+ // A storage without getIds() gives its entries: the values stay on the server
327
+ if (storage.getEntries) return (await storage.getEntries()).map(([id]) => id);
328
+ throw new Error(`The storage of ${collectionName} cannot give the ids of its items`);
329
+ },
330
+ }),
331
+
202
332
  // Changes: the items that changed after a version, for a delta sync (roadmap R28)
203
333
  defineProcedure({
204
334
  path: [...basePath, "changes"],
@@ -213,6 +343,9 @@ export function createCollectionProcedures(collectionName: string): AnyProcedure
213
343
  const storage = getStorage(ctx);
214
344
  if (storage.getChangesSince) {
215
345
  const changes = await storage.getChangesSince(input.since);
346
+ // The chain of the change events starts at this version: the caller then applies the
347
+ // next event without a read (roadmap CLI-15)
348
+ if (topicRule) startChangeChain(ctx.bus, storage, topicOf(ctx), changes.version);
216
349
  return {
217
350
  version: changes.version,
218
351
  full: changes.full,
@@ -230,6 +363,51 @@ export function createCollectionProcedures(collectionName: string): AnyProcedure
230
363
  },
231
364
  }),
232
365
 
366
+ // Find: the items that match a JSON selector, with an order, a skip and a limit (roadmap CLI-14)
367
+ defineProcedure({
368
+ path: [...basePath, "find"],
369
+ input: findInputSchemaOf(queryPolicy),
370
+ output: findOutputSchema,
371
+ metadata: {
372
+ effects: { reads: [resource], writes: [] },
373
+ description: `Find the items of ${collectionName} that match a selector, with their ids, an order, a skip and a limit`,
374
+ tags: ["collections", collectionName, "read", "query"],
375
+ },
376
+ handler: async (input: FindInput, ctx: ProcedureContext) => {
377
+ const storage = getStorage(ctx);
378
+ const selector = input.selector ?? {};
379
+ const query = queryOptionsOf(input, queryPolicy);
380
+ // The storage runs the selector itself when it can: a database does not read each item. It
381
+ // gets the policy of the host, so it permits what the procedure permits.
382
+ if (storage.query) return (await storage.query(selector, query, queryPolicy)).map(([id, value]) => ({ id, value }));
383
+ if (!storage.getEntries) {
384
+ throw new Error(`The storage of ${collectionName} cannot run a selector or give the ids of its items`);
385
+ }
386
+ return queryEntries(await storage.getEntries(), selector, query, queryPolicy).map(([id, value]) => ({ id, value }));
387
+ },
388
+ }),
389
+
390
+ // Patch: merge fields into one item, a JSON Merge Patch (roadmap CLI-14)
391
+ defineProcedure({
392
+ path: [...basePath, "patch"],
393
+ input: patchInputSchema,
394
+ output: patchOutputSchema,
395
+ metadata: {
396
+ effects: { reads: [resource], writes: [resource], idempotent: true },
397
+ description: `Merge fields into one item of ${collectionName}: a JSON Merge Patch, or the operations $set and $unset`,
398
+ tags: ["collections", collectionName, "write"],
399
+ },
400
+ handler: async (input: PatchInput, ctx: ProcedureContext) => {
401
+ const storage = getStorage(ctx);
402
+ const write = writeOptionsOf(input);
403
+ const value = await changing(ctx, storage, [input.id], input.origin, () =>
404
+ storage.patch ? storage.patch(input.id, input.fields, write) : patchInParts(storage, input.id, input.fields, write),
405
+ );
406
+ const output: GetFoundOutput = value === undefined ? { found: false } : { found: true, value };
407
+ return output;
408
+ },
409
+ }),
410
+
233
411
  // Size
234
412
  defineProcedure({
235
413
  path: [...basePath, "size"],
@@ -256,9 +434,9 @@ export function createCollectionProcedures(collectionName: string): AnyProcedure
256
434
  description: `Clear all items from ${collectionName}`,
257
435
  tags: ["collections", collectionName, "write"],
258
436
  },
259
- handler: async (_input: ClearInput, ctx: ProcedureContext) => {
437
+ handler: async (input: ClearInput, ctx: ProcedureContext) => {
260
438
  const storage = getStorage(ctx);
261
- await storage.clear();
439
+ await changing(ctx, storage, "*", input.origin, () => storage.clear(writeOptionsOf(input)));
262
440
  },
263
441
  }),
264
442
 
@@ -296,7 +474,7 @@ export function createCollectionProcedures(collectionName: string): AnyProcedure
296
474
  item.id,
297
475
  item.value,
298
476
  ]);
299
- await storage.setBatch(items);
477
+ await changing(ctx, storage, items.map(([id]) => id), input.origin, () => storage.setBatch(items, writeOptionsOf(input)));
300
478
  },
301
479
  }),
302
480
 
@@ -312,7 +490,7 @@ export function createCollectionProcedures(collectionName: string): AnyProcedure
312
490
  },
313
491
  handler: async (input: DeleteBatchInput, ctx: ProcedureContext) => {
314
492
  const storage = getStorage(ctx);
315
- return storage.deleteBatch(input.ids);
493
+ return changing(ctx, storage, input.ids, input.origin, () => storage.deleteBatch(input.ids, writeOptionsOf(input)));
316
494
  },
317
495
  }),
318
496
  ];