@parsrun/service 0.1.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/README.md +375 -0
- package/dist/client.d.ts +55 -0
- package/dist/client.js +1474 -0
- package/dist/client.js.map +1 -0
- package/dist/define.d.ts +82 -0
- package/dist/define.js +120 -0
- package/dist/define.js.map +1 -0
- package/dist/events/index.d.ts +285 -0
- package/dist/events/index.js +853 -0
- package/dist/events/index.js.map +1 -0
- package/dist/handler-CmiDUWZv.d.ts +204 -0
- package/dist/index-CVOAoJjZ.d.ts +268 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.js +3589 -0
- package/dist/index.js.map +1 -0
- package/dist/resilience/index.d.ts +197 -0
- package/dist/resilience/index.js +387 -0
- package/dist/resilience/index.js.map +1 -0
- package/dist/rpc/index.d.ts +5 -0
- package/dist/rpc/index.js +1175 -0
- package/dist/rpc/index.js.map +1 -0
- package/dist/serialization/index.d.ts +37 -0
- package/dist/serialization/index.js +320 -0
- package/dist/serialization/index.js.map +1 -0
- package/dist/server-DFE8n2Sx.d.ts +106 -0
- package/dist/tracing/index.d.ts +406 -0
- package/dist/tracing/index.js +820 -0
- package/dist/tracing/index.js.map +1 -0
- package/dist/transports/cloudflare/index.d.ts +237 -0
- package/dist/transports/cloudflare/index.js +746 -0
- package/dist/transports/cloudflare/index.js.map +1 -0
- package/dist/types-n4LLSPQU.d.ts +473 -0
- package/package.json +91 -0
|
@@ -0,0 +1,1175 @@
|
|
|
1
|
+
// src/rpc/client.ts
|
|
2
|
+
import { generateId } from "@parsrun/core";
|
|
3
|
+
|
|
4
|
+
// src/config.ts
|
|
5
|
+
var DEFAULT_EVENT_CONFIG = {
|
|
6
|
+
format: "cloudevents",
|
|
7
|
+
internalCompact: true
|
|
8
|
+
};
|
|
9
|
+
var DEFAULT_SERIALIZATION_CONFIG = {
|
|
10
|
+
format: "json"
|
|
11
|
+
};
|
|
12
|
+
var DEFAULT_TRACING_CONFIG = {
|
|
13
|
+
enabled: true,
|
|
14
|
+
sampler: { ratio: 0.1 },
|
|
15
|
+
exporter: "console",
|
|
16
|
+
endpoint: "",
|
|
17
|
+
serviceName: "pars-service"
|
|
18
|
+
};
|
|
19
|
+
var DEFAULT_VERSIONING_CONFIG = {
|
|
20
|
+
strategy: "header",
|
|
21
|
+
defaultVersion: "1.x"
|
|
22
|
+
};
|
|
23
|
+
var DEFAULT_RESILIENCE_CONFIG = {
|
|
24
|
+
circuitBreaker: {
|
|
25
|
+
enabled: true,
|
|
26
|
+
failureThreshold: 5,
|
|
27
|
+
resetTimeout: 3e4,
|
|
28
|
+
successThreshold: 2
|
|
29
|
+
},
|
|
30
|
+
bulkhead: {
|
|
31
|
+
maxConcurrent: 100,
|
|
32
|
+
maxQueue: 50
|
|
33
|
+
},
|
|
34
|
+
timeout: 5e3,
|
|
35
|
+
retry: {
|
|
36
|
+
attempts: 3,
|
|
37
|
+
backoff: "exponential",
|
|
38
|
+
initialDelay: 100,
|
|
39
|
+
maxDelay: 1e4
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
var DEFAULT_DEAD_LETTER_CONFIG = {
|
|
43
|
+
enabled: true,
|
|
44
|
+
retention: "30d",
|
|
45
|
+
onFail: "alert",
|
|
46
|
+
alertThreshold: 10
|
|
47
|
+
};
|
|
48
|
+
var DEFAULT_SERVICE_CONFIG = {
|
|
49
|
+
events: DEFAULT_EVENT_CONFIG,
|
|
50
|
+
serialization: DEFAULT_SERIALIZATION_CONFIG,
|
|
51
|
+
tracing: DEFAULT_TRACING_CONFIG,
|
|
52
|
+
versioning: DEFAULT_VERSIONING_CONFIG,
|
|
53
|
+
resilience: DEFAULT_RESILIENCE_CONFIG,
|
|
54
|
+
deadLetter: DEFAULT_DEAD_LETTER_CONFIG
|
|
55
|
+
};
|
|
56
|
+
function mergeConfig(userConfig) {
|
|
57
|
+
if (!userConfig) {
|
|
58
|
+
return { ...DEFAULT_SERVICE_CONFIG };
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
events: {
|
|
62
|
+
...DEFAULT_EVENT_CONFIG,
|
|
63
|
+
...userConfig.events
|
|
64
|
+
},
|
|
65
|
+
serialization: {
|
|
66
|
+
...DEFAULT_SERIALIZATION_CONFIG,
|
|
67
|
+
...userConfig.serialization
|
|
68
|
+
},
|
|
69
|
+
tracing: {
|
|
70
|
+
...DEFAULT_TRACING_CONFIG,
|
|
71
|
+
...userConfig.tracing
|
|
72
|
+
},
|
|
73
|
+
versioning: {
|
|
74
|
+
...DEFAULT_VERSIONING_CONFIG,
|
|
75
|
+
...userConfig.versioning
|
|
76
|
+
},
|
|
77
|
+
resilience: {
|
|
78
|
+
...DEFAULT_RESILIENCE_CONFIG,
|
|
79
|
+
...userConfig.resilience,
|
|
80
|
+
circuitBreaker: {
|
|
81
|
+
...DEFAULT_RESILIENCE_CONFIG.circuitBreaker,
|
|
82
|
+
...userConfig.resilience?.circuitBreaker
|
|
83
|
+
},
|
|
84
|
+
bulkhead: {
|
|
85
|
+
...DEFAULT_RESILIENCE_CONFIG.bulkhead,
|
|
86
|
+
...userConfig.resilience?.bulkhead
|
|
87
|
+
},
|
|
88
|
+
retry: {
|
|
89
|
+
...DEFAULT_RESILIENCE_CONFIG.retry,
|
|
90
|
+
...userConfig.resilience?.retry
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
deadLetter: {
|
|
94
|
+
...DEFAULT_DEAD_LETTER_CONFIG,
|
|
95
|
+
...userConfig.deadLetter
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// src/rpc/errors.ts
|
|
101
|
+
import { ParsError } from "@parsrun/core";
|
|
102
|
+
var RpcError = class extends ParsError {
|
|
103
|
+
retryable;
|
|
104
|
+
retryAfter;
|
|
105
|
+
constructor(message, code, statusCode = 500, options) {
|
|
106
|
+
super(message, code, statusCode, options?.details);
|
|
107
|
+
this.name = "RpcError";
|
|
108
|
+
this.retryable = options?.retryable ?? false;
|
|
109
|
+
if (options?.retryAfter !== void 0) {
|
|
110
|
+
this.retryAfter = options.retryAfter;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
var ServiceNotFoundError = class extends RpcError {
|
|
115
|
+
constructor(serviceName) {
|
|
116
|
+
super(`Service not found: ${serviceName}`, "SERVICE_NOT_FOUND", 404, {
|
|
117
|
+
retryable: false,
|
|
118
|
+
details: { service: serviceName }
|
|
119
|
+
});
|
|
120
|
+
this.name = "ServiceNotFoundError";
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
var MethodNotFoundError = class extends RpcError {
|
|
124
|
+
constructor(serviceName, methodName) {
|
|
125
|
+
super(
|
|
126
|
+
`Method not found: ${serviceName}.${methodName}`,
|
|
127
|
+
"METHOD_NOT_FOUND",
|
|
128
|
+
404,
|
|
129
|
+
{
|
|
130
|
+
retryable: false,
|
|
131
|
+
details: { service: serviceName, method: methodName }
|
|
132
|
+
}
|
|
133
|
+
);
|
|
134
|
+
this.name = "MethodNotFoundError";
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
var VersionMismatchError = class extends RpcError {
|
|
138
|
+
constructor(serviceName, requested, available) {
|
|
139
|
+
super(
|
|
140
|
+
`Version mismatch for ${serviceName}: requested ${requested}, available ${available}`,
|
|
141
|
+
"VERSION_MISMATCH",
|
|
142
|
+
400,
|
|
143
|
+
{
|
|
144
|
+
retryable: false,
|
|
145
|
+
details: { service: serviceName, requested, available }
|
|
146
|
+
}
|
|
147
|
+
);
|
|
148
|
+
this.name = "VersionMismatchError";
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
var TimeoutError = class extends RpcError {
|
|
152
|
+
constructor(serviceName, methodName, timeoutMs) {
|
|
153
|
+
super(
|
|
154
|
+
`Request to ${serviceName}.${methodName} timed out after ${timeoutMs}ms`,
|
|
155
|
+
"TIMEOUT",
|
|
156
|
+
504,
|
|
157
|
+
{
|
|
158
|
+
retryable: true,
|
|
159
|
+
details: { service: serviceName, method: methodName, timeout: timeoutMs }
|
|
160
|
+
}
|
|
161
|
+
);
|
|
162
|
+
this.name = "TimeoutError";
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
var CircuitOpenError = class extends RpcError {
|
|
166
|
+
constructor(serviceName, resetAfterMs) {
|
|
167
|
+
super(
|
|
168
|
+
`Circuit breaker open for ${serviceName}`,
|
|
169
|
+
"CIRCUIT_OPEN",
|
|
170
|
+
503,
|
|
171
|
+
{
|
|
172
|
+
retryable: true,
|
|
173
|
+
retryAfter: Math.ceil(resetAfterMs / 1e3),
|
|
174
|
+
details: { service: serviceName, resetAfterMs }
|
|
175
|
+
}
|
|
176
|
+
);
|
|
177
|
+
this.name = "CircuitOpenError";
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
var BulkheadRejectedError = class extends RpcError {
|
|
181
|
+
constructor(serviceName) {
|
|
182
|
+
super(
|
|
183
|
+
`Request rejected by bulkhead for ${serviceName}: too many concurrent requests`,
|
|
184
|
+
"BULKHEAD_REJECTED",
|
|
185
|
+
503,
|
|
186
|
+
{
|
|
187
|
+
retryable: true,
|
|
188
|
+
retryAfter: 1,
|
|
189
|
+
details: { service: serviceName }
|
|
190
|
+
}
|
|
191
|
+
);
|
|
192
|
+
this.name = "BulkheadRejectedError";
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
var TransportError = class extends RpcError {
|
|
196
|
+
constructor(message, cause) {
|
|
197
|
+
const options = {
|
|
198
|
+
retryable: true
|
|
199
|
+
};
|
|
200
|
+
if (cause) {
|
|
201
|
+
options.details = { cause: cause.message };
|
|
202
|
+
}
|
|
203
|
+
super(message, "TRANSPORT_ERROR", 502, options);
|
|
204
|
+
this.name = "TransportError";
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
var SerializationError = class extends RpcError {
|
|
208
|
+
constructor(message, cause) {
|
|
209
|
+
const options = {
|
|
210
|
+
retryable: false
|
|
211
|
+
};
|
|
212
|
+
if (cause) {
|
|
213
|
+
options.details = { cause: cause.message };
|
|
214
|
+
}
|
|
215
|
+
super(message, "SERIALIZATION_ERROR", 400, options);
|
|
216
|
+
this.name = "SerializationError";
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
function toRpcError(error) {
|
|
220
|
+
if (error instanceof RpcError) {
|
|
221
|
+
return error;
|
|
222
|
+
}
|
|
223
|
+
if (error instanceof Error) {
|
|
224
|
+
return new RpcError(error.message, "INTERNAL_ERROR", 500, {
|
|
225
|
+
retryable: false,
|
|
226
|
+
details: { originalError: error.name }
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
return new RpcError(String(error), "UNKNOWN_ERROR", 500, {
|
|
230
|
+
retryable: false
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// src/resilience/circuit-breaker.ts
|
|
235
|
+
var CircuitBreaker = class {
|
|
236
|
+
_state = "closed";
|
|
237
|
+
failures = 0;
|
|
238
|
+
successes = 0;
|
|
239
|
+
lastFailureTime = 0;
|
|
240
|
+
options;
|
|
241
|
+
constructor(options) {
|
|
242
|
+
this.options = options;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Get current state
|
|
246
|
+
*/
|
|
247
|
+
get state() {
|
|
248
|
+
if (this._state === "open") {
|
|
249
|
+
const timeSinceFailure = Date.now() - this.lastFailureTime;
|
|
250
|
+
if (timeSinceFailure >= this.options.resetTimeout) {
|
|
251
|
+
this.transitionTo("half-open");
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return this._state;
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Execute a function with circuit breaker protection
|
|
258
|
+
*/
|
|
259
|
+
async execute(fn) {
|
|
260
|
+
const currentState = this.state;
|
|
261
|
+
if (currentState === "open") {
|
|
262
|
+
const resetAfter = this.options.resetTimeout - (Date.now() - this.lastFailureTime);
|
|
263
|
+
throw new CircuitOpenError("service", Math.max(0, resetAfter));
|
|
264
|
+
}
|
|
265
|
+
try {
|
|
266
|
+
const result = await fn();
|
|
267
|
+
this.onSuccess();
|
|
268
|
+
return result;
|
|
269
|
+
} catch (error) {
|
|
270
|
+
this.onFailure();
|
|
271
|
+
throw error;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Record a successful call
|
|
276
|
+
*/
|
|
277
|
+
onSuccess() {
|
|
278
|
+
if (this._state === "half-open") {
|
|
279
|
+
this.successes++;
|
|
280
|
+
if (this.successes >= this.options.successThreshold) {
|
|
281
|
+
this.transitionTo("closed");
|
|
282
|
+
}
|
|
283
|
+
} else if (this._state === "closed") {
|
|
284
|
+
this.failures = 0;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Record a failed call
|
|
289
|
+
*/
|
|
290
|
+
onFailure() {
|
|
291
|
+
this.lastFailureTime = Date.now();
|
|
292
|
+
if (this._state === "half-open") {
|
|
293
|
+
this.transitionTo("open");
|
|
294
|
+
} else if (this._state === "closed") {
|
|
295
|
+
this.failures++;
|
|
296
|
+
if (this.failures >= this.options.failureThreshold) {
|
|
297
|
+
this.transitionTo("open");
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Transition to a new state
|
|
303
|
+
*/
|
|
304
|
+
transitionTo(newState) {
|
|
305
|
+
const oldState = this._state;
|
|
306
|
+
this._state = newState;
|
|
307
|
+
if (newState === "closed") {
|
|
308
|
+
this.failures = 0;
|
|
309
|
+
this.successes = 0;
|
|
310
|
+
} else if (newState === "half-open") {
|
|
311
|
+
this.successes = 0;
|
|
312
|
+
}
|
|
313
|
+
this.options.onStateChange?.(oldState, newState);
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Manually reset the circuit breaker
|
|
317
|
+
*/
|
|
318
|
+
reset() {
|
|
319
|
+
this.transitionTo("closed");
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Get circuit breaker statistics
|
|
323
|
+
*/
|
|
324
|
+
getStats() {
|
|
325
|
+
return {
|
|
326
|
+
state: this.state,
|
|
327
|
+
failures: this.failures,
|
|
328
|
+
successes: this.successes,
|
|
329
|
+
lastFailureTime: this.lastFailureTime
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
// src/resilience/bulkhead.ts
|
|
335
|
+
var Bulkhead = class {
|
|
336
|
+
_concurrent = 0;
|
|
337
|
+
queue = [];
|
|
338
|
+
options;
|
|
339
|
+
constructor(options) {
|
|
340
|
+
this.options = options;
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Get current concurrent count
|
|
344
|
+
*/
|
|
345
|
+
get concurrent() {
|
|
346
|
+
return this._concurrent;
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Get current queue size
|
|
350
|
+
*/
|
|
351
|
+
get queued() {
|
|
352
|
+
return this.queue.length;
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Check if bulkhead is full
|
|
356
|
+
*/
|
|
357
|
+
get isFull() {
|
|
358
|
+
return this._concurrent >= this.options.maxConcurrent && this.queue.length >= this.options.maxQueue;
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Execute a function with bulkhead protection
|
|
362
|
+
*/
|
|
363
|
+
async execute(fn) {
|
|
364
|
+
if (this._concurrent < this.options.maxConcurrent) {
|
|
365
|
+
return this.doExecute(fn);
|
|
366
|
+
}
|
|
367
|
+
if (this.queue.length < this.options.maxQueue) {
|
|
368
|
+
return this.enqueue(fn);
|
|
369
|
+
}
|
|
370
|
+
this.options.onRejected?.();
|
|
371
|
+
throw new BulkheadRejectedError("service");
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Execute immediately
|
|
375
|
+
*/
|
|
376
|
+
async doExecute(fn) {
|
|
377
|
+
this._concurrent++;
|
|
378
|
+
try {
|
|
379
|
+
return await fn();
|
|
380
|
+
} finally {
|
|
381
|
+
this._concurrent--;
|
|
382
|
+
this.processQueue();
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Add to queue
|
|
387
|
+
*/
|
|
388
|
+
enqueue(fn) {
|
|
389
|
+
return new Promise((resolve, reject) => {
|
|
390
|
+
this.queue.push({
|
|
391
|
+
fn,
|
|
392
|
+
resolve,
|
|
393
|
+
reject
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Process queued requests
|
|
399
|
+
*/
|
|
400
|
+
processQueue() {
|
|
401
|
+
if (this.queue.length === 0) return;
|
|
402
|
+
if (this._concurrent >= this.options.maxConcurrent) return;
|
|
403
|
+
const queued = this.queue.shift();
|
|
404
|
+
if (!queued) return;
|
|
405
|
+
this.doExecute(queued.fn).then(queued.resolve).catch(queued.reject);
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Get bulkhead statistics
|
|
409
|
+
*/
|
|
410
|
+
getStats() {
|
|
411
|
+
return {
|
|
412
|
+
concurrent: this._concurrent,
|
|
413
|
+
queued: this.queue.length,
|
|
414
|
+
maxConcurrent: this.options.maxConcurrent,
|
|
415
|
+
maxQueue: this.options.maxQueue
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Clear the queue (reject all pending)
|
|
420
|
+
*/
|
|
421
|
+
clearQueue() {
|
|
422
|
+
const error = new BulkheadRejectedError("service");
|
|
423
|
+
while (this.queue.length > 0) {
|
|
424
|
+
const queued = this.queue.shift();
|
|
425
|
+
queued?.reject(error);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
// src/resilience/retry.ts
|
|
431
|
+
var defaultShouldRetry = (error) => {
|
|
432
|
+
if (error && typeof error === "object" && "retryable" in error) {
|
|
433
|
+
return error.retryable;
|
|
434
|
+
}
|
|
435
|
+
return false;
|
|
436
|
+
};
|
|
437
|
+
function calculateDelay(attempt, options) {
|
|
438
|
+
let delay;
|
|
439
|
+
if (options.backoff === "exponential") {
|
|
440
|
+
delay = options.initialDelay * Math.pow(2, attempt);
|
|
441
|
+
} else {
|
|
442
|
+
delay = options.initialDelay * (attempt + 1);
|
|
443
|
+
}
|
|
444
|
+
delay = Math.min(delay, options.maxDelay);
|
|
445
|
+
if (options.jitter && options.jitter > 0) {
|
|
446
|
+
const jitterRange = delay * options.jitter;
|
|
447
|
+
delay = delay - jitterRange / 2 + Math.random() * jitterRange;
|
|
448
|
+
}
|
|
449
|
+
return Math.round(delay);
|
|
450
|
+
}
|
|
451
|
+
function sleep(ms) {
|
|
452
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
453
|
+
}
|
|
454
|
+
function withRetry(fn, options) {
|
|
455
|
+
const shouldRetry = options.shouldRetry ?? defaultShouldRetry;
|
|
456
|
+
return async () => {
|
|
457
|
+
let lastError;
|
|
458
|
+
for (let attempt = 0; attempt <= options.attempts; attempt++) {
|
|
459
|
+
try {
|
|
460
|
+
return await fn();
|
|
461
|
+
} catch (error) {
|
|
462
|
+
lastError = error;
|
|
463
|
+
if (attempt >= options.attempts || !shouldRetry(error, attempt)) {
|
|
464
|
+
throw error;
|
|
465
|
+
}
|
|
466
|
+
const delay = calculateDelay(attempt, options);
|
|
467
|
+
options.onRetry?.(error, attempt + 1, delay);
|
|
468
|
+
await sleep(delay);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
throw lastError;
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// src/resilience/timeout.ts
|
|
476
|
+
var TimeoutExceededError = class extends Error {
|
|
477
|
+
timeout;
|
|
478
|
+
constructor(timeout) {
|
|
479
|
+
super(`Operation timed out after ${timeout}ms`);
|
|
480
|
+
this.name = "TimeoutExceededError";
|
|
481
|
+
this.timeout = timeout;
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
function withTimeout(fn, timeoutMs, onTimeout) {
|
|
485
|
+
return async () => {
|
|
486
|
+
let timeoutId;
|
|
487
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
488
|
+
timeoutId = setTimeout(() => {
|
|
489
|
+
if (onTimeout) {
|
|
490
|
+
try {
|
|
491
|
+
onTimeout();
|
|
492
|
+
} catch (error) {
|
|
493
|
+
reject(error);
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
reject(new TimeoutExceededError(timeoutMs));
|
|
498
|
+
}, timeoutMs);
|
|
499
|
+
});
|
|
500
|
+
try {
|
|
501
|
+
return await Promise.race([fn(), timeoutPromise]);
|
|
502
|
+
} finally {
|
|
503
|
+
if (timeoutId !== void 0) {
|
|
504
|
+
clearTimeout(timeoutId);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// src/rpc/client.ts
|
|
511
|
+
var RpcClient = class {
|
|
512
|
+
service;
|
|
513
|
+
transport;
|
|
514
|
+
config;
|
|
515
|
+
defaultMetadata;
|
|
516
|
+
circuitBreaker;
|
|
517
|
+
bulkhead;
|
|
518
|
+
constructor(options) {
|
|
519
|
+
this.service = options.service;
|
|
520
|
+
this.transport = options.transport;
|
|
521
|
+
this.config = mergeConfig(options.config);
|
|
522
|
+
this.defaultMetadata = options.defaultMetadata ?? {};
|
|
523
|
+
const cbConfig = this.config.resilience?.circuitBreaker;
|
|
524
|
+
if (cbConfig && cbConfig.enabled && cbConfig.failureThreshold !== void 0 && cbConfig.resetTimeout !== void 0 && cbConfig.successThreshold !== void 0) {
|
|
525
|
+
this.circuitBreaker = new CircuitBreaker({
|
|
526
|
+
failureThreshold: cbConfig.failureThreshold,
|
|
527
|
+
resetTimeout: cbConfig.resetTimeout,
|
|
528
|
+
successThreshold: cbConfig.successThreshold
|
|
529
|
+
});
|
|
530
|
+
} else {
|
|
531
|
+
this.circuitBreaker = null;
|
|
532
|
+
}
|
|
533
|
+
const bhConfig = this.config.resilience?.bulkhead;
|
|
534
|
+
if (bhConfig && bhConfig.maxConcurrent !== void 0 && bhConfig.maxQueue !== void 0) {
|
|
535
|
+
this.bulkhead = new Bulkhead({
|
|
536
|
+
maxConcurrent: bhConfig.maxConcurrent,
|
|
537
|
+
maxQueue: bhConfig.maxQueue
|
|
538
|
+
});
|
|
539
|
+
} else {
|
|
540
|
+
this.bulkhead = null;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* Execute a query
|
|
545
|
+
*/
|
|
546
|
+
async query(method, input, options) {
|
|
547
|
+
return this.call("query", method, input, options);
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Execute a mutation
|
|
551
|
+
*/
|
|
552
|
+
async mutate(method, input, options) {
|
|
553
|
+
return this.call("mutation", method, input, options);
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Internal call implementation
|
|
557
|
+
*/
|
|
558
|
+
async call(type, method, input, options) {
|
|
559
|
+
const request = {
|
|
560
|
+
id: generateId(),
|
|
561
|
+
service: this.service,
|
|
562
|
+
method,
|
|
563
|
+
type,
|
|
564
|
+
input,
|
|
565
|
+
metadata: {
|
|
566
|
+
...this.defaultMetadata,
|
|
567
|
+
...options?.metadata
|
|
568
|
+
}
|
|
569
|
+
};
|
|
570
|
+
const version = options?.version ?? this.config.versioning.defaultVersion;
|
|
571
|
+
if (version) {
|
|
572
|
+
request.version = version;
|
|
573
|
+
}
|
|
574
|
+
if (options?.traceContext) {
|
|
575
|
+
request.traceContext = options.traceContext;
|
|
576
|
+
}
|
|
577
|
+
const timeout = options?.timeout ?? this.config.resilience.timeout ?? 3e4;
|
|
578
|
+
const retryConfig = options?.retry ?? this.config.resilience.retry;
|
|
579
|
+
let execute = async () => {
|
|
580
|
+
const response = await this.transport.call(request);
|
|
581
|
+
if (!response.success) {
|
|
582
|
+
const error = toRpcError(
|
|
583
|
+
new Error(response.error?.message ?? "Unknown error")
|
|
584
|
+
);
|
|
585
|
+
throw error;
|
|
586
|
+
}
|
|
587
|
+
return response.output;
|
|
588
|
+
};
|
|
589
|
+
execute = withTimeout(execute, timeout, () => {
|
|
590
|
+
throw new TimeoutError(this.service, method, timeout);
|
|
591
|
+
});
|
|
592
|
+
const attempts = retryConfig?.attempts ?? 0;
|
|
593
|
+
if (attempts > 0) {
|
|
594
|
+
execute = withRetry(execute, {
|
|
595
|
+
attempts,
|
|
596
|
+
backoff: retryConfig?.backoff ?? "exponential",
|
|
597
|
+
initialDelay: retryConfig?.initialDelay ?? 100,
|
|
598
|
+
maxDelay: retryConfig?.maxDelay ?? 5e3,
|
|
599
|
+
shouldRetry: (error) => {
|
|
600
|
+
if (error instanceof Error && "retryable" in error) {
|
|
601
|
+
return error.retryable;
|
|
602
|
+
}
|
|
603
|
+
return false;
|
|
604
|
+
}
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
if (this.circuitBreaker) {
|
|
608
|
+
const cb = this.circuitBreaker;
|
|
609
|
+
const originalExecute = execute;
|
|
610
|
+
execute = async () => {
|
|
611
|
+
return cb.execute(originalExecute);
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
if (this.bulkhead) {
|
|
615
|
+
const bh = this.bulkhead;
|
|
616
|
+
const originalExecute = execute;
|
|
617
|
+
execute = async () => {
|
|
618
|
+
return bh.execute(originalExecute);
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
return execute();
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* Get circuit breaker state
|
|
625
|
+
*/
|
|
626
|
+
getCircuitState() {
|
|
627
|
+
return this.circuitBreaker?.state ?? null;
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* Get bulkhead stats
|
|
631
|
+
*/
|
|
632
|
+
getBulkheadStats() {
|
|
633
|
+
if (!this.bulkhead) return null;
|
|
634
|
+
return {
|
|
635
|
+
concurrent: this.bulkhead.concurrent,
|
|
636
|
+
queued: this.bulkhead.queued
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* Close the client and release resources
|
|
641
|
+
*/
|
|
642
|
+
async close() {
|
|
643
|
+
await this.transport.close?.();
|
|
644
|
+
}
|
|
645
|
+
};
|
|
646
|
+
function createRpcClient(options) {
|
|
647
|
+
return new RpcClient(options);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// src/rpc/server.ts
|
|
651
|
+
import { createLogger } from "@parsrun/core";
|
|
652
|
+
|
|
653
|
+
// src/define.ts
|
|
654
|
+
function satisfiesVersion(version, requirement) {
|
|
655
|
+
const versionParts = version.split(".").map((p) => parseInt(p, 10));
|
|
656
|
+
const requirementParts = requirement.split(".");
|
|
657
|
+
for (let i = 0; i < requirementParts.length; i++) {
|
|
658
|
+
const req = requirementParts[i];
|
|
659
|
+
if (req === "x" || req === "*") {
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
const reqNum = parseInt(req ?? "0", 10);
|
|
663
|
+
const verNum = versionParts[i] ?? 0;
|
|
664
|
+
if (verNum !== reqNum) {
|
|
665
|
+
return false;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
return true;
|
|
669
|
+
}
|
|
670
|
+
function isMethodDeprecated(definition, methodName, type) {
|
|
671
|
+
const methods = type === "query" ? definition.queries : definition.mutations;
|
|
672
|
+
const method = methods?.[methodName];
|
|
673
|
+
if (!method?.deprecated) {
|
|
674
|
+
return { deprecated: false };
|
|
675
|
+
}
|
|
676
|
+
const result = {
|
|
677
|
+
deprecated: true,
|
|
678
|
+
since: method.deprecated
|
|
679
|
+
};
|
|
680
|
+
if (method.replacement) {
|
|
681
|
+
result.replacement = method.replacement;
|
|
682
|
+
}
|
|
683
|
+
return result;
|
|
684
|
+
}
|
|
685
|
+
function getMethodTimeout(definition, methodName, type, defaultTimeout) {
|
|
686
|
+
const methods = type === "query" ? definition.queries : definition.mutations;
|
|
687
|
+
const method = methods?.[methodName];
|
|
688
|
+
return method?.timeout ?? defaultTimeout;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// src/rpc/server.ts
|
|
692
|
+
var RpcServer = class {
|
|
693
|
+
definition;
|
|
694
|
+
handlers;
|
|
695
|
+
logger;
|
|
696
|
+
defaultTimeout;
|
|
697
|
+
middleware;
|
|
698
|
+
constructor(options) {
|
|
699
|
+
this.definition = options.definition;
|
|
700
|
+
this.handlers = options.handlers;
|
|
701
|
+
this.logger = options.logger ?? createLogger({ name: `rpc:${options.definition.name}` });
|
|
702
|
+
this.defaultTimeout = options.defaultTimeout ?? 3e4;
|
|
703
|
+
this.middleware = options.middleware ?? [];
|
|
704
|
+
}
|
|
705
|
+
/**
|
|
706
|
+
* Handle an RPC request
|
|
707
|
+
*/
|
|
708
|
+
async handle(request) {
|
|
709
|
+
const startTime = Date.now();
|
|
710
|
+
const context = {
|
|
711
|
+
requestId: request.id,
|
|
712
|
+
service: request.service,
|
|
713
|
+
method: request.method,
|
|
714
|
+
type: request.type,
|
|
715
|
+
metadata: request.metadata ?? {},
|
|
716
|
+
logger: this.logger.child({ requestId: request.id, method: request.method })
|
|
717
|
+
};
|
|
718
|
+
if (request.traceContext) {
|
|
719
|
+
context.traceContext = request.traceContext;
|
|
720
|
+
}
|
|
721
|
+
try {
|
|
722
|
+
if (request.version && !satisfiesVersion(this.definition.version, request.version)) {
|
|
723
|
+
throw new VersionMismatchError(
|
|
724
|
+
this.definition.name,
|
|
725
|
+
request.version,
|
|
726
|
+
this.definition.version
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
const handler = this.getHandler(request.method, request.type);
|
|
730
|
+
if (!handler) {
|
|
731
|
+
throw new MethodNotFoundError(this.definition.name, request.method);
|
|
732
|
+
}
|
|
733
|
+
const deprecation = isMethodDeprecated(this.definition, request.method, request.type);
|
|
734
|
+
if (deprecation.deprecated) {
|
|
735
|
+
context.logger.warn(`Method ${request.method} is deprecated`, {
|
|
736
|
+
since: deprecation.since,
|
|
737
|
+
replacement: deprecation.replacement
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
const chain = this.buildMiddlewareChain(request, context, handler);
|
|
741
|
+
const timeout = getMethodTimeout(
|
|
742
|
+
this.definition,
|
|
743
|
+
request.method,
|
|
744
|
+
request.type,
|
|
745
|
+
this.defaultTimeout
|
|
746
|
+
);
|
|
747
|
+
const output = await Promise.race([
|
|
748
|
+
chain(),
|
|
749
|
+
new Promise(
|
|
750
|
+
(_, reject) => setTimeout(() => reject(new Error("Handler timeout")), timeout)
|
|
751
|
+
)
|
|
752
|
+
]);
|
|
753
|
+
const duration = Date.now() - startTime;
|
|
754
|
+
context.logger.info(`${request.type} ${request.method} completed`, { durationMs: duration });
|
|
755
|
+
const successResponse = {
|
|
756
|
+
id: request.id,
|
|
757
|
+
success: true,
|
|
758
|
+
version: this.definition.version,
|
|
759
|
+
output
|
|
760
|
+
};
|
|
761
|
+
if (request.traceContext) {
|
|
762
|
+
successResponse.traceContext = request.traceContext;
|
|
763
|
+
}
|
|
764
|
+
return successResponse;
|
|
765
|
+
} catch (error) {
|
|
766
|
+
const duration = Date.now() - startTime;
|
|
767
|
+
const rpcError = toRpcError(error);
|
|
768
|
+
context.logger.error(`${request.type} ${request.method} failed`, error, {
|
|
769
|
+
durationMs: duration,
|
|
770
|
+
errorCode: rpcError.code
|
|
771
|
+
});
|
|
772
|
+
const errorData = {
|
|
773
|
+
code: rpcError.code,
|
|
774
|
+
message: rpcError.message,
|
|
775
|
+
retryable: rpcError.retryable
|
|
776
|
+
};
|
|
777
|
+
if (rpcError.details) {
|
|
778
|
+
errorData.details = rpcError.details;
|
|
779
|
+
}
|
|
780
|
+
if (rpcError.retryAfter !== void 0) {
|
|
781
|
+
errorData.retryAfter = rpcError.retryAfter;
|
|
782
|
+
}
|
|
783
|
+
const errorResponse = {
|
|
784
|
+
id: request.id,
|
|
785
|
+
success: false,
|
|
786
|
+
version: this.definition.version,
|
|
787
|
+
error: errorData
|
|
788
|
+
};
|
|
789
|
+
if (request.traceContext) {
|
|
790
|
+
errorResponse.traceContext = request.traceContext;
|
|
791
|
+
}
|
|
792
|
+
return errorResponse;
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
/**
|
|
796
|
+
* Get handler for a method
|
|
797
|
+
*/
|
|
798
|
+
getHandler(method, type) {
|
|
799
|
+
const handlers = type === "query" ? this.handlers.queries : this.handlers.mutations;
|
|
800
|
+
return handlers?.[method];
|
|
801
|
+
}
|
|
802
|
+
/**
|
|
803
|
+
* Build middleware chain
|
|
804
|
+
*/
|
|
805
|
+
buildMiddlewareChain(request, context, handler) {
|
|
806
|
+
let index = -1;
|
|
807
|
+
const dispatch = async (i) => {
|
|
808
|
+
if (i <= index) {
|
|
809
|
+
throw new Error("next() called multiple times");
|
|
810
|
+
}
|
|
811
|
+
index = i;
|
|
812
|
+
if (i < this.middleware.length) {
|
|
813
|
+
const mw = this.middleware[i];
|
|
814
|
+
return mw(request, context, () => dispatch(i + 1));
|
|
815
|
+
}
|
|
816
|
+
return handler(request.input, context);
|
|
817
|
+
};
|
|
818
|
+
return () => dispatch(0);
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* Get service definition
|
|
822
|
+
*/
|
|
823
|
+
getDefinition() {
|
|
824
|
+
return this.definition;
|
|
825
|
+
}
|
|
826
|
+
/**
|
|
827
|
+
* Get registered methods
|
|
828
|
+
*/
|
|
829
|
+
getMethods() {
|
|
830
|
+
return {
|
|
831
|
+
queries: Object.keys(this.handlers.queries ?? {}),
|
|
832
|
+
mutations: Object.keys(this.handlers.mutations ?? {})
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
};
|
|
836
|
+
function createRpcServer(options) {
|
|
837
|
+
return new RpcServer(options);
|
|
838
|
+
}
|
|
839
|
+
function loggingMiddleware() {
|
|
840
|
+
return async (request, context, next) => {
|
|
841
|
+
context.logger.debug(`Handling ${request.type} ${request.method}`, {
|
|
842
|
+
inputKeys: Object.keys(request.input)
|
|
843
|
+
});
|
|
844
|
+
const result = await next();
|
|
845
|
+
context.logger.debug(`Completed ${request.type} ${request.method}`);
|
|
846
|
+
return result;
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
function validationMiddleware(validators) {
|
|
850
|
+
return async (request, _context, next) => {
|
|
851
|
+
const validator = validators[request.method];
|
|
852
|
+
if (validator) {
|
|
853
|
+
request.input = validator(request.input);
|
|
854
|
+
}
|
|
855
|
+
return next();
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
function tenantMiddleware() {
|
|
859
|
+
return async (_request, context, next) => {
|
|
860
|
+
const tenantId = context.metadata["tenantId"];
|
|
861
|
+
if (tenantId) {
|
|
862
|
+
context.logger = context.logger.child({ tenantId });
|
|
863
|
+
}
|
|
864
|
+
return next();
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// src/rpc/transports/embedded.ts
|
|
869
|
+
var EmbeddedTransport = class {
|
|
870
|
+
name = "embedded";
|
|
871
|
+
server;
|
|
872
|
+
constructor(server) {
|
|
873
|
+
this.server = server;
|
|
874
|
+
}
|
|
875
|
+
async call(request) {
|
|
876
|
+
return this.server.handle(request);
|
|
877
|
+
}
|
|
878
|
+
async close() {
|
|
879
|
+
}
|
|
880
|
+
};
|
|
881
|
+
function createEmbeddedTransport(server) {
|
|
882
|
+
return new EmbeddedTransport(server);
|
|
883
|
+
}
|
|
884
|
+
var EmbeddedRegistry = class _EmbeddedRegistry {
|
|
885
|
+
static instance = null;
|
|
886
|
+
servers = /* @__PURE__ */ new Map();
|
|
887
|
+
constructor() {
|
|
888
|
+
}
|
|
889
|
+
static getInstance() {
|
|
890
|
+
if (!_EmbeddedRegistry.instance) {
|
|
891
|
+
_EmbeddedRegistry.instance = new _EmbeddedRegistry();
|
|
892
|
+
}
|
|
893
|
+
return _EmbeddedRegistry.instance;
|
|
894
|
+
}
|
|
895
|
+
/**
|
|
896
|
+
* Register a service
|
|
897
|
+
*/
|
|
898
|
+
register(name, server) {
|
|
899
|
+
if (this.servers.has(name)) {
|
|
900
|
+
throw new Error(`Service already registered: ${name}`);
|
|
901
|
+
}
|
|
902
|
+
this.servers.set(name, server);
|
|
903
|
+
}
|
|
904
|
+
/**
|
|
905
|
+
* Unregister a service
|
|
906
|
+
*/
|
|
907
|
+
unregister(name) {
|
|
908
|
+
return this.servers.delete(name);
|
|
909
|
+
}
|
|
910
|
+
/**
|
|
911
|
+
* Get a service by name
|
|
912
|
+
*/
|
|
913
|
+
get(name) {
|
|
914
|
+
return this.servers.get(name);
|
|
915
|
+
}
|
|
916
|
+
/**
|
|
917
|
+
* Check if a service is registered
|
|
918
|
+
*/
|
|
919
|
+
has(name) {
|
|
920
|
+
return this.servers.has(name);
|
|
921
|
+
}
|
|
922
|
+
/**
|
|
923
|
+
* Get all registered service names
|
|
924
|
+
*/
|
|
925
|
+
getServiceNames() {
|
|
926
|
+
return Array.from(this.servers.keys());
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* Create a transport for a registered service
|
|
930
|
+
*/
|
|
931
|
+
createTransport(name) {
|
|
932
|
+
const server = this.servers.get(name);
|
|
933
|
+
if (!server) {
|
|
934
|
+
throw new Error(`Service not found: ${name}`);
|
|
935
|
+
}
|
|
936
|
+
return new EmbeddedTransport(server);
|
|
937
|
+
}
|
|
938
|
+
/**
|
|
939
|
+
* Clear all registered services
|
|
940
|
+
*/
|
|
941
|
+
clear() {
|
|
942
|
+
this.servers.clear();
|
|
943
|
+
}
|
|
944
|
+
/**
|
|
945
|
+
* Reset the singleton instance (for testing)
|
|
946
|
+
*/
|
|
947
|
+
static reset() {
|
|
948
|
+
_EmbeddedRegistry.instance = null;
|
|
949
|
+
}
|
|
950
|
+
};
|
|
951
|
+
function getEmbeddedRegistry() {
|
|
952
|
+
return EmbeddedRegistry.getInstance();
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
// src/serialization/index.ts
|
|
956
|
+
var jsonSerializer = {
|
|
957
|
+
encode(data) {
|
|
958
|
+
return JSON.stringify(data);
|
|
959
|
+
},
|
|
960
|
+
decode(raw) {
|
|
961
|
+
if (raw instanceof ArrayBuffer) {
|
|
962
|
+
const decoder = new TextDecoder();
|
|
963
|
+
return JSON.parse(decoder.decode(raw));
|
|
964
|
+
}
|
|
965
|
+
return JSON.parse(raw);
|
|
966
|
+
},
|
|
967
|
+
contentType: "application/json"
|
|
968
|
+
};
|
|
969
|
+
|
|
970
|
+
// src/rpc/transports/http.ts
|
|
971
|
+
var HttpTransport = class {
|
|
972
|
+
name = "http";
|
|
973
|
+
baseUrl;
|
|
974
|
+
serializer;
|
|
975
|
+
headers;
|
|
976
|
+
fetchFn;
|
|
977
|
+
timeout;
|
|
978
|
+
constructor(options) {
|
|
979
|
+
this.baseUrl = options.baseUrl.replace(/\/$/, "");
|
|
980
|
+
this.serializer = options.serializer ?? jsonSerializer;
|
|
981
|
+
this.headers = options.headers ?? {};
|
|
982
|
+
this.fetchFn = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
983
|
+
this.timeout = options.timeout ?? 3e4;
|
|
984
|
+
}
|
|
985
|
+
async call(request) {
|
|
986
|
+
const url = `${this.baseUrl}/rpc`;
|
|
987
|
+
const controller = new AbortController();
|
|
988
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
989
|
+
try {
|
|
990
|
+
let body;
|
|
991
|
+
try {
|
|
992
|
+
body = this.serializer.encode(request);
|
|
993
|
+
} catch (error) {
|
|
994
|
+
throw new SerializationError(
|
|
995
|
+
"Failed to serialize request",
|
|
996
|
+
error instanceof Error ? error : void 0
|
|
997
|
+
);
|
|
998
|
+
}
|
|
999
|
+
const response = await this.fetchFn(url, {
|
|
1000
|
+
method: "POST",
|
|
1001
|
+
headers: {
|
|
1002
|
+
"Content-Type": this.serializer.contentType,
|
|
1003
|
+
Accept: this.serializer.contentType,
|
|
1004
|
+
"X-Request-ID": request.id,
|
|
1005
|
+
"X-Service": request.service,
|
|
1006
|
+
"X-Method": request.method,
|
|
1007
|
+
"X-Method-Type": request.type,
|
|
1008
|
+
...request.version ? { "X-Service-Version": request.version } : {},
|
|
1009
|
+
...request.traceContext ? {
|
|
1010
|
+
traceparent: formatTraceparent(request.traceContext),
|
|
1011
|
+
...request.traceContext.traceState ? { tracestate: request.traceContext.traceState } : {}
|
|
1012
|
+
} : {},
|
|
1013
|
+
...this.headers
|
|
1014
|
+
},
|
|
1015
|
+
body: body instanceof ArrayBuffer ? body : body,
|
|
1016
|
+
signal: controller.signal
|
|
1017
|
+
});
|
|
1018
|
+
let responseData;
|
|
1019
|
+
try {
|
|
1020
|
+
const contentType = response.headers.get("Content-Type") ?? "";
|
|
1021
|
+
if (contentType.includes("msgpack")) {
|
|
1022
|
+
const buffer = await response.arrayBuffer();
|
|
1023
|
+
responseData = this.serializer.decode(buffer);
|
|
1024
|
+
} else {
|
|
1025
|
+
const text = await response.text();
|
|
1026
|
+
responseData = this.serializer.decode(text);
|
|
1027
|
+
}
|
|
1028
|
+
} catch (error) {
|
|
1029
|
+
throw new SerializationError(
|
|
1030
|
+
"Failed to deserialize response",
|
|
1031
|
+
error instanceof Error ? error : void 0
|
|
1032
|
+
);
|
|
1033
|
+
}
|
|
1034
|
+
return responseData;
|
|
1035
|
+
} catch (error) {
|
|
1036
|
+
if (error instanceof SerializationError) {
|
|
1037
|
+
throw error;
|
|
1038
|
+
}
|
|
1039
|
+
if (error instanceof Error) {
|
|
1040
|
+
if (error.name === "AbortError") {
|
|
1041
|
+
throw new TransportError(`Request timeout after ${this.timeout}ms`);
|
|
1042
|
+
}
|
|
1043
|
+
throw new TransportError(`HTTP request failed: ${error.message}`, error);
|
|
1044
|
+
}
|
|
1045
|
+
throw new TransportError("Unknown transport error");
|
|
1046
|
+
} finally {
|
|
1047
|
+
clearTimeout(timeoutId);
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
async close() {
|
|
1051
|
+
}
|
|
1052
|
+
};
|
|
1053
|
+
function createHttpTransport(options) {
|
|
1054
|
+
return new HttpTransport(options);
|
|
1055
|
+
}
|
|
1056
|
+
function formatTraceparent(ctx) {
|
|
1057
|
+
const flags = ctx.traceFlags.toString(16).padStart(2, "0");
|
|
1058
|
+
return `00-${ctx.traceId}-${ctx.spanId}-${flags}`;
|
|
1059
|
+
}
|
|
1060
|
+
function parseTraceparent(header) {
|
|
1061
|
+
const parts = header.split("-");
|
|
1062
|
+
if (parts.length !== 4) {
|
|
1063
|
+
return null;
|
|
1064
|
+
}
|
|
1065
|
+
const [version, traceId, spanId, flags] = parts;
|
|
1066
|
+
if (version !== "00" || !traceId || !spanId || !flags) {
|
|
1067
|
+
return null;
|
|
1068
|
+
}
|
|
1069
|
+
if (traceId.length !== 32 || spanId.length !== 16 || flags.length !== 2) {
|
|
1070
|
+
return null;
|
|
1071
|
+
}
|
|
1072
|
+
return {
|
|
1073
|
+
traceId,
|
|
1074
|
+
spanId,
|
|
1075
|
+
traceFlags: parseInt(flags, 16)
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
function createHttpHandler(server) {
|
|
1079
|
+
return async (request) => {
|
|
1080
|
+
try {
|
|
1081
|
+
const contentType = request.headers.get("Content-Type") ?? "application/json";
|
|
1082
|
+
let body;
|
|
1083
|
+
if (contentType.includes("msgpack")) {
|
|
1084
|
+
const buffer = await request.arrayBuffer();
|
|
1085
|
+
body = JSON.parse(new TextDecoder().decode(buffer));
|
|
1086
|
+
} else {
|
|
1087
|
+
body = await request.json();
|
|
1088
|
+
}
|
|
1089
|
+
const rpcRequest = body;
|
|
1090
|
+
const traceparent = request.headers.get("traceparent");
|
|
1091
|
+
if (traceparent) {
|
|
1092
|
+
const traceContext = parseTraceparent(traceparent);
|
|
1093
|
+
if (traceContext) {
|
|
1094
|
+
const tracestate = request.headers.get("tracestate");
|
|
1095
|
+
if (tracestate) {
|
|
1096
|
+
traceContext.traceState = tracestate;
|
|
1097
|
+
}
|
|
1098
|
+
rpcRequest.traceContext = traceContext;
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
const response = await server.handle(rpcRequest);
|
|
1102
|
+
return new Response(JSON.stringify(response), {
|
|
1103
|
+
status: response.success ? 200 : getHttpStatus(response.error?.code),
|
|
1104
|
+
headers: {
|
|
1105
|
+
"Content-Type": "application/json",
|
|
1106
|
+
"X-Request-ID": rpcRequest.id
|
|
1107
|
+
}
|
|
1108
|
+
});
|
|
1109
|
+
} catch (error) {
|
|
1110
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1111
|
+
return new Response(
|
|
1112
|
+
JSON.stringify({
|
|
1113
|
+
success: false,
|
|
1114
|
+
error: {
|
|
1115
|
+
code: "INTERNAL_ERROR",
|
|
1116
|
+
message
|
|
1117
|
+
}
|
|
1118
|
+
}),
|
|
1119
|
+
{
|
|
1120
|
+
status: 500,
|
|
1121
|
+
headers: { "Content-Type": "application/json" }
|
|
1122
|
+
}
|
|
1123
|
+
);
|
|
1124
|
+
}
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
function getHttpStatus(code) {
|
|
1128
|
+
switch (code) {
|
|
1129
|
+
case "METHOD_NOT_FOUND":
|
|
1130
|
+
case "SERVICE_NOT_FOUND":
|
|
1131
|
+
return 404;
|
|
1132
|
+
case "VERSION_MISMATCH":
|
|
1133
|
+
case "VALIDATION_ERROR":
|
|
1134
|
+
case "SERIALIZATION_ERROR":
|
|
1135
|
+
return 400;
|
|
1136
|
+
case "UNAUTHORIZED":
|
|
1137
|
+
return 401;
|
|
1138
|
+
case "FORBIDDEN":
|
|
1139
|
+
return 403;
|
|
1140
|
+
case "TIMEOUT":
|
|
1141
|
+
return 504;
|
|
1142
|
+
case "CIRCUIT_OPEN":
|
|
1143
|
+
case "BULKHEAD_REJECTED":
|
|
1144
|
+
return 503;
|
|
1145
|
+
default:
|
|
1146
|
+
return 500;
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
export {
|
|
1150
|
+
BulkheadRejectedError,
|
|
1151
|
+
CircuitOpenError,
|
|
1152
|
+
EmbeddedRegistry,
|
|
1153
|
+
EmbeddedTransport,
|
|
1154
|
+
HttpTransport,
|
|
1155
|
+
MethodNotFoundError,
|
|
1156
|
+
RpcClient,
|
|
1157
|
+
RpcError,
|
|
1158
|
+
RpcServer,
|
|
1159
|
+
SerializationError,
|
|
1160
|
+
ServiceNotFoundError,
|
|
1161
|
+
TimeoutError,
|
|
1162
|
+
TransportError,
|
|
1163
|
+
VersionMismatchError,
|
|
1164
|
+
createEmbeddedTransport,
|
|
1165
|
+
createHttpHandler,
|
|
1166
|
+
createHttpTransport,
|
|
1167
|
+
createRpcClient,
|
|
1168
|
+
createRpcServer,
|
|
1169
|
+
getEmbeddedRegistry,
|
|
1170
|
+
loggingMiddleware,
|
|
1171
|
+
tenantMiddleware,
|
|
1172
|
+
toRpcError,
|
|
1173
|
+
validationMiddleware
|
|
1174
|
+
};
|
|
1175
|
+
//# sourceMappingURL=index.js.map
|