@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.
- package/CHANGELOG.md +107 -0
- package/README.md +115 -11
- package/dist/.tsbuildinfo +1 -1
- package/dist/adapters/http/client/transport.d.ts +4 -0
- package/dist/adapters/http/client/transport.d.ts.map +1 -1
- package/dist/adapters/http/client/transport.js +11 -1
- package/dist/adapters/http/client/transport.js.map +1 -1
- package/dist/adapters/http/client/types.d.ts +19 -0
- package/dist/adapters/http/client/types.d.ts.map +1 -1
- package/dist/adapters/http/server/cors.d.ts +64 -0
- package/dist/adapters/http/server/cors.d.ts.map +1 -0
- package/dist/adapters/http/server/cors.js +113 -0
- package/dist/adapters/http/server/cors.js.map +1 -0
- package/dist/adapters/http/server/handler.d.ts +136 -0
- package/dist/adapters/http/server/handler.d.ts.map +1 -0
- package/dist/adapters/http/server/handler.js +495 -0
- package/dist/adapters/http/server/handler.js.map +1 -0
- package/dist/adapters/http/server/strategies.d.ts +14 -5
- package/dist/adapters/http/server/strategies.d.ts.map +1 -1
- package/dist/adapters/http/server/strategies.js.map +1 -1
- package/dist/adapters/http/server/transport.d.ts +36 -46
- package/dist/adapters/http/server/transport.d.ts.map +1 -1
- package/dist/adapters/http/server/transport.js +58 -433
- package/dist/adapters/http/server/transport.js.map +1 -1
- package/dist/adapters/http/server/types.d.ts +13 -0
- package/dist/adapters/http/server/types.d.ts.map +1 -1
- package/dist/adapters/websocket/client/transport.d.ts +7 -3
- package/dist/adapters/websocket/client/transport.d.ts.map +1 -1
- package/dist/adapters/websocket/client/transport.js +9 -3
- package/dist/adapters/websocket/client/transport.js.map +1 -1
- package/dist/adapters/websocket/server/transport.d.ts +56 -1
- package/dist/adapters/websocket/server/transport.d.ts.map +1 -1
- package/dist/adapters/websocket/server/transport.js +174 -17
- package/dist/adapters/websocket/server/transport.js.map +1 -1
- package/dist/adapters/websocket/server/types.d.ts +10 -3
- package/dist/adapters/websocket/server/types.d.ts.map +1 -1
- package/dist/adapters/websocket/server/upgrade-router.d.ts +28 -0
- package/dist/adapters/websocket/server/upgrade-router.d.ts.map +1 -0
- package/dist/adapters/websocket/server/upgrade-router.js +62 -0
- package/dist/adapters/websocket/server/upgrade-router.js.map +1 -0
- package/dist/adapters/websocket/server/upgrade.d.ts +68 -0
- package/dist/adapters/websocket/server/upgrade.d.ts.map +1 -0
- package/dist/adapters/websocket/server/upgrade.js +49 -0
- package/dist/adapters/websocket/server/upgrade.js.map +1 -0
- package/dist/adapters/websocket/shared/rpc.d.ts +2 -0
- package/dist/adapters/websocket/shared/rpc.d.ts.map +1 -1
- package/dist/adapters/websocket/shared/rpc.js +8 -2
- package/dist/adapters/websocket/shared/rpc.js.map +1 -1
- package/dist/browser.d.ts +14 -6
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +12 -3
- package/dist/browser.js.map +1 -1
- package/dist/client/client.d.ts +43 -0
- package/dist/client/client.d.ts.map +1 -1
- package/dist/client/client.js +69 -1
- package/dist/client/client.js.map +1 -1
- package/dist/client/errors/registry.d.ts.map +1 -1
- package/dist/client/errors/registry.js +1 -0
- package/dist/client/errors/registry.js.map +1 -1
- package/dist/client/middleware/retry.d.ts +4 -0
- package/dist/client/middleware/retry.d.ts.map +1 -1
- package/dist/client/middleware/retry.js +86 -78
- package/dist/client/middleware/retry.js.map +1 -1
- package/dist/events/bus.d.ts +32 -14
- package/dist/events/bus.d.ts.map +1 -1
- package/dist/events/bus.js +119 -65
- package/dist/events/bus.js.map +1 -1
- package/dist/events/index.d.ts +3 -2
- package/dist/events/index.d.ts.map +1 -1
- package/dist/events/index.js.map +1 -1
- package/dist/events/publisher.d.ts +14 -0
- package/dist/events/publisher.d.ts.map +1 -1
- package/dist/events/publisher.js +62 -10
- package/dist/events/publisher.js.map +1 -1
- package/dist/events/remote.d.ts +80 -18
- package/dist/events/remote.d.ts.map +1 -1
- package/dist/events/remote.js +448 -58
- package/dist/events/remote.js.map +1 -1
- package/dist/events/rows.d.ts +31 -0
- package/dist/events/rows.d.ts.map +1 -0
- package/dist/events/rows.js +39 -0
- package/dist/events/rows.js.map +1 -0
- package/dist/events/types.d.ts +13 -2
- package/dist/events/types.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/node.d.ts +14 -0
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +9 -0
- package/dist/node.js.map +1 -1
- package/dist/procedures/caller.d.ts +33 -0
- package/dist/procedures/caller.d.ts.map +1 -0
- package/dist/procedures/caller.js +27 -0
- package/dist/procedures/caller.js.map +1 -0
- package/dist/procedures/capability.d.ts +6 -0
- package/dist/procedures/capability.d.ts.map +1 -1
- package/dist/procedures/capability.js +25 -0
- package/dist/procedures/capability.js.map +1 -1
- package/dist/procedures/collection/changes.d.ts +94 -0
- package/dist/procedures/collection/changes.d.ts.map +1 -0
- package/dist/procedures/collection/changes.js +182 -0
- package/dist/procedures/collection/changes.js.map +1 -0
- package/dist/procedures/collection/index.d.ts +8 -3
- package/dist/procedures/collection/index.d.ts.map +1 -1
- package/dist/procedures/collection/index.js +6 -1
- package/dist/procedures/collection/index.js.map +1 -1
- package/dist/procedures/collection/procedures.d.ts +39 -2
- package/dist/procedures/collection/procedures.d.ts.map +1 -1
- package/dist/procedures/collection/procedures.js +131 -8
- package/dist/procedures/collection/procedures.js.map +1 -1
- package/dist/procedures/collection/query.d.ts +246 -0
- package/dist/procedures/collection/query.d.ts.map +1 -0
- package/dist/procedures/collection/query.js +854 -0
- package/dist/procedures/collection/query.js.map +1 -0
- package/dist/procedures/collection/schemas.d.ts +59 -0
- package/dist/procedures/collection/schemas.d.ts.map +1 -1
- package/dist/procedures/collection/schemas.js +172 -6
- package/dist/procedures/collection/schemas.js.map +1 -1
- package/dist/procedures/collection/storage.d.ts +64 -6
- package/dist/procedures/collection/storage.d.ts.map +1 -1
- package/dist/procedures/collection/storage.js +23 -0
- package/dist/procedures/collection/storage.js.map +1 -1
- package/dist/procedures/elicit.d.ts +190 -0
- package/dist/procedures/elicit.d.ts.map +1 -0
- package/dist/procedures/elicit.js +127 -0
- package/dist/procedures/elicit.js.map +1 -0
- package/dist/procedures/hints.d.ts +43 -0
- package/dist/procedures/hints.d.ts.map +1 -0
- package/dist/procedures/hints.js +74 -0
- package/dist/procedures/hints.js.map +1 -0
- package/dist/procedures/index.d.ts +12 -3
- package/dist/procedures/index.d.ts.map +1 -1
- package/dist/procedures/index.js +7 -1
- package/dist/procedures/index.js.map +1 -1
- package/dist/procedures/invoke.d.ts +20 -0
- package/dist/procedures/invoke.d.ts.map +1 -1
- package/dist/procedures/invoke.js +16 -0
- package/dist/procedures/invoke.js.map +1 -1
- package/dist/procedures/progress.d.ts +24 -0
- package/dist/procedures/progress.d.ts.map +1 -0
- package/dist/procedures/progress.js +11 -0
- package/dist/procedures/progress.js.map +1 -0
- package/dist/procedures/registry.d.ts.map +1 -1
- package/dist/procedures/registry.js +5 -0
- package/dist/procedures/registry.js.map +1 -1
- package/dist/procedures/types.d.ts +92 -0
- package/dist/procedures/types.d.ts.map +1 -1
- package/dist/procedures/types.js.map +1 -1
- package/dist/procedures/untrusted.d.ts +28 -0
- package/dist/procedures/untrusted.d.ts.map +1 -0
- package/dist/procedures/untrusted.js +32 -0
- package/dist/procedures/untrusted.js.map +1 -0
- package/dist/protocol.d.ts +1 -1
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +1 -0
- package/dist/protocol.js.map +1 -1
- package/dist/server/browser-guard.d.ts +12 -2
- package/dist/server/browser-guard.d.ts.map +1 -1
- package/dist/server/browser-guard.js +43 -6
- package/dist/server/browser-guard.js.map +1 -1
- package/dist/server/host-policy.d.ts +10 -1
- package/dist/server/host-policy.d.ts.map +1 -1
- package/dist/server/host-policy.js +5 -0
- package/dist/server/host-policy.js.map +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js.map +1 -1
- package/dist/server/procedure-server.d.ts +75 -9
- package/dist/server/procedure-server.d.ts.map +1 -1
- package/dist/server/procedure-server.js +151 -17
- package/dist/server/procedure-server.js.map +1 -1
- package/dist/server/request-auth.d.ts +51 -4
- package/dist/server/request-auth.d.ts.map +1 -1
- package/dist/server/request-auth.js +116 -5
- package/dist/server/request-auth.js.map +1 -1
- package/dist/server/types.d.ts +73 -5
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/types.js.map +1 -1
- package/dist/timers.d.ts +5 -0
- package/dist/timers.d.ts.map +1 -1
- package/dist/timers.js +3 -1
- package/dist/timers.js.map +1 -1
- package/package.json +5 -1
- package/src/adapters/http/client/transport.ts +11 -1
- package/src/adapters/http/client/types.ts +22 -0
- package/src/adapters/http/server/cors.ts +144 -0
- package/src/adapters/http/server/handler.ts +624 -0
- package/src/adapters/http/server/strategies.ts +16 -6
- package/src/adapters/http/server/transport.ts +66 -479
- package/src/adapters/http/server/types.ts +15 -0
- package/src/adapters/websocket/client/transport.ts +9 -3
- package/src/adapters/websocket/server/transport.ts +189 -18
- package/src/adapters/websocket/server/types.ts +11 -3
- package/src/adapters/websocket/server/upgrade-router.ts +77 -0
- package/src/adapters/websocket/server/upgrade.ts +88 -0
- package/src/adapters/websocket/shared/rpc.ts +9 -2
- package/src/browser.ts +85 -2
- package/src/client/client.ts +83 -1
- package/src/client/errors/registry.ts +1 -0
- package/src/client/middleware/retry.ts +89 -78
- package/src/events/bus.ts +141 -73
- package/src/events/index.ts +3 -0
- package/src/events/publisher.ts +71 -10
- package/src/events/remote.ts +575 -73
- package/src/events/rows.ts +48 -0
- package/src/events/types.ts +15 -2
- package/src/index.ts +1 -1
- package/src/node.ts +21 -0
- package/src/procedures/caller.ts +45 -0
- package/src/procedures/capability.ts +21 -0
- package/src/procedures/collection/changes.ts +245 -0
- package/src/procedures/collection/index.ts +56 -1
- package/src/procedures/collection/procedures.ts +185 -7
- package/src/procedures/collection/query.ts +1009 -0
- package/src/procedures/collection/schemas.ts +233 -7
- package/src/procedures/collection/storage.ts +84 -6
- package/src/procedures/elicit.ts +320 -0
- package/src/procedures/hints.ts +91 -0
- package/src/procedures/index.ts +82 -2
- package/src/procedures/invoke.ts +31 -0
- package/src/procedures/progress.ts +25 -0
- package/src/procedures/registry.ts +4 -0
- package/src/procedures/types.ts +102 -0
- package/src/procedures/untrusted.ts +50 -0
- package/src/protocol.ts +3 -1
- package/src/server/browser-guard.ts +48 -6
- package/src/server/host-policy.ts +13 -1
- package/src/server/index.ts +3 -0
- package/src/server/procedure-server.ts +185 -20
- package/src/server/request-auth.ts +128 -6
- package/src/server/types.ts +82 -5
- package/src/timers.ts +8 -1
package/src/events/bus.ts
CHANGED
|
@@ -11,8 +11,11 @@ import type {
|
|
|
11
11
|
EventHandler,
|
|
12
12
|
Unsubscribe,
|
|
13
13
|
EventBusOptions,
|
|
14
|
+
EventListenOptions,
|
|
14
15
|
EventWaitOptions,
|
|
15
16
|
} from "./types.js";
|
|
17
|
+
import { realClock, type Clock } from "../clock.js";
|
|
18
|
+
import { toMs, type Duration } from "../duration.js";
|
|
16
19
|
|
|
17
20
|
/** The error of a wait that its signal ended. */
|
|
18
21
|
function abortedError(channel: string): Error {
|
|
@@ -21,6 +24,16 @@ function abortedError(channel: string): Error {
|
|
|
21
24
|
return error;
|
|
22
25
|
}
|
|
23
26
|
|
|
27
|
+
/**
|
|
28
|
+
* The error of a wait that `clear()` ended. Its name is `AbortError`, so code that ignores a
|
|
29
|
+
* cancelled wait ignores this one too.
|
|
30
|
+
*/
|
|
31
|
+
function clearedError(channel: string): Error {
|
|
32
|
+
const error = new Error(`The wait on channel "${channel}" ended, because clear() removed the channel`);
|
|
33
|
+
error.name = "AbortError";
|
|
34
|
+
return error;
|
|
35
|
+
}
|
|
36
|
+
|
|
24
37
|
// =============================================================================
|
|
25
38
|
// Event Bus Implementation
|
|
26
39
|
// =============================================================================
|
|
@@ -32,8 +45,11 @@ function abortedError(channel: string): Error {
|
|
|
32
45
|
export class DefaultEventBus implements EventBus {
|
|
33
46
|
private readonly subscribers = new Map<string, Set<EventHandler<unknown>>>();
|
|
34
47
|
private readonly buffer = new Map<string, unknown[]>();
|
|
35
|
-
/**
|
|
36
|
-
|
|
48
|
+
/**
|
|
49
|
+
* For each channel, the functions that `clear()` runs: the `onClear` of each handler of `on()`
|
|
50
|
+
* that has one. Each open stream has one too, so `clear()` ends it.
|
|
51
|
+
*/
|
|
52
|
+
private readonly clearHandlers = new Map<string, Set<() => void>>();
|
|
37
53
|
private readonly options: Required<EventBusOptions>;
|
|
38
54
|
|
|
39
55
|
constructor(options: EventBusOptions = {}) {
|
|
@@ -85,9 +101,10 @@ export class DefaultEventBus implements EventBus {
|
|
|
85
101
|
}
|
|
86
102
|
|
|
87
103
|
/**
|
|
88
|
-
* Subscribe to events on a channel.
|
|
104
|
+
* Subscribe to events on a channel. The `onClear` of the options runs one time when `clear()`
|
|
105
|
+
* or `clearAll()` removes the handler, not at an unsubscribe.
|
|
89
106
|
*/
|
|
90
|
-
on<T>(channel: string, handler: EventHandler<T
|
|
107
|
+
on<T>(channel: string, handler: EventHandler<T>, options: EventListenOptions = {}): Unsubscribe {
|
|
91
108
|
if (this.options.debug) {
|
|
92
109
|
console.log(`[EventBus] subscribe "${channel}"`);
|
|
93
110
|
}
|
|
@@ -100,6 +117,30 @@ export class DefaultEventBus implements EventBus {
|
|
|
100
117
|
|
|
101
118
|
channelSubs.add(handler as EventHandler<unknown>);
|
|
102
119
|
|
|
120
|
+
// The unsubscribe function works once, and removes only the set it added to. (Before, a
|
|
121
|
+
// repeated call of an old unsubscribe removed the set of a newer subscriber: deep dive CORE-15.)
|
|
122
|
+
// A clear() also ends the handler: then its unsubscribe does nothing.
|
|
123
|
+
let subscribed = true;
|
|
124
|
+
const { onClear } = options;
|
|
125
|
+
let cleared: (() => void) | undefined;
|
|
126
|
+
if (onClear) {
|
|
127
|
+
cleared = () => {
|
|
128
|
+
if (!subscribed) return;
|
|
129
|
+
subscribed = false;
|
|
130
|
+
try {
|
|
131
|
+
onClear();
|
|
132
|
+
} catch (error) {
|
|
133
|
+
this.options.onError(error instanceof Error ? error : new Error(String(error)), channel);
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
let handlers = this.clearHandlers.get(channel);
|
|
137
|
+
if (!handlers) {
|
|
138
|
+
handlers = new Set();
|
|
139
|
+
this.clearHandlers.set(channel, handlers);
|
|
140
|
+
}
|
|
141
|
+
handlers.add(cleared);
|
|
142
|
+
}
|
|
143
|
+
|
|
103
144
|
// Replay buffered events if any
|
|
104
145
|
const buffered = this.buffer.get(channel);
|
|
105
146
|
if (buffered && buffered.length > 0) {
|
|
@@ -115,9 +156,6 @@ export class DefaultEventBus implements EventBus {
|
|
|
115
156
|
}
|
|
116
157
|
}
|
|
117
158
|
|
|
118
|
-
// The unsubscribe function works once, and removes only the set it added to. (Before, a
|
|
119
|
-
// repeated call of an old unsubscribe removed the set of a newer subscriber: deep dive CORE-15.)
|
|
120
|
-
let subscribed = true;
|
|
121
159
|
return () => {
|
|
122
160
|
if (!subscribed) return;
|
|
123
161
|
subscribed = false;
|
|
@@ -130,9 +168,23 @@ export class DefaultEventBus implements EventBus {
|
|
|
130
168
|
if (channelSubs.size === 0 && this.subscribers.get(channel) === channelSubs) {
|
|
131
169
|
this.subscribers.delete(channel);
|
|
132
170
|
}
|
|
171
|
+
if (cleared) {
|
|
172
|
+
const handlers = this.clearHandlers.get(channel);
|
|
173
|
+
if (handlers?.delete(cleared) && handlers.size === 0) this.clearHandlers.delete(channel);
|
|
174
|
+
}
|
|
133
175
|
};
|
|
134
176
|
}
|
|
135
177
|
|
|
178
|
+
/** This function runs the clear handlers of the channels, after it removed them from the bus. */
|
|
179
|
+
private runClearHandlers(channels: readonly string[]): void {
|
|
180
|
+
const handlers: Array<() => void> = [];
|
|
181
|
+
for (const channel of channels) {
|
|
182
|
+
handlers.push(...(this.clearHandlers.get(channel) ?? []));
|
|
183
|
+
this.clearHandlers.delete(channel);
|
|
184
|
+
}
|
|
185
|
+
for (const cleared of handlers) cleared();
|
|
186
|
+
}
|
|
187
|
+
|
|
136
188
|
/**
|
|
137
189
|
* Subscribe to a single event on a channel. On a buffered channel, the first buffered event
|
|
138
190
|
* resolves the promise. When the signal aborts first, the promise rejects with an `AbortError`.
|
|
@@ -153,14 +205,26 @@ export class DefaultEventBus implements EventBus {
|
|
|
153
205
|
reject(abortedError(channel));
|
|
154
206
|
};
|
|
155
207
|
// on() can replay a buffered event before it returns: the handler must not need
|
|
156
|
-
// `unsubscribe` yet (before, it threw a ReferenceError, and the promise never settled)
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
208
|
+
// `unsubscribe` yet (before, it threw a ReferenceError, and the promise never settled).
|
|
209
|
+
// clear() rejects the wait: before, the promise never settled.
|
|
210
|
+
unsubscribe = this.on<T>(
|
|
211
|
+
channel,
|
|
212
|
+
(data) => {
|
|
213
|
+
if (settled) return;
|
|
214
|
+
settled = true;
|
|
215
|
+
unsubscribe?.();
|
|
216
|
+
signal?.removeEventListener("abort", onAbort);
|
|
217
|
+
resolve(data);
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
onClear: () => {
|
|
221
|
+
if (settled) return;
|
|
222
|
+
settled = true;
|
|
223
|
+
signal?.removeEventListener("abort", onAbort);
|
|
224
|
+
reject(clearedError(channel));
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
);
|
|
164
228
|
if (settled) {
|
|
165
229
|
unsubscribe();
|
|
166
230
|
return;
|
|
@@ -182,23 +246,7 @@ export class DefaultEventBus implements EventBus {
|
|
|
182
246
|
const queue: T[] = [];
|
|
183
247
|
let resolve: ((value: IteratorResult<T>) => void) | null = null;
|
|
184
248
|
let done = signal?.aborted ?? false;
|
|
185
|
-
|
|
186
|
-
// Subscribe to channel
|
|
187
|
-
const unsubscribe = done
|
|
188
|
-
? () => {}
|
|
189
|
-
: self.on<T>(channel, (data) => {
|
|
190
|
-
if (done) return;
|
|
191
|
-
|
|
192
|
-
if (resolve) {
|
|
193
|
-
// Someone is waiting, resolve immediately
|
|
194
|
-
const r = resolve;
|
|
195
|
-
resolve = null;
|
|
196
|
-
r({ value: data, done: false });
|
|
197
|
-
} else {
|
|
198
|
-
// Queue for later
|
|
199
|
-
queue.push(data);
|
|
200
|
-
}
|
|
201
|
-
});
|
|
249
|
+
let unsubscribe: Unsubscribe = () => {};
|
|
202
250
|
|
|
203
251
|
// End the stream: no more events, and a waiting reader gets `done`
|
|
204
252
|
const end = (): void => {
|
|
@@ -206,19 +254,31 @@ export class DefaultEventBus implements EventBus {
|
|
|
206
254
|
done = true;
|
|
207
255
|
unsubscribe();
|
|
208
256
|
signal?.removeEventListener("abort", end);
|
|
209
|
-
self.streamEnders.get(channel)?.delete(end);
|
|
210
257
|
const r = resolve;
|
|
211
258
|
resolve = null;
|
|
212
259
|
r?.({ value: undefined, done: true });
|
|
213
260
|
};
|
|
261
|
+
|
|
262
|
+
// Subscribe to channel. A clear() of the channel ends the stream.
|
|
214
263
|
if (!done) {
|
|
264
|
+
unsubscribe = self.on<T>(
|
|
265
|
+
channel,
|
|
266
|
+
(data) => {
|
|
267
|
+
if (done) return;
|
|
268
|
+
|
|
269
|
+
if (resolve) {
|
|
270
|
+
// Someone is waiting, resolve immediately
|
|
271
|
+
const r = resolve;
|
|
272
|
+
resolve = null;
|
|
273
|
+
r({ value: data, done: false });
|
|
274
|
+
} else {
|
|
275
|
+
// Queue for later
|
|
276
|
+
queue.push(data);
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
{ onClear: end },
|
|
280
|
+
);
|
|
215
281
|
signal?.addEventListener("abort", end, { once: true });
|
|
216
|
-
let enders = self.streamEnders.get(channel);
|
|
217
|
-
if (!enders) {
|
|
218
|
-
enders = new Set();
|
|
219
|
-
self.streamEnders.set(channel, enders);
|
|
220
|
-
}
|
|
221
|
-
enders.add(end);
|
|
222
282
|
}
|
|
223
283
|
|
|
224
284
|
return {
|
|
@@ -276,7 +336,8 @@ export class DefaultEventBus implements EventBus {
|
|
|
276
336
|
}
|
|
277
337
|
|
|
278
338
|
/**
|
|
279
|
-
* Remove all subscribers from a channel.
|
|
339
|
+
* Remove all subscribers from a channel. The `onClear` of each removed handler runs, and each
|
|
340
|
+
* stream of the channel ends.
|
|
280
341
|
*/
|
|
281
342
|
clear(channel: string): void {
|
|
282
343
|
if (this.options.debug) {
|
|
@@ -284,13 +345,14 @@ export class DefaultEventBus implements EventBus {
|
|
|
284
345
|
}
|
|
285
346
|
this.subscribers.delete(channel);
|
|
286
347
|
this.buffer.delete(channel);
|
|
287
|
-
// The streams of the channel end: before, their readers waited forever (deep dive CORE-15)
|
|
288
|
-
|
|
289
|
-
this.
|
|
348
|
+
// The streams of the channel end: before, their readers waited forever (deep dive CORE-15).
|
|
349
|
+
// The remote subscriptions end with notice too (EVENTS.md).
|
|
350
|
+
this.runClearHandlers([channel]);
|
|
290
351
|
}
|
|
291
352
|
|
|
292
353
|
/**
|
|
293
|
-
* Remove all subscribers from all channels.
|
|
354
|
+
* Remove all subscribers from all channels. The `onClear` of each removed handler runs, and
|
|
355
|
+
* each stream ends.
|
|
294
356
|
*/
|
|
295
357
|
clearAll(): void {
|
|
296
358
|
if (this.options.debug) {
|
|
@@ -298,10 +360,7 @@ export class DefaultEventBus implements EventBus {
|
|
|
298
360
|
}
|
|
299
361
|
this.subscribers.clear();
|
|
300
362
|
this.buffer.clear();
|
|
301
|
-
|
|
302
|
-
for (const end of [...enders]) end();
|
|
303
|
-
}
|
|
304
|
-
this.streamEnders.clear();
|
|
363
|
+
this.runClearHandlers([...this.clearHandlers.keys()]);
|
|
305
364
|
}
|
|
306
365
|
}
|
|
307
366
|
|
|
@@ -398,7 +457,7 @@ export function withEventBusSignal(bus: EventBus, signal: AbortSignal): EventBus
|
|
|
398
457
|
const waitOptions = (options?: EventWaitOptions): EventWaitOptions => ({ signal: options?.signal ?? signal });
|
|
399
458
|
return {
|
|
400
459
|
emit: (channel, data) => bus.emit(channel, data),
|
|
401
|
-
on: (channel, handler) => bus.on(channel, handler),
|
|
460
|
+
on: (channel, handler, options) => bus.on(channel, handler, options),
|
|
402
461
|
once: (channel, options) => bus.once(channel, waitOptions(options)),
|
|
403
462
|
stream: (channel, options) => bus.stream(channel, waitOptions(options)),
|
|
404
463
|
subscriberCount: (channel) => bus.subscriberCount(channel),
|
|
@@ -435,8 +494,8 @@ export function createChannelPrefix(bus: EventBus, prefix: string) {
|
|
|
435
494
|
bus.emit(prefixed(channel), data);
|
|
436
495
|
},
|
|
437
496
|
|
|
438
|
-
on<T>(channel: string, handler: EventHandler<T
|
|
439
|
-
return bus.on(prefixed(channel), handler);
|
|
497
|
+
on<T>(channel: string, handler: EventHandler<T>, options?: EventListenOptions): Unsubscribe {
|
|
498
|
+
return bus.on(prefixed(channel), handler, options);
|
|
440
499
|
},
|
|
441
500
|
|
|
442
501
|
once<T>(channel: string, options?: EventWaitOptions): Promise<T> {
|
|
@@ -525,68 +584,77 @@ export function mapChannel<TIn, TOut>(
|
|
|
525
584
|
});
|
|
526
585
|
}
|
|
527
586
|
|
|
587
|
+
/** Options of `debounceChannel()` and `throttleChannel()`. */
|
|
588
|
+
export interface ChannelTimingOptions {
|
|
589
|
+
/** The clock of the timers. The default is the clock of the platform. A test gives a fake clock. */
|
|
590
|
+
clock?: Clock;
|
|
591
|
+
}
|
|
592
|
+
|
|
528
593
|
/**
|
|
529
|
-
*
|
|
594
|
+
* This function sends the last event of `source` to `target` after `delay` without a new event
|
|
595
|
+
* (a debounce). The delay is a `Duration`, such as `ms(300)`. A plain number throws a `TypeError`.
|
|
530
596
|
*
|
|
531
597
|
* @param bus - Event bus
|
|
532
598
|
* @param source - Source channel
|
|
533
599
|
* @param target - Target channel
|
|
534
|
-
* @param delay -
|
|
600
|
+
* @param delay - The time without an event before the send
|
|
535
601
|
* @returns Unsubscribe function
|
|
536
602
|
*/
|
|
537
603
|
export function debounceChannel<T>(
|
|
538
604
|
bus: EventBus,
|
|
539
605
|
source: string,
|
|
540
606
|
target: string,
|
|
541
|
-
delay:
|
|
607
|
+
delay: Duration,
|
|
608
|
+
options: ChannelTimingOptions = {},
|
|
542
609
|
): Unsubscribe {
|
|
543
|
-
|
|
610
|
+
const delayMs = toMs(delay, "debounceChannel delay");
|
|
611
|
+
const clock = options.clock ?? realClock;
|
|
612
|
+
let cancel: (() => void) | null = null;
|
|
544
613
|
let lastData: T | undefined;
|
|
545
614
|
|
|
546
615
|
const unsub = bus.on<T>(source, (data) => {
|
|
547
616
|
lastData = data;
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
timer = setTimeout(() => {
|
|
617
|
+
cancel?.();
|
|
618
|
+
cancel = clock.after(delayMs, () => {
|
|
619
|
+
cancel = null;
|
|
554
620
|
if (lastData !== undefined) {
|
|
555
621
|
bus.emit(target, lastData);
|
|
556
622
|
}
|
|
557
|
-
|
|
558
|
-
}, delay);
|
|
623
|
+
});
|
|
559
624
|
});
|
|
560
625
|
|
|
561
626
|
return () => {
|
|
562
627
|
unsub();
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
}
|
|
628
|
+
cancel?.();
|
|
629
|
+
cancel = null;
|
|
566
630
|
};
|
|
567
631
|
}
|
|
568
632
|
|
|
569
633
|
/**
|
|
570
|
-
*
|
|
634
|
+
* This function sends an event of `source` to `target` at most one time in each `interval` (a
|
|
635
|
+
* throttle). It drops the events between. The interval is a `Duration`, such as `seconds(1)`. A
|
|
636
|
+
* plain number throws a `TypeError`.
|
|
571
637
|
*
|
|
572
638
|
* @param bus - Event bus
|
|
573
639
|
* @param source - Source channel
|
|
574
640
|
* @param target - Target channel
|
|
575
|
-
* @param interval -
|
|
641
|
+
* @param interval - The shortest time between two sends
|
|
576
642
|
* @returns Unsubscribe function
|
|
577
643
|
*/
|
|
578
644
|
export function throttleChannel<T>(
|
|
579
645
|
bus: EventBus,
|
|
580
646
|
source: string,
|
|
581
647
|
target: string,
|
|
582
|
-
interval:
|
|
648
|
+
interval: Duration,
|
|
649
|
+
options: ChannelTimingOptions = {},
|
|
583
650
|
): Unsubscribe {
|
|
584
|
-
|
|
651
|
+
const intervalMs = toMs(interval, "throttleChannel interval");
|
|
652
|
+
const clock = options.clock ?? realClock;
|
|
653
|
+
let lastEmit: number | undefined;
|
|
585
654
|
|
|
586
655
|
return bus.on<T>(source, (data) => {
|
|
587
|
-
const now =
|
|
588
|
-
|
|
589
|
-
if (now - lastEmit >= interval) {
|
|
656
|
+
const now = clock.now();
|
|
657
|
+
if (lastEmit === undefined || now - lastEmit >= intervalMs) {
|
|
590
658
|
lastEmit = now;
|
|
591
659
|
bus.emit(target, data);
|
|
592
660
|
}
|
package/src/events/index.ts
CHANGED
|
@@ -40,6 +40,7 @@ export type {
|
|
|
40
40
|
ProcedureChannels,
|
|
41
41
|
DefaultChannels,
|
|
42
42
|
EventBusOptions,
|
|
43
|
+
EventListenOptions,
|
|
43
44
|
EventWaitOptions,
|
|
44
45
|
} from "./types.js";
|
|
45
46
|
|
|
@@ -65,6 +66,7 @@ export type {
|
|
|
65
66
|
RemoteEvent,
|
|
66
67
|
RemoteEventHandler,
|
|
67
68
|
RemoteEventsOptions,
|
|
69
|
+
ResubscribeBackoff,
|
|
68
70
|
SubscribeInput,
|
|
69
71
|
SubscribeOptions,
|
|
70
72
|
Subscription,
|
|
@@ -81,3 +83,4 @@ export {
|
|
|
81
83
|
debounceChannel,
|
|
82
84
|
throttleChannel,
|
|
83
85
|
} from "./bus.js";
|
|
86
|
+
export type { ChannelTimingOptions } from "./bus.js";
|
package/src/events/publisher.ts
CHANGED
|
@@ -8,11 +8,17 @@
|
|
|
8
8
|
*
|
|
9
9
|
* - The topic rule (`topics`) is the expose rule of the topics. A topic outside it gives
|
|
10
10
|
* `TOPIC_NOT_FOUND`.
|
|
11
|
+
* - `reads` gives the resources that a subscriber of a topic reads. The capability of the call
|
|
12
|
+
* must permit them, as for a procedure that reads them (roadmap CLI-15). By default, a topic
|
|
13
|
+
* `collections.<name>` reads `collection:<name>`.
|
|
11
14
|
* - `allow` examines each topic with the capability of the call. A refusal gives `FORBIDDEN` with
|
|
12
15
|
* the reason `topic-not-allowed`.
|
|
13
16
|
* - The procedure declares that it reads the resource `events`, so the capability of the host
|
|
14
17
|
* applies to it as to each procedure.
|
|
15
18
|
*
|
|
19
|
+
* - A value with rows (`withRows()`) reaches each subscriber through the output filter of its host
|
|
20
|
+
* (`ctx.admit`): each row passes the filter, and the subscriber gets only the admitted rows.
|
|
21
|
+
*
|
|
16
22
|
* The delivery is at most once. The queue of a subscription holds at most `buffer` events. When
|
|
17
23
|
* it is full, the publisher drops the oldest event, and the next event tells the number of lost
|
|
18
24
|
* events (`dropped`). The bus never waits for a subscriber.
|
|
@@ -21,10 +27,13 @@
|
|
|
21
27
|
import { z } from "zod";
|
|
22
28
|
import { createProcedure } from "../procedures/define.js";
|
|
23
29
|
import { InvocationError } from "../procedures/invoke.js";
|
|
30
|
+
import { capabilityRefusal } from "../procedures/capability.js";
|
|
31
|
+
import { collectionTopicReads } from "../procedures/collection/changes.js";
|
|
24
32
|
import type { AnyProcedure, Capability, ProcedureContext } from "../procedures/types.js";
|
|
25
33
|
import type { EventBus, Unsubscribe } from "./types.js";
|
|
26
34
|
import { getGlobalEventBus } from "./bus.js";
|
|
27
35
|
import { EVENTS_SUBSCRIBE_PATH, type EventItem, type SubscribeInput } from "./remote.js";
|
|
36
|
+
import { admittedValue, rowsOf } from "./rows.js";
|
|
28
37
|
|
|
29
38
|
/** The number of topics that one subscription can name. */
|
|
30
39
|
export const MAX_SUBSCRIPTION_TOPICS = 64;
|
|
@@ -64,6 +73,18 @@ export interface EventPublisherOptions {
|
|
|
64
73
|
* @default 1024
|
|
65
74
|
*/
|
|
66
75
|
buffer?: number | undefined;
|
|
76
|
+
/**
|
|
77
|
+
* The resources that a subscriber of a topic reads (roadmap CLI-15). The publisher refuses a
|
|
78
|
+
* topic whose reads the capability of the call does not permit, as for a procedure that reads
|
|
79
|
+
* them: `FORBIDDEN` with the reason `effects-exceeded`.
|
|
80
|
+
* @default `collectionTopicReads`: a topic `collections.<name>` reads `collection:<name>`
|
|
81
|
+
*/
|
|
82
|
+
reads?: ((topic: string) => readonly string[]) | undefined;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** A procedure that reads resources, for the admission of a topic under a capability. */
|
|
86
|
+
function topicReader(reads: readonly string[]): AnyProcedure {
|
|
87
|
+
return { path: [...EVENTS_SUBSCRIBE_PATH], metadata: { effects: { reads: [...reads], writes: [] } } } as unknown as AnyProcedure;
|
|
67
88
|
}
|
|
68
89
|
|
|
69
90
|
/** This function gives the test of a topic rule. */
|
|
@@ -92,6 +113,14 @@ function stoppedError(): Error {
|
|
|
92
113
|
return Object.assign(new Error("The publisher of the events stops"), { code: "UNAVAILABLE", retryable: true });
|
|
93
114
|
}
|
|
94
115
|
|
|
116
|
+
/**
|
|
117
|
+
* The error that ends a subscription when `clear()` or `clearAll()` of the bus removes the
|
|
118
|
+
* listener of one of its topics. The subscriber does not resubscribe: the host removed the topic.
|
|
119
|
+
*/
|
|
120
|
+
function clearedError(topic: string): Error {
|
|
121
|
+
return Object.assign(new Error(`The host cleared the topic "${topic}", so the subscription ends`), { code: "TOPIC_CLEARED", retryable: false });
|
|
122
|
+
}
|
|
123
|
+
|
|
95
124
|
const topicSchema = z.string().min(1).max(MAX_TOPIC_LENGTH);
|
|
96
125
|
|
|
97
126
|
const subscribeInput = z.object({
|
|
@@ -115,14 +144,17 @@ const eventItemSchema = z.discriminatedUnion("type", [
|
|
|
115
144
|
/**
|
|
116
145
|
* One subscription: the listeners on the bus and the queue of its events. It is the async
|
|
117
146
|
* iterator of the stream of the call. `return()`, the abort of the signal of the call and
|
|
118
|
-
* `fail()` remove the listeners.
|
|
147
|
+
* `fail()` remove the listeners. When `clear()` of the bus removes a listener, the subscription
|
|
148
|
+
* gives the events of its queue, and then it ends with `TOPIC_CLEARED`.
|
|
119
149
|
*/
|
|
120
150
|
class TopicSubscription implements AsyncIterableIterator<EventItem> {
|
|
121
151
|
private confirmation: EventItem | undefined;
|
|
122
152
|
private readonly queue: Array<Extract<EventItem, { type: "event" }>> = [];
|
|
123
153
|
private seq = 0;
|
|
124
154
|
private dropped = 0;
|
|
125
|
-
private waiter:
|
|
155
|
+
private waiter:
|
|
156
|
+
| { resolve: (result: IteratorResult<EventItem> | PromiseLike<IteratorResult<EventItem>>) => void; reject: (error: unknown) => void }
|
|
157
|
+
| undefined;
|
|
126
158
|
private closed = false;
|
|
127
159
|
private failure: unknown;
|
|
128
160
|
private readonly unsubscribes: Unsubscribe[];
|
|
@@ -133,9 +165,14 @@ class TopicSubscription implements AsyncIterableIterator<EventItem> {
|
|
|
133
165
|
private readonly buffer: number,
|
|
134
166
|
private readonly signal: AbortSignal | undefined,
|
|
135
167
|
private readonly onClose: (subscription: TopicSubscription) => void,
|
|
168
|
+
/** The output filter of the host for this subscriber: each row of a value passes it */
|
|
169
|
+
private readonly admit: ((item: unknown) => Promise<boolean>) | undefined,
|
|
136
170
|
) {
|
|
137
171
|
this.confirmation = { type: "subscribed", topics };
|
|
138
|
-
|
|
172
|
+
// Before, clear() removed the listeners, and the subscriber saw no end (EVENTS.md)
|
|
173
|
+
this.unsubscribes = topics.map((topic) =>
|
|
174
|
+
bus.on(topic, (data) => this.push(topic, data), { onClear: () => this.fail(clearedError(topic), { keepQueue: true }) }),
|
|
175
|
+
);
|
|
139
176
|
signal?.addEventListener("abort", this.onAbort, { once: true });
|
|
140
177
|
}
|
|
141
178
|
|
|
@@ -154,7 +191,7 @@ class TopicSubscription implements AsyncIterableIterator<EventItem> {
|
|
|
154
191
|
const waiter = this.waiter;
|
|
155
192
|
if (waiter && this.queue.length === 0) {
|
|
156
193
|
this.waiter = undefined;
|
|
157
|
-
waiter.resolve(
|
|
194
|
+
waiter.resolve(this.deliver(event));
|
|
158
195
|
return;
|
|
159
196
|
}
|
|
160
197
|
if (this.queue.length >= this.buffer) {
|
|
@@ -165,6 +202,16 @@ class TopicSubscription implements AsyncIterableIterator<EventItem> {
|
|
|
165
202
|
this.queue.push(event);
|
|
166
203
|
}
|
|
167
204
|
|
|
205
|
+
/**
|
|
206
|
+
* The item of an event for the subscriber. The rows of its data pass the output filter of the
|
|
207
|
+
* host for this subscriber, and the subscriber gets only the admitted rows.
|
|
208
|
+
*/
|
|
209
|
+
private deliver(event: Extract<EventItem, { type: "event" }>): Promise<IteratorResult<EventItem>> {
|
|
210
|
+
const item = this.withDropped(event) as Extract<EventItem, { type: "event" }>;
|
|
211
|
+
if (!this.admit || !rowsOf(item.data)) return Promise.resolve({ value: item, done: false });
|
|
212
|
+
return admittedValue(item.data, this.admit).then((data): IteratorResult<EventItem> => ({ value: { ...item, data }, done: false }));
|
|
213
|
+
}
|
|
214
|
+
|
|
168
215
|
/** The event with the number of events that the publisher dropped before it. */
|
|
169
216
|
private withDropped(event: Extract<EventItem, { type: "event" }>): EventItem {
|
|
170
217
|
if (this.dropped === 0) return event;
|
|
@@ -180,7 +227,7 @@ class TopicSubscription implements AsyncIterableIterator<EventItem> {
|
|
|
180
227
|
return Promise.resolve({ value: confirmation, done: false });
|
|
181
228
|
}
|
|
182
229
|
const event = this.queue.shift();
|
|
183
|
-
if (event) return
|
|
230
|
+
if (event) return this.deliver(event);
|
|
184
231
|
if (this.failure !== undefined) {
|
|
185
232
|
const failure = this.failure;
|
|
186
233
|
this.failure = undefined;
|
|
@@ -201,14 +248,19 @@ class TopicSubscription implements AsyncIterableIterator<EventItem> {
|
|
|
201
248
|
return this;
|
|
202
249
|
}
|
|
203
250
|
|
|
204
|
-
/**
|
|
205
|
-
|
|
251
|
+
/**
|
|
252
|
+
* This function ends the subscription with an error: the reader gets it at its next item. With
|
|
253
|
+
* `keepQueue`, the reader first gets the confirmation and the events of the queue.
|
|
254
|
+
*/
|
|
255
|
+
fail(error: unknown, options: { keepQueue?: boolean } = {}): void {
|
|
206
256
|
if (this.closed) return;
|
|
207
257
|
const waiter = this.waiter;
|
|
208
258
|
this.waiter = undefined;
|
|
209
259
|
this.release();
|
|
210
|
-
|
|
211
|
-
|
|
260
|
+
if (!options.keepQueue) {
|
|
261
|
+
this.queue.length = 0;
|
|
262
|
+
this.confirmation = undefined;
|
|
263
|
+
}
|
|
212
264
|
if (waiter) waiter.reject(error);
|
|
213
265
|
else this.failure = error;
|
|
214
266
|
}
|
|
@@ -251,6 +303,7 @@ export class EventPublisher {
|
|
|
251
303
|
readonly procedure: AnyProcedure;
|
|
252
304
|
private readonly known: (topic: string) => boolean;
|
|
253
305
|
private readonly allow: EventPublisherOptions["allow"];
|
|
306
|
+
private readonly reads: (topic: string) => readonly string[];
|
|
254
307
|
private readonly buffer: number;
|
|
255
308
|
private readonly open = new Set<TopicSubscription>();
|
|
256
309
|
|
|
@@ -258,6 +311,7 @@ export class EventPublisher {
|
|
|
258
311
|
this.bus = options.bus ?? getGlobalEventBus();
|
|
259
312
|
this.known = topicMatcher(options.topics);
|
|
260
313
|
this.allow = options.allow;
|
|
314
|
+
this.reads = options.reads ?? collectionTopicReads;
|
|
261
315
|
this.buffer = Math.max(1, Math.floor(options.buffer ?? 1024));
|
|
262
316
|
this.procedure = createProcedure()
|
|
263
317
|
.path([...EVENTS_SUBSCRIBE_PATH])
|
|
@@ -299,6 +353,13 @@ export class EventPublisher {
|
|
|
299
353
|
for (const topic of unique) {
|
|
300
354
|
if (!this.known(topic)) throw new TopicNotFoundError(`The host has no topic "${topic}"`);
|
|
301
355
|
}
|
|
356
|
+
// A subscriber of a topic reads its resources: the capability of the call must permit them
|
|
357
|
+
for (const topic of unique) {
|
|
358
|
+
const reads = this.reads(topic);
|
|
359
|
+
if (reads.length === 0) continue;
|
|
360
|
+
const refusal = capabilityRefusal(topicReader(reads), ctx.capability);
|
|
361
|
+
if (refusal) throw new InvocationError("FORBIDDEN", `The topic "${topic}": ${refusal.message}`, ctx.path, refusal.reason);
|
|
362
|
+
}
|
|
302
363
|
if (this.allow) {
|
|
303
364
|
const call: TopicCall = { capability: ctx.capability, metadata: ctx.metadata };
|
|
304
365
|
for (const topic of unique) {
|
|
@@ -307,7 +368,7 @@ export class EventPublisher {
|
|
|
307
368
|
}
|
|
308
369
|
}
|
|
309
370
|
}
|
|
310
|
-
const subscription = new TopicSubscription(this.bus, unique, this.buffer, ctx.signal, (closed) => this.open.delete(closed));
|
|
371
|
+
const subscription = new TopicSubscription(this.bus, unique, this.buffer, ctx.signal, (closed) => this.open.delete(closed), ctx.admit);
|
|
311
372
|
this.open.add(subscription);
|
|
312
373
|
return subscription.closeIfAborted();
|
|
313
374
|
}
|