@aztec/foundation 0.86.0 → 0.87.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 (216) hide show
  1. package/dest/array/array.d.ts.map +1 -1
  2. package/dest/bigint/index.d.ts +2 -0
  3. package/dest/bigint/index.d.ts.map +1 -1
  4. package/dest/bigint/index.js +6 -0
  5. package/dest/bigint-buffer/index.d.ts +0 -2
  6. package/dest/bigint-buffer/index.d.ts.map +1 -1
  7. package/dest/buffer/buffer16.d.ts +80 -0
  8. package/dest/buffer/buffer16.d.ts.map +1 -0
  9. package/dest/buffer/buffer16.js +100 -0
  10. package/dest/buffer/buffer32.d.ts +3 -7
  11. package/dest/buffer/buffer32.d.ts.map +1 -1
  12. package/dest/buffer/buffer32.js +6 -6
  13. package/dest/buffer/index.d.ts +1 -0
  14. package/dest/buffer/index.d.ts.map +1 -1
  15. package/dest/buffer/index.js +1 -0
  16. package/dest/collection/array.d.ts +0 -2
  17. package/dest/collection/array.d.ts.map +1 -1
  18. package/dest/collection/object.d.ts +2 -0
  19. package/dest/collection/object.d.ts.map +1 -1
  20. package/dest/collection/object.js +8 -0
  21. package/dest/config/env_var.d.ts +1 -1
  22. package/dest/config/env_var.d.ts.map +1 -1
  23. package/dest/crypto/aes128/index.d.ts +2 -4
  24. package/dest/crypto/aes128/index.d.ts.map +1 -1
  25. package/dest/crypto/ecdsa/index.d.ts +0 -2
  26. package/dest/crypto/ecdsa/index.d.ts.map +1 -1
  27. package/dest/crypto/ecdsa/signature.d.ts +1 -3
  28. package/dest/crypto/ecdsa/signature.d.ts.map +1 -1
  29. package/dest/crypto/grumpkin/index.d.ts +0 -2
  30. package/dest/crypto/grumpkin/index.d.ts.map +1 -1
  31. package/dest/crypto/keccak/index.d.ts +2 -4
  32. package/dest/crypto/keccak/index.d.ts.map +1 -1
  33. package/dest/crypto/keys/index.d.ts +0 -2
  34. package/dest/crypto/keys/index.d.ts.map +1 -1
  35. package/dest/crypto/pedersen/pedersen.noble.d.ts +2 -4
  36. package/dest/crypto/pedersen/pedersen.noble.d.ts.map +1 -1
  37. package/dest/crypto/pedersen/pedersen.wasm.d.ts +2 -4
  38. package/dest/crypto/pedersen/pedersen.wasm.d.ts.map +1 -1
  39. package/dest/crypto/poseidon/index.d.ts +0 -2
  40. package/dest/crypto/poseidon/index.d.ts.map +1 -1
  41. package/dest/crypto/random/index.d.ts +1 -3
  42. package/dest/crypto/random/index.d.ts.map +1 -1
  43. package/dest/crypto/random/randomness_singleton.d.ts +0 -2
  44. package/dest/crypto/random/randomness_singleton.d.ts.map +1 -1
  45. package/dest/crypto/schnorr/signature.d.ts +3 -5
  46. package/dest/crypto/schnorr/signature.d.ts.map +1 -1
  47. package/dest/crypto/secp256k1/index.d.ts +4 -6
  48. package/dest/crypto/secp256k1/index.d.ts.map +1 -1
  49. package/dest/crypto/secp256k1-signer/utils.d.ts +0 -2
  50. package/dest/crypto/secp256k1-signer/utils.d.ts.map +1 -1
  51. package/dest/crypto/serialize.d.ts +4 -6
  52. package/dest/crypto/serialize.d.ts.map +1 -1
  53. package/dest/crypto/sha256/index.d.ts +2 -4
  54. package/dest/crypto/sha256/index.d.ts.map +1 -1
  55. package/dest/crypto/sha512/index.d.ts +1 -3
  56. package/dest/crypto/sha512/index.d.ts.map +1 -1
  57. package/dest/crypto/signature/index.d.ts +0 -2
  58. package/dest/crypto/signature/index.d.ts.map +1 -1
  59. package/dest/crypto/sync/pedersen/index.d.ts +2 -4
  60. package/dest/crypto/sync/pedersen/index.d.ts.map +1 -1
  61. package/dest/crypto/sync/poseidon/index.d.ts +0 -2
  62. package/dest/crypto/sync/poseidon/index.d.ts.map +1 -1
  63. package/dest/decorators/memoize.d.ts.map +1 -1
  64. package/dest/eth-address/index.d.ts +2 -5
  65. package/dest/eth-address/index.d.ts.map +1 -1
  66. package/dest/eth-signature/eth_signature.d.ts +0 -2
  67. package/dest/eth-signature/eth_signature.d.ts.map +1 -1
  68. package/dest/fields/coordinate.d.ts +0 -2
  69. package/dest/fields/coordinate.d.ts.map +1 -1
  70. package/dest/fields/fields.d.ts +0 -3
  71. package/dest/fields/fields.d.ts.map +1 -1
  72. package/dest/fields/point.d.ts +2 -4
  73. package/dest/fields/point.d.ts.map +1 -1
  74. package/dest/fields/point.js +2 -2
  75. package/dest/iterable/filter.js +1 -1
  76. package/dest/iterable/map.js +1 -1
  77. package/dest/json-rpc/client/fetch.d.ts +2 -2
  78. package/dest/json-rpc/client/fetch.d.ts.map +1 -1
  79. package/dest/json-rpc/client/fetch.js +16 -30
  80. package/dest/json-rpc/client/safe_json_rpc_client.d.ts +7 -3
  81. package/dest/json-rpc/client/safe_json_rpc_client.d.ts.map +1 -1
  82. package/dest/json-rpc/client/safe_json_rpc_client.js +135 -13
  83. package/dest/json-rpc/client/undici.d.ts.map +1 -1
  84. package/dest/json-rpc/client/undici.js +6 -7
  85. package/dest/json-rpc/convert.d.ts +1 -1
  86. package/dest/json-rpc/convert.d.ts.map +1 -1
  87. package/dest/json-rpc/convert.js +1 -1
  88. package/dest/json-rpc/errors.d.ts +4 -0
  89. package/dest/json-rpc/errors.d.ts.map +1 -0
  90. package/dest/json-rpc/errors.js +6 -0
  91. package/dest/json-rpc/fixtures/class_a.d.ts +5 -3
  92. package/dest/json-rpc/fixtures/class_a.d.ts.map +1 -1
  93. package/dest/json-rpc/fixtures/class_b.d.ts +5 -3
  94. package/dest/json-rpc/fixtures/class_b.d.ts.map +1 -1
  95. package/dest/json-rpc/index.d.ts +1 -0
  96. package/dest/json-rpc/index.d.ts.map +1 -1
  97. package/dest/json-rpc/index.js +1 -0
  98. package/dest/json-rpc/js_utils.d.ts.map +1 -1
  99. package/dest/json-rpc/server/safe_json_rpc_server.d.ts +2 -1
  100. package/dest/json-rpc/server/safe_json_rpc_server.d.ts.map +1 -1
  101. package/dest/json-rpc/server/safe_json_rpc_server.js +112 -39
  102. package/dest/json-rpc/test/integration.d.ts +0 -1
  103. package/dest/json-rpc/test/integration.d.ts.map +1 -1
  104. package/dest/log/console.d.ts.map +1 -1
  105. package/dest/log/gcloud-logger-config.d.ts.map +1 -1
  106. package/dest/log/gcloud-logger-config.js +0 -4
  107. package/dest/log/pino-logger.d.ts +1 -2
  108. package/dest/log/pino-logger.d.ts.map +1 -1
  109. package/dest/message/index.d.ts.map +1 -1
  110. package/dest/mutex/mutex_database.d.ts.map +1 -1
  111. package/dest/queue/bounded_serial_queue.d.ts.map +1 -1
  112. package/dest/schemas/api.d.ts.map +1 -1
  113. package/dest/schemas/parse.js +1 -1
  114. package/dest/schemas/schemas.d.ts +3 -5
  115. package/dest/schemas/schemas.d.ts.map +1 -1
  116. package/dest/schemas/utils.d.ts +1 -3
  117. package/dest/schemas/utils.d.ts.map +1 -1
  118. package/dest/serialize/buffer_reader.d.ts +9 -2
  119. package/dest/serialize/buffer_reader.d.ts.map +1 -1
  120. package/dest/serialize/buffer_reader.js +15 -2
  121. package/dest/serialize/field_reader.d.ts +2 -1
  122. package/dest/serialize/field_reader.d.ts.map +1 -1
  123. package/dest/serialize/field_reader.js +4 -1
  124. package/dest/serialize/free_funcs.d.ts +15 -9
  125. package/dest/serialize/free_funcs.d.ts.map +1 -1
  126. package/dest/serialize/free_funcs.js +11 -0
  127. package/dest/serialize/serialize.d.ts +3 -5
  128. package/dest/serialize/serialize.d.ts.map +1 -1
  129. package/dest/string/index.d.ts +2 -2
  130. package/dest/string/index.d.ts.map +1 -1
  131. package/dest/string/index.js +6 -0
  132. package/dest/testing/files/index.d.ts +0 -2
  133. package/dest/testing/files/index.d.ts.map +1 -1
  134. package/dest/timer/timeout.d.ts.map +1 -1
  135. package/dest/transport/dispatch/create_dispatch_fn.d.ts.map +1 -1
  136. package/dest/transport/interface/connector.d.ts.map +1 -1
  137. package/dest/transport/interface/listener.d.ts +0 -1
  138. package/dest/transport/interface/listener.d.ts.map +1 -1
  139. package/dest/transport/interface/socket.d.ts.map +1 -1
  140. package/dest/transport/node/node_connector.d.ts +0 -1
  141. package/dest/transport/node/node_connector.d.ts.map +1 -1
  142. package/dest/transport/node/node_connector_socket.d.ts +0 -1
  143. package/dest/transport/node/node_connector_socket.d.ts.map +1 -1
  144. package/dest/transport/node/node_listener.d.ts +0 -1
  145. package/dest/transport/node/node_listener.d.ts.map +1 -1
  146. package/dest/transport/node/node_listener_socket.d.ts +0 -1
  147. package/dest/transport/node/node_listener_socket.d.ts.map +1 -1
  148. package/dest/transport/transport_client.d.ts +0 -1
  149. package/dest/transport/transport_client.d.ts.map +1 -1
  150. package/dest/transport/transport_server.d.ts.map +1 -1
  151. package/dest/trees/hasher.d.ts +4 -6
  152. package/dest/trees/hasher.d.ts.map +1 -1
  153. package/dest/trees/indexed_merkle_tree.d.ts +0 -2
  154. package/dest/trees/indexed_merkle_tree.d.ts.map +1 -1
  155. package/dest/trees/indexed_merkle_tree_calculator.d.ts +1 -3
  156. package/dest/trees/indexed_merkle_tree_calculator.d.ts.map +1 -1
  157. package/dest/trees/indexed_tree_leaf.d.ts +0 -2
  158. package/dest/trees/indexed_tree_leaf.d.ts.map +1 -1
  159. package/dest/trees/membership_witness.d.ts +1 -3
  160. package/dest/trees/membership_witness.d.ts.map +1 -1
  161. package/dest/trees/merkle_tree.d.ts +0 -2
  162. package/dest/trees/merkle_tree.d.ts.map +1 -1
  163. package/dest/trees/merkle_tree_calculator.d.ts +1 -3
  164. package/dest/trees/merkle_tree_calculator.d.ts.map +1 -1
  165. package/dest/trees/sibling_path.d.ts +6 -8
  166. package/dest/trees/sibling_path.d.ts.map +1 -1
  167. package/dest/trees/unbalanced_merkle_tree.d.ts +0 -2
  168. package/dest/trees/unbalanced_merkle_tree.d.ts.map +1 -1
  169. package/dest/types/index.d.ts +0 -2
  170. package/dest/types/index.d.ts.map +1 -1
  171. package/dest/url/index.d.ts.map +1 -1
  172. package/dest/url/index.js +1 -1
  173. package/package.json +16 -15
  174. package/src/bigint/index.ts +8 -0
  175. package/src/buffer/buffer16.ts +133 -0
  176. package/src/buffer/buffer32.ts +8 -6
  177. package/src/buffer/index.ts +1 -0
  178. package/src/collection/object.ts +10 -0
  179. package/src/config/env_var.ts +13 -3
  180. package/src/fields/point.ts +0 -2
  181. package/src/iterable/filter.ts +1 -1
  182. package/src/iterable/map.ts +1 -1
  183. package/src/json-rpc/client/fetch.ts +14 -33
  184. package/src/json-rpc/client/safe_json_rpc_client.ts +171 -13
  185. package/src/json-rpc/client/undici.ts +6 -13
  186. package/src/json-rpc/convert.ts +2 -2
  187. package/src/json-rpc/errors.ts +6 -0
  188. package/src/json-rpc/fixtures/class_a.ts +4 -1
  189. package/src/json-rpc/fixtures/class_b.ts +4 -1
  190. package/src/json-rpc/index.ts +1 -0
  191. package/src/json-rpc/server/safe_json_rpc_server.ts +78 -25
  192. package/src/log/console.ts +4 -1
  193. package/src/log/gcloud-logger-config.ts +2 -2
  194. package/src/log/pino-logger.ts +2 -2
  195. package/src/message/index.ts +5 -1
  196. package/src/mutex/mutex_database.ts +2 -3
  197. package/src/queue/bounded_serial_queue.ts +4 -1
  198. package/src/schemas/api.ts +4 -4
  199. package/src/schemas/parse.ts +1 -1
  200. package/src/serialize/buffer_reader.ts +23 -3
  201. package/src/serialize/field_reader.ts +11 -3
  202. package/src/serialize/free_funcs.ts +13 -0
  203. package/src/string/index.ts +9 -1
  204. package/src/timer/timeout.ts +5 -1
  205. package/src/transport/interface/connector.ts +0 -1
  206. package/src/transport/interface/listener.ts +2 -3
  207. package/src/transport/interface/socket.ts +2 -3
  208. package/src/transport/transport_client.ts +3 -4
  209. package/src/transport/transport_server.ts +4 -1
  210. package/src/trees/hasher.ts +4 -4
  211. package/src/trees/indexed_merkle_tree.ts +5 -1
  212. package/src/trees/indexed_merkle_tree_calculator.ts +2 -2
  213. package/src/trees/merkle_tree.ts +4 -1
  214. package/src/trees/merkle_tree_calculator.ts +8 -3
  215. package/src/types/index.ts +0 -5
  216. package/src/url/index.ts +0 -1
@@ -1,27 +1,58 @@
1
1
  import { format } from 'util';
2
2
 
3
3
  import { type Logger, createLogger } from '../../log/pino-logger.js';
4
+ import { type PromiseWithResolvers, promiseWithResolvers } from '../../promise/utils.js';
4
5
  import { type ApiSchema, type ApiSchemaFor, schemaHasMethod } from '../../schemas/api.js';
5
6
  import { type JsonRpcFetch, defaultFetch } from './fetch.js';
6
7
 
8
+ // batch window of 0 would capture all requests in the current sync iteration of the event loop
9
+ // and send them all at once in a single batch
10
+ // minimal latency
11
+ const DEFAULT_BATCH_WINDOW_MS = 0;
12
+
7
13
  export type SafeJsonRpcClientOptions = {
8
- useApiEndpoints?: boolean;
9
14
  namespaceMethods?: string | false;
10
15
  fetch?: JsonRpcFetch;
11
16
  log?: Logger;
17
+ batchWindowMS?: number;
12
18
  onResponse?: (res: {
13
19
  response: any;
14
20
  headers: { get: (header: string) => string | null | undefined };
15
21
  }) => Promise<void>;
16
22
  };
17
23
 
24
+ type JsonRpcRequest = {
25
+ jsonrpc: '2.0';
26
+ id?: number;
27
+ method: string;
28
+ params?: Array<any>;
29
+ };
30
+
31
+ type JsonRpcResponse =
32
+ | {
33
+ jsonrpc: '2.0';
34
+ id?: number;
35
+ result: any;
36
+ }
37
+ | {
38
+ jsonrpc: '2.0';
39
+ id?: number;
40
+ error: { code: number; data?: any; message: string };
41
+ };
42
+
43
+ // expose helpful information on the RPC clients such that we can recognize them later
44
+ const SEND_BATCH = Symbol('JsonRpcClient.sendBatch');
45
+ const CLIENT_ID = Symbol('JsonRpcClient.clientId');
46
+
47
+ let nextClientId = 1;
48
+ // keep a reference to clients so that we can force send a batch
49
+ const clients = new Map<number, WeakRef<{ [SEND_BATCH]: () => Promise<void> }>>();
50
+
18
51
  /**
19
52
  * Creates a Proxy object that delegates over RPC and validates outputs against a given schema.
20
53
  * The server is expected to be a JsonRpcServer.
21
54
  * @param host - The host URL.
22
55
  * @param schema - The api schema to validate returned data against.
23
- * @param useApiEndpoints - Whether to use the API endpoints or the default RPC endpoint.
24
- * @param namespaceMethods - String value (or false/empty) to namespace all methods sent to the server. e.g. 'getInfo' -\> 'pxe_getInfo'
25
56
  * @param fetch - The fetch implementation to use.
26
57
  */
27
58
  export function createSafeJsonRpcClient<T extends object>(
@@ -31,25 +62,118 @@ export function createSafeJsonRpcClient<T extends object>(
31
62
  ): T {
32
63
  const fetch = config.fetch ?? defaultFetch;
33
64
  const log = config.log ?? createLogger('json-rpc:client');
34
- const { useApiEndpoints = false, namespaceMethods = false } = config;
65
+ const { namespaceMethods = false, batchWindowMS = DEFAULT_BATCH_WINDOW_MS } = config;
35
66
 
36
67
  let id = 0;
68
+ let sendBatchTimeoutHandle: NodeJS.Timeout | undefined;
69
+ let queue: Array<{ request: JsonRpcRequest; deferred: PromiseWithResolvers<JsonRpcResponse> }> = [];
70
+
71
+ const sendBatch = async () => {
72
+ if (sendBatchTimeoutHandle !== undefined) {
73
+ clearTimeout(sendBatchTimeoutHandle);
74
+ sendBatchTimeoutHandle = undefined;
75
+ }
76
+
77
+ const rpcCalls = queue;
78
+ queue = [];
79
+
80
+ if (rpcCalls.length === 0) {
81
+ return;
82
+ }
83
+
84
+ log.debug(`Executing JSON-RPC batch of size: ${rpcCalls.length}`, {
85
+ methods: rpcCalls.map(({ request }) => request.method),
86
+ });
87
+ try {
88
+ const { response, headers } = await fetch(
89
+ host,
90
+ rpcCalls.map(({ request }) => request),
91
+ );
92
+
93
+ if (config.onResponse) {
94
+ await config.onResponse({ response, headers });
95
+ }
96
+
97
+ if (!Array.isArray(response) || response.length !== rpcCalls.length) {
98
+ log.warn(
99
+ `Invalid response received from JSON-RPC server. Expected array of responses of length ${rpcCalls.length}`,
100
+ { response },
101
+ );
102
+ for (let i = 0; i < rpcCalls.length; i++) {
103
+ const { request, deferred } = rpcCalls[i];
104
+ deferred.resolve({
105
+ id: request.id,
106
+ jsonrpc: '2.0',
107
+ error: {
108
+ code: -32000,
109
+ data: response,
110
+ message: response.message ?? 'Failed request',
111
+ },
112
+ });
113
+ }
114
+ } else {
115
+ for (let i = 0; i < response.length; i++) {
116
+ const resp: JsonRpcResponse = response[i];
117
+ const { request, deferred } = rpcCalls[i];
118
+
119
+ if (resp.id !== request.id) {
120
+ log.warn(`Invalid response received at index ${i} from JSON-RPC server: id mismatch`, {
121
+ requestMethod: request.method,
122
+ requestId: request.id,
123
+ responseId: resp.id,
124
+ });
125
+ deferred.resolve({
126
+ id: request.id,
127
+ jsonrpc: '2.0',
128
+ error: {
129
+ code: -32001,
130
+ data: resp,
131
+ message: 'RPC id mismatch',
132
+ },
133
+ });
134
+ } else {
135
+ deferred.resolve(resp);
136
+ }
137
+ }
138
+ }
139
+ } catch (err) {
140
+ log.warn(`Failed to fetch from the remote server`, err);
141
+ for (let i = 0; i < rpcCalls.length; i++) {
142
+ const { request, deferred } = rpcCalls[i];
143
+ deferred.resolve({
144
+ id: request.id,
145
+ jsonrpc: '2.0',
146
+ error: {
147
+ code: -32000,
148
+ data: err,
149
+ message: (err as any).message ?? 'Failed request',
150
+ },
151
+ });
152
+ }
153
+ }
154
+ };
155
+
37
156
  const request = async (methodName: string, params: any[]): Promise<any> => {
38
157
  if (!schemaHasMethod(schema, methodName)) {
39
158
  throw new Error(`Unspecified method ${methodName} in client schema`);
40
159
  }
41
160
  const method = namespaceMethods ? `${namespaceMethods}_${methodName}` : methodName;
42
- const body = { jsonrpc: '2.0', id: id++, method, params };
161
+ const body: JsonRpcRequest = { jsonrpc: '2.0', id: id++, method, params };
162
+
163
+ const deferred = promiseWithResolvers<JsonRpcResponse>();
164
+ queue.push({ request: body, deferred });
165
+
166
+ if (sendBatchTimeoutHandle === undefined) {
167
+ // eslint-disable-next-line @typescript-eslint/no-misused-promises
168
+ sendBatchTimeoutHandle = setTimeout(sendBatch, batchWindowMS);
169
+ }
43
170
 
44
171
  log.debug(format(`request`, method, params));
45
- const { response, headers } = await fetch(host, method, body, useApiEndpoints);
172
+ const response = await deferred.promise;
46
173
  log.debug(format(`result`, method, response));
47
174
 
48
- if (config.onResponse) {
49
- await config.onResponse({ response, headers });
50
- }
51
- if (response.error) {
52
- throw response.error;
175
+ if ('error' in response) {
176
+ throw new Error(response.error.message, { cause: response.error });
53
177
  }
54
178
  // TODO(palla/schemas): Find a better way to handle null responses (JSON.stringify(null) is string "null").
55
179
  if ([null, undefined, 'null', 'undefined'].includes(response.result)) {
@@ -58,10 +182,44 @@ export function createSafeJsonRpcClient<T extends object>(
58
182
  return (schema as ApiSchema)[methodName].returnType().parseAsync(response.result);
59
183
  };
60
184
 
61
- const proxy: any = {};
185
+ const clientId = nextClientId++;
186
+ const proxy: any = { [CLIENT_ID]: clientId, [SEND_BATCH]: sendBatch };
62
187
  for (const method of Object.keys(schema)) {
63
- proxy[method] = (...params: any[]) => request(method, params);
188
+ // attach the clientId to the promise so that if we want to trigger a batch immediately, we can do that
189
+ proxy[method] = (...params: any[]) => Object.assign(request(method, params), { [CLIENT_ID]: clientId });
64
190
  }
65
191
 
192
+ clients.set(clientId, new WeakRef(proxy));
193
+
66
194
  return proxy as T;
67
195
  }
196
+
197
+ /**
198
+ * Triggers a batch to be sent immediately
199
+ */
200
+ export async function batch<T extends readonly unknown[]>(
201
+ values: T,
202
+ ): Promise<{ -readonly [P in keyof T]: Awaited<T[P]> }> {
203
+ const clientIdsSeen = new Set<number>();
204
+
205
+ await Promise.allSettled(
206
+ values.map(val => {
207
+ if (typeof val === 'object' && val && Object.hasOwn(val, CLIENT_ID)) {
208
+ const clientId = (val as { [CLIENT_ID]: any })[CLIENT_ID];
209
+ if (typeof clientId === 'number') {
210
+ if (clientIdsSeen.has(clientId)) {
211
+ return;
212
+ }
213
+
214
+ clientIdsSeen.add(clientId);
215
+ const client = clients.get(clientId)?.deref();
216
+ if (client) {
217
+ return client[SEND_BATCH]();
218
+ }
219
+ }
220
+ }
221
+ }),
222
+ );
223
+
224
+ return Promise.all(values);
225
+ }
@@ -10,21 +10,14 @@ const log = createLogger('json-rpc:json_rpc_client:undici');
10
10
  export { Agent };
11
11
 
12
12
  export function makeUndiciFetch(client = new Agent()): JsonRpcFetch {
13
- return async (
14
- host: string,
15
- rpcMethod: string,
16
- body: any,
17
- useApiEndpoints: boolean,
18
- extraHeaders: Record<string, string> = {},
19
- noRetry = false,
20
- ) => {
21
- log.trace(`JsonRpcClient.fetch: ${host} ${rpcMethod}`, { host, rpcMethod, body });
13
+ return async (host: string, body: unknown, extraHeaders: Record<string, string> = {}, noRetry = false) => {
14
+ log.trace(`JsonRpcClient.fetch: ${host}`, { host, body });
22
15
  let resp: Dispatcher.ResponseData;
23
16
  try {
24
17
  resp = await client.request({
25
18
  method: 'POST',
26
19
  origin: new URL(host),
27
- path: useApiEndpoints ? rpcMethod : '/',
20
+ path: '/',
28
21
  body: jsonStringify(body),
29
22
  headers: {
30
23
  ...extraHeaders,
@@ -32,7 +25,7 @@ export function makeUndiciFetch(client = new Agent()): JsonRpcFetch {
32
25
  },
33
26
  });
34
27
  } catch (err) {
35
- const errorMessage = `Error fetching from host ${host} with method ${rpcMethod}: ${String(err)}`;
28
+ const errorMessage = `Error fetching from host ${host}: ${String(err)}`;
36
29
  throw new Error(errorMessage);
37
30
  }
38
31
 
@@ -40,7 +33,7 @@ export function makeUndiciFetch(client = new Agent()): JsonRpcFetch {
40
33
  const responseOk = resp.statusCode >= 200 && resp.statusCode <= 299;
41
34
  try {
42
35
  responseJson = await resp.body.json();
43
- } catch (err) {
36
+ } catch {
44
37
  if (!responseOk) {
45
38
  throw new Error('HTTP ' + resp.statusCode);
46
39
  }
@@ -48,7 +41,7 @@ export function makeUndiciFetch(client = new Agent()): JsonRpcFetch {
48
41
  }
49
42
 
50
43
  if (!responseOk) {
51
- const errorMessage = `Error ${resp.statusCode} response from server ${host} on ${rpcMethod}: ${responseJson.error.message}`;
44
+ const errorMessage = `Error ${resp.statusCode} response from server ${host}: ${responseJson}`;
52
45
  if (noRetry || (resp.statusCode >= 400 && resp.statusCode < 500)) {
53
46
  throw new NoRetryError(errorMessage);
54
47
  } else {
@@ -26,7 +26,7 @@ export function jsonParseWithSchemaSync<T>(json: string, schema: ZodFor<T>): T {
26
26
  * @param obj - The object to be stringified.
27
27
  * @returns The resulting string.
28
28
  */
29
- export function jsonStringify(obj: object, prettify?: boolean): string {
29
+ export function jsonStringify(obj: unknown, prettify?: boolean): string {
30
30
  return JSON.stringify(
31
31
  obj,
32
32
  (_key, value) => {
@@ -55,7 +55,7 @@ export function jsonStringify(obj: object, prettify?: boolean): string {
55
55
  export function tryJsonStringify(obj: any, prettify?: boolean): string | undefined {
56
56
  try {
57
57
  return jsonStringify(obj, prettify);
58
- } catch (e) {
58
+ } catch {
59
59
  return undefined;
60
60
  }
61
61
  }
@@ -0,0 +1,6 @@
1
+ export class BadRequestError extends Error {
2
+ constructor(message: string) {
3
+ super(message);
4
+ this.name = 'BadRequestError';
5
+ }
6
+ }
@@ -2,7 +2,10 @@
2
2
  * Test class for testing string converter.
3
3
  */
4
4
  export class ToStringClass {
5
- constructor(/** A value */ public readonly x: string, /** Another value */ public readonly y: string) {}
5
+ constructor(
6
+ /** A value */ public readonly x: string,
7
+ /** Another value */ public readonly y: string,
8
+ ) {}
6
9
 
7
10
  toString(): string {
8
11
  return [this.x, this.y].join('-');
@@ -2,7 +2,10 @@
2
2
  * Test class for testing string converter.
3
3
  */
4
4
  export class ToStringClass {
5
- constructor(/** A value */ public readonly x: string, /** Another value */ public readonly y: string) {}
5
+ constructor(
6
+ /** A value */ public readonly x: string,
7
+ /** Another value */ public readonly y: string,
8
+ ) {}
6
9
 
7
10
  toString(): string {
8
11
  return [this.x, this.y].join('-');
@@ -1 +1,2 @@
1
1
  export { jsonStringify, jsonParseWithSchema, jsonParseWithSchemaSync, tryJsonStringify } from './convert.js';
2
+ export { BadRequestError } from './errors.js';
@@ -63,20 +63,13 @@ export class SafeJsonRpcServer {
63
63
  await next();
64
64
  } catch (err: any) {
65
65
  const method = (ctx.request.body as any)?.method ?? 'unknown';
66
- this.log.warn(`Error in JSON RPC server call ${method}: ${inspect(err)}`);
67
- if (err instanceof SyntaxError) {
66
+ this.log.warn(`Uncaught error in JSON RPC server call ${method}: ${inspect(err)}`);
67
+ if (err && 'name' in err && err.name === 'BadRequestError') {
68
68
  ctx.status = 400;
69
- ctx.body = { jsonrpc: '2.0', id: null, error: { code: -32700, message: `Parse error: ${err.message}` } };
70
- } else if (err instanceof ZodError) {
71
- const message = err.issues.map(e => `${e.message} (${e.path.join('.')})`).join('. ') || 'Validation error';
69
+ ctx.body = { jsonrpc: '2.0', id: null, error: { code: -32000, message: `Bad request: ${err.message}` } };
70
+ } else if (err && err instanceof SyntaxError) {
72
71
  ctx.status = 400;
73
- ctx.body = { jsonrpc: '2.0', id: null, error: { code: -32701, message } };
74
- } else if (this.http200OnError) {
75
- ctx.body = {
76
- jsonrpc: '2.0',
77
- id: null,
78
- error: { code: err.code || -32600, data: err.data, message: err.message },
79
- };
72
+ ctx.body = { jsonrpc: '2.0', id: null, error: { code: -32700, message: `Parse error: ${err.message}` } };
80
73
  } else {
81
74
  ctx.status = 500;
82
75
  ctx.body = { jsonrpc: '2.0', id: null, error: { code: -32600, message: err.message ?? 'Internal error' } };
@@ -124,23 +117,77 @@ export class SafeJsonRpcServer {
124
117
  const router = new Router({ prefix });
125
118
  // "JSON RPC mode" where a single endpoint is used and the method is given in the request body
126
119
  router.post('/', async (ctx: Koa.Context) => {
127
- const { params = [], jsonrpc, id, method } = ctx.request.body as any;
128
- // Fail if not a registered function in the proxy
129
- if (typeof method !== 'string' || method === 'constructor' || !this.proxy.hasMethod(method)) {
130
- ctx.status = 400;
131
- const code = -32601;
132
- const message = `Method not found: ${method}`;
133
- ctx.body = { jsonrpc, id, error: { code, message } };
120
+ if (Array.isArray(ctx.request.body)) {
121
+ const resp = await this.processBatch(ctx.request.body);
122
+ if (Array.isArray(resp)) {
123
+ ctx.status = 200;
124
+ ctx.body = resp;
125
+ } else {
126
+ ctx.status = this.http200OnError ? 200 : 400;
127
+ ctx.body = resp;
128
+ }
134
129
  } else {
135
- ctx.status = 200;
136
- const result = await this.proxy.call(method, params);
137
- ctx.body = { jsonrpc, id, result };
130
+ const resp = await this.processRequest(ctx.request.body);
131
+ if ('error' in resp) {
132
+ ctx.status = this.http200OnError ? 200 : 400;
133
+ }
134
+
135
+ ctx.body = resp;
138
136
  }
139
137
  });
140
138
 
141
139
  return router;
142
140
  }
143
141
 
142
+ private async processBatch(requests: any[]) {
143
+ if (requests.length === 0) {
144
+ return { jsonrpc: '2.0', error: { code: -32600, message: 'Invalid Request' }, id: null };
145
+ }
146
+ const results = await Promise.allSettled(requests.map(req => this.processRequest(req)));
147
+ return results.map(res => {
148
+ if (res.status === 'fulfilled') {
149
+ return res.value;
150
+ }
151
+
152
+ this.log.warn(`Uncaught error executing request in batch: ${res.reason}.`);
153
+ return { jsonrpc: '2.0', error: { code: -32600, message: 'Invalid Request' }, id: null };
154
+ });
155
+ }
156
+
157
+ private async processRequest(request: any) {
158
+ if (!request || typeof request !== 'object') {
159
+ return { jsonrpc: '2.0', error: { code: -32600, message: 'Invalid Request' }, id: null };
160
+ }
161
+
162
+ const { params = [], jsonrpc, id, method } = request;
163
+ // Fail if not a registered function in the proxy
164
+ if (typeof method !== 'string' || method === 'constructor' || !this.proxy.hasMethod(method)) {
165
+ return { jsonrpc, id, error: { code: -32601, message: `Method not found: ${method}` } };
166
+ } else {
167
+ try {
168
+ const result = await this.proxy.call(method, params);
169
+ return { jsonrpc, id, result };
170
+ } catch (err: any) {
171
+ if (err && err instanceof ZodError) {
172
+ const message = err.issues.map(e => `${e.message} (${e.path.join('.')})`).join('. ') || 'Validation error';
173
+ return { jsonrpc: '2.0', id, error: { code: -32701, message } };
174
+ } else if (err) {
175
+ return {
176
+ jsonrpc,
177
+ id,
178
+ error: { code: -32702, data: err.data, message: err.message },
179
+ };
180
+ } else {
181
+ return {
182
+ jsonrpc,
183
+ id,
184
+ error: { code: -32702, message: 'Error executing request' },
185
+ };
186
+ }
187
+ }
188
+ }
189
+ }
190
+
144
191
  /**
145
192
  * Start this server with koa.
146
193
  * @param port - Port number.
@@ -201,7 +248,10 @@ export class SafeJsonProxy<T extends object = any> implements Proxy {
201
248
  private log = createLogger('json-rpc:proxy');
202
249
  private schema: ApiSchema;
203
250
 
204
- constructor(private handler: T, schema: ApiSchemaFor<T>) {
251
+ constructor(
252
+ private handler: T,
253
+ schema: ApiSchemaFor<T>,
254
+ ) {
205
255
  this.schema = schema;
206
256
  }
207
257
 
@@ -265,7 +315,10 @@ function makeAggregateHealthcheck(namedHandlers: NamespacedApiHandlers, log?: Lo
265
315
  return async () => {
266
316
  try {
267
317
  const results = await Promise.all(
268
- Object.entries(namedHandlers).map(([name, [, , healthCheck]]) => [name, healthCheck ? healthCheck() : true]),
318
+ Object.entries(namedHandlers).map(async ([name, [, , healthCheck]]) => [
319
+ name,
320
+ healthCheck ? await healthCheck() : true,
321
+ ]),
269
322
  );
270
323
  const failed = results.filter(([_, result]) => !result);
271
324
  if (failed.length > 0) {
@@ -326,7 +379,7 @@ export function createStatusRouter(getCurrentStatus: StatusCheckFn, apiPrefix =
326
379
  let ok: boolean;
327
380
  try {
328
381
  ok = (await getCurrentStatus()) === true;
329
- } catch (err) {
382
+ } catch {
330
383
  ok = false;
331
384
  }
332
385
 
@@ -7,7 +7,10 @@ import type { LogFn } from './log_fn.js';
7
7
  * which can be useful for controlling the format of the output or redirecting logs to a different destination.
8
8
  */
9
9
  class ConsoleLogger {
10
- constructor(private prefix: string, private logger: (...args: any[]) => void = console.log) {}
10
+ constructor(
11
+ private prefix: string,
12
+ private logger: (...args: any[]) => void = console.log,
13
+ ) {}
11
14
 
12
15
  /**
13
16
  * Log messages with the specified prefix using the provided logger.
@@ -39,7 +39,7 @@ export const GoogleCloudLoggerConfig = {
39
39
  // Severity labels https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#LogSeverity
40
40
  let severity: string;
41
41
 
42
- switch (label as pino.Level | keyof typeof customLevels) {
42
+ switch (label as pino.Level | keyof CustomLevels) {
43
43
  case 'trace':
44
44
  case 'debug':
45
45
  severity = 'DEBUG';
@@ -68,4 +68,4 @@ export const GoogleCloudLoggerConfig = {
68
68
  } satisfies pino.LoggerOptions;
69
69
 
70
70
  // Define custom logging levels for pino. Duplicate from pino-logger.ts.
71
- const customLevels = { verbose: 25 };
71
+ type CustomLevels = { verbose: 25 };
@@ -248,7 +248,7 @@ export function registerLoggingStream(stream: Writable): void {
248
248
  }
249
249
 
250
250
  /** Log function that accepts an exception object */
251
- type ErrorLogFn = (msg: string, err?: Error | unknown, data?: LogData) => void;
251
+ type ErrorLogFn = (msg: string, err?: unknown, data?: LogData) => void;
252
252
 
253
253
  /**
254
254
  * Logger that supports multiple severity levels.
@@ -265,6 +265,6 @@ export type Logger = { [K in LogLevel]: LogFn } & { /** Error log function */ er
265
265
  * @param err - Error to log
266
266
  * @returns A string with both the log message and the error message.
267
267
  */
268
- function formatErr(msg: string, err?: Error | unknown): string {
268
+ function formatErr(msg: string, err?: unknown): string {
269
269
  return err ? `${msg}: ${inspect(err)}` : msg;
270
270
  }
@@ -31,7 +31,11 @@ interface TypedMessageLike {
31
31
  }
32
32
 
33
33
  export class TypedMessage<T, B> {
34
- public constructor(public readonly msgType: T, public readonly header: MessageHeader, public readonly value: B) {}
34
+ public constructor(
35
+ public readonly msgType: T,
36
+ public readonly header: MessageHeader,
37
+ public readonly value: B,
38
+ ) {}
35
39
 
36
40
  static fromMessagePack<T, B>(data: TypedMessageLike): TypedMessage<T, B> {
37
41
  return new TypedMessage<T, B>(data['msgType'] as T, MessageHeader.fromMessagePack(data['header']), data['value']);
@@ -3,10 +3,9 @@
3
3
  * Provides functionality for acquiring, extending, and releasing locks on resources to ensure exclusive access and prevent conflicts in concurrent applications.
4
4
  */
5
5
  export interface MutexDatabase {
6
- // eslint-disable-next-line jsdoc/require-jsdoc
7
6
  acquireLock(name: string, timeout: number): Promise<boolean>;
8
- // eslint-disable-next-line jsdoc/require-jsdoc
7
+
9
8
  extendLock(name: string, timeout: number): Promise<void>;
10
- // eslint-disable-next-line jsdoc/require-jsdoc
9
+
11
10
  releaseLock(name: string): Promise<void>;
12
11
  }
@@ -10,7 +10,10 @@ export class BoundedSerialQueue {
10
10
  private readonly queue = new SerialQueue();
11
11
  private semaphore: Semaphore;
12
12
 
13
- constructor(maxQueueSize: number, private log = createLogger('foundation:bounded_serial_queue')) {
13
+ constructor(
14
+ maxQueueSize: number,
15
+ private log = createLogger('foundation:bounded_serial_queue'),
16
+ ) {
14
17
  this.semaphore = new Semaphore(maxQueueSize);
15
18
  }
16
19
 
@@ -19,10 +19,10 @@ type ZodReturnTypeFor<T> = z.ZodType<T, z.ZodTypeDef, any>;
19
19
  type ZodMapParameterTypes<T> = T extends []
20
20
  ? []
21
21
  : T extends [item: infer Head, ...infer Rest]
22
- ? [ZodParameterTypeFor<Head>, ...{ [K in keyof Rest]: ZodParameterTypeFor<Rest[K]> }]
23
- : T extends [item?: infer Head, ...infer Rest]
24
- ? [ZodNullableOptional<ZodParameterTypeFor<Head>>, ...{ [K in keyof Rest]: ZodParameterTypeFor<Rest[K]> }]
25
- : never;
22
+ ? [ZodParameterTypeFor<Head>, ...{ [K in keyof Rest]: ZodParameterTypeFor<Rest[K]> }]
23
+ : T extends [item?: infer Head, ...infer Rest]
24
+ ? [ZodNullableOptional<ZodParameterTypeFor<Head>>, ...{ [K in keyof Rest]: ZodParameterTypeFor<Rest[K]> }]
25
+ : never;
26
26
 
27
27
  /** Maps all functions in an interface to their schema representation. */
28
28
  export type ApiSchemaFor<T> = {
@@ -22,7 +22,7 @@ export function parseWithOptionals<T extends z.AnyZodTuple>(args: any[], schema:
22
22
  function isOptional(schema: z.ZodTypeAny) {
23
23
  try {
24
24
  return schema.isOptional();
25
- } catch (err) {
25
+ } catch {
26
26
  // See https://github.com/colinhacks/zod/issues/1911
27
27
  return schema._def.typeName === 'ZodOptional';
28
28
  }
@@ -19,7 +19,10 @@ import type { Tuple } from './types.js';
19
19
  */
20
20
  export class BufferReader {
21
21
  private index: number;
22
- constructor(private buffer: Buffer, offset = 0) {
22
+ constructor(
23
+ private buffer: Buffer,
24
+ offset = 0,
25
+ ) {
23
26
  this.index = offset;
24
27
  }
25
28
 
@@ -70,6 +73,23 @@ export class BufferReader {
70
73
  return result as Tuple<number, N>;
71
74
  }
72
75
 
76
+ /**
77
+ * Reads a 256-bit unsigned integer from the buffer at the current index position.
78
+ * Updates the index position by 32 bytes after reading the number.
79
+ *
80
+ * Assumes the number is stored in big-endian format.
81
+ *
82
+ * @returns The read 256 bit value as a bigint.
83
+ */
84
+ public readUInt64(): bigint {
85
+ this.#rangeCheck(8);
86
+
87
+ const result = this.buffer.readBigUInt64BE(this.index);
88
+
89
+ this.index += 8;
90
+ return result;
91
+ }
92
+
73
93
  /**
74
94
  * Reads a 256-bit unsigned integer from the buffer at the current index position.
75
95
  * Updates the index position by 32 bytes after reading the number.
@@ -82,8 +102,8 @@ export class BufferReader {
82
102
  this.#rangeCheck(32);
83
103
 
84
104
  let result = BigInt(0);
85
- for (let i = 0; i < 32; i++) {
86
- result = (result << BigInt(8)) | BigInt(this.buffer[this.index + i]);
105
+ for (let i = 0; i < 4; i++) {
106
+ result = (result << BigInt(64)) | this.buffer.readBigUInt64BE(this.index + i * 8);
87
107
  }
88
108
 
89
109
  this.index += 32;