@procedures/client 0.35.1 → 0.36.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 +33 -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
package/src/events/recheck.ts
CHANGED
|
@@ -27,6 +27,24 @@ import type { Principal } from "../server/types.js";
|
|
|
27
27
|
*/
|
|
28
28
|
export type RightsChange = string | ((principal: Principal | undefined) => boolean);
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* The rows that a change of the rights touches (roadmap CLI-78): the rows of the collections and
|
|
32
|
+
* of the partitions of the scope. The partition is the partition after the resolver. Without a
|
|
33
|
+
* collection, the scope touches each collection. Without a partition, it touches each partition.
|
|
34
|
+
*
|
|
35
|
+
* - A string matches one name.
|
|
36
|
+
* - A test matches each name that it admits, for example each partition of a tenant. A test that
|
|
37
|
+
* throws matches: a `reset` hint too many is safe, and a missing one is not.
|
|
38
|
+
* - A partition matches only a topic with a partition.
|
|
39
|
+
*/
|
|
40
|
+
export interface RightsScope {
|
|
41
|
+
readonly collection?: string | ((collection: string) => boolean) | undefined;
|
|
42
|
+
readonly partition?: string | ((partition: string) => boolean) | undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** One scope or a list of scopes of a change of the rights. A list touches the rows of each scope. */
|
|
46
|
+
export type RightsScopes = RightsScope | readonly RightsScope[];
|
|
47
|
+
|
|
30
48
|
/** The options of the re-check of the open subscriptions of a publisher (roadmap CLI-44). */
|
|
31
49
|
export interface SubscriptionRecheck {
|
|
32
50
|
/**
|
|
@@ -41,9 +59,12 @@ export interface SubscriptionRecheck {
|
|
|
41
59
|
* removes the listener. Each call of the listener is a call of `rightsChanged()`: without a
|
|
42
60
|
* change, each subscription gets a check.
|
|
43
61
|
*/
|
|
44
|
-
readonly rightsChanges?: ((listener: (change?: RightsChange) => void) => () => void) | undefined;
|
|
62
|
+
readonly rightsChanges?: ((listener: (change?: RightsChange, scope?: RightsScopes) => void) => () => void) | undefined;
|
|
45
63
|
}
|
|
46
64
|
|
|
65
|
+
/** The cause of a re-check: a change of the rights, or the max age. */
|
|
66
|
+
export type RecheckCause = "rights" | "age";
|
|
67
|
+
|
|
47
68
|
/** A subscription that the re-checks can hold and watch. */
|
|
48
69
|
export interface RecheckedSubscription {
|
|
49
70
|
/**
|
|
@@ -61,7 +82,7 @@ interface Watch {
|
|
|
61
82
|
/** The timer of the max age, or undefined. */
|
|
62
83
|
timer: Cancel | undefined;
|
|
63
84
|
/** This function starts a check at once. The promise ends when the check ends. */
|
|
64
|
-
readonly recheck: () => Promise<void>;
|
|
85
|
+
readonly recheck: (cause: RecheckCause, scope?: RightsScopes) => Promise<void>;
|
|
65
86
|
}
|
|
66
87
|
|
|
67
88
|
/** True when the change touches a subscription of the principal. A test that throws touches it. */
|
|
@@ -76,10 +97,13 @@ function touches(change: RightsChange | undefined, principal: Principal | undefi
|
|
|
76
97
|
}
|
|
77
98
|
|
|
78
99
|
/** The promise of a check. A check that throws at once gives a rejected promise. */
|
|
79
|
-
function started(check:
|
|
80
|
-
return new Promise<void>((resolve) => resolve(check()));
|
|
100
|
+
function started(check: RecheckCheck, cause: RecheckCause, scope: RightsScopes | undefined): Promise<void> {
|
|
101
|
+
return new Promise<void>((resolve) => resolve(check(cause, scope)));
|
|
81
102
|
}
|
|
82
103
|
|
|
104
|
+
/** The check of a subscription. A check of a change of the rights can get the scope of the change. */
|
|
105
|
+
type RecheckCheck = (cause: RecheckCause, scope?: RightsScopes) => Promise<void>;
|
|
106
|
+
|
|
83
107
|
/**
|
|
84
108
|
* The re-checks of the open subscriptions of one publisher (roadmap CLI-44). The publisher gives
|
|
85
109
|
* each new subscription to `watch()`, with the check of its admission.
|
|
@@ -110,15 +134,16 @@ export class SubscriptionRechecks {
|
|
|
110
134
|
|
|
111
135
|
/**
|
|
112
136
|
* This function watches a subscription until it ends. A change of the rights of its principal
|
|
113
|
-
* and its max age start the check. The check
|
|
137
|
+
* and its max age start the check. The check gets the cause and the scope of the change. It must
|
|
138
|
+
* throw the refusal.
|
|
114
139
|
*/
|
|
115
|
-
watch(subscription: RecheckedSubscription, principal: Principal | undefined, check:
|
|
140
|
+
watch(subscription: RecheckedSubscription, principal: Principal | undefined, check: RecheckCheck): void {
|
|
116
141
|
const watch: Watch = {
|
|
117
142
|
principal,
|
|
118
143
|
timer: undefined,
|
|
119
|
-
recheck: () => {
|
|
144
|
+
recheck: (cause, scope) => {
|
|
120
145
|
this.stopTimer(watch);
|
|
121
|
-
return subscription.hold(started(check)).then(() => {
|
|
146
|
+
return subscription.hold(started(check, cause, scope)).then(() => {
|
|
122
147
|
// A check that passes gives the admission a new age
|
|
123
148
|
if (this.watches.has(watch)) this.startTimer(watch);
|
|
124
149
|
});
|
|
@@ -130,7 +155,7 @@ export class SubscriptionRechecks {
|
|
|
130
155
|
this.startTimer(watch);
|
|
131
156
|
if (this.source && !this.stopListening) {
|
|
132
157
|
try {
|
|
133
|
-
this.stopListening = this.source((change) => void this.rightsChanged(change));
|
|
158
|
+
this.stopListening = this.source((change, scope) => void this.rightsChanged(change, scope));
|
|
134
159
|
} catch (error) {
|
|
135
160
|
// A source that cannot listen cannot tell a revocation: the subscription ends
|
|
136
161
|
void subscription.hold(Promise.reject(error));
|
|
@@ -140,12 +165,12 @@ export class SubscriptionRechecks {
|
|
|
140
165
|
|
|
141
166
|
/**
|
|
142
167
|
* This function starts a check of each watched subscription that the change touches. The
|
|
143
|
-
* promise ends when each check ends.
|
|
168
|
+
* promise ends when each check ends. Each check gets the scope of the change.
|
|
144
169
|
*/
|
|
145
|
-
rightsChanged(change?: RightsChange): Promise<void> {
|
|
170
|
+
rightsChanged(change?: RightsChange, scope?: RightsScopes): Promise<void> {
|
|
146
171
|
const checks: Array<Promise<void>> = [];
|
|
147
172
|
for (const watch of [...this.watches]) {
|
|
148
|
-
if (touches(change, watch.principal)) checks.push(watch.recheck());
|
|
173
|
+
if (touches(change, watch.principal)) checks.push(watch.recheck("rights", scope));
|
|
149
174
|
}
|
|
150
175
|
return Promise.all(checks).then(() => undefined);
|
|
151
176
|
}
|
|
@@ -166,7 +191,7 @@ export class SubscriptionRechecks {
|
|
|
166
191
|
this.stopTimer(watch);
|
|
167
192
|
const timer = this.clock.after(this.maxAgeMs, () => {
|
|
168
193
|
if (watch.timer === timer) watch.timer = undefined;
|
|
169
|
-
void watch.recheck();
|
|
194
|
+
void watch.recheck("age");
|
|
170
195
|
});
|
|
171
196
|
// A check does not keep a Node process alive
|
|
172
197
|
timer.unref?.();
|
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
* `admission` of `createCollectionProcedures()` gives the rules as data:
|
|
5
5
|
*
|
|
6
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
|
|
8
|
-
* subscriber
|
|
7
|
+
* the output filter of a host. The publisher of the change events gives it the rows of each
|
|
8
|
+
* event for each subscriber (roadmap CLI-78). Thus the reads and the events obey one rule.
|
|
9
|
+
* - `admitsAll` tells that `admit` admits each row of a partition for a call. The events of such a
|
|
10
|
+
* subscriber then pass without the filter of each row.
|
|
9
11
|
* - `guard` examines each write `{ op, id, doc, prev }` before the storage changes. A refused write
|
|
10
12
|
* gets `FORBIDDEN` with the reason `write-not-allowed`.
|
|
11
13
|
* - `versionOf` gives the version of a stored item (roadmap CLI-55). With it, a guarded write
|
|
@@ -16,8 +18,11 @@
|
|
|
16
18
|
* (`admittedStorage()`). Each read and each write of each procedure goes through this view.
|
|
17
19
|
*
|
|
18
20
|
* - A refused row is absent: `get` gives undefined, `has` gives false, and the lists, `size`,
|
|
19
|
-
* `find` and `changes` do not have it.
|
|
20
|
-
*
|
|
21
|
+
* `find` and `changes` do not have it.
|
|
22
|
+
* - A delta of `changes` gives only ids that the caller can read (roadmap CLI-78). The view
|
|
23
|
+
* does not know which rows the caller read before the delta. Thus a delta with a refused row or
|
|
24
|
+
* with a deleted row becomes a full answer of the admitted rows. The copy of the caller then
|
|
25
|
+
* drops each other row. When `admitsAll` admits the caller, the delta stays as it is.
|
|
21
26
|
* - An error of `admit` fails the read. It does not hide the rows, so a sync does not delete them.
|
|
22
27
|
* - The guard reads the stored item before the write. With `versionOf`, the write expects the
|
|
23
28
|
* version of that item (roadmap CLI-16). A change of another writer between the read and the
|
|
@@ -66,10 +71,23 @@ export type CollectionWriteGuard = (write: CollectionWrite, invocation: Invocati
|
|
|
66
71
|
export interface CollectionAdmission {
|
|
67
72
|
/**
|
|
68
73
|
* The rule of the rows. It gets each row `{ id, value }` of a read and the invocation of the
|
|
69
|
-
* call, and it gives true to admit the row. The change events
|
|
70
|
-
* subscriber, with the invocation of the subscription
|
|
74
|
+
* call, and it gives true to admit the row. The publisher of the change events applies it to the
|
|
75
|
+
* rows of each event for each subscriber, with the invocation of the subscription (roadmap
|
|
76
|
+
* CLI-78). A subscriber gets only the ids of the rows that it can read.
|
|
71
77
|
*/
|
|
72
78
|
admit?: AdmitItem | undefined;
|
|
79
|
+
/**
|
|
80
|
+
* This function tells that the rule `admit` admits each row of a partition for a call (roadmap
|
|
81
|
+
* CLI-78). It gets the invocation and the partition (undefined for the collection without a
|
|
82
|
+
* partition). True: the change events reach the subscriber without the filter of each row, as
|
|
83
|
+
* before CLI-78. The publisher asks at the start of a subscription and at each re-check.
|
|
84
|
+
*
|
|
85
|
+
* - Give true only when `admit` admits each row of the partition. Else the subscriber gets the
|
|
86
|
+
* ids of rows that it cannot read.
|
|
87
|
+
* - An error gives false.
|
|
88
|
+
* - Without it, the publisher examines each row of each event for each subscriber.
|
|
89
|
+
*/
|
|
90
|
+
admitsAll?: ((invocation: Invocation, partition: string | undefined) => boolean | Promise<boolean>) | undefined;
|
|
73
91
|
/** The guard of each `set`, `patch` and `delete`, also of the batches and of `clear`. */
|
|
74
92
|
guard?: CollectionWriteGuard | undefined;
|
|
75
93
|
/**
|
|
@@ -154,8 +172,9 @@ function withoutValue(conflict: VersionConflict): VersionConflict {
|
|
|
154
172
|
* @param storage - The storage of the call: of the collection, or of its partition
|
|
155
173
|
* @param admission - The rules of the collection
|
|
156
174
|
* @param ctx - The context of the call. The rules get its invocation.
|
|
175
|
+
* @param partition - The partition of the call. `admitsAll` gets it.
|
|
157
176
|
*/
|
|
158
|
-
export function admittedStorage<T>(storage: CollectionStorage<T>, admission: CollectionAdmission | undefined, ctx: ProcedureContext): CollectionStorage<T> {
|
|
177
|
+
export function admittedStorage<T>(storage: CollectionStorage<T>, admission: CollectionAdmission | undefined, ctx: ProcedureContext, partition?: string): CollectionStorage<T> {
|
|
159
178
|
const admit = admission?.admit;
|
|
160
179
|
const guard = admission?.guard;
|
|
161
180
|
if (!admit && !guard) return storage;
|
|
@@ -234,6 +253,23 @@ export function admittedStorage<T>(storage: CollectionStorage<T>, admission: Col
|
|
|
234
253
|
return new VersionConflictError(kept, error instanceof Error ? error.message : undefined);
|
|
235
254
|
};
|
|
236
255
|
|
|
256
|
+
/** This function tells if `admitsAll` admits the caller. An error gives false. */
|
|
257
|
+
const readsAll = async (): Promise<boolean> => {
|
|
258
|
+
if (!admission?.admitsAll || invocation === undefined) return false;
|
|
259
|
+
try {
|
|
260
|
+
return (await admission.admitsAll(invocation, partition)) === true;
|
|
261
|
+
} catch {
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
/** The full answer of `changes`: each admitted row, at a version that the rows include. */
|
|
266
|
+
const fullAnswer = async (): Promise<StorageChanges<T>> => {
|
|
267
|
+
const all = await storage.getChangesSince!(undefined);
|
|
268
|
+
if (all.full) return { version: all.version, full: true, changed: await admitted(all.changed), deleted: [] };
|
|
269
|
+
// A log that does not give all rows: the rows, read after the version, include that version
|
|
270
|
+
return { version: all.version, full: true, changed: await entries(), deleted: [] };
|
|
271
|
+
};
|
|
272
|
+
|
|
237
273
|
const view: CollectionStorage<T> = {
|
|
238
274
|
async get(id) {
|
|
239
275
|
const value = await storage.get(id);
|
|
@@ -322,10 +358,15 @@ export function admittedStorage<T>(storage: CollectionStorage<T>, admission: Col
|
|
|
322
358
|
if (!admit) return changes;
|
|
323
359
|
const changed = await admitted(changes.changed);
|
|
324
360
|
if (changes.full) return { ...changes, changed };
|
|
325
|
-
// A delta: the copy of the caller drops each refused row
|
|
326
361
|
const kept = new Set(changed.map(([id]) => id));
|
|
327
362
|
const refused = changes.changed.filter(([id]) => !kept.has(id)).map(([id]) => id);
|
|
328
|
-
|
|
363
|
+
// A delta of admitted rows only gives no id that the caller cannot read
|
|
364
|
+
if (refused.length === 0 && changes.deleted.length === 0) return { ...changes, changed };
|
|
365
|
+
// A caller that reads each row of the partition gets the delta as before (roadmap CLI-78)
|
|
366
|
+
if (await readsAll()) return { ...changes, changed, deleted: [...changes.deleted, ...refused] };
|
|
367
|
+
// The view does not know if the caller read a refused or a deleted row before. A full answer
|
|
368
|
+
// gives no other id, and the copy of the caller drops each row that it cannot read.
|
|
369
|
+
return fullAnswer();
|
|
329
370
|
};
|
|
330
371
|
}
|
|
331
372
|
if (storage.query) {
|
|
@@ -22,12 +22,18 @@
|
|
|
22
22
|
* listens to the topic, the procedures publish nothing, and the chain stops.
|
|
23
23
|
* - The publishes of one storage and one topic run one at a time, in the order of the writes. A
|
|
24
24
|
* write resolves after its event is on the bus. A failed publish never fails the write.
|
|
25
|
+
* - With the rule `admission.admit`, the publisher filters the ids of each event for each
|
|
26
|
+
* subscriber (roadmap CLI-78, `hint-filter.ts`). The event carries the rows after the changes,
|
|
27
|
+
* from the change log. While a subscriber has a filter, each write first reads the rows before
|
|
28
|
+
* it (`getBatch`). Thus a row that leaves the rule reaches that subscriber as a delete.
|
|
25
29
|
*/
|
|
26
30
|
|
|
27
31
|
import type { EventBus } from "../../events/types.js";
|
|
28
32
|
import { withRows } from "../../events/rows.js";
|
|
29
33
|
import type { AdmitItem } from "../invoke.js";
|
|
30
34
|
import type { StorageChanges } from "./storage.js";
|
|
35
|
+
import type { CollectionAdmission } from "./admission.js";
|
|
36
|
+
import { hasFilteredSubscriber, withHintImages, type HintRowState } from "./hint-filter.js";
|
|
31
37
|
import type { RepositoryProvider } from "../types.js";
|
|
32
38
|
import { collectionTopicTarget } from "./topic.js";
|
|
33
39
|
|
|
@@ -52,7 +58,10 @@ export interface CollectionChangeEvent {
|
|
|
52
58
|
* Without the option, the event has no values: read them with `changes`.
|
|
53
59
|
*/
|
|
54
60
|
readonly values?: ReadonlyArray<{ readonly id: string; readonly value: unknown }> | undefined;
|
|
55
|
-
/**
|
|
61
|
+
/**
|
|
62
|
+
* With the option `values`: the changed ids whose values the output filter of the host refused.
|
|
63
|
+
* Read them with `changes`. A row that the rule of the rows refuses is not in the event (roadmap CLI-78).
|
|
64
|
+
*/
|
|
56
65
|
readonly withheld?: readonly string[] | undefined;
|
|
57
66
|
/**
|
|
58
67
|
* The version of each changed item, as `versionOf(id)` of the storage gives it (roadmap CLI-77).
|
|
@@ -90,6 +99,8 @@ export function collectionTopicReads(topic: string, repository?: RepositoryProvi
|
|
|
90
99
|
/** The part of a storage that the change events read. */
|
|
91
100
|
interface ChangeLog {
|
|
92
101
|
getChangesSince?(since?: string): Promise<StorageChanges<unknown>>;
|
|
102
|
+
/** The stored items of the ids: the rows before a write, for the row filter of the subscribers (roadmap CLI-78). */
|
|
103
|
+
getBatch?(ids: string[]): Promise<Map<string, unknown>>;
|
|
93
104
|
}
|
|
94
105
|
|
|
95
106
|
/** A write that ended, and that no event told yet. */
|
|
@@ -99,6 +110,8 @@ interface EndedWrite {
|
|
|
99
110
|
readonly origin: string | undefined;
|
|
100
111
|
/** True when the chain had a version when the write started. */
|
|
101
112
|
readonly anchored: boolean;
|
|
113
|
+
/** The stored items of the ids before the write, when a subscriber has a row filter (roadmap CLI-78). */
|
|
114
|
+
before?: ReadonlyMap<string, unknown> | undefined;
|
|
102
115
|
}
|
|
103
116
|
|
|
104
117
|
/** The chain of the events of one storage and one topic. */
|
|
@@ -152,7 +165,8 @@ function hint(version: string | undefined, origin: string | undefined): Collecti
|
|
|
152
165
|
/**
|
|
153
166
|
* The event of a delta with the values of the changed items. Each row `{ id, value }` passes the
|
|
154
167
|
* output filter of each subscriber and the rule of the rows of the collection, and a refused row
|
|
155
|
-
* goes from `values` to `withheld`.
|
|
168
|
+
* goes from `values` to `withheld`. The row filter of a subscriber (roadmap CLI-78) first takes the
|
|
169
|
+
* rows that the rule refuses out of the event. Thus only the output filter of the host withholds a row.
|
|
156
170
|
*/
|
|
157
171
|
function withValues(event: CollectionChangeEvent, entries: ReadonlyArray<{ readonly id: string; readonly value: unknown }>, admit: AdmitItem | undefined): CollectionChangeEvent {
|
|
158
172
|
return withRows(
|
|
@@ -174,7 +188,8 @@ function withValues(event: CollectionChangeEvent, entries: ReadonlyArray<{ reado
|
|
|
174
188
|
}
|
|
175
189
|
|
|
176
190
|
/** One publish: the event of the writes that ended since the last publish. */
|
|
177
|
-
async function publish(chain: ChangeChain, storage: ChangeLog, bus: EventBus, topic: string, values: boolean,
|
|
191
|
+
async function publish(chain: ChangeChain, storage: ChangeLog, bus: EventBus, topic: string, values: boolean, admission: CollectionAdmission | undefined): Promise<void> {
|
|
192
|
+
const admit = admission?.admit;
|
|
178
193
|
const writes = chain.ended.splice(0);
|
|
179
194
|
// An earlier publish told these writes
|
|
180
195
|
if (writes.length === 0) return;
|
|
@@ -211,7 +226,33 @@ async function publish(chain: ChangeChain, storage: ChangeLog, bus: EventBus, to
|
|
|
211
226
|
deleted: changes.deleted,
|
|
212
227
|
...(origin !== undefined && { origin }),
|
|
213
228
|
};
|
|
214
|
-
|
|
229
|
+
const out = values ? withValues(event, changes.changed.map(([id, value]) => ({ id, value })), admit) : event;
|
|
230
|
+
// The rows of the event for the row filter of each subscriber (roadmap CLI-78)
|
|
231
|
+
bus.emit(topic, admit ? withHintImages(out, { after: afterOf(changes), before: beforeOf(writes), admission: admission! }) : out);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** The rows after the changes, from the change log: a changed id has its item, a deleted id has none. */
|
|
235
|
+
function afterOf(changes: StorageChanges<unknown>): Map<string, HintRowState> {
|
|
236
|
+
const after = new Map<string, HintRowState>();
|
|
237
|
+
for (const id of changes.deleted) after.set(id, { found: false });
|
|
238
|
+
for (const [id, value] of changes.changed) after.set(id, { found: true, value });
|
|
239
|
+
return after;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* The rows before the changes: for each id, the read of the first write that read it. An id
|
|
244
|
+
* without a read is unknown, so the filter of a subscriber makes a hint without ids for it.
|
|
245
|
+
*/
|
|
246
|
+
function beforeOf(writes: readonly EndedWrite[]): Map<string, HintRowState> {
|
|
247
|
+
const before = new Map<string, HintRowState>();
|
|
248
|
+
for (const write of writes) {
|
|
249
|
+
if (!write.before || write.ids === "*") continue;
|
|
250
|
+
for (const id of write.ids) {
|
|
251
|
+
if (before.has(id)) continue;
|
|
252
|
+
before.set(id, write.before.has(id) ? { found: true, value: write.before.get(id) } : { found: false });
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return before;
|
|
215
256
|
}
|
|
216
257
|
|
|
217
258
|
/**
|
|
@@ -227,7 +268,7 @@ async function publish(chain: ChangeChain, storage: ChangeLog, bus: EventBus, to
|
|
|
227
268
|
* @param origin - The origin of the write
|
|
228
269
|
* @param write - The write
|
|
229
270
|
* @param values - True: the events carry the values, for the output filter of each subscriber
|
|
230
|
-
* @param
|
|
271
|
+
* @param admission - The rules of the rows of the collection: each subscriber applies `admit` to the rows of the event (roadmap CLI-41, CLI-78)
|
|
231
272
|
*/
|
|
232
273
|
export async function writeWithChangeEvent<R>(
|
|
233
274
|
bus: EventBus | undefined,
|
|
@@ -237,16 +278,24 @@ export async function writeWithChangeEvent<R>(
|
|
|
237
278
|
origin: string | undefined,
|
|
238
279
|
write: () => Promise<R>,
|
|
239
280
|
values = false,
|
|
240
|
-
|
|
281
|
+
admission?: CollectionAdmission,
|
|
241
282
|
): Promise<R> {
|
|
242
283
|
if (bus === undefined || topic === undefined) return write();
|
|
243
284
|
const chain = chainOf(storage, topic);
|
|
244
285
|
const started: EndedWrite = { ids, origin, anchored: chain.anchor !== undefined };
|
|
286
|
+
// A subscriber with a row filter: the rows before the write tell it which rows left the rule (roadmap CLI-78)
|
|
287
|
+
if (admission?.admit && ids !== "*" && ids.length > 0 && storage.getBatch && hasFilteredSubscriber(bus, topic) && bus.hasSubscribers(topic)) {
|
|
288
|
+
try {
|
|
289
|
+
started.before = await storage.getBatch([...ids]);
|
|
290
|
+
} catch {
|
|
291
|
+
// Without the rows before the write, the filter of a subscriber makes a hint without ids
|
|
292
|
+
}
|
|
293
|
+
}
|
|
245
294
|
try {
|
|
246
295
|
return await write();
|
|
247
296
|
} finally {
|
|
248
297
|
chain.ended.push(started);
|
|
249
|
-
const run = chain.tail.then(() => publish(chain, storage, bus, topic, values,
|
|
298
|
+
const run = chain.tail.then(() => publish(chain, storage, bus, topic, values, admission));
|
|
250
299
|
chain.tail = run.catch(() => undefined);
|
|
251
300
|
await chain.tail;
|
|
252
301
|
}
|