rpc-express-toolkit 2.3.3 → 3.0.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/src/batch.js CHANGED
@@ -1,237 +1,256 @@
1
- "use strict";
2
-
3
- /**
4
- * Batch request handler for JSON-RPC 2.0
5
- */
6
- class BatchHandler {
7
- constructor(endpoint) {
8
- this.endpoint = endpoint;
9
- }
10
-
11
- /**
12
- * Check if request is a batch request
13
- * @param {any} body
14
- * @returns {boolean}
15
- */
16
- isBatchRequest(body) {
17
- return Array.isArray(body) && body.length > 0;
18
- }
19
-
20
- /**
21
- * Validate batch request
22
- * @param {Array} batch
23
- * @returns {Object}
24
- */
25
- validateBatch(batch) {
26
- if (!Array.isArray(batch)) {
27
- return {
28
- valid: false,
29
- error: { code: -32600, message: 'Invalid Request: Batch must be an array' }
30
- };
31
- }
32
-
33
- if (batch.length === 0) {
34
- return {
35
- valid: false,
36
- error: { code: -32600, message: 'Invalid Request: Batch cannot be empty' }
37
- };
38
- }
39
-
40
- // Check for duplicate IDs in the batch
41
- const ids = batch.map(req => req.id).filter(id => id !== undefined && id !== null);
42
- const uniqueIds = new Set(ids);
43
-
44
- if (ids.length !== uniqueIds.size) {
45
- return {
46
- valid: false,
47
- error: { code: -32600, message: 'Invalid Request: Duplicate IDs in batch' }
48
- };
49
- }
50
-
51
- return { valid: true };
52
- }
53
-
54
- /**
55
- * Process a batch of JSON-RPC requests
56
- * @param {Array} batch
57
- * @param {Object} req
58
- * @param {Object} res
59
- * @param {any} context
60
- * @returns {Promise<Array>}
61
- */
62
- async processBatch(batch, req, res, context) {
63
- const validation = this.validateBatch(batch);
64
- if (!validation.valid) {
65
- return [{
66
- jsonrpc: '2.0',
67
- id: null,
68
- error: validation.error
69
- }];
70
- }
71
-
72
- // Process all requests in parallel
73
- const promises = batch.map(async (request, index) => {
74
- try {
75
- return await this.processSingleRequest(request, req, context, index);
76
- } catch (error) {
77
- return {
78
- jsonrpc: '2.0',
79
- id: request.id || null,
80
- error: {
81
- code: error.code || -32603,
82
- message: error.message || 'Internal error',
83
- data: { batchIndex: index }
84
- }
85
- };
86
- }
87
- });
88
-
89
- const results = await Promise.all(promises);
90
-
91
- // Filter out notifications (requests without id)
92
- return results.filter(result => result !== null);
93
- }
94
-
95
- /**
96
- * Process a single request within a batch
97
- * @param {Object} request
98
- * @param {Object} req
99
- * @param {any} context
100
- * @param {number} batchIndex
101
- * @returns {Promise<Object|null>}
102
- */
103
- async processSingleRequest(request, req, context, batchIndex) {
104
- const { jsonrpc, method, params, id } = request;
105
-
106
- // Validate JSON-RPC 2.0 request structure
107
- if (jsonrpc !== "2.0") {
108
- return {
109
- jsonrpc: '2.0',
110
- id: id || null,
111
- error: {
112
- code: -32600,
113
- message: `Invalid Request: 'jsonrpc' must be '2.0'`,
114
- data: { batchIndex }
115
- }
116
- };
117
- }
118
-
119
- if (typeof method !== "string") {
120
- return {
121
- jsonrpc: '2.0',
122
- id: id || null,
123
- error: {
124
- code: -32600,
125
- message: `Invalid Request: 'method' must be a string`,
126
- data: { batchIndex }
127
- }
128
- };
129
- }
130
-
131
- const handler = this.endpoint.methods[method];
132
- if (!handler) {
133
- return {
134
- jsonrpc: '2.0',
135
- id: id || null,
136
- error: {
137
- code: -32601,
138
- message: `Method "${method}" not found`,
139
- data: { batchIndex }
140
- }
141
- };
142
- }
143
-
144
- // If no id is provided, this is a notification - don't return response
145
- const isNotification = id === undefined || id === null;
146
-
147
- try {
148
- // Execute middleware
149
- let middlewareContext = {
150
- req,
151
- res: null, // No response object for batch items
152
- method,
153
- params,
154
- context,
155
- batchIndex,
156
- isNotification
157
- };
158
-
159
- if (this.endpoint.middleware) {
160
- middlewareContext = await this.endpoint.middleware.execute('beforeCall', middlewareContext);
161
- }
162
-
163
- // Execute the handler
164
- const result = await Promise.resolve(handler(req, context, middlewareContext.params));
165
-
166
- // Execute after middleware
167
- if (this.endpoint.middleware) {
168
- middlewareContext.result = result;
169
- await this.endpoint.middleware.execute('afterCall', middlewareContext);
170
- }
171
-
172
- // For notifications, return null (will be filtered out)
173
- if (isNotification) {
174
- return null;
175
- }
176
-
177
- // Serialize the result
178
- const safeResult = this.endpoint.serializeBigIntsAndDates(result);
179
-
180
- return {
181
- jsonrpc: '2.0',
182
- id,
183
- result: safeResult
184
- };
185
-
186
- } catch (error) {
187
- // Execute error middleware
188
- if (this.endpoint.middleware) {
189
- try {
190
- await this.endpoint.middleware.execute('onError', {
191
- ...middlewareContext,
192
- error,
193
- batchIndex
194
- });
195
- } catch (middlewareError) {
196
- // Ignore middleware errors in error handling
197
- }
198
- }
199
-
200
- // For notifications, return null even on error
201
- if (isNotification) {
202
- return null;
203
- }
204
-
205
- return {
206
- jsonrpc: '2.0',
207
- id: id || null,
208
- error: {
209
- code: error.code || -32603,
210
- message: error.message || 'Internal error',
211
- data: { batchIndex }
212
- }
213
- };
214
- }
215
- }
216
-
217
- /**
218
- * Get batch statistics
219
- * @param {Array} batch
220
- * @returns {Object}
221
- */
222
- getBatchStats(batch) {
223
- const notifications = batch.filter(req => req.id === undefined || req.id === null).length;
224
- const requests = batch.length - notifications;
225
- const methods = [...new Set(batch.map(req => req.method))];
226
-
227
- return {
228
- total: batch.length,
229
- requests,
230
- notifications,
231
- uniqueMethods: methods.length,
232
- methods
233
- };
234
- }
235
- }
236
-
237
- module.exports = BatchHandler;
1
+ /**
2
+ * @file BatchHandler Class
3
+ * @description Handles JSON-RPC 2.0 batch requests processing multiple requests in a single call
4
+ */
5
+ class BatchHandler {
6
+ constructor(endpoint) {
7
+ this.endpoint = endpoint;
8
+ }
9
+
10
+ /**
11
+ * Check if request is a batch request
12
+ * @param {any} body
13
+ * @returns {boolean}
14
+ */
15
+ isBatchRequest(body) {
16
+ return Array.isArray(body) && body.length > 0;
17
+ }
18
+
19
+ /**
20
+ * Validate batch request
21
+ * @param {Array} batch
22
+ * @returns {Object}
23
+ */
24
+ validateBatch(batch) {
25
+ if (!Array.isArray(batch)) {
26
+ return {
27
+ valid: false,
28
+ error: {
29
+ code: -32600,
30
+ message: 'Invalid Request: Batch must be an array',
31
+ },
32
+ };
33
+ }
34
+
35
+ if (batch.length === 0) {
36
+ return {
37
+ valid: false,
38
+ error: {
39
+ code: -32600,
40
+ message: 'Invalid Request: Batch cannot be empty',
41
+ },
42
+ };
43
+ }
44
+
45
+ // Check for duplicate IDs in the batch
46
+ const ids = batch
47
+ .map((req) => req.id)
48
+ .filter((id) => id !== undefined && id !== null);
49
+ const uniqueIds = new Set(ids);
50
+
51
+ if (ids.length !== uniqueIds.size) {
52
+ return {
53
+ valid: false,
54
+ error: {
55
+ code: -32600,
56
+ message: 'Invalid Request: Duplicate IDs in batch',
57
+ },
58
+ };
59
+ }
60
+
61
+ return { valid: true };
62
+ }
63
+
64
+ /**
65
+ * Process a batch of JSON-RPC requests
66
+ * @param {Array} batch
67
+ * @param {Object} req
68
+ * @param {Object} res
69
+ * @param {any} context
70
+ * @returns {Promise<Array>}
71
+ */
72
+ async processBatch(batch, req, res, context) {
73
+ const validation = this.validateBatch(batch);
74
+ if (!validation.valid) {
75
+ return [
76
+ {
77
+ jsonrpc: '2.0',
78
+ id: null,
79
+ error: validation.error,
80
+ },
81
+ ];
82
+ }
83
+
84
+ // Process all requests in parallel
85
+ const promises = batch.map(async (request, index) => {
86
+ try {
87
+ return await this.processSingleRequest(request, req, context, index);
88
+ } catch (error) {
89
+ return {
90
+ jsonrpc: '2.0',
91
+ id: request.id || null,
92
+ error: {
93
+ code: error.code || -32603,
94
+ message: error.message || 'Internal error',
95
+ data: { batchIndex: index },
96
+ },
97
+ };
98
+ }
99
+ });
100
+
101
+ const results = await Promise.all(promises);
102
+
103
+ // Filter out notifications (requests without id)
104
+ return results.filter((result) => result !== null);
105
+ }
106
+
107
+ /**
108
+ * Process a single request within a batch
109
+ * @param {Object} request
110
+ * @param {Object} req
111
+ * @param {any} context
112
+ * @param {number} batchIndex
113
+ * @returns {Promise<Object|null>}
114
+ */
115
+ async processSingleRequest(request, req, context, batchIndex) {
116
+ const { jsonrpc, method, params, id } = request;
117
+
118
+ // Validate JSON-RPC 2.0 request structure
119
+ if (jsonrpc !== '2.0') {
120
+ return {
121
+ jsonrpc: '2.0',
122
+ id: id || null,
123
+ error: {
124
+ code: -32600,
125
+ message: `Invalid Request: 'jsonrpc' must be '2.0'`,
126
+ data: { batchIndex },
127
+ },
128
+ };
129
+ }
130
+
131
+ if (typeof method !== 'string') {
132
+ return {
133
+ jsonrpc: '2.0',
134
+ id: id || null,
135
+ error: {
136
+ code: -32600,
137
+ message: `Invalid Request: 'method' must be a string`,
138
+ data: { batchIndex },
139
+ },
140
+ };
141
+ }
142
+
143
+ const handler = this.endpoint.methods[method];
144
+ if (!handler) {
145
+ return {
146
+ jsonrpc: '2.0',
147
+ id: id || null,
148
+ error: {
149
+ code: -32601,
150
+ message: `Method "${method}" not found`,
151
+ data: { batchIndex },
152
+ },
153
+ };
154
+ }
155
+
156
+ // If no id is provided, this is a notification - don't return response
157
+ const isNotification = id === undefined || id === null;
158
+
159
+ // Initialize middleware context
160
+ let middlewareContext = {
161
+ req,
162
+ res: null, // No response object for batch items
163
+ method,
164
+ params,
165
+ context,
166
+ batchIndex,
167
+ isNotification,
168
+ };
169
+
170
+ try {
171
+ // Execute middleware
172
+ if (this.endpoint.middleware) {
173
+ middlewareContext = await this.endpoint.middleware.execute(
174
+ 'beforeCall',
175
+ middlewareContext
176
+ );
177
+ }
178
+
179
+ // Execute the handler
180
+ const result = await Promise.resolve(
181
+ handler(req, context, middlewareContext.params)
182
+ );
183
+
184
+ // Execute after middleware
185
+ if (this.endpoint.middleware) {
186
+ middlewareContext.result = result;
187
+ await this.endpoint.middleware.execute('afterCall', middlewareContext);
188
+ }
189
+
190
+ // For notifications, return null (will be filtered out)
191
+ if (isNotification) {
192
+ return null;
193
+ }
194
+
195
+ // Serialize the result
196
+ const safeResult = this.endpoint.serializeBigIntsAndDates(result);
197
+
198
+ return {
199
+ jsonrpc: '2.0',
200
+ id,
201
+ result: safeResult,
202
+ };
203
+ } catch (error) {
204
+ // Execute error middleware
205
+ if (this.endpoint.middleware) {
206
+ try {
207
+ await this.endpoint.middleware.execute('onError', {
208
+ ...middlewareContext,
209
+ error,
210
+ batchIndex,
211
+ });
212
+ } catch (middlewareError) {
213
+ // Ignore middleware errors in error handling
214
+ }
215
+ }
216
+
217
+ // For notifications, return null even on error
218
+ if (isNotification) {
219
+ return null;
220
+ }
221
+
222
+ return {
223
+ jsonrpc: '2.0',
224
+ id: id || null,
225
+ error: {
226
+ code: error.code || -32603,
227
+ message: error.message || 'Internal error',
228
+ data: { batchIndex },
229
+ },
230
+ };
231
+ }
232
+ }
233
+
234
+ /**
235
+ * Get batch statistics
236
+ * @param {Array} batch
237
+ * @returns {Object}
238
+ */
239
+ getBatchStats(batch) {
240
+ const notifications = batch.filter(
241
+ (req) => req.id === undefined || req.id === null
242
+ ).length;
243
+ const requests = batch.length - notifications;
244
+ const methods = [...new Set(batch.map((req) => req.method))];
245
+
246
+ return {
247
+ total: batch.length,
248
+ requests,
249
+ notifications,
250
+ uniqueMethods: methods.length,
251
+ methods,
252
+ };
253
+ }
254
+ }
255
+
256
+ module.exports = BatchHandler;