@procedures/client 0.47.2 → 0.48.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.
Files changed (121) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/adapters/http/client/batch.d.ts.map +1 -1
  4. package/dist/adapters/http/client/batch.js.map +1 -1
  5. package/dist/adapters/http/server/body.d.ts.map +1 -1
  6. package/dist/adapters/http/server/body.js.map +1 -1
  7. package/dist/adapters/http/shared/constants.d.ts +82 -78
  8. package/dist/adapters/http/shared/constants.d.ts.map +1 -1
  9. package/dist/adapters/http/shared/constants.js +78 -80
  10. package/dist/adapters/http/shared/constants.js.map +1 -1
  11. package/dist/adapters/http/shared/errors.d.ts +15 -13
  12. package/dist/adapters/http/shared/errors.d.ts.map +1 -1
  13. package/dist/adapters/http/shared/errors.js +13 -14
  14. package/dist/adapters/http/shared/errors.js.map +1 -1
  15. package/dist/adapters/port/transport.d.ts.map +1 -1
  16. package/dist/adapters/port/transport.js.map +1 -1
  17. package/dist/adapters/sequence.d.ts.map +1 -1
  18. package/dist/adapters/sequence.js.map +1 -1
  19. package/dist/adapters/websocket/client/types.d.ts +16 -6
  20. package/dist/adapters/websocket/client/types.d.ts.map +1 -1
  21. package/dist/adapters/websocket/client/types.js +7 -8
  22. package/dist/adapters/websocket/client/types.js.map +1 -1
  23. package/dist/adapters/websocket/shared/rpc.d.ts.map +1 -1
  24. package/dist/adapters/websocket/shared/rpc.js.map +1 -1
  25. package/dist/browser.d.ts +1 -1
  26. package/dist/browser.d.ts.map +1 -1
  27. package/dist/browser.js.map +1 -1
  28. package/dist/client/batch-executor.d.ts.map +1 -1
  29. package/dist/client/batch-executor.js.map +1 -1
  30. package/dist/client/client.d.ts.map +1 -1
  31. package/dist/client/client.js +3 -4
  32. package/dist/client/client.js.map +1 -1
  33. package/dist/client/errors/codes.d.ts +66 -64
  34. package/dist/client/errors/codes.d.ts.map +1 -1
  35. package/dist/client/errors/codes.js +64 -65
  36. package/dist/client/errors/codes.js.map +1 -1
  37. package/dist/client/errors/http-status.d.ts +69 -67
  38. package/dist/client/errors/http-status.d.ts.map +1 -1
  39. package/dist/client/errors/http-status.js +67 -68
  40. package/dist/client/errors/http-status.js.map +1 -1
  41. package/dist/client/errors/types.d.ts +38 -15
  42. package/dist/client/errors/types.d.ts.map +1 -1
  43. package/dist/client/errors/types.js +17 -19
  44. package/dist/client/errors/types.js.map +1 -1
  45. package/dist/client/middleware/circuit-breaker.d.ts.map +1 -1
  46. package/dist/client/middleware/circuit-breaker.js.map +1 -1
  47. package/dist/client/middleware/lru.d.ts +1 -1
  48. package/dist/client/middleware/lru.d.ts.map +1 -1
  49. package/dist/client/middleware/lru.js.map +1 -1
  50. package/dist/client/middleware/rate-limit.d.ts.map +1 -1
  51. package/dist/client/middleware/rate-limit.js.map +1 -1
  52. package/dist/client/middleware/resilience.d.ts.map +1 -1
  53. package/dist/client/middleware/resilience.js +2 -1
  54. package/dist/client/middleware/resilience.js.map +1 -1
  55. package/dist/client/route-resolver.d.ts.map +1 -1
  56. package/dist/client/route-resolver.js.map +1 -1
  57. package/dist/client/types.d.ts.map +1 -1
  58. package/dist/client/types.js.map +1 -1
  59. package/dist/client/validation/types.d.ts +1 -7
  60. package/dist/client/validation/types.d.ts.map +1 -1
  61. package/dist/client/validation/types.js +4 -7
  62. package/dist/client/validation/types.js.map +1 -1
  63. package/dist/events/bus.d.ts +35 -0
  64. package/dist/events/bus.d.ts.map +1 -1
  65. package/dist/events/bus.js +135 -22
  66. package/dist/events/bus.js.map +1 -1
  67. package/dist/events/index.d.ts +1 -1
  68. package/dist/events/index.d.ts.map +1 -1
  69. package/dist/events/index.js.map +1 -1
  70. package/dist/events/publisher.d.ts.map +1 -1
  71. package/dist/events/publisher.js +4 -7
  72. package/dist/events/publisher.js.map +1 -1
  73. package/dist/events/recheck.d.ts.map +1 -1
  74. package/dist/events/recheck.js.map +1 -1
  75. package/dist/events/remote.d.ts +1 -7
  76. package/dist/events/remote.d.ts.map +1 -1
  77. package/dist/events/remote.js +4 -7
  78. package/dist/events/remote.js.map +1 -1
  79. package/dist/events/types.d.ts +30 -0
  80. package/dist/events/types.d.ts.map +1 -1
  81. package/dist/middleware/compose.d.ts.map +1 -1
  82. package/dist/middleware/compose.js +6 -10
  83. package/dist/middleware/compose.js.map +1 -1
  84. package/dist/procedures/collection/hint-filter.d.ts +1 -7
  85. package/dist/procedures/collection/hint-filter.d.ts.map +1 -1
  86. package/dist/procedures/collection/hint-filter.js +4 -7
  87. package/dist/procedures/collection/hint-filter.js.map +1 -1
  88. package/dist/server/types.d.ts.map +1 -1
  89. package/dist/server/types.js.map +1 -1
  90. package/package.json +7 -4
  91. package/src/adapters/http/client/batch.ts +4 -1
  92. package/src/adapters/http/server/body.ts +4 -1
  93. package/src/adapters/http/shared/constants.ts +84 -78
  94. package/src/adapters/http/shared/errors.ts +16 -13
  95. package/src/adapters/port/transport.ts +4 -1
  96. package/src/adapters/sequence.ts +5 -1
  97. package/src/adapters/websocket/client/types.ts +19 -6
  98. package/src/adapters/websocket/shared/rpc.ts +15 -3
  99. package/src/browser.ts +1 -0
  100. package/src/client/batch-executor.ts +5 -1
  101. package/src/client/client.ts +3 -4
  102. package/src/client/errors/README.md +12 -12
  103. package/src/client/errors/codes.ts +67 -64
  104. package/src/client/errors/http-status.ts +70 -67
  105. package/src/client/errors/types.ts +44 -15
  106. package/src/client/middleware/circuit-breaker.ts +7 -2
  107. package/src/client/middleware/lru.ts +7 -2
  108. package/src/client/middleware/rate-limit.ts +4 -1
  109. package/src/client/middleware/resilience.ts +2 -1
  110. package/src/client/route-resolver.ts +5 -1
  111. package/src/client/types.ts +7 -2
  112. package/src/client/validation/types.ts +13 -6
  113. package/src/events/bus.ts +152 -23
  114. package/src/events/index.ts +1 -0
  115. package/src/events/publisher.ts +22 -9
  116. package/src/events/recheck.ts +4 -1
  117. package/src/events/remote.ts +33 -12
  118. package/src/events/types.ts +34 -0
  119. package/src/middleware/compose.ts +6 -22
  120. package/src/procedures/collection/hint-filter.ts +13 -6
  121. package/src/server/types.ts +7 -2
@@ -226,6 +226,16 @@ function resetHint(): unknown {
226
226
  * gives the events of its queue, and then it ends with `TOPIC_CLEARED`.
227
227
  */
228
228
  class TopicSubscription implements AsyncIterableIterator<EventItem> {
229
+ private readonly buffer: number;
230
+ private readonly signal: AbortSignal | undefined;
231
+ private readonly onClose: (subscription: TopicSubscription) => void;
232
+ /** The output filter of the host for this subscriber: each row of a value passes it */
233
+ private readonly admit: ((item: unknown) => Promise<boolean>) | undefined;
234
+ /** The invocation of the subscription: the rule of the rows of a value gets it (roadmap CLI-41) */
235
+ private readonly invocation?: Invocation | undefined;
236
+ /** The row filter of each topic of a collection, by the topic of the subscriber (roadmap CLI-78) */
237
+ private readonly filters?: ReadonlyMap<string, RowHintFilter> | undefined;
238
+
229
239
  private confirmation: EventItem | undefined;
230
240
  private readonly queue: QueuedEvent[] = [];
231
241
  private seq = 0;
@@ -245,16 +255,19 @@ class TopicSubscription implements AsyncIterableIterator<EventItem> {
245
255
  bus: EventBus,
246
256
  /** The topics of the subscriber, each with the topic of the bus that it listens to */
247
257
  routes: ReadonlyArray<Route>,
248
- private readonly buffer: number,
249
- private readonly signal: AbortSignal | undefined,
250
- private readonly onClose: (subscription: TopicSubscription) => void,
251
- /** The output filter of the host for this subscriber: each row of a value passes it */
252
- private readonly admit: ((item: unknown) => Promise<boolean>) | undefined,
253
- /** The invocation of the subscription: the rule of the rows of a value gets it (roadmap CLI-41) */
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,
258
+ buffer: number,
259
+ signal: AbortSignal | undefined,
260
+ onClose: (subscription: TopicSubscription) => void,
261
+ admit: ((item: unknown) => Promise<boolean>) | undefined,
262
+ invocation?: Invocation | undefined,
263
+ filters?: ReadonlyMap<string, RowHintFilter> | undefined,
257
264
  ) {
265
+ this.buffer = buffer;
266
+ this.signal = signal;
267
+ this.onClose = onClose;
268
+ this.admit = admit;
269
+ this.invocation = invocation;
270
+ this.filters = filters;
258
271
  this.confirmation = { type: "subscribed", topics: routes.map(({ topic }) => topic) };
259
272
  // Before, clear() removed the listeners, and the subscriber saw no end (EVENTS.md)
260
273
  this.unsubscribes = routes.map(({ topic, busTopic }) =>
@@ -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
- private readonly clock: Clock,
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`);
@@ -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
- /** The code: a code of the protocol, for example `TOPIC_NOT_FOUND`. */
175
- readonly code: string,
181
+ code: string,
176
182
  message: string,
177
- /** True when a resubscribe can repair the failure. */
178
- readonly retryable: boolean,
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(readonly id: string) {}
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
- 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
- ) {}
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 {
@@ -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.reduceRight(
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 middlewares.reduceRight((currentRunner, currentMiddleware) => {
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
- // Compose middleware from right to left (onion model)
119
- const combined = middleware.reduceRight(
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 (next) =>
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
- /** The rules of the collection from the registry of the call. */
217
- private readonly lookup: () => CollectionAdmission | null | undefined,
218
- /** The invocation of the topic (`collectionInvocation()`): the rules get it. */
219
- readonly invocation: Invocation | undefined,
220
- /** The partition of the topic: `admitsAll` gets it. */
221
- private readonly partition: string | undefined,
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
 
@@ -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
- public readonly code: string = "INTERNAL_ERROR",
394
- public readonly retryable: boolean = false
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
  }