@procedures/client 0.13.2 → 0.14.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 +71 -0
- package/README.md +12 -3
- package/dist/.tsbuildinfo +1 -1
- package/dist/adapters/http/server/body.d.ts +17 -0
- package/dist/adapters/http/server/body.d.ts.map +1 -0
- package/dist/adapters/http/server/body.js +44 -0
- package/dist/adapters/http/server/body.js.map +1 -0
- package/dist/adapters/http/server/cors.d.ts +24 -1
- package/dist/adapters/http/server/cors.d.ts.map +1 -1
- package/dist/adapters/http/server/cors.js +33 -1
- package/dist/adapters/http/server/cors.js.map +1 -1
- package/dist/adapters/http/server/handler.d.ts +3 -1
- package/dist/adapters/http/server/handler.d.ts.map +1 -1
- package/dist/adapters/http/server/handler.js +9 -34
- package/dist/adapters/http/server/handler.js.map +1 -1
- package/dist/browser.d.ts +4 -3
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +4 -1
- package/dist/browser.js.map +1 -1
- package/dist/events/publisher.d.ts.map +1 -1
- package/dist/events/publisher.js +10 -5
- package/dist/events/publisher.js.map +1 -1
- package/dist/events/rows.d.ts +12 -4
- package/dist/events/rows.d.ts.map +1 -1
- package/dist/events/rows.js +13 -16
- package/dist/events/rows.js.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -2
- package/dist/index.js.map +1 -1
- package/dist/node.d.ts +10 -1
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +11 -0
- package/dist/node.js.map +1 -1
- package/dist/procedures/collection/admission.d.ts +77 -0
- package/dist/procedures/collection/admission.d.ts.map +1 -0
- package/dist/procedures/collection/admission.js +204 -0
- package/dist/procedures/collection/admission.js.map +1 -0
- package/dist/procedures/collection/changes.d.ts +3 -1
- package/dist/procedures/collection/changes.d.ts.map +1 -1
- package/dist/procedures/collection/changes.js +10 -6
- package/dist/procedures/collection/changes.js.map +1 -1
- package/dist/procedures/collection/index.d.ts +2 -0
- package/dist/procedures/collection/index.d.ts.map +1 -1
- package/dist/procedures/collection/index.js +2 -0
- package/dist/procedures/collection/index.js.map +1 -1
- package/dist/procedures/collection/patch-in-parts.d.ts +15 -0
- package/dist/procedures/collection/patch-in-parts.d.ts.map +1 -0
- package/dist/procedures/collection/patch-in-parts.js +47 -0
- package/dist/procedures/collection/patch-in-parts.js.map +1 -0
- package/dist/procedures/collection/procedures.d.ts +8 -0
- package/dist/procedures/collection/procedures.d.ts.map +1 -1
- package/dist/procedures/collection/procedures.js +54 -77
- package/dist/procedures/collection/procedures.js.map +1 -1
- package/dist/procedures/index.d.ts +2 -2
- package/dist/procedures/index.d.ts.map +1 -1
- package/dist/procedures/index.js +3 -1
- package/dist/procedures/index.js.map +1 -1
- package/dist/procedures/invoke.d.ts +5 -2
- package/dist/procedures/invoke.d.ts.map +1 -1
- package/dist/procedures/invoke.js +5 -1
- package/dist/procedures/invoke.js.map +1 -1
- package/dist/procedures/types.d.ts +7 -0
- package/dist/procedures/types.d.ts.map +1 -1
- package/dist/procedures/types.js.map +1 -1
- package/dist/server/index.d.ts +70 -5
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +26 -3
- package/dist/server/index.js.map +1 -1
- package/dist/server/procedure-server.d.ts +1 -0
- package/dist/server/procedure-server.d.ts.map +1 -1
- package/dist/server/procedure-server.js +6 -3
- package/dist/server/procedure-server.js.map +1 -1
- package/package.json +12 -1
- package/src/adapters/http/server/body.ts +43 -0
- package/src/adapters/http/server/cors.ts +42 -1
- package/src/adapters/http/server/handler.ts +11 -32
- package/src/browser.ts +9 -1
- package/src/events/publisher.ts +8 -5
- package/src/events/rows.ts +19 -6
- package/src/index.ts +9 -6
- package/src/node.ts +13 -1
- package/src/procedures/collection/admission.ts +243 -0
- package/src/procedures/collection/changes.ts +11 -5
- package/src/procedures/collection/index.ts +4 -0
- package/src/procedures/collection/patch-in-parts.ts +56 -0
- package/src/procedures/collection/procedures.ts +58 -80
- package/src/procedures/index.ts +7 -0
- package/src/procedures/invoke.ts +7 -2
- package/src/procedures/types.ts +8 -0
- package/src/server/index.ts +94 -17
- package/src/server/procedure-server.ts +6 -3
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The rules of the rows of a served collection (roadmap CLI-41). A right can depend on the
|
|
3
|
+
* document: a `when` condition of Graph, or `accessAdmitItem()` of `@procedures/access`. The option
|
|
4
|
+
* `admission` of `createCollectionProcedures()` gives the rules as data:
|
|
5
|
+
*
|
|
6
|
+
* - `admit` examines each row `{ id, value }` that a read gives. It has the shape of `AdmitItem`,
|
|
7
|
+
* the output filter of a host. The change events with values give it the same rows for each
|
|
8
|
+
* subscriber, so the reads and the events obey one rule.
|
|
9
|
+
* - `guard` examines each write `{ op, id, doc, prev }` before the storage changes. A refused write
|
|
10
|
+
* gets `FORBIDDEN` with the reason `write-not-allowed`.
|
|
11
|
+
*
|
|
12
|
+
* The procedures apply the rules in one place: the view of the storage of a call
|
|
13
|
+
* (`admittedStorage()`). Each read and each write of each procedure goes through this view.
|
|
14
|
+
*
|
|
15
|
+
* - A refused row is absent: `get` gives undefined, `has` gives false, and the lists, `size`,
|
|
16
|
+
* `find` and `changes` do not have it. A delta of `changes` puts the id of a refused row in
|
|
17
|
+
* `deleted`, so a copy of the caller drops it.
|
|
18
|
+
* - An error of `admit` fails the read. It does not hide the rows, so a sync does not delete them.
|
|
19
|
+
* - The guard reads the stored item before the write. CLI-16 (`expectedVersion`) closes the gap of
|
|
20
|
+
* a write of another writer between that read and the write.
|
|
21
|
+
* - The rules get the invocation of the call (`ctx.invocation`). A call without an invocation gets
|
|
22
|
+
* no row and makes no write.
|
|
23
|
+
*/
|
|
24
|
+
import { InvocationError, type AdmitItem, type Invocation } from "../invoke.js";
|
|
25
|
+
import type { ProcedureContext } from "../types.js";
|
|
26
|
+
import { applyPatch, type StoragePatch, type StorageQueryOptions, type StorageQueryPolicy, type StorageSelector } from "./query.js";
|
|
27
|
+
import type { CollectionStorage, StorageChanges, StorageWriteOptions } from "./storage.js";
|
|
28
|
+
import { patchInParts } from "./patch-in-parts.js";
|
|
29
|
+
|
|
30
|
+
/** A row of a collection: the id and the stored item. The rule `admit` examines one row at a time. */
|
|
31
|
+
export interface CollectionRow {
|
|
32
|
+
readonly id: string;
|
|
33
|
+
readonly value: unknown;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** A write of a collection, as the guard examines it. */
|
|
37
|
+
export interface CollectionWrite {
|
|
38
|
+
/**
|
|
39
|
+
* The operation. `setBatch` gives one `set` for each item. `deleteBatch` and `clear` give one
|
|
40
|
+
* `delete` for each id.
|
|
41
|
+
*/
|
|
42
|
+
readonly op: "set" | "patch" | "delete";
|
|
43
|
+
/** The id of the item. */
|
|
44
|
+
readonly id: string;
|
|
45
|
+
/** The item after the write: the value of a `set`, or the merged item of a `patch`. Undefined for a `delete`. */
|
|
46
|
+
readonly doc: unknown;
|
|
47
|
+
/** The stored item before the write. Undefined when the id has no item. */
|
|
48
|
+
readonly prev: unknown;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The guard of the writes of a collection. It gets each write and the invocation of the call. It
|
|
53
|
+
* gives true to permit the write. An error that it throws passes through, and the storage does
|
|
54
|
+
* not change.
|
|
55
|
+
*/
|
|
56
|
+
export type CollectionWriteGuard = (write: CollectionWrite, invocation: Invocation) => boolean | Promise<boolean>;
|
|
57
|
+
|
|
58
|
+
/** The rules of the rows of a served collection (roadmap CLI-41). Each rule is optional. */
|
|
59
|
+
export interface CollectionAdmission {
|
|
60
|
+
/**
|
|
61
|
+
* The rule of the rows. It gets each row `{ id, value }` of a read and the invocation of the
|
|
62
|
+
* call, and it gives true to admit the row. The change events with values apply it for each
|
|
63
|
+
* subscriber, with the invocation of the subscription.
|
|
64
|
+
*/
|
|
65
|
+
admit?: AdmitItem | undefined;
|
|
66
|
+
/** The guard of each `set`, `patch` and `delete`, also of the batches and of `clear`. */
|
|
67
|
+
guard?: CollectionWriteGuard | undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The size of a page of a query of the storage, when the policy gives no `maxLimit`. */
|
|
71
|
+
const QUERY_PAGE = 1000;
|
|
72
|
+
|
|
73
|
+
/** The refusal of a write. */
|
|
74
|
+
function writeRefused(ctx: ProcedureContext, write: CollectionWrite): InvocationError {
|
|
75
|
+
return new InvocationError("FORBIDDEN", `The rule of the collection does not permit the ${write.op} of "${write.id}"`, ctx.path, "write-not-allowed");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* This function tells if the rule of the rows admits a row for a call. Without a rule, it gives
|
|
80
|
+
* true. A call without an invocation gets no row.
|
|
81
|
+
*/
|
|
82
|
+
export async function admitsRow(admission: CollectionAdmission | undefined, ctx: ProcedureContext, id: string, value: unknown): Promise<boolean> {
|
|
83
|
+
const admit = admission?.admit;
|
|
84
|
+
if (!admit) return true;
|
|
85
|
+
const invocation = ctx.invocation;
|
|
86
|
+
return invocation !== undefined && (await admit({ id, value }, invocation)) === true;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* This function gives the view of the storage of a call: the storage, with the rules of the
|
|
91
|
+
* collection for each read and each write. Without rules, it gives the storage itself.
|
|
92
|
+
*
|
|
93
|
+
* @param storage - The storage of the call: of the collection, or of its partition
|
|
94
|
+
* @param admission - The rules of the collection
|
|
95
|
+
* @param ctx - The context of the call. The rules get its invocation.
|
|
96
|
+
*/
|
|
97
|
+
export function admittedStorage<T>(storage: CollectionStorage<T>, admission: CollectionAdmission | undefined, ctx: ProcedureContext): CollectionStorage<T> {
|
|
98
|
+
const admit = admission?.admit;
|
|
99
|
+
const guard = admission?.guard;
|
|
100
|
+
if (!admit && !guard) return storage;
|
|
101
|
+
const invocation = ctx.invocation;
|
|
102
|
+
|
|
103
|
+
const admits = (id: string, value: unknown): Promise<boolean> => admitsRow(admission, ctx, id, value);
|
|
104
|
+
/** The entries that the rule admits, in their order. */
|
|
105
|
+
const admitted = async (entries: Array<[string, T]>): Promise<Array<[string, T]>> => {
|
|
106
|
+
if (!admit) return entries;
|
|
107
|
+
const keep = await Promise.all(entries.map(([id, value]) => admits(id, value)));
|
|
108
|
+
return entries.filter((_, index) => keep[index]);
|
|
109
|
+
};
|
|
110
|
+
/** The entries of the storage that the rule admits. The rule needs the ids, so the storage must give them. */
|
|
111
|
+
const entries = async (): Promise<Array<[string, T]>> => {
|
|
112
|
+
if (!storage.getEntries) throw new Error("The storage cannot give the ids of its items, so the rule of the rows cannot examine them");
|
|
113
|
+
return admitted(await storage.getEntries());
|
|
114
|
+
};
|
|
115
|
+
/** This function throws `FORBIDDEN` unless the guard permits each write. */
|
|
116
|
+
const permit = async (writes: readonly CollectionWrite[]): Promise<void> => {
|
|
117
|
+
if (!guard) return;
|
|
118
|
+
for (const write of writes) {
|
|
119
|
+
if (invocation === undefined || (await guard(write, invocation)) !== true) throw writeRefused(ctx, write);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const view: CollectionStorage<T> = {
|
|
124
|
+
async get(id) {
|
|
125
|
+
const value = await storage.get(id);
|
|
126
|
+
return value === undefined || (await admits(id, value)) ? value : undefined;
|
|
127
|
+
},
|
|
128
|
+
async getAll() {
|
|
129
|
+
return admit ? (await entries()).map(([, value]) => value) : storage.getAll();
|
|
130
|
+
},
|
|
131
|
+
async find(predicate) {
|
|
132
|
+
return admit ? (await entries()).map(([, value]) => value).filter(predicate) : storage.find(predicate);
|
|
133
|
+
},
|
|
134
|
+
async has(id) {
|
|
135
|
+
return admit ? (await view.get(id)) !== undefined : storage.has(id);
|
|
136
|
+
},
|
|
137
|
+
async size() {
|
|
138
|
+
return admit ? (await entries()).length : storage.size();
|
|
139
|
+
},
|
|
140
|
+
async getBatch(ids) {
|
|
141
|
+
const found = await storage.getBatch(ids);
|
|
142
|
+
if (!admit) return found;
|
|
143
|
+
const kept = await admitted([...found.entries()]);
|
|
144
|
+
return new Map(kept);
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
async set(id, value, options) {
|
|
148
|
+
if (guard) await permit([{ op: "set", id, doc: value, prev: await storage.get(id) }]);
|
|
149
|
+
return storage.set(id, value, options);
|
|
150
|
+
},
|
|
151
|
+
async delete(id, options) {
|
|
152
|
+
if (guard) await permit([{ op: "delete", id, doc: undefined, prev: await storage.get(id) }]);
|
|
153
|
+
return storage.delete(id, options);
|
|
154
|
+
},
|
|
155
|
+
async clear(options) {
|
|
156
|
+
if (!guard) return storage.clear(options);
|
|
157
|
+
// Each item is a delete that the guard examines. The storage deletes only the items that it examined.
|
|
158
|
+
if (!storage.getEntries) throw new Error("The storage cannot give the ids of its items, so the guard cannot examine a clear");
|
|
159
|
+
const all = await storage.getEntries();
|
|
160
|
+
await permit(all.map(([id, prev]) => ({ op: "delete" as const, id, doc: undefined, prev })));
|
|
161
|
+
if (all.length > 0) await storage.deleteBatch(all.map(([id]) => id), options);
|
|
162
|
+
},
|
|
163
|
+
async setBatch(items, options) {
|
|
164
|
+
if (guard) {
|
|
165
|
+
const before = await storage.getBatch(items.map(([id]) => id));
|
|
166
|
+
await permit(items.map(([id, value]) => ({ op: "set" as const, id, doc: value, prev: before.get(id) })));
|
|
167
|
+
}
|
|
168
|
+
return storage.setBatch(items, options);
|
|
169
|
+
},
|
|
170
|
+
async deleteBatch(ids, options) {
|
|
171
|
+
if (guard) {
|
|
172
|
+
const before = await storage.getBatch(ids);
|
|
173
|
+
await permit(ids.map((id) => ({ op: "delete" as const, id, doc: undefined, prev: before.get(id) })));
|
|
174
|
+
}
|
|
175
|
+
return storage.deleteBatch(ids, options);
|
|
176
|
+
},
|
|
177
|
+
async patch(id: string, fields: StoragePatch, options?: StorageWriteOptions): Promise<T | undefined> {
|
|
178
|
+
const check = async (prev: unknown, doc: unknown): Promise<void> => permit([{ op: "patch", id, doc, prev }]);
|
|
179
|
+
if (!storage.patch) return patchInParts(storage as CollectionStorage<unknown>, id, fields, options, guard ? check : undefined) as Promise<T | undefined>;
|
|
180
|
+
if (guard) {
|
|
181
|
+
const prev = await storage.get(id);
|
|
182
|
+
// A patch of a missing id changes nothing
|
|
183
|
+
if (prev === undefined) return undefined;
|
|
184
|
+
await check(prev, applyPatch(prev, fields));
|
|
185
|
+
}
|
|
186
|
+
return storage.patch(id, fields, options);
|
|
187
|
+
},
|
|
188
|
+
close: () => storage.close(),
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
if (storage.getEntries) view.getEntries = async () => (admit ? entries() : storage.getEntries!());
|
|
192
|
+
if (admit ? storage.getEntries : storage.getIds) {
|
|
193
|
+
view.getIds = async () => (admit ? (await entries()).map(([id]) => id) : storage.getIds!());
|
|
194
|
+
}
|
|
195
|
+
if (storage.getChangesSince) {
|
|
196
|
+
view.getChangesSince = async (since?: string): Promise<StorageChanges<T>> => {
|
|
197
|
+
const changes = await storage.getChangesSince!(since);
|
|
198
|
+
if (!admit) return changes;
|
|
199
|
+
const changed = await admitted(changes.changed);
|
|
200
|
+
if (changes.full) return { ...changes, changed };
|
|
201
|
+
// A delta: the copy of the caller drops each refused row
|
|
202
|
+
const kept = new Set(changed.map(([id]) => id));
|
|
203
|
+
const refused = changes.changed.filter(([id]) => !kept.has(id)).map(([id]) => id);
|
|
204
|
+
return { ...changes, changed, deleted: [...changes.deleted, ...refused] };
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
if (storage.query) {
|
|
208
|
+
view.query = async (selector: StorageSelector, options?: StorageQueryOptions, policy?: StorageQueryPolicy) =>
|
|
209
|
+
admit ? admittedQuery(storage, admitted, selector, options ?? {}, policy) : storage.query!(selector, options, policy);
|
|
210
|
+
}
|
|
211
|
+
return view;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* The query of a storage with the rule of the rows. The storage runs the selector and the order,
|
|
216
|
+
* page by page. The skip and the limit count the admitted rows only, so a page of the caller is full.
|
|
217
|
+
*/
|
|
218
|
+
async function admittedQuery<T>(
|
|
219
|
+
storage: CollectionStorage<T>,
|
|
220
|
+
admitted: (entries: Array<[string, T]>) => Promise<Array<[string, T]>>,
|
|
221
|
+
selector: StorageSelector,
|
|
222
|
+
options: StorageQueryOptions,
|
|
223
|
+
policy: StorageQueryPolicy | undefined,
|
|
224
|
+
): Promise<Array<[string, T]>> {
|
|
225
|
+
const page = policy?.maxLimit !== undefined && Number.isFinite(policy.maxLimit) ? policy.maxLimit : QUERY_PAGE;
|
|
226
|
+
const skip = options.skip ?? 0;
|
|
227
|
+
const limit = options.limit;
|
|
228
|
+
const out: Array<[string, T]> = [];
|
|
229
|
+
let skipped = 0;
|
|
230
|
+
for (let offset = 0; ; offset += page) {
|
|
231
|
+
const rows = await storage.query!(selector, { ...(options.sort !== undefined && { sort: options.sort }), skip: offset, limit: page }, policy);
|
|
232
|
+
for (const entry of await admitted(rows)) {
|
|
233
|
+
if (skipped < skip) {
|
|
234
|
+
skipped++;
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
out.push(entry);
|
|
238
|
+
if (limit !== undefined && out.length >= limit) return out;
|
|
239
|
+
}
|
|
240
|
+
if (rows.length < page) return out;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
|
|
27
27
|
import type { EventBus } from "../../events/types.js";
|
|
28
28
|
import { withRows } from "../../events/rows.js";
|
|
29
|
+
import type { AdmitItem } from "../invoke.js";
|
|
29
30
|
import type { StorageChanges } from "./storage.js";
|
|
30
31
|
|
|
31
32
|
/**
|
|
@@ -137,13 +138,16 @@ function hint(version: string | undefined, origin: string | undefined): Collecti
|
|
|
137
138
|
|
|
138
139
|
/**
|
|
139
140
|
* The event of a delta with the values of the changed items. Each row `{ id, value }` passes the
|
|
140
|
-
* output filter of each subscriber
|
|
141
|
+
* output filter of each subscriber and the rule of the rows of the collection, and a refused row
|
|
142
|
+
* goes from `values` to `withheld`.
|
|
141
143
|
*/
|
|
142
|
-
function withValues(event: CollectionChangeEvent, entries: ReadonlyArray<{ readonly id: string; readonly value: unknown }
|
|
144
|
+
function withValues(event: CollectionChangeEvent, entries: ReadonlyArray<{ readonly id: string; readonly value: unknown }>, admit: AdmitItem | undefined): CollectionChangeEvent {
|
|
143
145
|
return withRows(
|
|
144
146
|
{ ...event, values: entries },
|
|
145
147
|
{
|
|
146
148
|
rows: entries,
|
|
149
|
+
// The rule of the rows of the collection (roadmap CLI-41): each subscriber applies it too
|
|
150
|
+
...(admit && { admit }),
|
|
147
151
|
keep: (admitted) => {
|
|
148
152
|
const withheld = entries.filter((_, index) => !admitted[index]).map(({ id }) => id);
|
|
149
153
|
return {
|
|
@@ -157,7 +161,7 @@ function withValues(event: CollectionChangeEvent, entries: ReadonlyArray<{ reado
|
|
|
157
161
|
}
|
|
158
162
|
|
|
159
163
|
/** One publish: the event of the writes that ended since the last publish. */
|
|
160
|
-
async function publish(chain: ChangeChain, storage: ChangeLog, bus: EventBus, topic: string, values: boolean): Promise<void> {
|
|
164
|
+
async function publish(chain: ChangeChain, storage: ChangeLog, bus: EventBus, topic: string, values: boolean, admit: AdmitItem | undefined): Promise<void> {
|
|
161
165
|
const writes = chain.ended.splice(0);
|
|
162
166
|
// An earlier publish told these writes
|
|
163
167
|
if (writes.length === 0) return;
|
|
@@ -194,7 +198,7 @@ async function publish(chain: ChangeChain, storage: ChangeLog, bus: EventBus, to
|
|
|
194
198
|
deleted: changes.deleted,
|
|
195
199
|
...(origin !== undefined && { origin }),
|
|
196
200
|
};
|
|
197
|
-
bus.emit(topic, values ? withValues(event, changes.changed.map(([id, value]) => ({ id, value }))) : event);
|
|
201
|
+
bus.emit(topic, values ? withValues(event, changes.changed.map(([id, value]) => ({ id, value })), admit) : event);
|
|
198
202
|
}
|
|
199
203
|
|
|
200
204
|
/**
|
|
@@ -210,6 +214,7 @@ async function publish(chain: ChangeChain, storage: ChangeLog, bus: EventBus, to
|
|
|
210
214
|
* @param origin - The origin of the write
|
|
211
215
|
* @param write - The write
|
|
212
216
|
* @param values - True: the events carry the values, for the output filter of each subscriber
|
|
217
|
+
* @param admit - The rule of the rows of the collection: each subscriber applies it to the values (roadmap CLI-41)
|
|
213
218
|
*/
|
|
214
219
|
export async function writeWithChangeEvent<R>(
|
|
215
220
|
bus: EventBus | undefined,
|
|
@@ -219,6 +224,7 @@ export async function writeWithChangeEvent<R>(
|
|
|
219
224
|
origin: string | undefined,
|
|
220
225
|
write: () => Promise<R>,
|
|
221
226
|
values = false,
|
|
227
|
+
admit?: AdmitItem,
|
|
222
228
|
): Promise<R> {
|
|
223
229
|
if (bus === undefined || topic === undefined) return write();
|
|
224
230
|
const chain = chainOf(storage, topic);
|
|
@@ -227,7 +233,7 @@ export async function writeWithChangeEvent<R>(
|
|
|
227
233
|
return await write();
|
|
228
234
|
} finally {
|
|
229
235
|
chain.ended.push(started);
|
|
230
|
-
const run = chain.tail.then(() => publish(chain, storage, bus, topic, values));
|
|
236
|
+
const run = chain.tail.then(() => publish(chain, storage, bus, topic, values, admit));
|
|
231
237
|
chain.tail = run.catch(() => undefined);
|
|
232
238
|
await chain.tail;
|
|
233
239
|
}
|
|
@@ -74,6 +74,10 @@ export type { CollectionChangeEvent } from "./changes.js";
|
|
|
74
74
|
export { PARTITION_PATTERN, collectionTopicPartition, collectionTopicTarget, partitionOfCall, storageOfCall, withPartition } from "./partition.js";
|
|
75
75
|
export type { CollectionPartitions, PartitionInput, PartitionResolver } from "./partition.js";
|
|
76
76
|
|
|
77
|
+
// The rules of the rows of a collection: a filter of the reads and a guard of the writes (roadmap CLI-41)
|
|
78
|
+
export { admittedStorage, admitsRow } from "./admission.js";
|
|
79
|
+
export type { CollectionAdmission, CollectionRow, CollectionWrite, CollectionWriteGuard } from "./admission.js";
|
|
80
|
+
|
|
77
81
|
// The storage interface of the core (the shape of client-collections' CollectionStorage)
|
|
78
82
|
export type {
|
|
79
83
|
CollectionStorage,
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The patch of an item of a storage (roadmap CLI-14): `patch()` of the storage, or a read, a merge
|
|
3
|
+
* and a write for a storage without it.
|
|
4
|
+
*/
|
|
5
|
+
import { applyPatch, type StoragePatch } from "./query.js";
|
|
6
|
+
import type { CollectionStorage, StorageWriteOptions } from "./storage.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The patches of a storage without `patch()` that run, for each id. A patch of an id waits for the
|
|
10
|
+
* patch before it, so two patches of one process keep each field. A write of another writer
|
|
11
|
+
* between the read and the write of a patch is lost: give the storage a `patch()` for one step.
|
|
12
|
+
*/
|
|
13
|
+
const PATCHES = new WeakMap<object, Map<string, Promise<unknown>>>();
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* This function patches an item of a storage without `patch()`: a read, a merge and a write. The
|
|
17
|
+
* check gets the stored item and the merged item before the write. An error of the check stops
|
|
18
|
+
* the patch, and the storage does not change.
|
|
19
|
+
*/
|
|
20
|
+
export function patchInParts(
|
|
21
|
+
storage: CollectionStorage<unknown>,
|
|
22
|
+
id: string,
|
|
23
|
+
fields: StoragePatch,
|
|
24
|
+
options?: StorageWriteOptions,
|
|
25
|
+
check?: (prev: unknown, next: unknown) => Promise<void>,
|
|
26
|
+
): Promise<unknown> {
|
|
27
|
+
let running = PATCHES.get(storage);
|
|
28
|
+
if (!running) {
|
|
29
|
+
running = new Map();
|
|
30
|
+
PATCHES.set(storage, running);
|
|
31
|
+
}
|
|
32
|
+
const queue = running;
|
|
33
|
+
const before = queue.get(id) ?? Promise.resolve();
|
|
34
|
+
const patch = before.then(async () => {
|
|
35
|
+
const current = await storage.get(id);
|
|
36
|
+
if (current === undefined) return undefined;
|
|
37
|
+
const next = applyPatch(current, fields);
|
|
38
|
+
if (check) await check(current, next);
|
|
39
|
+
await storage.set(id, next, options);
|
|
40
|
+
return next;
|
|
41
|
+
});
|
|
42
|
+
const settled = patch.then(
|
|
43
|
+
() => undefined,
|
|
44
|
+
() => undefined,
|
|
45
|
+
);
|
|
46
|
+
queue.set(id, settled);
|
|
47
|
+
void settled.then(() => {
|
|
48
|
+
if (queue.get(id) === settled) queue.delete(id);
|
|
49
|
+
});
|
|
50
|
+
return patch;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** This function patches an item: with `patch()` of the storage, or in parts. It gives the new item, or undefined for a missing id. */
|
|
54
|
+
export function patchItem(storage: CollectionStorage<unknown>, id: string, fields: StoragePatch, options?: StorageWriteOptions): Promise<unknown> {
|
|
55
|
+
return storage.patch ? storage.patch(id, fields, options) : patchInParts(storage, id, fields, options);
|
|
56
|
+
}
|