@procedures/client 0.35.1 → 0.37.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 +37 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/browser.d.ts +3 -3
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +2 -0
- package/dist/browser.js.map +1 -1
- package/dist/events/bus.d.ts.map +1 -1
- package/dist/events/bus.js +5 -1
- package/dist/events/bus.js.map +1 -1
- package/dist/events/index.d.ts +1 -1
- package/dist/events/index.d.ts.map +1 -1
- package/dist/events/publisher.d.ts +18 -2
- package/dist/events/publisher.d.ts.map +1 -1
- package/dist/events/publisher.js +142 -25
- package/dist/events/publisher.js.map +1 -1
- package/dist/events/recheck.d.ts +27 -5
- package/dist/events/recheck.d.ts.map +1 -1
- package/dist/events/recheck.js +11 -10
- package/dist/events/recheck.js.map +1 -1
- package/dist/procedures/collection/admission.d.ts +26 -7
- package/dist/procedures/collection/admission.d.ts.map +1 -1
- package/dist/procedures/collection/admission.js +39 -7
- package/dist/procedures/collection/admission.js.map +1 -1
- package/dist/procedures/collection/changes.d.ts +13 -4
- package/dist/procedures/collection/changes.d.ts.map +1 -1
- package/dist/procedures/collection/changes.js +50 -6
- package/dist/procedures/collection/changes.js.map +1 -1
- package/dist/procedures/collection/hint-filter.d.ts +143 -0
- package/dist/procedures/collection/hint-filter.d.ts.map +1 -0
- package/dist/procedures/collection/hint-filter.js +311 -0
- package/dist/procedures/collection/hint-filter.js.map +1 -0
- 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/procedures.d.ts +2 -2
- package/dist/procedures/collection/procedures.d.ts.map +1 -1
- package/dist/procedures/collection/procedures.js +6 -4
- 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/package.json +1 -1
- package/src/browser.ts +7 -0
- package/src/events/bus.ts +6 -1
- package/src/events/index.ts +1 -1
- package/src/events/publisher.ts +150 -26
- package/src/events/recheck.ts +38 -13
- package/src/procedures/collection/admission.ts +50 -9
- package/src/procedures/collection/changes.ts +56 -7
- package/src/procedures/collection/hint-filter.ts +376 -0
- package/src/procedures/collection/index.ts +3 -0
- package/src/procedures/collection/procedures.ts +8 -6
- package/src/procedures/index.ts +5 -0
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The row filter of the change events of a collection (roadmap CLI-78).
|
|
3
|
+
*
|
|
4
|
+
* A change event names the ids that changed. Before CLI-78, each subscriber of a topic got all the
|
|
5
|
+
* ids, so only a principal that reads the whole partition could subscribe. With CLI-78, the publisher
|
|
6
|
+
* filters each event row by row for each subscriber, with the rule of the rows of the collection
|
|
7
|
+
* (`admission.admit`). The reads apply the same rule (`admittedStorage()`).
|
|
8
|
+
*
|
|
9
|
+
* - The rule comes from the registry of the call: `createCollectionProcedures()` puts its rules on
|
|
10
|
+
* the metadata of its procedures. An event can also carry the rules in its images.
|
|
11
|
+
* - The rule examines the rows of the images of the event (`withHintImages()`): the row after the
|
|
12
|
+
* changes, and the row before them. The publisher does not read a storage.
|
|
13
|
+
* - A changed row that the rule admits stays in `changed`. A changed row that the rule refuses, or
|
|
14
|
+
* a deleted row, goes to `deleted` only when the rule admitted its row before the changes.
|
|
15
|
+
* Otherwise the subscriber does not get its id.
|
|
16
|
+
* - When an image is missing, or the rule fails, the subscriber gets a hint without ids and
|
|
17
|
+
* without `since`. It then reads the changes, through the rule of the reads.
|
|
18
|
+
* - An event without ids, and a `reset` event, pass without ids.
|
|
19
|
+
* - When the subscriber gets no id of an event, it gets no event. The `since` of its next event is
|
|
20
|
+
* then the `version` of the last event that it got. Thus it applies the next delta without a read.
|
|
21
|
+
* - `admission.admitsAll` tells that a subscriber reads each row of its partition. Its events then
|
|
22
|
+
* pass as before CLI-78, with no cost.
|
|
23
|
+
*/
|
|
24
|
+
import type { Invocation } from "../invoke.js";
|
|
25
|
+
import type { AnyProcedure, ProcedureContext } from "../types.js";
|
|
26
|
+
import type { CollectionAdmission } from "./admission.js";
|
|
27
|
+
import { withRows } from "../../events/rows.js";
|
|
28
|
+
import type { EventBus } from "../../events/types.js";
|
|
29
|
+
|
|
30
|
+
/** The state of one row in an image: the stored item, or no item. */
|
|
31
|
+
export type HintRowState = { readonly found: true; readonly value: unknown } | { readonly found: false };
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The rows of a change event, for the row filter of each subscriber (roadmap CLI-78). A missing id
|
|
35
|
+
* in a map is unknown. The images are on a symbol key, so they stay in this process: JSON does
|
|
36
|
+
* not take them.
|
|
37
|
+
*/
|
|
38
|
+
export interface HintImages {
|
|
39
|
+
/** The rows after the changes of the event, by id. */
|
|
40
|
+
readonly after?: ReadonlyMap<string, HintRowState> | undefined;
|
|
41
|
+
/** The rows before the changes of the event (at its `since`), by id. */
|
|
42
|
+
readonly before?: ReadonlyMap<string, HintRowState> | undefined;
|
|
43
|
+
/**
|
|
44
|
+
* The rules of the rows of the collection. The publisher uses the rules from its registry
|
|
45
|
+
* first, and these rules only when the registry has none.
|
|
46
|
+
*/
|
|
47
|
+
readonly admission?: CollectionAdmission | undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The key of the images of an event. */
|
|
51
|
+
const IMAGES: unique symbol = Symbol.for("procedures.hintImages");
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* This function gives the event with its images, for the row filter of each subscriber (roadmap
|
|
55
|
+
* CLI-78). The images are not enumerable: a copy or a comparison of the event does not see them.
|
|
56
|
+
*/
|
|
57
|
+
export function withHintImages<T extends object>(event: T, images: HintImages): T {
|
|
58
|
+
return Object.defineProperty(event, IMAGES, { value: images, enumerable: false, configurable: true });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The images of an event, or undefined for an event without images. */
|
|
62
|
+
export function hintImagesOf(event: unknown): HintImages | undefined {
|
|
63
|
+
if (typeof event !== "object" || event === null) return undefined;
|
|
64
|
+
const images = (event as { [IMAGES]?: unknown })[IMAGES];
|
|
65
|
+
return typeof images === "object" && images !== null ? (images as HintImages) : undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The key of the rules of a collection on the metadata of its procedures. */
|
|
69
|
+
const RULES: unique symbol = Symbol.for("procedures.collectionRules");
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* This function puts the rules of a collection on the metadata of its procedures, so the publisher
|
|
73
|
+
* of the events finds them in the registry. The key is not enumerable.
|
|
74
|
+
*/
|
|
75
|
+
export function withCollectionRules(procedures: AnyProcedure[], admission: CollectionAdmission | undefined): AnyProcedure[] {
|
|
76
|
+
for (const procedure of procedures) {
|
|
77
|
+
const metadata = procedure.metadata as object | undefined;
|
|
78
|
+
if (metadata) Object.defineProperty(metadata, RULES, { value: { admission }, enumerable: false, configurable: true });
|
|
79
|
+
}
|
|
80
|
+
return procedures;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The rules of a collection in the registry of a call. It gives the rules, or `null` for a
|
|
85
|
+
* collection without rules. It gives undefined when the registry does not know the procedures.
|
|
86
|
+
*/
|
|
87
|
+
export function collectionRulesOf(ctx: ProcedureContext, collection: string): CollectionAdmission | null | undefined {
|
|
88
|
+
const registry = ctx.registry;
|
|
89
|
+
if (!registry) return undefined;
|
|
90
|
+
for (const operation of ["changes", "get"]) {
|
|
91
|
+
const metadata = registry.get(["collections", collection, operation])?.metadata as { [RULES]?: { admission?: CollectionAdmission | undefined } } | undefined;
|
|
92
|
+
const rules = metadata?.[RULES];
|
|
93
|
+
if (rules) return rules.admission ?? null;
|
|
94
|
+
}
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The invocation that the rules of a collection get for the events of one topic (roadmap CLI-78).
|
|
100
|
+
* It has the principal, the capability, the metadata and the signal of the subscription. Its path
|
|
101
|
+
* is the path of `changes` of the collection, and its input has the partition of the topic. Thus
|
|
102
|
+
* a rule that reads `invocation.input.partition` sees the same input as in a read of the topic.
|
|
103
|
+
*
|
|
104
|
+
* @param base - The invocation of the subscription
|
|
105
|
+
* @param ctx - The context of the subscription. Its registry gives the procedure of `changes`.
|
|
106
|
+
* @param collection - The collection of the topic
|
|
107
|
+
* @param partition - The partition that the topic names, before the resolver
|
|
108
|
+
*/
|
|
109
|
+
export function collectionInvocation(base: Invocation | undefined, ctx: ProcedureContext, collection: string, partition: string | undefined): Invocation | undefined {
|
|
110
|
+
if (base === undefined) return undefined;
|
|
111
|
+
const procedure = ctx.registry?.get(["collections", collection, "changes"]);
|
|
112
|
+
return {
|
|
113
|
+
...base,
|
|
114
|
+
...(procedure !== undefined && { procedure, path: procedure.path }),
|
|
115
|
+
input: partition === undefined ? {} : { partition },
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** The subscriptions with a row filter, by bus and by topic: the writes read the rows before them only for these. */
|
|
120
|
+
const FILTERED = new WeakMap<object, Map<string, number>>();
|
|
121
|
+
|
|
122
|
+
/** The key of the bus under a view of a bus (`withEventBusSignal()`). */
|
|
123
|
+
const BASE_BUS = Symbol.for("procedures.baseBus");
|
|
124
|
+
|
|
125
|
+
/** The bus under a view of a bus, or the bus itself. */
|
|
126
|
+
function baseOf(bus: EventBus): object {
|
|
127
|
+
return ((bus as { [BASE_BUS]?: EventBus })[BASE_BUS] as object | undefined) ?? bus;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* This function marks a topic of a bus: a subscription of the topic has a row filter. It gives the
|
|
132
|
+
* function that removes the mark. A second call of that function does nothing.
|
|
133
|
+
*/
|
|
134
|
+
export function markFilteredTopic(bus: EventBus, topic: string): () => void {
|
|
135
|
+
const base = baseOf(bus);
|
|
136
|
+
let topics = FILTERED.get(base);
|
|
137
|
+
if (!topics) {
|
|
138
|
+
topics = new Map();
|
|
139
|
+
FILTERED.set(base, topics);
|
|
140
|
+
}
|
|
141
|
+
topics.set(topic, (topics.get(topic) ?? 0) + 1);
|
|
142
|
+
let marked = true;
|
|
143
|
+
return () => {
|
|
144
|
+
if (!marked) return;
|
|
145
|
+
marked = false;
|
|
146
|
+
const count = (topics.get(topic) ?? 1) - 1;
|
|
147
|
+
if (count > 0) topics.set(topic, count);
|
|
148
|
+
else topics.delete(topic);
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** True when a subscription of the topic of the bus has a row filter. */
|
|
153
|
+
export function hasFilteredSubscriber(bus: EventBus, topic: string): boolean {
|
|
154
|
+
return (FILTERED.get(baseOf(bus))?.get(topic) ?? 0) > 0;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** The result of the filter for an event that the subscriber does not get. */
|
|
158
|
+
export const DROPPED: unique symbol = Symbol("procedures.droppedHint");
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* The mode of a filter. `pass` gives each event as before CLI-78, and `filter` examines each row.
|
|
162
|
+
* `unknown` uses the rules of the images of each event.
|
|
163
|
+
*/
|
|
164
|
+
export type HintFilterMode = "pass" | "filter" | "unknown";
|
|
165
|
+
|
|
166
|
+
/** The fields of a change event that the filter reads. */
|
|
167
|
+
interface ChangeEventShape {
|
|
168
|
+
readonly since?: unknown;
|
|
169
|
+
readonly version?: unknown;
|
|
170
|
+
readonly changed: readonly string[];
|
|
171
|
+
readonly deleted: readonly string[];
|
|
172
|
+
readonly values?: ReadonlyArray<{ readonly id: string; readonly value: unknown }> | undefined;
|
|
173
|
+
readonly versions?: ReadonlyArray<{ readonly id: string; readonly version: string }> | undefined;
|
|
174
|
+
readonly reset?: unknown;
|
|
175
|
+
readonly origin?: unknown;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** True for a value with the shape of a change event: the arrays `changed` and `deleted`. */
|
|
179
|
+
function isChangeEvent(data: unknown): data is ChangeEventShape {
|
|
180
|
+
if (typeof data !== "object" || data === null) return false;
|
|
181
|
+
const event = data as { changed?: unknown; deleted?: unknown };
|
|
182
|
+
return Array.isArray(event.changed) && Array.isArray(event.deleted);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** The fields `version` and `origin` of an event, when they are strings. */
|
|
186
|
+
function headOf(event: ChangeEventShape): { version?: string; origin?: string } {
|
|
187
|
+
return {
|
|
188
|
+
...(typeof event.version === "string" && { version: event.version }),
|
|
189
|
+
...(typeof event.origin === "string" && { origin: event.origin }),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** What the rule tells of one row: admitted, refused, or unknown (no image, or an error). */
|
|
194
|
+
type Verdict = boolean | undefined;
|
|
195
|
+
|
|
196
|
+
/** This function does nothing: the end of a filter that failed. */
|
|
197
|
+
function ignore(): void {}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* The row filter of one topic of one subscription (roadmap CLI-78). The publisher gives each event
|
|
201
|
+
* of the topic to `apply()`, one at a time, in the order of the bus.
|
|
202
|
+
*/
|
|
203
|
+
export class RowHintFilter {
|
|
204
|
+
/** The rules from the registry: undefined when the registry does not know the collection. */
|
|
205
|
+
private rules: CollectionAdmission | null | undefined;
|
|
206
|
+
/** The result of `admitsAll` for one set of rules. */
|
|
207
|
+
private all: { readonly admission: CollectionAdmission; readonly value: boolean } | undefined;
|
|
208
|
+
/** The `version` of the last event of the chain, also of an event that the subscriber did not get. */
|
|
209
|
+
private head: string | undefined;
|
|
210
|
+
/** The `since` that the next event of the chain gets: the `version` of the last event that the subscriber got. */
|
|
211
|
+
private base: string | undefined;
|
|
212
|
+
/** The filters, one at a time. */
|
|
213
|
+
private tail: Promise<unknown> = Promise.resolve();
|
|
214
|
+
|
|
215
|
+
constructor(
|
|
216
|
+
/** The rules of the collection from the registry of the call. */
|
|
217
|
+
private readonly lookup: () => CollectionAdmission | null | undefined,
|
|
218
|
+
/** The invocation of the topic (`collectionInvocation()`): the rules get it. */
|
|
219
|
+
readonly invocation: Invocation | undefined,
|
|
220
|
+
/** The partition of the topic: `admitsAll` gets it. */
|
|
221
|
+
private readonly partition: string | undefined,
|
|
222
|
+
) {
|
|
223
|
+
this.rules = lookup();
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** The mode of the filter, from the rules and from the last result of `admitsAll`. */
|
|
227
|
+
get mode(): HintFilterMode {
|
|
228
|
+
const rules = this.rules;
|
|
229
|
+
if (rules === undefined) return "unknown";
|
|
230
|
+
if (rules === null || !rules.admit) return "pass";
|
|
231
|
+
return this.all?.admission === rules && this.all.value ? "pass" : "filter";
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* This function reads the rules again and asks `admitsAll` again: at the start of the
|
|
236
|
+
* subscription and at each re-check (roadmap CLI-44). It gives the new mode.
|
|
237
|
+
*/
|
|
238
|
+
async refresh(): Promise<HintFilterMode> {
|
|
239
|
+
this.rules = this.lookup();
|
|
240
|
+
this.all = undefined;
|
|
241
|
+
if (this.rules?.admit) await this.admitsAll(this.rules);
|
|
242
|
+
return this.mode;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** This function forgets the chain: the next event keeps its own `since`. */
|
|
246
|
+
forget(): void {
|
|
247
|
+
this.head = undefined;
|
|
248
|
+
this.base = undefined;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* The event for the subscriber. It gives undefined when the event passes as it is (no wait).
|
|
253
|
+
* Otherwise it gives a promise of the event for the subscriber, or of `DROPPED` for no event.
|
|
254
|
+
*/
|
|
255
|
+
apply(data: unknown): Promise<unknown> | undefined {
|
|
256
|
+
if (!isChangeEvent(data)) return undefined;
|
|
257
|
+
const mode = this.mode;
|
|
258
|
+
if (mode === "pass") return undefined;
|
|
259
|
+
const admission = this.rules ?? hintImagesOf(data)?.admission;
|
|
260
|
+
if (!admission?.admit) return undefined;
|
|
261
|
+
const run = this.tail.then(() => this.filter(data, admission));
|
|
262
|
+
this.tail = run.catch(ignore);
|
|
263
|
+
return run;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** The result of `admitsAll` for a set of rules. An error gives false. */
|
|
267
|
+
private async admitsAll(admission: CollectionAdmission): Promise<boolean> {
|
|
268
|
+
if (this.all?.admission === admission) return this.all.value;
|
|
269
|
+
let value = false;
|
|
270
|
+
const invocation = this.invocation;
|
|
271
|
+
if (admission.admitsAll && invocation) {
|
|
272
|
+
try {
|
|
273
|
+
value = (await admission.admitsAll(invocation, this.partition)) === true;
|
|
274
|
+
} catch {
|
|
275
|
+
value = false;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
this.all = { admission, value };
|
|
279
|
+
return value;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** The filter of one event with the rule of the rows. */
|
|
283
|
+
private async filter(event: ChangeEventShape, admission: CollectionAdmission): Promise<unknown> {
|
|
284
|
+
// Rules from the images of an event: the subscriber can read each row
|
|
285
|
+
if (await this.admitsAll(admission)) return event;
|
|
286
|
+
if (event.reset === true) {
|
|
287
|
+
this.forget();
|
|
288
|
+
return { ...headOf(event), changed: [], deleted: [], reset: true };
|
|
289
|
+
}
|
|
290
|
+
const since = typeof event.since === "string" ? event.since : undefined;
|
|
291
|
+
if (event.changed.length === 0 && event.deleted.length === 0) {
|
|
292
|
+
if (since === undefined) {
|
|
293
|
+
// A hint: the subscriber reads the changes through the rule of the reads
|
|
294
|
+
this.forget();
|
|
295
|
+
return { ...headOf(event), changed: [], deleted: [] };
|
|
296
|
+
}
|
|
297
|
+
return this.skip(since, event);
|
|
298
|
+
}
|
|
299
|
+
const admit = admission.admit!;
|
|
300
|
+
const invocation = this.invocation;
|
|
301
|
+
const images = hintImagesOf(event);
|
|
302
|
+
const verdict = async (id: string, state: HintRowState | undefined): Promise<Verdict> => {
|
|
303
|
+
if (state === undefined) return undefined;
|
|
304
|
+
if (!state.found || invocation === undefined) return false;
|
|
305
|
+
try {
|
|
306
|
+
return (await admit({ id, value: state.value }, invocation)) === true;
|
|
307
|
+
} catch {
|
|
308
|
+
return undefined;
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
const before = (id: string): Promise<Verdict> => verdict(id, images?.before?.get(id));
|
|
312
|
+
const changed = await Promise.all(
|
|
313
|
+
event.changed.map(async (id): Promise<"changed" | "deleted" | "none" | undefined> => {
|
|
314
|
+
const now = await verdict(id, images?.after?.get(id));
|
|
315
|
+
if (now === true) return "changed";
|
|
316
|
+
if (now === undefined) return undefined;
|
|
317
|
+
// The row left the rule, or it has no item: a delete for a subscriber that had it
|
|
318
|
+
const was = await before(id);
|
|
319
|
+
return was === undefined ? undefined : was ? "deleted" : "none";
|
|
320
|
+
}),
|
|
321
|
+
);
|
|
322
|
+
const deleted = await Promise.all(event.deleted.map(before));
|
|
323
|
+
if (changed.includes(undefined) || deleted.includes(undefined)) {
|
|
324
|
+
// The filter cannot tell the rows: the subscriber reads the changes through the rule of the reads
|
|
325
|
+
this.forget();
|
|
326
|
+
return { ...headOf(event), changed: [], deleted: [] };
|
|
327
|
+
}
|
|
328
|
+
const visible = event.changed.filter((_, index) => changed[index] === "changed");
|
|
329
|
+
const gone = [...event.changed.filter((_, index) => changed[index] === "deleted"), ...event.deleted.filter((_, index) => deleted[index] === true)];
|
|
330
|
+
if (visible.length === 0 && gone.length === 0) {
|
|
331
|
+
if (since === undefined) return DROPPED;
|
|
332
|
+
return this.skip(since, event);
|
|
333
|
+
}
|
|
334
|
+
const kept = new Set(visible);
|
|
335
|
+
const chained = since === undefined ? undefined : since === this.head ? this.base : since;
|
|
336
|
+
if (since !== undefined && typeof event.version === "string") {
|
|
337
|
+
this.head = event.version;
|
|
338
|
+
this.base = event.version;
|
|
339
|
+
} else {
|
|
340
|
+
this.forget();
|
|
341
|
+
}
|
|
342
|
+
const out = {
|
|
343
|
+
...(chained !== undefined && { since: chained }),
|
|
344
|
+
...(typeof event.version === "string" && { version: event.version }),
|
|
345
|
+
changed: visible,
|
|
346
|
+
deleted: gone,
|
|
347
|
+
...(event.versions && { versions: event.versions.filter(({ id }) => kept.has(id)) }),
|
|
348
|
+
...(typeof event.origin === "string" && { origin: event.origin }),
|
|
349
|
+
};
|
|
350
|
+
if (!event.values) return out;
|
|
351
|
+
const values = event.values.filter(({ id }) => kept.has(id));
|
|
352
|
+
// The rows pass the output filter of the host for the subscriber, as before
|
|
353
|
+
return withRows(
|
|
354
|
+
{ ...out, values },
|
|
355
|
+
{
|
|
356
|
+
rows: values,
|
|
357
|
+
keep: (admitted) => {
|
|
358
|
+
const withheld = values.filter((_, index) => !admitted[index]).map(({ id }) => id);
|
|
359
|
+
return { ...out, values: values.filter((_, index) => admitted[index]), ...(withheld.length > 0 && { withheld }) };
|
|
360
|
+
},
|
|
361
|
+
},
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** An event of the chain that the subscriber does not get: the next event keeps the `since` of the last one. */
|
|
366
|
+
private skip(since: string, event: ChangeEventShape): typeof DROPPED {
|
|
367
|
+
const chained = since === this.head ? this.base : since;
|
|
368
|
+
if (typeof event.version === "string" && chained !== undefined) {
|
|
369
|
+
this.head = event.version;
|
|
370
|
+
this.base = chained;
|
|
371
|
+
} else {
|
|
372
|
+
this.forget();
|
|
373
|
+
}
|
|
374
|
+
return DROPPED;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
@@ -81,6 +81,9 @@ export type { CollectionPartitions, PartitionInput, PartitionResolver } from "./
|
|
|
81
81
|
// The rules of the rows of a collection: a filter of the reads and a guard of the writes (roadmap CLI-41)
|
|
82
82
|
export { admittedStorage, admitsRow } from "./admission.js";
|
|
83
83
|
export type { CollectionAdmission, CollectionRow, CollectionWrite, CollectionWriteGuard } from "./admission.js";
|
|
84
|
+
// The images of the rows of a change event, for the row filter of each subscriber (roadmap CLI-78)
|
|
85
|
+
export { withHintImages, hintImagesOf } from "./hint-filter.js";
|
|
86
|
+
export type { HintImages, HintRowState } from "./hint-filter.js";
|
|
84
87
|
|
|
85
88
|
// The storage interface of the core (the shape of client-collections' CollectionStorage)
|
|
86
89
|
export type {
|
|
@@ -62,6 +62,7 @@ import { collectionChangeTopic, startChangeChain, writeWithChangeEvent } from ".
|
|
|
62
62
|
import { partitionOfCall, storageOfCall, withPartition, type PartitionInput } from "./partition.js";
|
|
63
63
|
import { admitsRow, admittedStorage, checkAdmission, type CollectionAdmission } from "./admission.js";
|
|
64
64
|
import { patchItem } from "./patch-in-parts.js";
|
|
65
|
+
import { withCollectionRules } from "./hint-filter.js";
|
|
65
66
|
|
|
66
67
|
// =============================================================================
|
|
67
68
|
// Handler Factory
|
|
@@ -125,8 +126,8 @@ export interface CollectionProceduresOptions {
|
|
|
125
126
|
*/
|
|
126
127
|
changeEvents?: boolean | CollectionChangeEventsOptions | undefined;
|
|
127
128
|
/**
|
|
128
|
-
* The rules of the rows (roadmap CLI-41). `admit` examines each row of each read and of
|
|
129
|
-
* change event
|
|
129
|
+
* The rules of the rows (roadmap CLI-41). `admit` examines each row of each read, and each row of
|
|
130
|
+
* each change event for each subscriber (roadmap CLI-78). `guard` examines each write before the storage changes, and a
|
|
130
131
|
* refused write gets `FORBIDDEN` (reason `write-not-allowed`). With `versionOf`, a guarded write
|
|
131
132
|
* expects the version that the guard read. `maxScanned` stops the scan of a `find` (roadmap
|
|
132
133
|
* CLI-55). Without it, the procedures check only the partition.
|
|
@@ -185,7 +186,7 @@ export function createCollectionProcedures(collectionName: string, options: Coll
|
|
|
185
186
|
checkAdmission(admission);
|
|
186
187
|
/** A write, and then its change event on the bus of the context. The chain of the events belongs to the storage, not to its view. */
|
|
187
188
|
const changing = <R>(ctx: ProcedureContext, partition: string | undefined, storage: CollectionStorage<unknown>, ids: readonly string[] | "*", origin: string | undefined, write: () => Promise<R>): Promise<R> =>
|
|
188
|
-
writeWithChangeEvent(ctx.bus, storage, topicRule ? topicOf(ctx, partition) : undefined, ids, origin, write, withValues, admission
|
|
189
|
+
writeWithChangeEvent(ctx.bus, storage, topicRule ? topicOf(ctx, partition) : undefined, ids, origin, write, withValues, admission);
|
|
189
190
|
/**
|
|
190
191
|
* The partition and the storage of a call (roadmap CLI-17). The repository of the call has the
|
|
191
192
|
* partitions of the collection and their resolver: the tenant comes from the principal. Each read
|
|
@@ -195,10 +196,11 @@ export function createCollectionProcedures(collectionName: string, options: Coll
|
|
|
195
196
|
const collection = getCollectionFromPath(ctx.path);
|
|
196
197
|
const partition = await partitionOfCall(ctx, collection, input.partition);
|
|
197
198
|
const storage = storageOfCall<unknown>(ctx, collection, partition);
|
|
198
|
-
return { storage, view: admittedStorage(storage, admission, ctx), partition };
|
|
199
|
+
return { storage, view: admittedStorage(storage, admission, ctx, partition), partition };
|
|
199
200
|
};
|
|
200
201
|
|
|
201
|
-
|
|
202
|
+
// The rules of the rows on the metadata: the publisher of the events filters each event with them (roadmap CLI-78)
|
|
203
|
+
return withCollectionRules([
|
|
202
204
|
// Get
|
|
203
205
|
defineProcedure({
|
|
204
206
|
path: [...basePath, "get"],
|
|
@@ -485,5 +487,5 @@ export function createCollectionProcedures(collectionName: string, options: Coll
|
|
|
485
487
|
return changing(ctx, partition, storage, input.ids, input.origin, () => view.deleteBatch(input.ids, writeOptionsOf(input)));
|
|
486
488
|
},
|
|
487
489
|
}),
|
|
488
|
-
];
|
|
490
|
+
], admission);
|
|
489
491
|
}
|
package/src/procedures/index.ts
CHANGED
|
@@ -141,6 +141,9 @@ export {
|
|
|
141
141
|
// The rules of the rows of a collection (roadmap CLI-41)
|
|
142
142
|
admittedStorage,
|
|
143
143
|
admitsRow,
|
|
144
|
+
// The images of the rows of a change event, for the row filter of each subscriber (roadmap CLI-78)
|
|
145
|
+
withHintImages,
|
|
146
|
+
hintImagesOf,
|
|
144
147
|
DEFAULT_FIND_LIMIT,
|
|
145
148
|
DEFAULT_QUERY_OPERATORS,
|
|
146
149
|
DEFAULT_QUERY_POLICY,
|
|
@@ -177,6 +180,8 @@ export type {
|
|
|
177
180
|
CollectionChangeEvent,
|
|
178
181
|
CollectionAdmission,
|
|
179
182
|
CollectionRow,
|
|
183
|
+
HintImages,
|
|
184
|
+
HintRowState,
|
|
180
185
|
CollectionWrite,
|
|
181
186
|
CollectionWriteGuard,
|
|
182
187
|
CollectionPartitions,
|