rpc-express-toolkit 2.0.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/LICENSE +21 -0
- package/README.md +796 -0
- package/examples/example.js +41 -0
- package/examples/server-client-example.js +114 -0
- package/package.json +95 -0
- package/src/batch.js +237 -0
- package/src/clients/rpc-client.js +249 -0
- package/src/clients/rpc-client.min.js +1 -0
- package/src/clients/rpc-client.min.mjs +1 -0
- package/src/clients/rpc-client.mjs +275 -0
- package/src/index.d.ts +147 -0
- package/src/index.js +580 -0
- package/src/logger.js +153 -0
- package/src/middleware.js +224 -0
- package/src/validation.js +221 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON-RPC 2.0 Client for browser and Node.js environments.
|
|
3
|
+
* Handles BigInt and Date serialization/deserialization automatically.
|
|
4
|
+
*
|
|
5
|
+
* Note: We extend BigInt.prototype.toJSON to handle automatic serialization
|
|
6
|
+
* when JSON.stringify() is called, as BigInt doesn't have native JSON support.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Extend BigInt prototype to handle JSON serialization
|
|
10
|
+
if (typeof BigInt !== 'undefined' && !BigInt.prototype.toJSON) {
|
|
11
|
+
BigInt.prototype.toJSON = function() { return this.toString(); };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Polyfill for Node.js environments that don't have fetch
|
|
15
|
+
let fetchFn;
|
|
16
|
+
if (typeof fetch !== 'undefined') {
|
|
17
|
+
fetchFn = fetch;
|
|
18
|
+
} else {
|
|
19
|
+
// Try to use node-fetch if available, otherwise use built-in fetch in Node.js 18+
|
|
20
|
+
try {
|
|
21
|
+
fetchFn = require('node-fetch');
|
|
22
|
+
} catch (e) {
|
|
23
|
+
// For Node.js 18+, use global fetch
|
|
24
|
+
if (typeof globalThis !== 'undefined' && globalThis.fetch) {
|
|
25
|
+
fetchFn = globalThis.fetch;
|
|
26
|
+
} else {
|
|
27
|
+
throw new Error('fetch is not available. Please install node-fetch or use Node.js 18+');
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
class RpcClient {
|
|
33
|
+
#endpoint;
|
|
34
|
+
#defaultHeaders;
|
|
35
|
+
#requestId;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @param {string} endpoint The JSON-RPC endpoint URL.
|
|
39
|
+
* @param {Object} [defaultHeaders={}] Optional default headers to include in requests.
|
|
40
|
+
*/
|
|
41
|
+
constructor(endpoint, defaultHeaders = {}) {
|
|
42
|
+
this.#endpoint = endpoint;
|
|
43
|
+
this.#defaultHeaders = {
|
|
44
|
+
"Content-Type": "application/json", // Default header
|
|
45
|
+
...defaultHeaders, // Merge with user-provided defaults
|
|
46
|
+
};
|
|
47
|
+
// Initialize request ID counter with timestamp + random component for uniqueness
|
|
48
|
+
this.#requestId = Date.now() * 1000 + Math.floor(Math.random() * 1000);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Generate a unique request ID
|
|
53
|
+
* @returns {number} Unique request ID
|
|
54
|
+
*/
|
|
55
|
+
#generateId() {
|
|
56
|
+
return ++this.#requestId;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Make a JSON-RPC call to the server.
|
|
61
|
+
* @param {string} method The RPC method name.
|
|
62
|
+
* @param {any} params The parameters to pass to the RPC method.
|
|
63
|
+
* @param {string|number|null} [id] The request ID (auto-generated if not provided).
|
|
64
|
+
* @param {Object} [overrideHeaders={}] Optional headers to override defaults for this request.
|
|
65
|
+
* @returns {Promise<any>} The result of the RPC call.
|
|
66
|
+
*/
|
|
67
|
+
async call(method, params = {}, id = undefined, overrideHeaders = {}) {
|
|
68
|
+
// Auto-generate ID if not provided (null means notification)
|
|
69
|
+
const requestId = id === undefined ? this.#generateId() : id;
|
|
70
|
+
|
|
71
|
+
const requestBody = {
|
|
72
|
+
jsonrpc: "2.0",
|
|
73
|
+
method,
|
|
74
|
+
params,
|
|
75
|
+
id: requestId,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const response = await fetchFn(this.#endpoint, {
|
|
80
|
+
method: "POST",
|
|
81
|
+
headers: {
|
|
82
|
+
...this.#defaultHeaders,
|
|
83
|
+
...overrideHeaders,
|
|
84
|
+
},
|
|
85
|
+
// Since we have BigInt.prototype.toJSON, we don't need manual serialization
|
|
86
|
+
// JSON.stringify will handle BigInt and Date automatically
|
|
87
|
+
body: JSON.stringify(requestBody),
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
if (!response.ok) {
|
|
91
|
+
throw new Error(`HTTP Error: ${response.status} ${response.statusText}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const responseBody = await response.json();
|
|
95
|
+
|
|
96
|
+
if (responseBody.error) { throw responseBody.error; }
|
|
97
|
+
|
|
98
|
+
// Convert back BigInts and Dates in the result
|
|
99
|
+
return this.deserializeBigIntsAndDates(responseBody.result);
|
|
100
|
+
} catch (error) {
|
|
101
|
+
console.error("RPC call failed:", error);
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Send a notification (no response expected).
|
|
108
|
+
* @param {string} method The RPC method name.
|
|
109
|
+
* @param {any} params The parameters to pass to the RPC method.
|
|
110
|
+
* @param {Object} [overrideHeaders={}] Optional headers to override defaults for this request.
|
|
111
|
+
* @returns {Promise<void>} Promise that resolves when notification is sent.
|
|
112
|
+
*/
|
|
113
|
+
async notify(method, params = {}, overrideHeaders = {}) {
|
|
114
|
+
await this.call(method, params, null, overrideHeaders);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Make multiple JSON-RPC calls in a single batch request.
|
|
119
|
+
* @param {Array<{method: string, params?: any, id?: string|number}>} requests Array of request objects.
|
|
120
|
+
* @param {Object} [overrideHeaders={}] Optional headers to override defaults for this request.
|
|
121
|
+
* @returns {Promise<Array<any>>} Array of results in the same order as requests.
|
|
122
|
+
*/
|
|
123
|
+
async batch(requests, overrideHeaders = {}) {
|
|
124
|
+
const batchRequests = requests.map(req => ({
|
|
125
|
+
jsonrpc: "2.0",
|
|
126
|
+
method: req.method,
|
|
127
|
+
params: req.params || {},
|
|
128
|
+
id: req.id !== undefined ? req.id : this.#generateId()
|
|
129
|
+
}));
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
const response = await fetchFn(this.#endpoint, {
|
|
133
|
+
method: "POST",
|
|
134
|
+
headers: {
|
|
135
|
+
...this.#defaultHeaders,
|
|
136
|
+
...overrideHeaders,
|
|
137
|
+
},
|
|
138
|
+
body: JSON.stringify(batchRequests),
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
if (!response.ok) {
|
|
142
|
+
throw new Error(`HTTP Error: ${response.status} ${response.statusText}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const responseBody = await response.json();
|
|
146
|
+
|
|
147
|
+
// Handle batch response
|
|
148
|
+
if (Array.isArray(responseBody)) {
|
|
149
|
+
return responseBody.map(res => {
|
|
150
|
+
if (res.error) {
|
|
151
|
+
throw res.error;
|
|
152
|
+
}
|
|
153
|
+
return this.deserializeBigIntsAndDates(res.result);
|
|
154
|
+
});
|
|
155
|
+
} else {
|
|
156
|
+
// Single response in batch
|
|
157
|
+
if (responseBody.error) {
|
|
158
|
+
throw responseBody.error;
|
|
159
|
+
}
|
|
160
|
+
return [this.deserializeBigIntsAndDates(responseBody.result)];
|
|
161
|
+
}
|
|
162
|
+
} catch (error) {
|
|
163
|
+
console.error("Batch RPC call failed:", error);
|
|
164
|
+
throw error;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Recursively convert BigInt values to strings and Date objects to ISO strings
|
|
170
|
+
* so they can be JSON-serialized.
|
|
171
|
+
* @param {any} value
|
|
172
|
+
* @returns {any}
|
|
173
|
+
*/
|
|
174
|
+
serializeBigIntsAndDates(value) {
|
|
175
|
+
if (typeof value === "bigint") {
|
|
176
|
+
// Convert BigInt to string
|
|
177
|
+
return value.toString();
|
|
178
|
+
} else if (Array.isArray(value)) {
|
|
179
|
+
// Recurse into arrays
|
|
180
|
+
return value.map((v) => this.serializeBigIntsAndDates(v));
|
|
181
|
+
} else if (value instanceof Date) {
|
|
182
|
+
// Convert Date to ISO string (UTC)
|
|
183
|
+
return value.toISOString();
|
|
184
|
+
} else if (value && typeof value === "object") {
|
|
185
|
+
// Recurse into plain objects
|
|
186
|
+
const result = {};
|
|
187
|
+
for (const [key, val] of Object.entries(value)) {
|
|
188
|
+
result[key] = this.serializeBigIntsAndDates(val);
|
|
189
|
+
}
|
|
190
|
+
return result;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// If it's neither an array, an object, a Date, nor a bigint, return as-is
|
|
194
|
+
return value;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Recursively convert stringified BigInt values back to BigInt
|
|
199
|
+
* and ISO 8601 date strings back to Date objects.
|
|
200
|
+
*
|
|
201
|
+
* @param {any} value The value to deserialize.
|
|
202
|
+
* @returns {any}
|
|
203
|
+
*/
|
|
204
|
+
deserializeBigIntsAndDates(value) {
|
|
205
|
+
// More comprehensive ISO date regex that handles:
|
|
206
|
+
// - UTC: 2023-01-01T12:00:00.000Z
|
|
207
|
+
// - With timezone: 2023-01-01T12:00:00.000+01:00
|
|
208
|
+
// - Without timezone: 2023-01-01T12:00:00.000 (treated as local)
|
|
209
|
+
const ISO_DATE_REGEX = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})?$/;
|
|
210
|
+
|
|
211
|
+
// 1. Check if it's a string that might be a BigInt or a Date
|
|
212
|
+
if (typeof value === "string") {
|
|
213
|
+
// BigInt check: matches digits (including negative), optionally ending in "n"
|
|
214
|
+
// e.g., "42n", "42", "-42n", "-42"
|
|
215
|
+
if (/^-?\d+n?$/.test(value)) {
|
|
216
|
+
return BigInt(value.replace(/n$/, ""));
|
|
217
|
+
}
|
|
218
|
+
// Date check: matches an ISO 8601 string
|
|
219
|
+
if (ISO_DATE_REGEX.test(value)) {
|
|
220
|
+
const date = new Date(value);
|
|
221
|
+
// Ensure it's valid
|
|
222
|
+
if (!isNaN(date.getTime())) {
|
|
223
|
+
return date;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// 2. If it's an array, handle each element
|
|
229
|
+
if (Array.isArray(value)) {
|
|
230
|
+
return value.map((v) => this.deserializeBigIntsAndDates(v));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// 3. If it's a plain object, recurse into each property
|
|
234
|
+
if (value && typeof value === "object") {
|
|
235
|
+
return Object.fromEntries(
|
|
236
|
+
Object.entries(value).map(([key, val]) => [
|
|
237
|
+
key,
|
|
238
|
+
this.deserializeBigIntsAndDates(val),
|
|
239
|
+
])
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// 4. Fallback for primitives, etc.
|
|
244
|
+
return value;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
module.exports = RpcClient;
|
|
249
|
+
module.exports.RpcClient = RpcClient;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
let fetchFn;if("undefined"==typeof BigInt||BigInt.prototype.toJSON||(BigInt.prototype.toJSON=function(){return this.toString()}),"undefined"!=typeof fetch)fetchFn=fetch;else try{fetchFn=require("node-fetch")}catch(t){if("undefined"==typeof globalThis||!globalThis.fetch)throw new Error("fetch is not available. Please install node-fetch or use Node.js 18+");fetchFn=globalThis.fetch}class RpcClient{#t;#e;#r;constructor(t,e={}){this.#t=t,this.#e={"Content-Type":"application/json",...e},this.#r=1e3*Date.now()+Math.floor(1e3*Math.random())}#i(){return++this.#r}async call(t,e={},r=void 0,i={}){const s={jsonrpc:"2.0",method:t,params:e,id:void 0===r?this.#i():r};try{const t=await fetchFn(this.#t,{method:"POST",headers:{...this.#e,...i},body:JSON.stringify(s)});if(!t.ok)throw new Error(`HTTP Error: ${t.status} ${t.statusText}`);const e=await t.json();if(e.error)throw e.error;return this.deserializeBigIntsAndDates(e.result)}catch(t){throw console.error("RPC call failed:",t),t}}async notify(t,e={},r={}){await this.call(t,e,null,r)}async batch(t,e={}){const r=t.map(t=>({jsonrpc:"2.0",method:t.method,params:t.params||{},id:void 0!==t.id?t.id:this.#i()}));try{const t=await fetchFn(this.#t,{method:"POST",headers:{...this.#e,...e},body:JSON.stringify(r)});if(!t.ok)throw new Error(`HTTP Error: ${t.status} ${t.statusText}`);const i=await t.json();if(Array.isArray(i))return i.map(t=>{if(t.error)throw t.error;return this.deserializeBigIntsAndDates(t.result)});if(i.error)throw i.error;return[this.deserializeBigIntsAndDates(i.result)]}catch(t){throw console.error("Batch RPC call failed:",t),t}}serializeBigIntsAndDates(t){if("bigint"==typeof t)return t.toString();if(Array.isArray(t))return t.map(t=>this.serializeBigIntsAndDates(t));if(t instanceof Date)return t.toISOString();if(t&&"object"==typeof t){const e={};for(const[r,i]of Object.entries(t))e[r]=this.serializeBigIntsAndDates(i);return e}return t}deserializeBigIntsAndDates(t){const e=/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})?$/;if("string"==typeof t){if(/^-?\d+n?$/.test(t))return BigInt(t.replace(/n$/,""));if(e.test(t)){const e=new Date(t);if(!isNaN(e.getTime()))return e}}return Array.isArray(t)?t.map(t=>this.deserializeBigIntsAndDates(t)):t&&"object"==typeof t?Object.fromEntries(Object.entries(t).map(([t,e])=>[t,this.deserializeBigIntsAndDates(e)])):t}}module.exports=RpcClient,module.exports.RpcClient=RpcClient;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
let t;if("undefined"==typeof BigInt||BigInt.prototype.toJSON||(BigInt.prototype.toJSON=function(){return this.toString()}),"undefined"!=typeof fetch)t=fetch;else try{const{default:e}=await import("node-fetch");t=e}catch(e){if("undefined"==typeof globalThis||!globalThis.fetch)throw new Error("fetch is not available. Please install node-fetch or use Node.js 18+");t=globalThis.fetch}export class RpcClient{#t;#e;#r;constructor(t,e={}){this.#t=t,this.#e={"Content-Type":"application/json",...e},this.#r=1e3*Date.now()+Math.floor(1e3*Math.random())}async call(e,r={},i=null,s={}){null===i&&(i=++this.#r);const n={jsonrpc:"2.0",method:e,params:r,id:i};try{const e=await t(this.#t,{method:"POST",headers:{...this.#e,...s},body:JSON.stringify(n)});if(!e.ok)throw new Error(`HTTP Error: ${e.status} ${e.statusText}`);const r=await e.json();if(r.error)throw r.error;return this.deserializeBigIntsAndDates(r.result)}catch(t){throw console.error("RPC call failed:",t),t}}async notify(e,r={},i={}){const s={jsonrpc:"2.0",method:e,params:r};try{const e=await t(this.#t,{method:"POST",headers:{...this.#e,...i},body:JSON.stringify(s)});if(!e.ok)throw new Error(`HTTP Error: ${e.status} ${e.statusText}`)}catch(t){throw console.error("RPC notification failed:",t),t}}async batch(e,r={}){const i=e.map(t=>{const e={jsonrpc:"2.0",method:t.method,params:t.params||{}};return t.notification||(e.id=void 0!==t.id?t.id:++this.#r),e});try{const e=await t(this.#t,{method:"POST",headers:{...this.#e,...r},body:JSON.stringify(i)});if(!e.ok)throw new Error(`HTTP Error: ${e.status} ${e.statusText}`);const s=await e.json();if(Array.isArray(s))return s.map(t=>{if(t.error)throw t.error;return this.deserializeBigIntsAndDates(t.result)});if(s.error)throw s.error;return[this.deserializeBigIntsAndDates(s.result)]}catch(t){throw console.error("RPC batch call failed:",t),t}}serializeBigIntsAndDates(t){if("bigint"==typeof t)return t.toString();if(Array.isArray(t))return t.map(t=>this.serializeBigIntsAndDates(t));if(t instanceof Date)return t.toISOString();if(t&&"object"==typeof t){const e={};for(const[r,i]of Object.entries(t))e[r]=this.serializeBigIntsAndDates(i);return e}return t}deserializeBigIntsAndDates(t){const e=/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})?$/;if("string"==typeof t){if(/^-?\d+n?$/.test(t))return BigInt(t.replace(/n$/,""));if(e.test(t)){const e=new Date(t);if(!isNaN(e.getTime()))return e}}return Array.isArray(t)?t.map(t=>this.deserializeBigIntsAndDates(t)):t&&"object"==typeof t?Object.fromEntries(Object.entries(t).map(([t,e])=>[t,this.deserializeBigIntsAndDates(e)])):t}}export{RpcClient};
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON-RPC 2.0 Client for browser and Node.js environments (ES Module).
|
|
3
|
+
* Handles BigInt and Date serialization/deserialization automatically.
|
|
4
|
+
*
|
|
5
|
+
* Note: We extend BigInt.prototype.toJSON to handle automatic serialization
|
|
6
|
+
* when JSON.stringify() is called, as BigInt doesn't have native JSON support.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Extend BigInt prototype to handle JSON serialization
|
|
10
|
+
if (typeof BigInt !== 'undefined' && !BigInt.prototype.toJSON) {
|
|
11
|
+
BigInt.prototype.toJSON = function() { return this.toString(); };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Polyfill for Node.js environments that don't have fetch
|
|
15
|
+
let fetchFn;
|
|
16
|
+
if (typeof fetch !== 'undefined') {
|
|
17
|
+
fetchFn = fetch;
|
|
18
|
+
} else {
|
|
19
|
+
// Try to use node-fetch if available, otherwise use built-in fetch in Node.js 18+
|
|
20
|
+
try {
|
|
21
|
+
const { default: nodeFetch } = await import('node-fetch');
|
|
22
|
+
fetchFn = nodeFetch;
|
|
23
|
+
} catch (e) {
|
|
24
|
+
// For Node.js 18+, use global fetch
|
|
25
|
+
if (typeof globalThis !== 'undefined' && globalThis.fetch) {
|
|
26
|
+
fetchFn = globalThis.fetch;
|
|
27
|
+
} else {
|
|
28
|
+
throw new Error('fetch is not available. Please install node-fetch or use Node.js 18+');
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class RpcClient {
|
|
34
|
+
#endpoint;
|
|
35
|
+
#defaultHeaders;
|
|
36
|
+
#requestId;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @param {string} endpoint The JSON-RPC endpoint URL.
|
|
40
|
+
* @param {Object} [defaultHeaders={}] Optional default headers to include in requests.
|
|
41
|
+
*/
|
|
42
|
+
constructor(endpoint, defaultHeaders = {}) {
|
|
43
|
+
this.#endpoint = endpoint;
|
|
44
|
+
this.#defaultHeaders = {
|
|
45
|
+
"Content-Type": "application/json", // Default header
|
|
46
|
+
...defaultHeaders, // Merge with user-provided defaults
|
|
47
|
+
};
|
|
48
|
+
// Initialize request ID counter with timestamp + random component for uniqueness
|
|
49
|
+
this.#requestId = Date.now() * 1000 + Math.floor(Math.random() * 1000);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Make a JSON-RPC call to the server.
|
|
54
|
+
* @param {string} method The RPC method name.
|
|
55
|
+
* @param {any} params The parameters to pass to the RPC method.
|
|
56
|
+
* @param {string|number|null} [id] The request ID (optional for notifications). If null, auto-generates unique ID.
|
|
57
|
+
* @param {Object} [overrideHeaders={}] Optional headers to override defaults for this request.
|
|
58
|
+
* @returns {Promise<any>} The result of the RPC call.
|
|
59
|
+
*/
|
|
60
|
+
async call(method, params = {}, id = null, overrideHeaders = {}) {
|
|
61
|
+
// Auto-generate unique ID if not provided
|
|
62
|
+
if (id === null) {
|
|
63
|
+
id = ++this.#requestId;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const requestBody = {
|
|
67
|
+
jsonrpc: "2.0",
|
|
68
|
+
method,
|
|
69
|
+
params,
|
|
70
|
+
id,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const response = await fetchFn(this.#endpoint, {
|
|
75
|
+
method: "POST",
|
|
76
|
+
headers: {
|
|
77
|
+
...this.#defaultHeaders,
|
|
78
|
+
...overrideHeaders,
|
|
79
|
+
},
|
|
80
|
+
// Since we have BigInt.prototype.toJSON, we don't need manual serialization
|
|
81
|
+
// JSON.stringify will handle BigInt and Date automatically
|
|
82
|
+
body: JSON.stringify(requestBody),
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
if (!response.ok) {
|
|
86
|
+
throw new Error(`HTTP Error: ${response.status} ${response.statusText}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const responseBody = await response.json();
|
|
90
|
+
|
|
91
|
+
if (responseBody.error) { throw responseBody.error; }
|
|
92
|
+
|
|
93
|
+
// Convert back BigInts and Dates in the result
|
|
94
|
+
return this.deserializeBigIntsAndDates(responseBody.result);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
console.error("RPC call failed:", error);
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Make a JSON-RPC notification (no response expected).
|
|
103
|
+
* @param {string} method The RPC method name.
|
|
104
|
+
* @param {any} params The parameters to pass to the RPC method.
|
|
105
|
+
* @param {Object} [overrideHeaders={}] Optional headers to override defaults for this request.
|
|
106
|
+
* @returns {Promise<void>}
|
|
107
|
+
*/
|
|
108
|
+
async notify(method, params = {}, overrideHeaders = {}) {
|
|
109
|
+
const requestBody = {
|
|
110
|
+
jsonrpc: "2.0",
|
|
111
|
+
method,
|
|
112
|
+
params,
|
|
113
|
+
// No ID for notifications
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
const response = await fetchFn(this.#endpoint, {
|
|
118
|
+
method: "POST",
|
|
119
|
+
headers: {
|
|
120
|
+
...this.#defaultHeaders,
|
|
121
|
+
...overrideHeaders,
|
|
122
|
+
},
|
|
123
|
+
body: JSON.stringify(requestBody),
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
if (!response.ok) {
|
|
127
|
+
throw new Error(`HTTP Error: ${response.status} ${response.statusText}`);
|
|
128
|
+
}
|
|
129
|
+
} catch (error) {
|
|
130
|
+
console.error("RPC notification failed:", error);
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Make a batch JSON-RPC call to the server.
|
|
137
|
+
* @param {Array<{method: string, params: any, id?: string|number|null, notification?: boolean}>} requests Array of request objects.
|
|
138
|
+
* @param {Object} [overrideHeaders={}] Optional headers to override defaults for this request.
|
|
139
|
+
* @returns {Promise<Array<any>>} Array of results corresponding to the requests.
|
|
140
|
+
*/
|
|
141
|
+
async batch(requests, overrideHeaders = {}) {
|
|
142
|
+
const requestBodies = requests.map(req => {
|
|
143
|
+
const body = {
|
|
144
|
+
jsonrpc: "2.0",
|
|
145
|
+
method: req.method,
|
|
146
|
+
params: req.params || {},
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// If it's not a notification, add ID
|
|
150
|
+
if (!req.notification) {
|
|
151
|
+
body.id = req.id !== undefined ? req.id : ++this.#requestId;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return body;
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
const response = await fetchFn(this.#endpoint, {
|
|
159
|
+
method: "POST",
|
|
160
|
+
headers: {
|
|
161
|
+
...this.#defaultHeaders,
|
|
162
|
+
...overrideHeaders,
|
|
163
|
+
},
|
|
164
|
+
body: JSON.stringify(requestBodies),
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
if (!response.ok) {
|
|
168
|
+
throw new Error(`HTTP Error: ${response.status} ${response.statusText}`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const responseBody = await response.json();
|
|
172
|
+
|
|
173
|
+
// Handle batch response
|
|
174
|
+
if (Array.isArray(responseBody)) {
|
|
175
|
+
return responseBody.map(res => {
|
|
176
|
+
if (res.error) {
|
|
177
|
+
throw res.error;
|
|
178
|
+
}
|
|
179
|
+
return this.deserializeBigIntsAndDates(res.result);
|
|
180
|
+
});
|
|
181
|
+
} else {
|
|
182
|
+
// Single response (shouldn't happen with batch, but handle gracefully)
|
|
183
|
+
if (responseBody.error) {
|
|
184
|
+
throw responseBody.error;
|
|
185
|
+
}
|
|
186
|
+
return [this.deserializeBigIntsAndDates(responseBody.result)];
|
|
187
|
+
}
|
|
188
|
+
} catch (error) {
|
|
189
|
+
console.error("RPC batch call failed:", error);
|
|
190
|
+
throw error;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Recursively convert BigInt values to strings and Date objects to ISO strings
|
|
196
|
+
* so they can be JSON-serialized.
|
|
197
|
+
* @param {any} value
|
|
198
|
+
* @returns {any}
|
|
199
|
+
*/
|
|
200
|
+
serializeBigIntsAndDates(value) {
|
|
201
|
+
if (typeof value === "bigint") {
|
|
202
|
+
// Convert BigInt to string
|
|
203
|
+
return value.toString();
|
|
204
|
+
} else if (Array.isArray(value)) {
|
|
205
|
+
// Recurse into arrays
|
|
206
|
+
return value.map((v) => this.serializeBigIntsAndDates(v));
|
|
207
|
+
} else if (value instanceof Date) {
|
|
208
|
+
// Convert Date to ISO string (UTC)
|
|
209
|
+
return value.toISOString();
|
|
210
|
+
} else if (value && typeof value === "object") {
|
|
211
|
+
// Recurse into plain objects
|
|
212
|
+
const result = {};
|
|
213
|
+
for (const [key, val] of Object.entries(value)) {
|
|
214
|
+
result[key] = this.serializeBigIntsAndDates(val);
|
|
215
|
+
}
|
|
216
|
+
return result;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// If it's neither an array, an object, a Date, nor a bigint, return as-is
|
|
220
|
+
return value;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Recursively convert stringified BigInt values back to BigInt
|
|
225
|
+
* and ISO 8601 date strings back to Date objects.
|
|
226
|
+
*
|
|
227
|
+
* @param {any} value The value to deserialize.
|
|
228
|
+
* @returns {any}
|
|
229
|
+
*/
|
|
230
|
+
deserializeBigIntsAndDates(value) {
|
|
231
|
+
// More comprehensive ISO date regex that handles:
|
|
232
|
+
// - UTC: 2023-01-01T12:00:00.000Z
|
|
233
|
+
// - With timezone: 2023-01-01T12:00:00.000+01:00
|
|
234
|
+
// - Without timezone: 2023-01-01T12:00:00.000 (treated as local)
|
|
235
|
+
const ISO_DATE_REGEX = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})?$/;
|
|
236
|
+
|
|
237
|
+
// 1. Check if it's a string that might be a BigInt or a Date
|
|
238
|
+
if (typeof value === "string") {
|
|
239
|
+
// BigInt check: matches digits (including negative), optionally ending in "n"
|
|
240
|
+
// e.g., "42n", "42", "-42n", "-42"
|
|
241
|
+
if (/^-?\d+n?$/.test(value)) {
|
|
242
|
+
return BigInt(value.replace(/n$/, ""));
|
|
243
|
+
}
|
|
244
|
+
// Date check: matches an ISO 8601 string
|
|
245
|
+
if (ISO_DATE_REGEX.test(value)) {
|
|
246
|
+
const date = new Date(value);
|
|
247
|
+
// Ensure it's valid
|
|
248
|
+
if (!isNaN(date.getTime())) {
|
|
249
|
+
return date;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// 2. If it's an array, handle each element
|
|
255
|
+
if (Array.isArray(value)) {
|
|
256
|
+
return value.map((v) => this.deserializeBigIntsAndDates(v));
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// 3. If it's a plain object, recurse into each property
|
|
260
|
+
if (value && typeof value === "object") {
|
|
261
|
+
return Object.fromEntries(
|
|
262
|
+
Object.entries(value).map(([key, val]) => [
|
|
263
|
+
key,
|
|
264
|
+
this.deserializeBigIntsAndDates(val),
|
|
265
|
+
])
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// 4. Fallback for primitives, etc.
|
|
270
|
+
return value;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Export only the main class
|
|
275
|
+
export { RpcClient };
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// Type definitions for rpc-express-toolkit
|
|
2
|
+
|
|
3
|
+
import type { Router, Request, Response } from "express";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Definiamo il tipo della funzione "handler" che aggiungiamo tramite `addMethod`.
|
|
7
|
+
*/
|
|
8
|
+
type JSONRPCHandler<C> = (req: Request, context: C, params: any) => any | Promise<any>;
|
|
9
|
+
|
|
10
|
+
interface JSONRPCError {
|
|
11
|
+
code: number;
|
|
12
|
+
message: string;
|
|
13
|
+
data?: any;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface JSONRPCResponsePayload {
|
|
17
|
+
id?: string | number | null;
|
|
18
|
+
result?: any;
|
|
19
|
+
error?: JSONRPCError;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The main RpcEndpoint class for JSON-RPC 2.0 endpoints.
|
|
24
|
+
* The generics `<C>` represents the "context" type.
|
|
25
|
+
*/
|
|
26
|
+
declare class RpcEndpoint<C> {
|
|
27
|
+
/**
|
|
28
|
+
* Constructor.
|
|
29
|
+
*
|
|
30
|
+
* @param router - An Express.Router object
|
|
31
|
+
* @param context - Generic object that will be passed to method handlers
|
|
32
|
+
* @param endpoint - Path on which to respond to JSON-RPC calls (default: "/api")
|
|
33
|
+
*/
|
|
34
|
+
constructor(router: Router, context: C, endpoint?: string);
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Returns the endpoint path, e.g. "/api".
|
|
38
|
+
*/
|
|
39
|
+
get endpoint(): string;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Returns the map of all registered methods.
|
|
43
|
+
*/
|
|
44
|
+
get methods(): { [methodName: string]: JSONRPCHandler<C> };
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Adds a JSON-RPC method (e.g. "getUserData").
|
|
48
|
+
*
|
|
49
|
+
* @param name - Method name
|
|
50
|
+
/**
|
|
51
|
+
* Adds a JSON-RPC method (e.g. "getUserData").
|
|
52
|
+
*
|
|
53
|
+
* @param name - Method name
|
|
54
|
+
* @param handler - Handler function that receives (req, context, params)
|
|
55
|
+
*/
|
|
56
|
+
addMethod(name: string, handler: JSONRPCHandler<C>): void;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Serve client scripts (for calling JSON-RPC from browser).
|
|
60
|
+
*
|
|
61
|
+
* @param router - An Express.Router object
|
|
62
|
+
* @param url - Path from which to serve scripts (default: "/vendor/rpc-client")
|
|
63
|
+
*/
|
|
64
|
+
static serveScripts(router: Router, url?: string): void;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Send a JSON-RPC 2.0 response to the client.
|
|
68
|
+
*/
|
|
69
|
+
reply(res: Response, responsePayload: JSONRPCResponsePayload): void;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Recursively serialize BigInt and Date to JSON-safe string formats.
|
|
73
|
+
*/
|
|
74
|
+
serializeBigIntsAndDates(value: any): any;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Recursively deserialize strings representing BigInt and Date
|
|
78
|
+
* (produced by `serializeBigIntsAndDates`) to reconstruct original types.
|
|
79
|
+
*/
|
|
80
|
+
deserializeBigIntsAndDates(value: any): any;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* JSON-RPC 2.0 Client for Node.js environments.
|
|
85
|
+
* Handles BigInt and Date serialization/deserialization automatically.
|
|
86
|
+
*/
|
|
87
|
+
declare class RpcClient {
|
|
88
|
+
/**
|
|
89
|
+
* Client constructor.
|
|
90
|
+
*
|
|
91
|
+
* @param endpoint - JSON-RPC endpoint URL
|
|
92
|
+
* @param defaultHeaders - Default headers to include in requests
|
|
93
|
+
*/
|
|
94
|
+
constructor(endpoint: string, defaultHeaders?: Record<string, string>);
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Make a JSON-RPC call to the server.
|
|
98
|
+
*
|
|
99
|
+
* @param method - RPC method name
|
|
100
|
+
* @param params - Parameters to pass to the method
|
|
101
|
+
* @param id - Request ID (optional for notifications)
|
|
102
|
+
* @param overrideHeaders - Headers to override defaults for this request
|
|
103
|
+
* @returns Promise with the RPC call result
|
|
104
|
+
*/
|
|
105
|
+
call(method: string, params?: any, id?: string | number | null, overrideHeaders?: Record<string, string>): Promise<any>;
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Make a JSON-RPC notification (no response expected).
|
|
109
|
+
*
|
|
110
|
+
* @param method - RPC method name
|
|
111
|
+
* @param params - Parameters to pass to the method
|
|
112
|
+
* @param overrideHeaders - Headers to override defaults for this request
|
|
113
|
+
*/
|
|
114
|
+
notify(method: string, params?: any, overrideHeaders?: Record<string, string>): Promise<void>;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Make a batch JSON-RPC call.
|
|
118
|
+
*
|
|
119
|
+
* @param requests - Array of request objects
|
|
120
|
+
* @param overrideHeaders - Headers to override defaults for this request
|
|
121
|
+
* @returns Promise with array of results
|
|
122
|
+
*/
|
|
123
|
+
batch(requests: Array<{method: string, params?: any, id?: string|number|null, notification?: boolean}>, overrideHeaders?: Record<string, string>): Promise<any[]>;
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Recursively serialize BigInt and Date to JSON-safe string formats.
|
|
127
|
+
*/
|
|
128
|
+
serializeBigIntsAndDates(value: any): any;
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Recursively deserialize strings representing BigInt and Date.
|
|
132
|
+
*/
|
|
133
|
+
deserializeBigIntsAndDates(value: any): any;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Main export structure.
|
|
138
|
+
*/
|
|
139
|
+
declare const Main: {
|
|
140
|
+
new <C>(router: Router, context: C, endpoint?: string): RpcEndpoint<C>;
|
|
141
|
+
prototype: RpcEndpoint<any>;
|
|
142
|
+
serveScripts(router: Router, url?: string): void;
|
|
143
|
+
RpcEndpoint: typeof RpcEndpoint;
|
|
144
|
+
RpcClient: typeof RpcClient;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
export = Main;
|