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.
@@ -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;