@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.
- package/CHANGELOG.md +16 -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 +1 -1
- package/dist/browser.d.ts.map +1 -1
- 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 +1 -1
- package/dist/events/index.d.ts.map +1 -1
- package/dist/events/index.js.map +1 -1
- package/dist/events/publisher.d.ts.map +1 -1
- package/dist/events/publisher.js +4 -7
- 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/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 +1 -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 +1 -0
- package/src/events/publisher.ts +22 -9
- 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/types.ts +7 -2
|
@@ -1,82 +1,85 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* HTTP Status Code Constants
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* All HTTP status codes.
|
|
5
5
|
* NO MAGIC NUMBERS - use these constants everywhere!
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
export
|
|
8
|
+
export const HTTPStatus = {
|
|
9
9
|
// 1xx Informational
|
|
10
|
-
CONTINUE
|
|
11
|
-
SWITCHING_PROTOCOLS
|
|
12
|
-
PROCESSING
|
|
13
|
-
EARLY_HINTS
|
|
10
|
+
CONTINUE: 100,
|
|
11
|
+
SWITCHING_PROTOCOLS: 101,
|
|
12
|
+
PROCESSING: 102,
|
|
13
|
+
EARLY_HINTS: 103,
|
|
14
14
|
|
|
15
15
|
// 2xx Success
|
|
16
|
-
OK
|
|
17
|
-
CREATED
|
|
18
|
-
ACCEPTED
|
|
19
|
-
NON_AUTHORITATIVE_INFORMATION
|
|
20
|
-
NO_CONTENT
|
|
21
|
-
RESET_CONTENT
|
|
22
|
-
PARTIAL_CONTENT
|
|
23
|
-
MULTI_STATUS
|
|
24
|
-
ALREADY_REPORTED
|
|
25
|
-
IM_USED
|
|
16
|
+
OK: 200,
|
|
17
|
+
CREATED: 201,
|
|
18
|
+
ACCEPTED: 202,
|
|
19
|
+
NON_AUTHORITATIVE_INFORMATION: 203,
|
|
20
|
+
NO_CONTENT: 204,
|
|
21
|
+
RESET_CONTENT: 205,
|
|
22
|
+
PARTIAL_CONTENT: 206,
|
|
23
|
+
MULTI_STATUS: 207,
|
|
24
|
+
ALREADY_REPORTED: 208,
|
|
25
|
+
IM_USED: 226,
|
|
26
26
|
|
|
27
27
|
// 3xx Redirection
|
|
28
|
-
MULTIPLE_CHOICES
|
|
29
|
-
MOVED_PERMANENTLY
|
|
30
|
-
FOUND
|
|
31
|
-
SEE_OTHER
|
|
32
|
-
NOT_MODIFIED
|
|
33
|
-
USE_PROXY
|
|
34
|
-
TEMPORARY_REDIRECT
|
|
35
|
-
PERMANENT_REDIRECT
|
|
28
|
+
MULTIPLE_CHOICES: 300,
|
|
29
|
+
MOVED_PERMANENTLY: 301,
|
|
30
|
+
FOUND: 302,
|
|
31
|
+
SEE_OTHER: 303,
|
|
32
|
+
NOT_MODIFIED: 304,
|
|
33
|
+
USE_PROXY: 305,
|
|
34
|
+
TEMPORARY_REDIRECT: 307,
|
|
35
|
+
PERMANENT_REDIRECT: 308,
|
|
36
36
|
|
|
37
37
|
// 4xx Client Errors
|
|
38
|
-
BAD_REQUEST
|
|
39
|
-
UNAUTHORIZED
|
|
40
|
-
PAYMENT_REQUIRED
|
|
41
|
-
FORBIDDEN
|
|
42
|
-
NOT_FOUND
|
|
43
|
-
METHOD_NOT_ALLOWED
|
|
44
|
-
NOT_ACCEPTABLE
|
|
45
|
-
PROXY_AUTHENTICATION_REQUIRED
|
|
46
|
-
REQUEST_TIMEOUT
|
|
47
|
-
CONFLICT
|
|
48
|
-
GONE
|
|
49
|
-
LENGTH_REQUIRED
|
|
50
|
-
PRECONDITION_FAILED
|
|
51
|
-
PAYLOAD_TOO_LARGE
|
|
52
|
-
URI_TOO_LONG
|
|
53
|
-
UNSUPPORTED_MEDIA_TYPE
|
|
54
|
-
RANGE_NOT_SATISFIABLE
|
|
55
|
-
EXPECTATION_FAILED
|
|
56
|
-
IM_A_TEAPOT
|
|
57
|
-
MISDIRECTED_REQUEST
|
|
58
|
-
UNPROCESSABLE_ENTITY
|
|
59
|
-
LOCKED
|
|
60
|
-
FAILED_DEPENDENCY
|
|
61
|
-
TOO_EARLY
|
|
62
|
-
UPGRADE_REQUIRED
|
|
63
|
-
PRECONDITION_REQUIRED
|
|
64
|
-
TOO_MANY_REQUESTS
|
|
65
|
-
REQUEST_HEADER_FIELDS_TOO_LARGE
|
|
66
|
-
UNAVAILABLE_FOR_LEGAL_REASONS
|
|
67
|
-
CLIENT_CLOSED_REQUEST
|
|
38
|
+
BAD_REQUEST: 400,
|
|
39
|
+
UNAUTHORIZED: 401,
|
|
40
|
+
PAYMENT_REQUIRED: 402,
|
|
41
|
+
FORBIDDEN: 403,
|
|
42
|
+
NOT_FOUND: 404,
|
|
43
|
+
METHOD_NOT_ALLOWED: 405,
|
|
44
|
+
NOT_ACCEPTABLE: 406,
|
|
45
|
+
PROXY_AUTHENTICATION_REQUIRED: 407,
|
|
46
|
+
REQUEST_TIMEOUT: 408,
|
|
47
|
+
CONFLICT: 409,
|
|
48
|
+
GONE: 410,
|
|
49
|
+
LENGTH_REQUIRED: 411,
|
|
50
|
+
PRECONDITION_FAILED: 412,
|
|
51
|
+
PAYLOAD_TOO_LARGE: 413,
|
|
52
|
+
URI_TOO_LONG: 414,
|
|
53
|
+
UNSUPPORTED_MEDIA_TYPE: 415,
|
|
54
|
+
RANGE_NOT_SATISFIABLE: 416,
|
|
55
|
+
EXPECTATION_FAILED: 417,
|
|
56
|
+
IM_A_TEAPOT: 418,
|
|
57
|
+
MISDIRECTED_REQUEST: 421,
|
|
58
|
+
UNPROCESSABLE_ENTITY: 422,
|
|
59
|
+
LOCKED: 423,
|
|
60
|
+
FAILED_DEPENDENCY: 424,
|
|
61
|
+
TOO_EARLY: 425,
|
|
62
|
+
UPGRADE_REQUIRED: 426,
|
|
63
|
+
PRECONDITION_REQUIRED: 428,
|
|
64
|
+
TOO_MANY_REQUESTS: 429,
|
|
65
|
+
REQUEST_HEADER_FIELDS_TOO_LARGE: 431,
|
|
66
|
+
UNAVAILABLE_FOR_LEGAL_REASONS: 451,
|
|
67
|
+
CLIENT_CLOSED_REQUEST: 499, // nginx-specific
|
|
68
68
|
|
|
69
69
|
// 5xx Server Errors
|
|
70
|
-
INTERNAL_SERVER_ERROR
|
|
71
|
-
NOT_IMPLEMENTED
|
|
72
|
-
BAD_GATEWAY
|
|
73
|
-
SERVICE_UNAVAILABLE
|
|
74
|
-
GATEWAY_TIMEOUT
|
|
75
|
-
HTTP_VERSION_NOT_SUPPORTED
|
|
76
|
-
VARIANT_ALSO_NEGOTIATES
|
|
77
|
-
INSUFFICIENT_STORAGE
|
|
78
|
-
LOOP_DETECTED
|
|
79
|
-
NOT_EXTENDED
|
|
80
|
-
NETWORK_AUTHENTICATION_REQUIRED
|
|
81
|
-
NETWORK_CONNECT_TIMEOUT
|
|
82
|
-
}
|
|
70
|
+
INTERNAL_SERVER_ERROR: 500,
|
|
71
|
+
NOT_IMPLEMENTED: 501,
|
|
72
|
+
BAD_GATEWAY: 502,
|
|
73
|
+
SERVICE_UNAVAILABLE: 503,
|
|
74
|
+
GATEWAY_TIMEOUT: 504,
|
|
75
|
+
HTTP_VERSION_NOT_SUPPORTED: 505,
|
|
76
|
+
VARIANT_ALSO_NEGOTIATES: 506,
|
|
77
|
+
INSUFFICIENT_STORAGE: 507,
|
|
78
|
+
LOOP_DETECTED: 508,
|
|
79
|
+
NOT_EXTENDED: 510,
|
|
80
|
+
NETWORK_AUTHENTICATION_REQUIRED: 511,
|
|
81
|
+
NETWORK_CONNECT_TIMEOUT: 599, // non-standard
|
|
82
|
+
} as const;
|
|
83
|
+
|
|
84
|
+
/** This type is the union of the values of `HTTPStatus`. */
|
|
85
|
+
export type HTTPStatus = (typeof HTTPStatus)[keyof typeof HTTPStatus];
|
|
@@ -8,39 +8,68 @@
|
|
|
8
8
|
/**
|
|
9
9
|
* Error severity levels.
|
|
10
10
|
*/
|
|
11
|
-
export
|
|
11
|
+
export const ErrorSeverity = {
|
|
12
12
|
/** Informational - not an actual error */
|
|
13
|
-
INFO
|
|
13
|
+
INFO: "info",
|
|
14
14
|
/** Warning - degraded but functional */
|
|
15
|
-
WARNING
|
|
15
|
+
WARNING: "warning",
|
|
16
16
|
/** Error - operation failed */
|
|
17
|
-
ERROR
|
|
17
|
+
ERROR: "error",
|
|
18
18
|
/** Critical - system-level failure */
|
|
19
|
-
CRITICAL
|
|
19
|
+
CRITICAL: "critical",
|
|
20
|
+
} as const;
|
|
21
|
+
|
|
22
|
+
/** This type is the union of the values of `ErrorSeverity`. */
|
|
23
|
+
export type ErrorSeverity = (typeof ErrorSeverity)[keyof typeof ErrorSeverity];
|
|
24
|
+
|
|
25
|
+
/** These types keep each member of `ErrorSeverity` usable as a type. */
|
|
26
|
+
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
27
|
+
export declare namespace ErrorSeverity {
|
|
28
|
+
export type INFO = typeof ErrorSeverity.INFO;
|
|
29
|
+
export type WARNING = typeof ErrorSeverity.WARNING;
|
|
30
|
+
export type ERROR = typeof ErrorSeverity.ERROR;
|
|
31
|
+
export type CRITICAL = typeof ErrorSeverity.CRITICAL;
|
|
20
32
|
}
|
|
21
33
|
|
|
22
34
|
/**
|
|
23
35
|
* Error categories for classification.
|
|
24
36
|
*/
|
|
25
|
-
export
|
|
37
|
+
export const ErrorCategory = {
|
|
26
38
|
/** Network-level errors (connectivity, DNS, etc.) */
|
|
27
|
-
NETWORK
|
|
39
|
+
NETWORK: "network",
|
|
28
40
|
/** Client-side errors (validation, configuration) */
|
|
29
|
-
CLIENT
|
|
41
|
+
CLIENT: "client",
|
|
30
42
|
/** Server-side errors (5xx, internal errors) */
|
|
31
|
-
SERVER
|
|
43
|
+
SERVER: "server",
|
|
32
44
|
/** Protocol-level errors (parsing, encoding) */
|
|
33
|
-
PROTOCOL
|
|
45
|
+
PROTOCOL: "protocol",
|
|
34
46
|
/** Authentication/authorization errors */
|
|
35
|
-
AUTH
|
|
47
|
+
AUTH: "auth",
|
|
36
48
|
/** Rate limiting and throttling */
|
|
37
|
-
RATE_LIMIT
|
|
49
|
+
RATE_LIMIT: "rate_limit",
|
|
38
50
|
/** Timeout errors */
|
|
39
|
-
TIMEOUT
|
|
51
|
+
TIMEOUT: "timeout",
|
|
40
52
|
/** Cancellation errors */
|
|
41
|
-
CANCELLED
|
|
53
|
+
CANCELLED: "cancelled",
|
|
42
54
|
/** Unknown/uncategorized errors */
|
|
43
|
-
UNKNOWN
|
|
55
|
+
UNKNOWN: "unknown",
|
|
56
|
+
} as const;
|
|
57
|
+
|
|
58
|
+
/** This type is the union of the values of `ErrorCategory`. */
|
|
59
|
+
export type ErrorCategory = (typeof ErrorCategory)[keyof typeof ErrorCategory];
|
|
60
|
+
|
|
61
|
+
/** These types keep each member of `ErrorCategory` usable as a type. */
|
|
62
|
+
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
63
|
+
export declare namespace ErrorCategory {
|
|
64
|
+
export type NETWORK = typeof ErrorCategory.NETWORK;
|
|
65
|
+
export type CLIENT = typeof ErrorCategory.CLIENT;
|
|
66
|
+
export type SERVER = typeof ErrorCategory.SERVER;
|
|
67
|
+
export type PROTOCOL = typeof ErrorCategory.PROTOCOL;
|
|
68
|
+
export type AUTH = typeof ErrorCategory.AUTH;
|
|
69
|
+
export type RATE_LIMIT = typeof ErrorCategory.RATE_LIMIT;
|
|
70
|
+
export type TIMEOUT = typeof ErrorCategory.TIMEOUT;
|
|
71
|
+
export type CANCELLED = typeof ErrorCategory.CANCELLED;
|
|
72
|
+
export type UNKNOWN = typeof ErrorCategory.UNKNOWN;
|
|
44
73
|
}
|
|
45
74
|
|
|
46
75
|
/**
|
|
@@ -116,13 +116,18 @@ export interface CircuitBreakerStats {
|
|
|
116
116
|
* Circuit breaker error thrown when circuit is open.
|
|
117
117
|
*/
|
|
118
118
|
export class CircuitBreakerError extends Error {
|
|
119
|
+
public readonly state: CircuitState;
|
|
120
|
+
public readonly lastError: Error | null;
|
|
121
|
+
|
|
119
122
|
constructor(
|
|
120
|
-
|
|
121
|
-
|
|
123
|
+
state: CircuitState,
|
|
124
|
+
lastError: Error | null
|
|
122
125
|
) {
|
|
123
126
|
super(
|
|
124
127
|
`Circuit breaker is ${state}${lastError ? `: ${lastError.message}` : ""}`
|
|
125
128
|
);
|
|
129
|
+
this.state = state;
|
|
130
|
+
this.lastError = lastError;
|
|
126
131
|
this.name = "CircuitBreakerError";
|
|
127
132
|
}
|
|
128
133
|
}
|
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
* `hashMap` of that package).
|
|
9
9
|
*/
|
|
10
10
|
export class LruMap<K, V> {
|
|
11
|
+
private readonly capacity: number;
|
|
12
|
+
private readonly onEvict?: ((key: K, value: V) => void) | undefined;
|
|
13
|
+
|
|
11
14
|
private readonly entries = new Map<K, V>();
|
|
12
15
|
|
|
13
16
|
/**
|
|
@@ -15,9 +18,11 @@ export class LruMap<K, V> {
|
|
|
15
18
|
* @param onEvict - Called for each key that a write removes to keep the capacity.
|
|
16
19
|
*/
|
|
17
20
|
constructor(
|
|
18
|
-
|
|
19
|
-
|
|
21
|
+
capacity: number,
|
|
22
|
+
onEvict?: (key: K, value: V) => void
|
|
20
23
|
) {
|
|
24
|
+
this.capacity = capacity;
|
|
25
|
+
this.onEvict = onEvict;
|
|
21
26
|
if (!Number.isInteger(capacity) || capacity < 1) {
|
|
22
27
|
throw new RangeError(`The capacity of an LRU map must be a positive whole number, not ${capacity}`);
|
|
23
28
|
}
|
|
@@ -89,11 +89,14 @@ export interface RateLimitStats {
|
|
|
89
89
|
* Rate limit error thrown when limit exceeded.
|
|
90
90
|
*/
|
|
91
91
|
export class RateLimitError extends Error {
|
|
92
|
+
public readonly stats: RateLimitStats;
|
|
93
|
+
|
|
92
94
|
constructor(
|
|
93
95
|
message: string,
|
|
94
|
-
|
|
96
|
+
stats: RateLimitStats
|
|
95
97
|
) {
|
|
96
98
|
super(message);
|
|
99
|
+
this.stats = stats;
|
|
97
100
|
this.name = "RateLimitError";
|
|
98
101
|
}
|
|
99
102
|
}
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* call goes to the server as `procedure-timeout` with each try (`client/deadline.ts`).
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
+
import { compose } from "@mark1russell7/collections/layers";
|
|
19
20
|
import type { ClientMiddleware, ClientRunner, TypedClientMiddleware } from "../types.js";
|
|
20
21
|
import type { CacheContext, CircuitBreakerContext, RateLimitContext, RetryContext, TimeoutContext } from "./contexts.js";
|
|
21
22
|
import { createCacheMiddleware, type CacheOptions } from "./cache.js";
|
|
@@ -76,6 +77,6 @@ export function resilience(options: ResilienceOptions): TypedClientMiddleware<Re
|
|
|
76
77
|
layers.push(createTimeoutMiddleware({ perAttempt: options.perAttempt, ...(message ? { message: `${message} (per-attempt)` } : {}) }) as ClientMiddleware);
|
|
77
78
|
}
|
|
78
79
|
const composed = <TReq, TRes>(next: ClientRunner<TReq, TRes>): ClientRunner<TReq, TRes> =>
|
|
79
|
-
|
|
80
|
+
compose(...(layers as ClientMiddleware<TReq, TRes>[]))(next);
|
|
80
81
|
return composed as TypedClientMiddleware<ResilienceContext, {}>;
|
|
81
82
|
}
|
|
@@ -99,7 +99,11 @@ export interface RouteResolutionResult {
|
|
|
99
99
|
* ```
|
|
100
100
|
*/
|
|
101
101
|
export class RouteResolver {
|
|
102
|
-
|
|
102
|
+
private readonly registry: ProcedureRegistry;
|
|
103
|
+
|
|
104
|
+
constructor(registry: ProcedureRegistry) {
|
|
105
|
+
this.registry = registry;
|
|
106
|
+
}
|
|
103
107
|
|
|
104
108
|
/**
|
|
105
109
|
* Resolve a route structure to procedures.
|
package/src/client/types.ts
CHANGED
|
@@ -326,6 +326,9 @@ export type AccumulateContext<
|
|
|
326
326
|
* Client error thrown when response has error status.
|
|
327
327
|
*/
|
|
328
328
|
export class ClientError extends Error {
|
|
329
|
+
public readonly status: Extract<Status, { type: "error" }>;
|
|
330
|
+
public readonly responseId: string;
|
|
331
|
+
|
|
329
332
|
/**
|
|
330
333
|
* The metadata of the error response, for example `reason` of a `FORBIDDEN` error, or
|
|
331
334
|
* `conflicts` of a `VERSION_CONFLICT` error (roadmap CLI-16). Empty when the response has none.
|
|
@@ -333,11 +336,13 @@ export class ClientError extends Error {
|
|
|
333
336
|
public readonly metadata: Metadata;
|
|
334
337
|
|
|
335
338
|
constructor(
|
|
336
|
-
|
|
337
|
-
|
|
339
|
+
status: Extract<Status, { type: "error" }>,
|
|
340
|
+
responseId: string,
|
|
338
341
|
metadata: Metadata = {},
|
|
339
342
|
) {
|
|
340
343
|
super(status.message);
|
|
344
|
+
this.status = status;
|
|
345
|
+
this.responseId = responseId;
|
|
341
346
|
this.name = "ClientError";
|
|
342
347
|
this.metadata = metadata;
|
|
343
348
|
}
|
|
@@ -114,18 +114,25 @@ export type ValidationPhase = "input" | "output";
|
|
|
114
114
|
* Error thrown when validation fails.
|
|
115
115
|
*/
|
|
116
116
|
export class ValidationError extends Error {
|
|
117
|
+
/** Which phase of validation failed */
|
|
118
|
+
public readonly phase: ValidationPhase;
|
|
119
|
+
/** The underlying Zod error */
|
|
120
|
+
public readonly zodError: ZodErrorLike;
|
|
121
|
+
/** The method being called */
|
|
122
|
+
public readonly method: Method;
|
|
123
|
+
|
|
117
124
|
override readonly name = "ValidationError";
|
|
118
125
|
|
|
119
126
|
constructor(
|
|
120
127
|
message: string,
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
public readonly zodError: ZodErrorLike,
|
|
125
|
-
/** The method being called */
|
|
126
|
-
public readonly method: Method,
|
|
128
|
+
phase: ValidationPhase,
|
|
129
|
+
zodError: ZodErrorLike,
|
|
130
|
+
method: Method,
|
|
127
131
|
) {
|
|
128
132
|
super(message);
|
|
133
|
+
this.phase = phase;
|
|
134
|
+
this.zodError = zodError;
|
|
135
|
+
this.method = method;
|
|
129
136
|
|
|
130
137
|
// Maintain proper stack trace in V8 environments
|
|
131
138
|
if (Error.captureStackTrace) {
|
package/src/events/bus.ts
CHANGED
|
@@ -11,6 +11,7 @@ import type {
|
|
|
11
11
|
EventHandler,
|
|
12
12
|
Unsubscribe,
|
|
13
13
|
EventBusOptions,
|
|
14
|
+
EventFanOutOptions,
|
|
14
15
|
EventListenOptions,
|
|
15
16
|
EventWaitOptions,
|
|
16
17
|
} from "./types.js";
|
|
@@ -34,6 +35,31 @@ function clearedError(channel: string): Error {
|
|
|
34
35
|
return error;
|
|
35
36
|
}
|
|
36
37
|
|
|
38
|
+
/** The default of `fanOut.maxPending`. No bench chose it: a run of a slow channel can change it. */
|
|
39
|
+
const DEFAULT_MAX_PENDING = 1024;
|
|
40
|
+
|
|
41
|
+
/** The default of `fanOut.schedule`: a later task of the event loop. */
|
|
42
|
+
function nextTask(slice: () => void): void {
|
|
43
|
+
if (typeof setImmediate === "function") setImmediate(slice);
|
|
44
|
+
else setTimeout(slice, 0);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The handlers of a channel, each with the number of its subscription, in the order of these numbers. */
|
|
48
|
+
type ChannelHandlers = Map<EventHandler<unknown>, number>;
|
|
49
|
+
|
|
50
|
+
/** One event that waits for its slices. */
|
|
51
|
+
interface PendingEvent {
|
|
52
|
+
readonly channel: string;
|
|
53
|
+
readonly data: unknown;
|
|
54
|
+
/** The handlers of the channel at `emit()`. A new map of the channel gets no old event. */
|
|
55
|
+
readonly handlers: ChannelHandlers;
|
|
56
|
+
/** The live cursor: a removed handler gets no call, with no copy of the handlers per event. */
|
|
57
|
+
readonly cursor: Iterator<[EventHandler<unknown>, number]>;
|
|
58
|
+
/** The last subscription at `emit()`. A later handler does not get this event. */
|
|
59
|
+
readonly lastSubscription: number;
|
|
60
|
+
done: boolean;
|
|
61
|
+
}
|
|
62
|
+
|
|
37
63
|
// =============================================================================
|
|
38
64
|
// Event Bus Implementation
|
|
39
65
|
// =============================================================================
|
|
@@ -43,16 +69,34 @@ function clearedError(channel: string): Error {
|
|
|
43
69
|
* Supports pub/sub messaging with async iteration.
|
|
44
70
|
*/
|
|
45
71
|
export class DefaultEventBus implements EventBus {
|
|
46
|
-
private readonly subscribers = new Map<string,
|
|
72
|
+
private readonly subscribers = new Map<string, ChannelHandlers>();
|
|
73
|
+
/** The number of the last subscription of this bus. */
|
|
74
|
+
private lastSubscription = 0;
|
|
75
|
+
/** The events that wait for their slices, oldest first, from the index `pendingHead`. */
|
|
76
|
+
private pending: PendingEvent[] = [];
|
|
77
|
+
private pendingHead = 0;
|
|
78
|
+
/** For each channel, the number of its events in `pending`. */
|
|
79
|
+
private readonly pendingOn = new Map<string, number>();
|
|
80
|
+
private sliceScheduled = false;
|
|
81
|
+
private drainWaiters: Array<() => void> = [];
|
|
47
82
|
private readonly buffer = new Map<string, unknown[]>();
|
|
48
83
|
/**
|
|
49
84
|
* For each channel, the functions that `clear()` runs: the `onClear` of each handler of `on()`
|
|
50
85
|
* that has one. Each open stream has one too, so `clear()` ends it.
|
|
51
86
|
*/
|
|
52
87
|
private readonly clearHandlers = new Map<string, Set<() => void>>();
|
|
53
|
-
private readonly options: Required<EventBusOptions
|
|
88
|
+
private readonly options: Required<Omit<EventBusOptions, "fanOut">>;
|
|
89
|
+
private readonly fanOut: Required<EventFanOutOptions> | undefined;
|
|
54
90
|
|
|
55
91
|
constructor(options: EventBusOptions = {}) {
|
|
92
|
+
const { fanOut } = options;
|
|
93
|
+
if (fanOut && (!Number.isInteger(fanOut.sliceSize) || fanOut.sliceSize < 1)) {
|
|
94
|
+
throw new RangeError(`The option fanOut.sliceSize must be a whole number from 1 up, not ${fanOut.sliceSize}`);
|
|
95
|
+
}
|
|
96
|
+
if (fanOut?.maxPending !== undefined && (!Number.isInteger(fanOut.maxPending) || fanOut.maxPending < 1)) {
|
|
97
|
+
throw new RangeError(`The option fanOut.maxPending must be a whole number from 1 up, not ${fanOut.maxPending}`);
|
|
98
|
+
}
|
|
99
|
+
this.fanOut = fanOut && { maxPending: DEFAULT_MAX_PENDING, schedule: nextTask, ...fanOut };
|
|
56
100
|
this.options = {
|
|
57
101
|
bufferSize: options.bufferSize ?? 0,
|
|
58
102
|
onError: options.onError ?? ((error, channel) => {
|
|
@@ -64,6 +108,12 @@ export class DefaultEventBus implements EventBus {
|
|
|
64
108
|
|
|
65
109
|
/**
|
|
66
110
|
* Emit an event on a channel.
|
|
111
|
+
*
|
|
112
|
+
* Without the option `fanOut`, each handler runs before this method returns. With it, a channel
|
|
113
|
+
* with more than `fanOut.sliceSize` handlers delivers later, in slices (roadmap CLI-90). One
|
|
114
|
+
* handler gets the events of a channel in the order of `emit()`. An unsubscribe stops the calls
|
|
115
|
+
* that wait for that handler. A handler that comes after `emit()` does not get the event. The
|
|
116
|
+
* events of two different channels can reach a handler in another order.
|
|
67
117
|
*/
|
|
68
118
|
emit<T>(channel: string, data: T): void {
|
|
69
119
|
if (this.options.debug) {
|
|
@@ -87,19 +137,104 @@ export class DefaultEventBus implements EventBus {
|
|
|
87
137
|
const channelSubs = this.subscribers.get(channel);
|
|
88
138
|
if (!channelSubs) return;
|
|
89
139
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
140
|
+
if (this.fanOut && (channelSubs.size > this.fanOut.sliceSize || this.pendingOn.has(channel))) {
|
|
141
|
+
this.enqueue(channel, data, channelSubs, this.fanOut);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
for (const handler of channelSubs.keys()) this.call(handler, data, channel);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** This function calls one handler. An error goes to `onError`, and it stops no other handler. */
|
|
149
|
+
private call(handler: EventHandler<unknown>, data: unknown, channel: string): void {
|
|
150
|
+
try {
|
|
151
|
+
const result = handler(data);
|
|
152
|
+
// Handle async handlers
|
|
153
|
+
if (result instanceof Promise) {
|
|
154
|
+
result.catch((error) => this.options.onError(error, channel));
|
|
155
|
+
}
|
|
156
|
+
} catch (error) {
|
|
157
|
+
this.options.onError(error instanceof Error ? error : new Error(String(error)), channel);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** This function puts an event in the queue of the slices. A full queue finishes its oldest events. */
|
|
162
|
+
private enqueue(channel: string, data: unknown, handlers: ChannelHandlers, fanOut: Required<EventFanOutOptions>): void {
|
|
163
|
+
this.pending.push({ channel, data, handlers, cursor: handlers.entries(), lastSubscription: this.lastSubscription, done: false });
|
|
164
|
+
this.pendingOn.set(channel, (this.pendingOn.get(channel) ?? 0) + 1);
|
|
165
|
+
while (this.pending.length - this.pendingHead > fanOut.maxPending) {
|
|
166
|
+
const oldest = this.pending[this.pendingHead]!;
|
|
167
|
+
this.deliverPart(oldest, Number.POSITIVE_INFINITY);
|
|
168
|
+
this.retire(oldest);
|
|
169
|
+
}
|
|
170
|
+
this.scheduleSlice(fanOut);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private scheduleSlice(fanOut: Required<EventFanOutOptions>): void {
|
|
174
|
+
if (this.sliceScheduled || this.pendingHead === this.pending.length) return;
|
|
175
|
+
this.sliceScheduled = true;
|
|
176
|
+
fanOut.schedule(() => {
|
|
177
|
+
this.sliceScheduled = false;
|
|
178
|
+
this.deliver(fanOut.sliceSize);
|
|
179
|
+
this.scheduleSlice(fanOut);
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** This function calls at most `limit` handlers of the oldest events. */
|
|
184
|
+
private deliver(limit: number): void {
|
|
185
|
+
let left = limit;
|
|
186
|
+
while (left > 0 && this.pendingHead < this.pending.length) {
|
|
187
|
+
const event = this.pending[this.pendingHead]!;
|
|
188
|
+
left -= this.deliverPart(event, left);
|
|
189
|
+
if (event.done) this.retire(event);
|
|
190
|
+
}
|
|
191
|
+
if (this.pendingHead === this.pending.length) this.drainWaiters.splice(0).forEach((resolve) => resolve());
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** This function calls at most `limit` handlers of one event, and returns the count of calls. */
|
|
195
|
+
private deliverPart(event: PendingEvent, limit: number): number {
|
|
196
|
+
let calls = 0;
|
|
197
|
+
while (calls < limit && !event.done) {
|
|
198
|
+
// clear(), or the last unsubscribe: the map of this event is not the map of the channel now
|
|
199
|
+
const step = this.subscribers.get(event.channel) === event.handlers ? event.cursor.next() : undefined;
|
|
200
|
+
if (!step || step.done || step.value[1] > event.lastSubscription) {
|
|
201
|
+
event.done = true;
|
|
202
|
+
break;
|
|
99
203
|
}
|
|
204
|
+
this.call(step.value[0], event.data, event.channel);
|
|
205
|
+
calls++;
|
|
206
|
+
}
|
|
207
|
+
return calls;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* This function takes a finished event from the queue. A handler in a slice can emit, and that
|
|
212
|
+
* emit can finish the event first, so this function examines the head.
|
|
213
|
+
*/
|
|
214
|
+
private retire(event: PendingEvent): void {
|
|
215
|
+
if (this.pending[this.pendingHead] !== event) return;
|
|
216
|
+
this.pendingHead++;
|
|
217
|
+
const left = (this.pendingOn.get(event.channel) ?? 1) - 1;
|
|
218
|
+
if (left === 0) this.pendingOn.delete(event.channel);
|
|
219
|
+
else this.pendingOn.set(event.channel, left);
|
|
220
|
+
if (this.pendingHead === this.pending.length) {
|
|
221
|
+
this.pending = [];
|
|
222
|
+
this.pendingHead = 0;
|
|
223
|
+
} else if (this.pendingHead > 1024 && this.pendingHead * 2 > this.pending.length) {
|
|
224
|
+
this.pending = this.pending.slice(this.pendingHead);
|
|
225
|
+
this.pendingHead = 0;
|
|
100
226
|
}
|
|
101
227
|
}
|
|
102
228
|
|
|
229
|
+
/**
|
|
230
|
+
* This method resolves when no event waits for a slice. A host calls it before it stops. Without
|
|
231
|
+
* the option `fanOut`, it resolves at once.
|
|
232
|
+
*/
|
|
233
|
+
drained(): Promise<void> {
|
|
234
|
+
if (this.pendingHead === this.pending.length) return Promise.resolve();
|
|
235
|
+
return new Promise((resolve) => this.drainWaiters.push(resolve));
|
|
236
|
+
}
|
|
237
|
+
|
|
103
238
|
/**
|
|
104
239
|
* Subscribe to events on a channel. The `onClear` of the options runs one time when `clear()`
|
|
105
240
|
* or `clearAll()` removes the handler, not at an unsubscribe.
|
|
@@ -111,11 +246,14 @@ export class DefaultEventBus implements EventBus {
|
|
|
111
246
|
|
|
112
247
|
let channelSubs = this.subscribers.get(channel);
|
|
113
248
|
if (!channelSubs) {
|
|
114
|
-
channelSubs = new
|
|
249
|
+
channelSubs = new Map();
|
|
115
250
|
this.subscribers.set(channel, channelSubs);
|
|
116
251
|
}
|
|
117
252
|
|
|
118
|
-
|
|
253
|
+
// A handler that is already on the channel keeps its place and its number
|
|
254
|
+
if (!channelSubs.has(handler as EventHandler<unknown>)) {
|
|
255
|
+
channelSubs.set(handler as EventHandler<unknown>, ++this.lastSubscription);
|
|
256
|
+
}
|
|
119
257
|
|
|
120
258
|
// The unsubscribe function works once, and removes only the set it added to. (Before, a
|
|
121
259
|
// repeated call of an old unsubscribe removed the set of a newer subscriber: deep dive CORE-15.)
|
|
@@ -144,16 +282,7 @@ export class DefaultEventBus implements EventBus {
|
|
|
144
282
|
// Replay buffered events if any
|
|
145
283
|
const buffered = this.buffer.get(channel);
|
|
146
284
|
if (buffered && buffered.length > 0) {
|
|
147
|
-
for (const data of buffered)
|
|
148
|
-
try {
|
|
149
|
-
const result = handler(data as T);
|
|
150
|
-
if (result instanceof Promise) {
|
|
151
|
-
result.catch((error) => this.options.onError(error, channel));
|
|
152
|
-
}
|
|
153
|
-
} catch (error) {
|
|
154
|
-
this.options.onError(error instanceof Error ? error : new Error(String(error)), channel);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
285
|
+
for (const data of buffered) this.call(handler as EventHandler<unknown>, data, channel);
|
|
157
286
|
}
|
|
158
287
|
|
|
159
288
|
return () => {
|