@procedures/client 0.47.2 → 0.48.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 +24 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/adapters/http/client/batch.d.ts.map +1 -1
- package/dist/adapters/http/client/batch.js.map +1 -1
- package/dist/adapters/http/server/body.d.ts.map +1 -1
- package/dist/adapters/http/server/body.js.map +1 -1
- package/dist/adapters/http/shared/constants.d.ts +82 -78
- package/dist/adapters/http/shared/constants.d.ts.map +1 -1
- package/dist/adapters/http/shared/constants.js +78 -80
- package/dist/adapters/http/shared/constants.js.map +1 -1
- package/dist/adapters/http/shared/errors.d.ts +15 -13
- package/dist/adapters/http/shared/errors.d.ts.map +1 -1
- package/dist/adapters/http/shared/errors.js +13 -14
- package/dist/adapters/http/shared/errors.js.map +1 -1
- package/dist/adapters/port/transport.d.ts.map +1 -1
- package/dist/adapters/port/transport.js.map +1 -1
- package/dist/adapters/sequence.d.ts.map +1 -1
- package/dist/adapters/sequence.js.map +1 -1
- package/dist/adapters/websocket/client/types.d.ts +16 -6
- package/dist/adapters/websocket/client/types.d.ts.map +1 -1
- package/dist/adapters/websocket/client/types.js +7 -8
- package/dist/adapters/websocket/client/types.js.map +1 -1
- package/dist/adapters/websocket/shared/rpc.d.ts.map +1 -1
- package/dist/adapters/websocket/shared/rpc.js.map +1 -1
- package/dist/browser.d.ts +3 -1
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +2 -0
- package/dist/browser.js.map +1 -1
- package/dist/client/batch-executor.d.ts.map +1 -1
- package/dist/client/batch-executor.js.map +1 -1
- package/dist/client/client.d.ts.map +1 -1
- package/dist/client/client.js +3 -4
- package/dist/client/client.js.map +1 -1
- package/dist/client/errors/codes.d.ts +66 -64
- package/dist/client/errors/codes.d.ts.map +1 -1
- package/dist/client/errors/codes.js +64 -65
- package/dist/client/errors/codes.js.map +1 -1
- package/dist/client/errors/http-status.d.ts +69 -67
- package/dist/client/errors/http-status.d.ts.map +1 -1
- package/dist/client/errors/http-status.js +67 -68
- package/dist/client/errors/http-status.js.map +1 -1
- package/dist/client/errors/types.d.ts +38 -15
- package/dist/client/errors/types.d.ts.map +1 -1
- package/dist/client/errors/types.js +17 -19
- package/dist/client/errors/types.js.map +1 -1
- package/dist/client/middleware/circuit-breaker.d.ts.map +1 -1
- package/dist/client/middleware/circuit-breaker.js.map +1 -1
- package/dist/client/middleware/lru.d.ts +1 -1
- package/dist/client/middleware/lru.d.ts.map +1 -1
- package/dist/client/middleware/lru.js.map +1 -1
- package/dist/client/middleware/rate-limit.d.ts.map +1 -1
- package/dist/client/middleware/rate-limit.js.map +1 -1
- package/dist/client/middleware/resilience.d.ts.map +1 -1
- package/dist/client/middleware/resilience.js +2 -1
- package/dist/client/middleware/resilience.js.map +1 -1
- package/dist/client/route-resolver.d.ts.map +1 -1
- package/dist/client/route-resolver.js.map +1 -1
- package/dist/client/types.d.ts.map +1 -1
- package/dist/client/types.js.map +1 -1
- package/dist/client/validation/types.d.ts +1 -7
- package/dist/client/validation/types.d.ts.map +1 -1
- package/dist/client/validation/types.js +4 -7
- package/dist/client/validation/types.js.map +1 -1
- package/dist/events/bus.d.ts +35 -0
- package/dist/events/bus.d.ts.map +1 -1
- package/dist/events/bus.js +135 -22
- package/dist/events/bus.js.map +1 -1
- package/dist/events/index.d.ts +5 -1
- package/dist/events/index.d.ts.map +1 -1
- package/dist/events/index.js +3 -0
- package/dist/events/index.js.map +1 -1
- package/dist/events/live-state.d.ts +289 -0
- package/dist/events/live-state.d.ts.map +1 -0
- package/dist/events/live-state.js +501 -0
- package/dist/events/live-state.js.map +1 -0
- package/dist/events/live-view.d.ts +38 -0
- package/dist/events/live-view.d.ts.map +1 -0
- package/dist/events/live-view.js +109 -0
- package/dist/events/live-view.js.map +1 -0
- package/dist/events/publisher.d.ts +18 -1
- package/dist/events/publisher.d.ts.map +1 -1
- package/dist/events/publisher.js +27 -8
- package/dist/events/publisher.js.map +1 -1
- package/dist/events/recheck.d.ts.map +1 -1
- package/dist/events/recheck.js.map +1 -1
- package/dist/events/remote.d.ts +1 -7
- package/dist/events/remote.d.ts.map +1 -1
- package/dist/events/remote.js +4 -7
- package/dist/events/remote.js.map +1 -1
- package/dist/events/types.d.ts +30 -0
- package/dist/events/types.d.ts.map +1 -1
- package/dist/middleware/compose.d.ts.map +1 -1
- package/dist/middleware/compose.js +6 -10
- package/dist/middleware/compose.js.map +1 -1
- package/dist/procedures/collection/hint-filter.d.ts +1 -7
- package/dist/procedures/collection/hint-filter.d.ts.map +1 -1
- package/dist/procedures/collection/hint-filter.js +4 -7
- package/dist/procedures/collection/hint-filter.js.map +1 -1
- package/dist/server/procedure-server.d.ts.map +1 -1
- package/dist/server/procedure-server.js +3 -0
- package/dist/server/procedure-server.js.map +1 -1
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/types.js.map +1 -1
- package/package.json +7 -4
- package/src/adapters/http/client/batch.ts +4 -1
- package/src/adapters/http/server/body.ts +4 -1
- package/src/adapters/http/shared/constants.ts +84 -78
- package/src/adapters/http/shared/errors.ts +16 -13
- package/src/adapters/port/transport.ts +4 -1
- package/src/adapters/sequence.ts +5 -1
- package/src/adapters/websocket/client/types.ts +19 -6
- package/src/adapters/websocket/shared/rpc.ts +15 -3
- package/src/browser.ts +20 -0
- package/src/client/batch-executor.ts +5 -1
- package/src/client/client.ts +3 -4
- package/src/client/errors/README.md +12 -12
- package/src/client/errors/codes.ts +67 -64
- package/src/client/errors/http-status.ts +70 -67
- package/src/client/errors/types.ts +44 -15
- package/src/client/middleware/circuit-breaker.ts +7 -2
- package/src/client/middleware/lru.ts +7 -2
- package/src/client/middleware/rate-limit.ts +4 -1
- package/src/client/middleware/resilience.ts +2 -1
- package/src/client/route-resolver.ts +5 -1
- package/src/client/types.ts +7 -2
- package/src/client/validation/types.ts +13 -6
- package/src/events/bus.ts +152 -23
- package/src/events/index.ts +30 -0
- package/src/events/live-state.ts +666 -0
- package/src/events/live-view.ts +134 -0
- package/src/events/publisher.ts +53 -10
- package/src/events/recheck.ts +4 -1
- package/src/events/remote.ts +33 -12
- package/src/events/types.ts +34 -0
- package/src/middleware/compose.ts +6 -22
- package/src/procedures/collection/hint-filter.ts +13 -6
- package/src/server/procedure-server.ts +2 -0
- package/src/server/types.ts +7 -2
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The reader of a live topic (documentation/LIVE-STATE-2026-11.md).
|
|
3
|
+
*
|
|
4
|
+
* The view subscribes first and reads the snapshot second. It applies the snapshot, then the events
|
|
5
|
+
* that came during the read, in their order. So no number is necessary: each set publishes an
|
|
6
|
+
* event, and the last event of a key decides. After a lost connection and after a loss of events,
|
|
7
|
+
* the view reads the snapshot again and replaces its values.
|
|
8
|
+
*/
|
|
9
|
+
import { pathToMethod } from "../server/method.js";
|
|
10
|
+
import type { Method } from "../client/types.js";
|
|
11
|
+
import { LIVE_SNAPSHOT_PATH, type LiveEntry, type LiveEvent } from "./live-state.js";
|
|
12
|
+
import type { RemoteEventHandler, SubscribeOptions, Subscription } from "./remote.js";
|
|
13
|
+
|
|
14
|
+
/** The parts of a client that a view uses. A `Client` has them. */
|
|
15
|
+
export interface LiveClient {
|
|
16
|
+
/** This method starts a subscription. */
|
|
17
|
+
subscribe<T = unknown>(topics: string | readonly string[], handler: RemoteEventHandler<T>, options?: SubscribeOptions): Subscription;
|
|
18
|
+
/** This method calls a procedure. */
|
|
19
|
+
call<TReq, TRes>(method: Method, payload: TReq): Promise<TRes>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The options of a view. */
|
|
23
|
+
export interface LiveViewOptions<T = unknown> extends SubscribeOptions {
|
|
24
|
+
/** This function gets the values after each change. */
|
|
25
|
+
onChange?: ((values: ReadonlyMap<string, LiveEntry<T>>) => void) | undefined;
|
|
26
|
+
/** This function gets the error of a read of the snapshot. The view keeps its values and its subscription. */
|
|
27
|
+
onReadError?: ((error: unknown) => void) | undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The live values of a topic for one reader. */
|
|
31
|
+
export interface LiveView<T = unknown> {
|
|
32
|
+
/** The live values by key. */
|
|
33
|
+
readonly values: ReadonlyMap<string, LiveEntry<T>>;
|
|
34
|
+
/** This promise resolves after the first snapshot. It rejects when the first subscription or read fails. */
|
|
35
|
+
readonly ready: Promise<void>;
|
|
36
|
+
/** This method ends the subscription. A second call does nothing. */
|
|
37
|
+
close(): void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* This function gives a view of a live topic.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```typescript
|
|
45
|
+
* const view = liveView(client, liveTopic("presence", "board-1"), { onChange: render });
|
|
46
|
+
* await view.ready;
|
|
47
|
+
* view.close();
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
export function liveView<T = unknown>(client: LiveClient, topic: string, options: LiveViewOptions<T> = {}): LiveView<T> {
|
|
51
|
+
const { onChange, onReadError, ...subscribeOptions } = options;
|
|
52
|
+
const values = new Map<string, LiveEntry<T>>();
|
|
53
|
+
/** This field holds the events that come during a read of the snapshot. It is undefined when no read runs. */
|
|
54
|
+
let held: Array<LiveEvent<T>> | undefined;
|
|
55
|
+
let reads = 0;
|
|
56
|
+
let closed = false;
|
|
57
|
+
let settled = false;
|
|
58
|
+
let resolveReady: () => void = () => undefined;
|
|
59
|
+
let rejectReady: (error: unknown) => void = () => undefined;
|
|
60
|
+
const ready = new Promise<void>((resolve, reject) => {
|
|
61
|
+
resolveReady = resolve;
|
|
62
|
+
rejectReady = reject;
|
|
63
|
+
});
|
|
64
|
+
// A view that nobody awaits does not give an unhandled rejection
|
|
65
|
+
ready.catch(() => undefined);
|
|
66
|
+
|
|
67
|
+
const apply = (event: LiveEvent<T>): void => {
|
|
68
|
+
for (const { key } of event.gone) values.delete(key);
|
|
69
|
+
for (const entry of event.set) values.set(entry.key, entry);
|
|
70
|
+
};
|
|
71
|
+
const applyHeld = (): void => {
|
|
72
|
+
for (const event of held ?? []) apply(event);
|
|
73
|
+
held = undefined;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const read = async (): Promise<void> => {
|
|
77
|
+
const number = ++reads;
|
|
78
|
+
held = [];
|
|
79
|
+
try {
|
|
80
|
+
const { entries } = await client.call<{ topic: string }, { entries: Array<LiveEntry<T>> }>(pathToMethod([...LIVE_SNAPSHOT_PATH]), { topic });
|
|
81
|
+
if (closed || number !== reads) return;
|
|
82
|
+
values.clear();
|
|
83
|
+
for (const entry of entries) values.set(entry.key, entry);
|
|
84
|
+
applyHeld();
|
|
85
|
+
settled = true;
|
|
86
|
+
resolveReady();
|
|
87
|
+
onChange?.(values);
|
|
88
|
+
} catch (error) {
|
|
89
|
+
if (closed || number !== reads) return;
|
|
90
|
+
applyHeld();
|
|
91
|
+
if (!settled) {
|
|
92
|
+
settled = true;
|
|
93
|
+
rejectReady(error);
|
|
94
|
+
}
|
|
95
|
+
onReadError?.(error);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const unsubscribe = client.subscribe<LiveEvent<T>>(
|
|
100
|
+
topic,
|
|
101
|
+
(event, meta) => {
|
|
102
|
+
if (closed) return;
|
|
103
|
+
// A loss of events: only a new read gives the keys back
|
|
104
|
+
if (meta.dropped > 0) void read();
|
|
105
|
+
if (held) {
|
|
106
|
+
held.push(event);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
apply(event);
|
|
110
|
+
onChange?.(values);
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
...subscribeOptions,
|
|
114
|
+
onSubscribed: (topics) => {
|
|
115
|
+
subscribeOptions.onSubscribed?.(topics);
|
|
116
|
+
if (!closed) void read();
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
);
|
|
120
|
+
unsubscribe.ready.catch((error: unknown) => {
|
|
121
|
+
if (settled) return;
|
|
122
|
+
settled = true;
|
|
123
|
+
rejectReady(error);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
values,
|
|
128
|
+
ready,
|
|
129
|
+
close: () => {
|
|
130
|
+
closed = true;
|
|
131
|
+
unsubscribe();
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
package/src/events/publisher.ts
CHANGED
|
@@ -42,6 +42,7 @@ import type { AnyProcedure, Capability, ProcedureContext } from "../procedures/t
|
|
|
42
42
|
import type { Principal } from "../server/types.js";
|
|
43
43
|
import type { EventBus, Unsubscribe } from "./types.js";
|
|
44
44
|
import { getGlobalEventBus } from "./bus.js";
|
|
45
|
+
import { LiveState, type LiveStateOptions } from "./live-state.js";
|
|
45
46
|
import { EVENTS_SUBSCRIBE_PATH, type EventItem, type SubscribeInput } from "./remote.js";
|
|
46
47
|
import { admittedValue, rowsOf } from "./rows.js";
|
|
47
48
|
import { realClock, type Clock } from "../clock.js";
|
|
@@ -117,6 +118,13 @@ export interface EventPublisherOptions {
|
|
|
117
118
|
* @default no check after the start, other than at a call of `rightsChanged()`
|
|
118
119
|
*/
|
|
119
120
|
recheck?: SubscriptionRecheck | undefined;
|
|
121
|
+
/**
|
|
122
|
+
* The live state of the host (LIVE-STATE-2026-11.md): keyed values in memory on the topics
|
|
123
|
+
* `live.<name>` and `live.<name>.<partition>`. The topic rule must name these topics. A server
|
|
124
|
+
* registers `live.set` and `live.snapshot`, and its expose rule applies to them.
|
|
125
|
+
* @default no live state
|
|
126
|
+
*/
|
|
127
|
+
live?: LiveStateOptions | undefined;
|
|
120
128
|
}
|
|
121
129
|
|
|
122
130
|
/** A procedure that reads resources, for the admission of a topic under a capability. */
|
|
@@ -226,6 +234,16 @@ function resetHint(): unknown {
|
|
|
226
234
|
* gives the events of its queue, and then it ends with `TOPIC_CLEARED`.
|
|
227
235
|
*/
|
|
228
236
|
class TopicSubscription implements AsyncIterableIterator<EventItem> {
|
|
237
|
+
private readonly buffer: number;
|
|
238
|
+
private readonly signal: AbortSignal | undefined;
|
|
239
|
+
private readonly onClose: (subscription: TopicSubscription) => void;
|
|
240
|
+
/** The output filter of the host for this subscriber: each row of a value passes it */
|
|
241
|
+
private readonly admit: ((item: unknown) => Promise<boolean>) | undefined;
|
|
242
|
+
/** The invocation of the subscription: the rule of the rows of a value gets it (roadmap CLI-41) */
|
|
243
|
+
private readonly invocation?: Invocation | undefined;
|
|
244
|
+
/** The row filter of each topic of a collection, by the topic of the subscriber (roadmap CLI-78) */
|
|
245
|
+
private readonly filters?: ReadonlyMap<string, RowHintFilter> | undefined;
|
|
246
|
+
|
|
229
247
|
private confirmation: EventItem | undefined;
|
|
230
248
|
private readonly queue: QueuedEvent[] = [];
|
|
231
249
|
private seq = 0;
|
|
@@ -245,16 +263,19 @@ class TopicSubscription implements AsyncIterableIterator<EventItem> {
|
|
|
245
263
|
bus: EventBus,
|
|
246
264
|
/** The topics of the subscriber, each with the topic of the bus that it listens to */
|
|
247
265
|
routes: ReadonlyArray<Route>,
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
private readonly invocation?: Invocation | undefined,
|
|
255
|
-
/** The row filter of each topic of a collection, by the topic of the subscriber (roadmap CLI-78) */
|
|
256
|
-
private readonly filters?: ReadonlyMap<string, RowHintFilter> | undefined,
|
|
266
|
+
buffer: number,
|
|
267
|
+
signal: AbortSignal | undefined,
|
|
268
|
+
onClose: (subscription: TopicSubscription) => void,
|
|
269
|
+
admit: ((item: unknown) => Promise<boolean>) | undefined,
|
|
270
|
+
invocation?: Invocation | undefined,
|
|
271
|
+
filters?: ReadonlyMap<string, RowHintFilter> | undefined,
|
|
257
272
|
) {
|
|
273
|
+
this.buffer = buffer;
|
|
274
|
+
this.signal = signal;
|
|
275
|
+
this.onClose = onClose;
|
|
276
|
+
this.admit = admit;
|
|
277
|
+
this.invocation = invocation;
|
|
278
|
+
this.filters = filters;
|
|
258
279
|
this.confirmation = { type: "subscribed", topics: routes.map(({ topic }) => topic) };
|
|
259
280
|
// Before, clear() removed the listeners, and the subscriber saw no end (EVENTS.md)
|
|
260
281
|
this.unsubscribes = routes.map(({ topic, busTopic }) =>
|
|
@@ -455,6 +476,8 @@ export class EventPublisher {
|
|
|
455
476
|
readonly bus: EventBus;
|
|
456
477
|
/** The procedure `events.subscribe` of this publisher. A host pins it, outside its registry. */
|
|
457
478
|
readonly procedure: AnyProcedure;
|
|
479
|
+
/** The live state, from the option `live`, or undefined. */
|
|
480
|
+
readonly live: LiveState | undefined;
|
|
458
481
|
private readonly known: (topic: string) => boolean;
|
|
459
482
|
private readonly allow: EventPublisherOptions["allow"];
|
|
460
483
|
private readonly reads: (topic: string) => readonly string[];
|
|
@@ -484,6 +507,14 @@ export class EventPublisher {
|
|
|
484
507
|
})
|
|
485
508
|
.handler((async (input: SubscribeInput, ctx: ProcedureContext) => this.subscribe(input.topics, ctx)) as never)
|
|
486
509
|
.build();
|
|
510
|
+
this.live = options.live
|
|
511
|
+
? new LiveState(options.live, {
|
|
512
|
+
bus: this.bus,
|
|
513
|
+
clock: options.clock,
|
|
514
|
+
topicSchema,
|
|
515
|
+
admit: (topic, ctx) => this.admitTopic(topic, ctx),
|
|
516
|
+
})
|
|
517
|
+
: undefined;
|
|
487
518
|
}
|
|
488
519
|
|
|
489
520
|
/** This function publishes an event on the bus: each subscription of the topic gets it. */
|
|
@@ -502,6 +533,7 @@ export class EventPublisher {
|
|
|
502
533
|
*/
|
|
503
534
|
end(): void {
|
|
504
535
|
for (const subscription of [...this.open]) subscription.fail(stoppedError());
|
|
536
|
+
this.live?.close();
|
|
505
537
|
}
|
|
506
538
|
|
|
507
539
|
/**
|
|
@@ -533,11 +565,22 @@ export class EventPublisher {
|
|
|
533
565
|
*/
|
|
534
566
|
private async route(topic: string, ctx: ProcedureContext): Promise<Route> {
|
|
535
567
|
const target = collectionTopicTarget(topic, ctx.repository);
|
|
536
|
-
if (!target) return { topic, busTopic: topic };
|
|
568
|
+
if (!target) return { topic, busTopic: (await this.live?.route(topic, ctx)) ?? topic };
|
|
537
569
|
const partition = await partitionOfCall(ctx, target.collection, target.partition);
|
|
538
570
|
return { topic, busTopic: collectionChangeTopic(target.collection, partition), collection: target.collection, partition, requested: target.partition };
|
|
539
571
|
}
|
|
540
572
|
|
|
573
|
+
/**
|
|
574
|
+
* This method runs the checks of a subscription on one topic: the topic rule, the reads, `allow`
|
|
575
|
+
* and the partition. A live set and a snapshot pass the same checks as a subscriber.
|
|
576
|
+
*
|
|
577
|
+
* @returns The topic of the bus
|
|
578
|
+
*/
|
|
579
|
+
async admitTopic(topic: string, ctx: ProcedureContext): Promise<string> {
|
|
580
|
+
this.admitTopics([topic], ctx);
|
|
581
|
+
return (await this.route(topic, ctx)).busTopic;
|
|
582
|
+
}
|
|
583
|
+
|
|
541
584
|
/**
|
|
542
585
|
* The resources that a subscriber of a topic reads. The default (`collectionTopicReads`) reads
|
|
543
586
|
* the collection of the topic with the rule of the listener (`collectionTopicTarget`) and the
|
package/src/events/recheck.ts
CHANGED
|
@@ -109,6 +109,8 @@ type RecheckCheck = (cause: RecheckCause, scope?: RightsScopes) => Promise<void>
|
|
|
109
109
|
* each new subscription to `watch()`, with the check of its admission.
|
|
110
110
|
*/
|
|
111
111
|
export class SubscriptionRechecks {
|
|
112
|
+
private readonly clock: Clock;
|
|
113
|
+
|
|
112
114
|
private readonly watches = new Set<Watch>();
|
|
113
115
|
private readonly maxAgeMs: number | undefined;
|
|
114
116
|
private readonly source: SubscriptionRecheck["rightsChanges"];
|
|
@@ -117,8 +119,9 @@ export class SubscriptionRechecks {
|
|
|
117
119
|
|
|
118
120
|
constructor(
|
|
119
121
|
options: SubscriptionRecheck,
|
|
120
|
-
|
|
122
|
+
clock: Clock,
|
|
121
123
|
) {
|
|
124
|
+
this.clock = clock;
|
|
122
125
|
const maxAgeMs = options.maxAge === undefined ? undefined : toMs(options.maxAge, "recheck.maxAge");
|
|
123
126
|
if (maxAgeMs !== undefined && !(maxAgeMs > 0)) {
|
|
124
127
|
throw new TypeError(`recheck.maxAge must be more than 0 ms, got ${maxAgeMs} ms`);
|
package/src/events/remote.ts
CHANGED
|
@@ -168,18 +168,25 @@ export interface Subscription {
|
|
|
168
168
|
|
|
169
169
|
/** The error that ends a subscription. */
|
|
170
170
|
export class SubscriptionError extends Error {
|
|
171
|
+
/** The code: a code of the protocol, for example `TOPIC_NOT_FOUND`. */
|
|
172
|
+
readonly code: string;
|
|
173
|
+
/** True when a resubscribe can repair the failure. */
|
|
174
|
+
readonly retryable: boolean;
|
|
175
|
+
/** The reason of a `FORBIDDEN` error, for example `topic-not-allowed`. */
|
|
176
|
+
readonly reason?: string | undefined;
|
|
177
|
+
|
|
171
178
|
override readonly name = "SubscriptionError";
|
|
172
179
|
|
|
173
180
|
constructor(
|
|
174
|
-
|
|
175
|
-
readonly code: string,
|
|
181
|
+
code: string,
|
|
176
182
|
message: string,
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
/** The reason of a `FORBIDDEN` error, for example `topic-not-allowed`. */
|
|
180
|
-
readonly reason?: string | undefined,
|
|
183
|
+
retryable: boolean,
|
|
184
|
+
reason?: string | undefined,
|
|
181
185
|
) {
|
|
182
186
|
super(message);
|
|
187
|
+
this.code = code;
|
|
188
|
+
this.retryable = retryable;
|
|
189
|
+
this.reason = reason;
|
|
183
190
|
}
|
|
184
191
|
}
|
|
185
192
|
|
|
@@ -462,6 +469,8 @@ type Entry =
|
|
|
462
469
|
* last event that the shared subscription got before the subscriber joined.
|
|
463
470
|
*/
|
|
464
471
|
class Subscriber {
|
|
472
|
+
readonly id: string;
|
|
473
|
+
|
|
465
474
|
/** The `seq` of the wire subscription before the first event of this subscriber. */
|
|
466
475
|
base = 0;
|
|
467
476
|
/** The error that ends this subscriber, after the entries of its queue. */
|
|
@@ -473,7 +482,9 @@ class Subscriber {
|
|
|
473
482
|
private waiter: ((entry: Entry | undefined) => void) | undefined;
|
|
474
483
|
private ended = false;
|
|
475
484
|
|
|
476
|
-
constructor(
|
|
485
|
+
constructor(id: string) {
|
|
486
|
+
this.id = id;
|
|
487
|
+
}
|
|
477
488
|
|
|
478
489
|
/** True when the subscriber waits for an entry. */
|
|
479
490
|
get waiting(): boolean {
|
|
@@ -546,6 +557,11 @@ class Subscriber {
|
|
|
546
557
|
* when its last subscriber leaves, or with the end of the wire subscription for each subscriber.
|
|
547
558
|
*/
|
|
548
559
|
class SharedSubscription {
|
|
560
|
+
private readonly shares: Map<string, SharedSubscription> | undefined;
|
|
561
|
+
private readonly key: string;
|
|
562
|
+
private readonly transport: EventTransport;
|
|
563
|
+
private readonly call: Omit<WireCall, "onSubscribed" | "onRetry">;
|
|
564
|
+
|
|
549
565
|
private readonly members = new Set<Subscriber>();
|
|
550
566
|
private readonly stop = new AbortController();
|
|
551
567
|
/** The topics of the current confirmation, or undefined before it and during a resubscribe. */
|
|
@@ -557,11 +573,16 @@ class SharedSubscription {
|
|
|
557
573
|
private finished = false;
|
|
558
574
|
|
|
559
575
|
constructor(
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
) {
|
|
576
|
+
shares: Map<string, SharedSubscription> | undefined,
|
|
577
|
+
key: string,
|
|
578
|
+
transport: EventTransport,
|
|
579
|
+
call: Omit<WireCall, "onSubscribed" | "onRetry">,
|
|
580
|
+
) {
|
|
581
|
+
this.shares = shares;
|
|
582
|
+
this.key = key;
|
|
583
|
+
this.transport = transport;
|
|
584
|
+
this.call = call;
|
|
585
|
+
}
|
|
565
586
|
|
|
566
587
|
/** True while a new subscriber can join. */
|
|
567
588
|
get open(): boolean {
|
package/src/events/types.ts
CHANGED
|
@@ -273,4 +273,38 @@ export interface EventBusOptions {
|
|
|
273
273
|
* Default: false
|
|
274
274
|
*/
|
|
275
275
|
debug?: boolean;
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* The bounded fan-out (roadmap CLI-90). Without it, `emit()` calls each handler before it
|
|
279
|
+
* returns, at each channel size.
|
|
280
|
+
*/
|
|
281
|
+
fanOut?: EventFanOutOptions;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* The options of the bounded fan-out of an event bus (roadmap CLI-90).
|
|
286
|
+
*
|
|
287
|
+
* A channel with more handlers than `sliceSize` delivers after `emit()` returns, in slices. The
|
|
288
|
+
* event loop runs between two slices, so one event does not hold it. A smaller channel stays
|
|
289
|
+
* synchronous, while no event of the channel waits.
|
|
290
|
+
*
|
|
291
|
+
* The limit is a count of handlers, not a time. The work of a stream reader runs in microtasks
|
|
292
|
+
* after its handler returns, so a clock in the slice does not see this work.
|
|
293
|
+
*/
|
|
294
|
+
export interface EventFanOutOptions {
|
|
295
|
+
/** The most handlers that one slice calls. A number from 1 up. */
|
|
296
|
+
sliceSize: number;
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* The most events that wait for their slices. The next event then finishes the oldest events
|
|
300
|
+
* in its `emit()`: the bus loses no event, and its memory stays bounded.
|
|
301
|
+
* Default: 1024
|
|
302
|
+
*/
|
|
303
|
+
maxPending?: number;
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* The function that runs a slice in a later task of the event loop.
|
|
307
|
+
* Default: `setImmediate`, or `setTimeout` where `setImmediate` does not exist
|
|
308
|
+
*/
|
|
309
|
+
schedule?: (slice: () => void) => void;
|
|
276
310
|
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* for any chain length - no manual overloads needed!
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { compose as foldLayers, type Layer } from "@mark1russell7/collections/layers";
|
|
9
10
|
import type {
|
|
10
11
|
Middleware,
|
|
11
12
|
MiddlewareRunner,
|
|
@@ -47,12 +48,7 @@ export function wrapWithAsyncMiddlewares<
|
|
|
47
48
|
true
|
|
48
49
|
>
|
|
49
50
|
): MiddlewareRunner<TBaseContext, TReturn, true> {
|
|
50
|
-
const combinedRunner = middlewares
|
|
51
|
-
(currentRunner, currentMiddleware) => {
|
|
52
|
-
return currentMiddleware(currentRunner);
|
|
53
|
-
},
|
|
54
|
-
fn
|
|
55
|
-
) as MiddlewareRunner<TBaseContext, TReturn, true>;
|
|
51
|
+
const combinedRunner = foldLayers(...(middlewares as readonly Layer[]))(fn) as MiddlewareRunner<TBaseContext, TReturn, true>;
|
|
56
52
|
|
|
57
53
|
// Wrap in async to ensure synchronous throws become rejected promises
|
|
58
54
|
return async (context) => {
|
|
@@ -85,9 +81,7 @@ export function wrapWithSyncMiddlewares<
|
|
|
85
81
|
false
|
|
86
82
|
>
|
|
87
83
|
): MiddlewareRunner<TBaseContext, TReturn, false> {
|
|
88
|
-
return
|
|
89
|
-
return currentMiddleware(currentRunner);
|
|
90
|
-
}, fn) as MiddlewareRunner<TBaseContext, TReturn, false>;
|
|
84
|
+
return foldLayers(...(middlewares as readonly Layer[]))(fn) as MiddlewareRunner<TBaseContext, TReturn, false>;
|
|
91
85
|
}
|
|
92
86
|
|
|
93
87
|
/**
|
|
@@ -115,13 +109,8 @@ export function compose<TAsync extends boolean = false>(
|
|
|
115
109
|
const fn = args[args.length - 1];
|
|
116
110
|
const middleware = args.slice(0, -1) as Middleware<any, any, any, TAsync>[];
|
|
117
111
|
|
|
118
|
-
//
|
|
119
|
-
|
|
120
|
-
(next, mw) => mw(next),
|
|
121
|
-
fn
|
|
122
|
-
) as MiddlewareRunner<any, any, TAsync>;
|
|
123
|
-
|
|
124
|
-
return combined;
|
|
112
|
+
// The first middleware is the outermost one (onion model)
|
|
113
|
+
return foldLayers(...(middleware as readonly Layer[]))(fn) as MiddlewareRunner<any, any, TAsync>;
|
|
125
114
|
}
|
|
126
115
|
|
|
127
116
|
/**
|
|
@@ -150,10 +139,5 @@ export function compose<TAsync extends boolean = false>(
|
|
|
150
139
|
export function bundle<TAsync extends boolean = false>(
|
|
151
140
|
...middleware: Middleware<any, any, any, TAsync>[]
|
|
152
141
|
): Middleware<any, any, any, TAsync> {
|
|
153
|
-
return (
|
|
154
|
-
middleware.reduceRight((acc, mw) => mw(acc), next) as MiddlewareRunner<
|
|
155
|
-
any,
|
|
156
|
-
any,
|
|
157
|
-
TAsync
|
|
158
|
-
>;
|
|
142
|
+
return foldLayers(...(middleware as readonly Layer[])) as Middleware<any, any, any, TAsync>;
|
|
159
143
|
}
|
|
@@ -201,6 +201,13 @@ function ignore(): void {}
|
|
|
201
201
|
* of the topic to `apply()`, one at a time, in the order of the bus.
|
|
202
202
|
*/
|
|
203
203
|
export class RowHintFilter {
|
|
204
|
+
/** The rules of the collection from the registry of the call. */
|
|
205
|
+
private readonly lookup: () => CollectionAdmission | null | undefined;
|
|
206
|
+
/** The invocation of the topic (`collectionInvocation()`): the rules get it. */
|
|
207
|
+
readonly invocation: Invocation | undefined;
|
|
208
|
+
/** The partition of the topic: `admitsAll` gets it. */
|
|
209
|
+
private readonly partition: string | undefined;
|
|
210
|
+
|
|
204
211
|
/** The rules from the registry: undefined when the registry does not know the collection. */
|
|
205
212
|
private rules: CollectionAdmission | null | undefined;
|
|
206
213
|
/** The result of `admitsAll` for one set of rules. */
|
|
@@ -213,13 +220,13 @@ export class RowHintFilter {
|
|
|
213
220
|
private tail: Promise<unknown> = Promise.resolve();
|
|
214
221
|
|
|
215
222
|
constructor(
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
readonly invocation: Invocation | undefined,
|
|
220
|
-
/** The partition of the topic: `admitsAll` gets it. */
|
|
221
|
-
private readonly partition: string | undefined,
|
|
223
|
+
lookup: () => CollectionAdmission | null | undefined,
|
|
224
|
+
invocation: Invocation | undefined,
|
|
225
|
+
partition: string | undefined,
|
|
222
226
|
) {
|
|
227
|
+
this.lookup = lookup;
|
|
228
|
+
this.invocation = invocation;
|
|
229
|
+
this.partition = partition;
|
|
223
230
|
this.rules = lookup();
|
|
224
231
|
}
|
|
225
232
|
|
|
@@ -516,6 +516,8 @@ export class ProcedureServer extends Server implements RepositoryProvider, StopP
|
|
|
516
516
|
const events = options.events;
|
|
517
517
|
this.publisher = events instanceof EventPublisher ? events : events ? new EventPublisher({ ...events, clock: events.clock ?? this.clock }) : undefined;
|
|
518
518
|
if (this.publisher) this.registerProcedure(this.publisher.procedure, { alwaysOn: true });
|
|
519
|
+
// The live state is a choice of the host, so its expose rule applies to these procedures
|
|
520
|
+
for (const procedure of this.publisher?.live?.procedures ?? []) this.registerProcedure(procedure);
|
|
519
521
|
|
|
520
522
|
// Configure collections
|
|
521
523
|
if (options.collections) {
|
package/src/server/types.ts
CHANGED
|
@@ -388,12 +388,17 @@ export class HandlerNotFoundError extends Error {
|
|
|
388
388
|
* before, the default was the string `"500"`).
|
|
389
389
|
*/
|
|
390
390
|
export class ServerError extends Error {
|
|
391
|
+
public readonly code: string;
|
|
392
|
+
public readonly retryable: boolean;
|
|
393
|
+
|
|
391
394
|
constructor(
|
|
392
395
|
message: string,
|
|
393
|
-
|
|
394
|
-
|
|
396
|
+
code: string = "INTERNAL_ERROR",
|
|
397
|
+
retryable: boolean = false
|
|
395
398
|
) {
|
|
396
399
|
super(message);
|
|
400
|
+
this.code = code;
|
|
401
|
+
this.retryable = retryable;
|
|
397
402
|
this.name = "ServerError";
|
|
398
403
|
}
|
|
399
404
|
}
|