@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
package/dist/client.js
ADDED
|
@@ -0,0 +1,1474 @@
|
|
|
1
|
+
// src/client.ts
|
|
2
|
+
import { generateId as generateId3 } 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/client.ts
|
|
101
|
+
import { generateId } from "@parsrun/core";
|
|
102
|
+
|
|
103
|
+
// src/rpc/errors.ts
|
|
104
|
+
import { ParsError } from "@parsrun/core";
|
|
105
|
+
var RpcError = class extends ParsError {
|
|
106
|
+
retryable;
|
|
107
|
+
retryAfter;
|
|
108
|
+
constructor(message, code, statusCode = 500, options) {
|
|
109
|
+
super(message, code, statusCode, options?.details);
|
|
110
|
+
this.name = "RpcError";
|
|
111
|
+
this.retryable = options?.retryable ?? false;
|
|
112
|
+
if (options?.retryAfter !== void 0) {
|
|
113
|
+
this.retryAfter = options.retryAfter;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
var TimeoutError = class extends RpcError {
|
|
118
|
+
constructor(serviceName, methodName, timeoutMs) {
|
|
119
|
+
super(
|
|
120
|
+
`Request to ${serviceName}.${methodName} timed out after ${timeoutMs}ms`,
|
|
121
|
+
"TIMEOUT",
|
|
122
|
+
504,
|
|
123
|
+
{
|
|
124
|
+
retryable: true,
|
|
125
|
+
details: { service: serviceName, method: methodName, timeout: timeoutMs }
|
|
126
|
+
}
|
|
127
|
+
);
|
|
128
|
+
this.name = "TimeoutError";
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
var CircuitOpenError = class extends RpcError {
|
|
132
|
+
constructor(serviceName, resetAfterMs) {
|
|
133
|
+
super(
|
|
134
|
+
`Circuit breaker open for ${serviceName}`,
|
|
135
|
+
"CIRCUIT_OPEN",
|
|
136
|
+
503,
|
|
137
|
+
{
|
|
138
|
+
retryable: true,
|
|
139
|
+
retryAfter: Math.ceil(resetAfterMs / 1e3),
|
|
140
|
+
details: { service: serviceName, resetAfterMs }
|
|
141
|
+
}
|
|
142
|
+
);
|
|
143
|
+
this.name = "CircuitOpenError";
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
var BulkheadRejectedError = class extends RpcError {
|
|
147
|
+
constructor(serviceName) {
|
|
148
|
+
super(
|
|
149
|
+
`Request rejected by bulkhead for ${serviceName}: too many concurrent requests`,
|
|
150
|
+
"BULKHEAD_REJECTED",
|
|
151
|
+
503,
|
|
152
|
+
{
|
|
153
|
+
retryable: true,
|
|
154
|
+
retryAfter: 1,
|
|
155
|
+
details: { service: serviceName }
|
|
156
|
+
}
|
|
157
|
+
);
|
|
158
|
+
this.name = "BulkheadRejectedError";
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
var TransportError = class extends RpcError {
|
|
162
|
+
constructor(message, cause) {
|
|
163
|
+
const options = {
|
|
164
|
+
retryable: true
|
|
165
|
+
};
|
|
166
|
+
if (cause) {
|
|
167
|
+
options.details = { cause: cause.message };
|
|
168
|
+
}
|
|
169
|
+
super(message, "TRANSPORT_ERROR", 502, options);
|
|
170
|
+
this.name = "TransportError";
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
var SerializationError = class extends RpcError {
|
|
174
|
+
constructor(message, cause) {
|
|
175
|
+
const options = {
|
|
176
|
+
retryable: false
|
|
177
|
+
};
|
|
178
|
+
if (cause) {
|
|
179
|
+
options.details = { cause: cause.message };
|
|
180
|
+
}
|
|
181
|
+
super(message, "SERIALIZATION_ERROR", 400, options);
|
|
182
|
+
this.name = "SerializationError";
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
function toRpcError(error) {
|
|
186
|
+
if (error instanceof RpcError) {
|
|
187
|
+
return error;
|
|
188
|
+
}
|
|
189
|
+
if (error instanceof Error) {
|
|
190
|
+
return new RpcError(error.message, "INTERNAL_ERROR", 500, {
|
|
191
|
+
retryable: false,
|
|
192
|
+
details: { originalError: error.name }
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
return new RpcError(String(error), "UNKNOWN_ERROR", 500, {
|
|
196
|
+
retryable: false
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// src/resilience/circuit-breaker.ts
|
|
201
|
+
var CircuitBreaker = class {
|
|
202
|
+
_state = "closed";
|
|
203
|
+
failures = 0;
|
|
204
|
+
successes = 0;
|
|
205
|
+
lastFailureTime = 0;
|
|
206
|
+
options;
|
|
207
|
+
constructor(options) {
|
|
208
|
+
this.options = options;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Get current state
|
|
212
|
+
*/
|
|
213
|
+
get state() {
|
|
214
|
+
if (this._state === "open") {
|
|
215
|
+
const timeSinceFailure = Date.now() - this.lastFailureTime;
|
|
216
|
+
if (timeSinceFailure >= this.options.resetTimeout) {
|
|
217
|
+
this.transitionTo("half-open");
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return this._state;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Execute a function with circuit breaker protection
|
|
224
|
+
*/
|
|
225
|
+
async execute(fn) {
|
|
226
|
+
const currentState = this.state;
|
|
227
|
+
if (currentState === "open") {
|
|
228
|
+
const resetAfter = this.options.resetTimeout - (Date.now() - this.lastFailureTime);
|
|
229
|
+
throw new CircuitOpenError("service", Math.max(0, resetAfter));
|
|
230
|
+
}
|
|
231
|
+
try {
|
|
232
|
+
const result = await fn();
|
|
233
|
+
this.onSuccess();
|
|
234
|
+
return result;
|
|
235
|
+
} catch (error) {
|
|
236
|
+
this.onFailure();
|
|
237
|
+
throw error;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Record a successful call
|
|
242
|
+
*/
|
|
243
|
+
onSuccess() {
|
|
244
|
+
if (this._state === "half-open") {
|
|
245
|
+
this.successes++;
|
|
246
|
+
if (this.successes >= this.options.successThreshold) {
|
|
247
|
+
this.transitionTo("closed");
|
|
248
|
+
}
|
|
249
|
+
} else if (this._state === "closed") {
|
|
250
|
+
this.failures = 0;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Record a failed call
|
|
255
|
+
*/
|
|
256
|
+
onFailure() {
|
|
257
|
+
this.lastFailureTime = Date.now();
|
|
258
|
+
if (this._state === "half-open") {
|
|
259
|
+
this.transitionTo("open");
|
|
260
|
+
} else if (this._state === "closed") {
|
|
261
|
+
this.failures++;
|
|
262
|
+
if (this.failures >= this.options.failureThreshold) {
|
|
263
|
+
this.transitionTo("open");
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Transition to a new state
|
|
269
|
+
*/
|
|
270
|
+
transitionTo(newState) {
|
|
271
|
+
const oldState = this._state;
|
|
272
|
+
this._state = newState;
|
|
273
|
+
if (newState === "closed") {
|
|
274
|
+
this.failures = 0;
|
|
275
|
+
this.successes = 0;
|
|
276
|
+
} else if (newState === "half-open") {
|
|
277
|
+
this.successes = 0;
|
|
278
|
+
}
|
|
279
|
+
this.options.onStateChange?.(oldState, newState);
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Manually reset the circuit breaker
|
|
283
|
+
*/
|
|
284
|
+
reset() {
|
|
285
|
+
this.transitionTo("closed");
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Get circuit breaker statistics
|
|
289
|
+
*/
|
|
290
|
+
getStats() {
|
|
291
|
+
return {
|
|
292
|
+
state: this.state,
|
|
293
|
+
failures: this.failures,
|
|
294
|
+
successes: this.successes,
|
|
295
|
+
lastFailureTime: this.lastFailureTime
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
// src/resilience/bulkhead.ts
|
|
301
|
+
var Bulkhead = class {
|
|
302
|
+
_concurrent = 0;
|
|
303
|
+
queue = [];
|
|
304
|
+
options;
|
|
305
|
+
constructor(options) {
|
|
306
|
+
this.options = options;
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Get current concurrent count
|
|
310
|
+
*/
|
|
311
|
+
get concurrent() {
|
|
312
|
+
return this._concurrent;
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Get current queue size
|
|
316
|
+
*/
|
|
317
|
+
get queued() {
|
|
318
|
+
return this.queue.length;
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Check if bulkhead is full
|
|
322
|
+
*/
|
|
323
|
+
get isFull() {
|
|
324
|
+
return this._concurrent >= this.options.maxConcurrent && this.queue.length >= this.options.maxQueue;
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Execute a function with bulkhead protection
|
|
328
|
+
*/
|
|
329
|
+
async execute(fn) {
|
|
330
|
+
if (this._concurrent < this.options.maxConcurrent) {
|
|
331
|
+
return this.doExecute(fn);
|
|
332
|
+
}
|
|
333
|
+
if (this.queue.length < this.options.maxQueue) {
|
|
334
|
+
return this.enqueue(fn);
|
|
335
|
+
}
|
|
336
|
+
this.options.onRejected?.();
|
|
337
|
+
throw new BulkheadRejectedError("service");
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Execute immediately
|
|
341
|
+
*/
|
|
342
|
+
async doExecute(fn) {
|
|
343
|
+
this._concurrent++;
|
|
344
|
+
try {
|
|
345
|
+
return await fn();
|
|
346
|
+
} finally {
|
|
347
|
+
this._concurrent--;
|
|
348
|
+
this.processQueue();
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Add to queue
|
|
353
|
+
*/
|
|
354
|
+
enqueue(fn) {
|
|
355
|
+
return new Promise((resolve, reject) => {
|
|
356
|
+
this.queue.push({
|
|
357
|
+
fn,
|
|
358
|
+
resolve,
|
|
359
|
+
reject
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Process queued requests
|
|
365
|
+
*/
|
|
366
|
+
processQueue() {
|
|
367
|
+
if (this.queue.length === 0) return;
|
|
368
|
+
if (this._concurrent >= this.options.maxConcurrent) return;
|
|
369
|
+
const queued = this.queue.shift();
|
|
370
|
+
if (!queued) return;
|
|
371
|
+
this.doExecute(queued.fn).then(queued.resolve).catch(queued.reject);
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Get bulkhead statistics
|
|
375
|
+
*/
|
|
376
|
+
getStats() {
|
|
377
|
+
return {
|
|
378
|
+
concurrent: this._concurrent,
|
|
379
|
+
queued: this.queue.length,
|
|
380
|
+
maxConcurrent: this.options.maxConcurrent,
|
|
381
|
+
maxQueue: this.options.maxQueue
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Clear the queue (reject all pending)
|
|
386
|
+
*/
|
|
387
|
+
clearQueue() {
|
|
388
|
+
const error = new BulkheadRejectedError("service");
|
|
389
|
+
while (this.queue.length > 0) {
|
|
390
|
+
const queued = this.queue.shift();
|
|
391
|
+
queued?.reject(error);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
// src/resilience/retry.ts
|
|
397
|
+
var defaultShouldRetry = (error) => {
|
|
398
|
+
if (error && typeof error === "object" && "retryable" in error) {
|
|
399
|
+
return error.retryable;
|
|
400
|
+
}
|
|
401
|
+
return false;
|
|
402
|
+
};
|
|
403
|
+
function calculateDelay(attempt, options) {
|
|
404
|
+
let delay;
|
|
405
|
+
if (options.backoff === "exponential") {
|
|
406
|
+
delay = options.initialDelay * Math.pow(2, attempt);
|
|
407
|
+
} else {
|
|
408
|
+
delay = options.initialDelay * (attempt + 1);
|
|
409
|
+
}
|
|
410
|
+
delay = Math.min(delay, options.maxDelay);
|
|
411
|
+
if (options.jitter && options.jitter > 0) {
|
|
412
|
+
const jitterRange = delay * options.jitter;
|
|
413
|
+
delay = delay - jitterRange / 2 + Math.random() * jitterRange;
|
|
414
|
+
}
|
|
415
|
+
return Math.round(delay);
|
|
416
|
+
}
|
|
417
|
+
function sleep(ms) {
|
|
418
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
419
|
+
}
|
|
420
|
+
function withRetry(fn, options) {
|
|
421
|
+
const shouldRetry = options.shouldRetry ?? defaultShouldRetry;
|
|
422
|
+
return async () => {
|
|
423
|
+
let lastError;
|
|
424
|
+
for (let attempt = 0; attempt <= options.attempts; attempt++) {
|
|
425
|
+
try {
|
|
426
|
+
return await fn();
|
|
427
|
+
} catch (error) {
|
|
428
|
+
lastError = error;
|
|
429
|
+
if (attempt >= options.attempts || !shouldRetry(error, attempt)) {
|
|
430
|
+
throw error;
|
|
431
|
+
}
|
|
432
|
+
const delay = calculateDelay(attempt, options);
|
|
433
|
+
options.onRetry?.(error, attempt + 1, delay);
|
|
434
|
+
await sleep(delay);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
throw lastError;
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// src/resilience/timeout.ts
|
|
442
|
+
var TimeoutExceededError = class extends Error {
|
|
443
|
+
timeout;
|
|
444
|
+
constructor(timeout) {
|
|
445
|
+
super(`Operation timed out after ${timeout}ms`);
|
|
446
|
+
this.name = "TimeoutExceededError";
|
|
447
|
+
this.timeout = timeout;
|
|
448
|
+
}
|
|
449
|
+
};
|
|
450
|
+
function withTimeout(fn, timeoutMs, onTimeout) {
|
|
451
|
+
return async () => {
|
|
452
|
+
let timeoutId;
|
|
453
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
454
|
+
timeoutId = setTimeout(() => {
|
|
455
|
+
if (onTimeout) {
|
|
456
|
+
try {
|
|
457
|
+
onTimeout();
|
|
458
|
+
} catch (error) {
|
|
459
|
+
reject(error);
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
reject(new TimeoutExceededError(timeoutMs));
|
|
464
|
+
}, timeoutMs);
|
|
465
|
+
});
|
|
466
|
+
try {
|
|
467
|
+
return await Promise.race([fn(), timeoutPromise]);
|
|
468
|
+
} finally {
|
|
469
|
+
if (timeoutId !== void 0) {
|
|
470
|
+
clearTimeout(timeoutId);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// src/rpc/client.ts
|
|
477
|
+
var RpcClient = class {
|
|
478
|
+
service;
|
|
479
|
+
transport;
|
|
480
|
+
config;
|
|
481
|
+
defaultMetadata;
|
|
482
|
+
circuitBreaker;
|
|
483
|
+
bulkhead;
|
|
484
|
+
constructor(options) {
|
|
485
|
+
this.service = options.service;
|
|
486
|
+
this.transport = options.transport;
|
|
487
|
+
this.config = mergeConfig(options.config);
|
|
488
|
+
this.defaultMetadata = options.defaultMetadata ?? {};
|
|
489
|
+
const cbConfig = this.config.resilience?.circuitBreaker;
|
|
490
|
+
if (cbConfig && cbConfig.enabled && cbConfig.failureThreshold !== void 0 && cbConfig.resetTimeout !== void 0 && cbConfig.successThreshold !== void 0) {
|
|
491
|
+
this.circuitBreaker = new CircuitBreaker({
|
|
492
|
+
failureThreshold: cbConfig.failureThreshold,
|
|
493
|
+
resetTimeout: cbConfig.resetTimeout,
|
|
494
|
+
successThreshold: cbConfig.successThreshold
|
|
495
|
+
});
|
|
496
|
+
} else {
|
|
497
|
+
this.circuitBreaker = null;
|
|
498
|
+
}
|
|
499
|
+
const bhConfig = this.config.resilience?.bulkhead;
|
|
500
|
+
if (bhConfig && bhConfig.maxConcurrent !== void 0 && bhConfig.maxQueue !== void 0) {
|
|
501
|
+
this.bulkhead = new Bulkhead({
|
|
502
|
+
maxConcurrent: bhConfig.maxConcurrent,
|
|
503
|
+
maxQueue: bhConfig.maxQueue
|
|
504
|
+
});
|
|
505
|
+
} else {
|
|
506
|
+
this.bulkhead = null;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Execute a query
|
|
511
|
+
*/
|
|
512
|
+
async query(method, input, options) {
|
|
513
|
+
return this.call("query", method, input, options);
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* Execute a mutation
|
|
517
|
+
*/
|
|
518
|
+
async mutate(method, input, options) {
|
|
519
|
+
return this.call("mutation", method, input, options);
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Internal call implementation
|
|
523
|
+
*/
|
|
524
|
+
async call(type, method, input, options) {
|
|
525
|
+
const request = {
|
|
526
|
+
id: generateId(),
|
|
527
|
+
service: this.service,
|
|
528
|
+
method,
|
|
529
|
+
type,
|
|
530
|
+
input,
|
|
531
|
+
metadata: {
|
|
532
|
+
...this.defaultMetadata,
|
|
533
|
+
...options?.metadata
|
|
534
|
+
}
|
|
535
|
+
};
|
|
536
|
+
const version = options?.version ?? this.config.versioning.defaultVersion;
|
|
537
|
+
if (version) {
|
|
538
|
+
request.version = version;
|
|
539
|
+
}
|
|
540
|
+
if (options?.traceContext) {
|
|
541
|
+
request.traceContext = options.traceContext;
|
|
542
|
+
}
|
|
543
|
+
const timeout = options?.timeout ?? this.config.resilience.timeout ?? 3e4;
|
|
544
|
+
const retryConfig = options?.retry ?? this.config.resilience.retry;
|
|
545
|
+
let execute = async () => {
|
|
546
|
+
const response = await this.transport.call(request);
|
|
547
|
+
if (!response.success) {
|
|
548
|
+
const error = toRpcError(
|
|
549
|
+
new Error(response.error?.message ?? "Unknown error")
|
|
550
|
+
);
|
|
551
|
+
throw error;
|
|
552
|
+
}
|
|
553
|
+
return response.output;
|
|
554
|
+
};
|
|
555
|
+
execute = withTimeout(execute, timeout, () => {
|
|
556
|
+
throw new TimeoutError(this.service, method, timeout);
|
|
557
|
+
});
|
|
558
|
+
const attempts = retryConfig?.attempts ?? 0;
|
|
559
|
+
if (attempts > 0) {
|
|
560
|
+
execute = withRetry(execute, {
|
|
561
|
+
attempts,
|
|
562
|
+
backoff: retryConfig?.backoff ?? "exponential",
|
|
563
|
+
initialDelay: retryConfig?.initialDelay ?? 100,
|
|
564
|
+
maxDelay: retryConfig?.maxDelay ?? 5e3,
|
|
565
|
+
shouldRetry: (error) => {
|
|
566
|
+
if (error instanceof Error && "retryable" in error) {
|
|
567
|
+
return error.retryable;
|
|
568
|
+
}
|
|
569
|
+
return false;
|
|
570
|
+
}
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
if (this.circuitBreaker) {
|
|
574
|
+
const cb = this.circuitBreaker;
|
|
575
|
+
const originalExecute = execute;
|
|
576
|
+
execute = async () => {
|
|
577
|
+
return cb.execute(originalExecute);
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
if (this.bulkhead) {
|
|
581
|
+
const bh = this.bulkhead;
|
|
582
|
+
const originalExecute = execute;
|
|
583
|
+
execute = async () => {
|
|
584
|
+
return bh.execute(originalExecute);
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
return execute();
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* Get circuit breaker state
|
|
591
|
+
*/
|
|
592
|
+
getCircuitState() {
|
|
593
|
+
return this.circuitBreaker?.state ?? null;
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* Get bulkhead stats
|
|
597
|
+
*/
|
|
598
|
+
getBulkheadStats() {
|
|
599
|
+
if (!this.bulkhead) return null;
|
|
600
|
+
return {
|
|
601
|
+
concurrent: this.bulkhead.concurrent,
|
|
602
|
+
queued: this.bulkhead.queued
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Close the client and release resources
|
|
607
|
+
*/
|
|
608
|
+
async close() {
|
|
609
|
+
await this.transport.close?.();
|
|
610
|
+
}
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
// src/rpc/transports/embedded.ts
|
|
614
|
+
var EmbeddedTransport = class {
|
|
615
|
+
name = "embedded";
|
|
616
|
+
server;
|
|
617
|
+
constructor(server) {
|
|
618
|
+
this.server = server;
|
|
619
|
+
}
|
|
620
|
+
async call(request) {
|
|
621
|
+
return this.server.handle(request);
|
|
622
|
+
}
|
|
623
|
+
async close() {
|
|
624
|
+
}
|
|
625
|
+
};
|
|
626
|
+
var EmbeddedRegistry = class _EmbeddedRegistry {
|
|
627
|
+
static instance = null;
|
|
628
|
+
servers = /* @__PURE__ */ new Map();
|
|
629
|
+
constructor() {
|
|
630
|
+
}
|
|
631
|
+
static getInstance() {
|
|
632
|
+
if (!_EmbeddedRegistry.instance) {
|
|
633
|
+
_EmbeddedRegistry.instance = new _EmbeddedRegistry();
|
|
634
|
+
}
|
|
635
|
+
return _EmbeddedRegistry.instance;
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Register a service
|
|
639
|
+
*/
|
|
640
|
+
register(name, server) {
|
|
641
|
+
if (this.servers.has(name)) {
|
|
642
|
+
throw new Error(`Service already registered: ${name}`);
|
|
643
|
+
}
|
|
644
|
+
this.servers.set(name, server);
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Unregister a service
|
|
648
|
+
*/
|
|
649
|
+
unregister(name) {
|
|
650
|
+
return this.servers.delete(name);
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* Get a service by name
|
|
654
|
+
*/
|
|
655
|
+
get(name) {
|
|
656
|
+
return this.servers.get(name);
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* Check if a service is registered
|
|
660
|
+
*/
|
|
661
|
+
has(name) {
|
|
662
|
+
return this.servers.has(name);
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* Get all registered service names
|
|
666
|
+
*/
|
|
667
|
+
getServiceNames() {
|
|
668
|
+
return Array.from(this.servers.keys());
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* Create a transport for a registered service
|
|
672
|
+
*/
|
|
673
|
+
createTransport(name) {
|
|
674
|
+
const server = this.servers.get(name);
|
|
675
|
+
if (!server) {
|
|
676
|
+
throw new Error(`Service not found: ${name}`);
|
|
677
|
+
}
|
|
678
|
+
return new EmbeddedTransport(server);
|
|
679
|
+
}
|
|
680
|
+
/**
|
|
681
|
+
* Clear all registered services
|
|
682
|
+
*/
|
|
683
|
+
clear() {
|
|
684
|
+
this.servers.clear();
|
|
685
|
+
}
|
|
686
|
+
/**
|
|
687
|
+
* Reset the singleton instance (for testing)
|
|
688
|
+
*/
|
|
689
|
+
static reset() {
|
|
690
|
+
_EmbeddedRegistry.instance = null;
|
|
691
|
+
}
|
|
692
|
+
};
|
|
693
|
+
function getEmbeddedRegistry() {
|
|
694
|
+
return EmbeddedRegistry.getInstance();
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// src/serialization/index.ts
|
|
698
|
+
var jsonSerializer = {
|
|
699
|
+
encode(data) {
|
|
700
|
+
return JSON.stringify(data);
|
|
701
|
+
},
|
|
702
|
+
decode(raw) {
|
|
703
|
+
if (raw instanceof ArrayBuffer) {
|
|
704
|
+
const decoder = new TextDecoder();
|
|
705
|
+
return JSON.parse(decoder.decode(raw));
|
|
706
|
+
}
|
|
707
|
+
return JSON.parse(raw);
|
|
708
|
+
},
|
|
709
|
+
contentType: "application/json"
|
|
710
|
+
};
|
|
711
|
+
|
|
712
|
+
// src/rpc/transports/http.ts
|
|
713
|
+
var HttpTransport = class {
|
|
714
|
+
name = "http";
|
|
715
|
+
baseUrl;
|
|
716
|
+
serializer;
|
|
717
|
+
headers;
|
|
718
|
+
fetchFn;
|
|
719
|
+
timeout;
|
|
720
|
+
constructor(options) {
|
|
721
|
+
this.baseUrl = options.baseUrl.replace(/\/$/, "");
|
|
722
|
+
this.serializer = options.serializer ?? jsonSerializer;
|
|
723
|
+
this.headers = options.headers ?? {};
|
|
724
|
+
this.fetchFn = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
725
|
+
this.timeout = options.timeout ?? 3e4;
|
|
726
|
+
}
|
|
727
|
+
async call(request) {
|
|
728
|
+
const url = `${this.baseUrl}/rpc`;
|
|
729
|
+
const controller = new AbortController();
|
|
730
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
731
|
+
try {
|
|
732
|
+
let body;
|
|
733
|
+
try {
|
|
734
|
+
body = this.serializer.encode(request);
|
|
735
|
+
} catch (error) {
|
|
736
|
+
throw new SerializationError(
|
|
737
|
+
"Failed to serialize request",
|
|
738
|
+
error instanceof Error ? error : void 0
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
const response = await this.fetchFn(url, {
|
|
742
|
+
method: "POST",
|
|
743
|
+
headers: {
|
|
744
|
+
"Content-Type": this.serializer.contentType,
|
|
745
|
+
Accept: this.serializer.contentType,
|
|
746
|
+
"X-Request-ID": request.id,
|
|
747
|
+
"X-Service": request.service,
|
|
748
|
+
"X-Method": request.method,
|
|
749
|
+
"X-Method-Type": request.type,
|
|
750
|
+
...request.version ? { "X-Service-Version": request.version } : {},
|
|
751
|
+
...request.traceContext ? {
|
|
752
|
+
traceparent: formatTraceparent(request.traceContext),
|
|
753
|
+
...request.traceContext.traceState ? { tracestate: request.traceContext.traceState } : {}
|
|
754
|
+
} : {},
|
|
755
|
+
...this.headers
|
|
756
|
+
},
|
|
757
|
+
body: body instanceof ArrayBuffer ? body : body,
|
|
758
|
+
signal: controller.signal
|
|
759
|
+
});
|
|
760
|
+
let responseData;
|
|
761
|
+
try {
|
|
762
|
+
const contentType = response.headers.get("Content-Type") ?? "";
|
|
763
|
+
if (contentType.includes("msgpack")) {
|
|
764
|
+
const buffer = await response.arrayBuffer();
|
|
765
|
+
responseData = this.serializer.decode(buffer);
|
|
766
|
+
} else {
|
|
767
|
+
const text = await response.text();
|
|
768
|
+
responseData = this.serializer.decode(text);
|
|
769
|
+
}
|
|
770
|
+
} catch (error) {
|
|
771
|
+
throw new SerializationError(
|
|
772
|
+
"Failed to deserialize response",
|
|
773
|
+
error instanceof Error ? error : void 0
|
|
774
|
+
);
|
|
775
|
+
}
|
|
776
|
+
return responseData;
|
|
777
|
+
} catch (error) {
|
|
778
|
+
if (error instanceof SerializationError) {
|
|
779
|
+
throw error;
|
|
780
|
+
}
|
|
781
|
+
if (error instanceof Error) {
|
|
782
|
+
if (error.name === "AbortError") {
|
|
783
|
+
throw new TransportError(`Request timeout after ${this.timeout}ms`);
|
|
784
|
+
}
|
|
785
|
+
throw new TransportError(`HTTP request failed: ${error.message}`, error);
|
|
786
|
+
}
|
|
787
|
+
throw new TransportError("Unknown transport error");
|
|
788
|
+
} finally {
|
|
789
|
+
clearTimeout(timeoutId);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
async close() {
|
|
793
|
+
}
|
|
794
|
+
};
|
|
795
|
+
function createHttpTransport(options) {
|
|
796
|
+
return new HttpTransport(options);
|
|
797
|
+
}
|
|
798
|
+
function formatTraceparent(ctx) {
|
|
799
|
+
const flags = ctx.traceFlags.toString(16).padStart(2, "0");
|
|
800
|
+
return `00-${ctx.traceId}-${ctx.spanId}-${flags}`;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// src/events/transports/memory.ts
|
|
804
|
+
import { createLogger as createLogger2 } from "@parsrun/core";
|
|
805
|
+
|
|
806
|
+
// src/events/handler.ts
|
|
807
|
+
import { createLogger } from "@parsrun/core";
|
|
808
|
+
|
|
809
|
+
// src/events/format.ts
|
|
810
|
+
import { generateId as generateId2 } from "@parsrun/core";
|
|
811
|
+
function matchEventType(type, pattern) {
|
|
812
|
+
if (pattern === "*" || pattern === "**") {
|
|
813
|
+
return true;
|
|
814
|
+
}
|
|
815
|
+
const typeParts = type.split(".");
|
|
816
|
+
const patternParts = pattern.split(".");
|
|
817
|
+
let ti = 0;
|
|
818
|
+
let pi = 0;
|
|
819
|
+
while (ti < typeParts.length && pi < patternParts.length) {
|
|
820
|
+
const pp = patternParts[pi];
|
|
821
|
+
if (pp === "**") {
|
|
822
|
+
if (pi === patternParts.length - 1) {
|
|
823
|
+
return true;
|
|
824
|
+
}
|
|
825
|
+
for (let i = ti; i <= typeParts.length; i++) {
|
|
826
|
+
const remaining = typeParts.slice(i).join(".");
|
|
827
|
+
const remainingPattern = patternParts.slice(pi + 1).join(".");
|
|
828
|
+
if (matchEventType(remaining, remainingPattern)) {
|
|
829
|
+
return true;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
return false;
|
|
833
|
+
}
|
|
834
|
+
if (pp === "*") {
|
|
835
|
+
ti++;
|
|
836
|
+
pi++;
|
|
837
|
+
continue;
|
|
838
|
+
}
|
|
839
|
+
if (pp !== typeParts[ti]) {
|
|
840
|
+
return false;
|
|
841
|
+
}
|
|
842
|
+
ti++;
|
|
843
|
+
pi++;
|
|
844
|
+
}
|
|
845
|
+
return ti === typeParts.length && pi === patternParts.length;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// src/events/handler.ts
|
|
849
|
+
var EventHandlerRegistry = class {
|
|
850
|
+
handlers = /* @__PURE__ */ new Map();
|
|
851
|
+
logger;
|
|
852
|
+
deadLetterQueue;
|
|
853
|
+
defaultOptions;
|
|
854
|
+
constructor(options = {}) {
|
|
855
|
+
this.logger = options.logger ?? createLogger({ name: "event-handler" });
|
|
856
|
+
if (options.deadLetterQueue) {
|
|
857
|
+
this.deadLetterQueue = options.deadLetterQueue;
|
|
858
|
+
}
|
|
859
|
+
const defaultOpts = {
|
|
860
|
+
retries: options.defaultOptions?.retries ?? 3,
|
|
861
|
+
backoff: options.defaultOptions?.backoff ?? "exponential",
|
|
862
|
+
maxDelay: options.defaultOptions?.maxDelay ?? 3e4,
|
|
863
|
+
onExhausted: options.defaultOptions?.onExhausted ?? "log"
|
|
864
|
+
};
|
|
865
|
+
if (options.defaultOptions?.deadLetter) {
|
|
866
|
+
defaultOpts.deadLetter = options.defaultOptions.deadLetter;
|
|
867
|
+
}
|
|
868
|
+
this.defaultOptions = defaultOpts;
|
|
869
|
+
}
|
|
870
|
+
/**
|
|
871
|
+
* Register an event handler
|
|
872
|
+
*/
|
|
873
|
+
register(pattern, handler, options) {
|
|
874
|
+
const registration = {
|
|
875
|
+
pattern,
|
|
876
|
+
handler,
|
|
877
|
+
options: {
|
|
878
|
+
...this.defaultOptions,
|
|
879
|
+
...options
|
|
880
|
+
}
|
|
881
|
+
};
|
|
882
|
+
const handlers = this.handlers.get(pattern) ?? [];
|
|
883
|
+
handlers.push(registration);
|
|
884
|
+
this.handlers.set(pattern, handlers);
|
|
885
|
+
this.logger.debug(`Handler registered for pattern: ${pattern}`);
|
|
886
|
+
return () => {
|
|
887
|
+
const currentHandlers = this.handlers.get(pattern);
|
|
888
|
+
if (currentHandlers) {
|
|
889
|
+
const index = currentHandlers.indexOf(registration);
|
|
890
|
+
if (index !== -1) {
|
|
891
|
+
currentHandlers.splice(index, 1);
|
|
892
|
+
if (currentHandlers.length === 0) {
|
|
893
|
+
this.handlers.delete(pattern);
|
|
894
|
+
}
|
|
895
|
+
this.logger.debug(`Handler unregistered for pattern: ${pattern}`);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
/**
|
|
901
|
+
* Handle an event
|
|
902
|
+
*/
|
|
903
|
+
async handle(event) {
|
|
904
|
+
const matchingHandlers = this.getMatchingHandlers(event.type);
|
|
905
|
+
if (matchingHandlers.length === 0) {
|
|
906
|
+
this.logger.debug(`No handlers for event type: ${event.type}`, {
|
|
907
|
+
eventId: event.id
|
|
908
|
+
});
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
this.logger.debug(`Handling event: ${event.type}`, {
|
|
912
|
+
eventId: event.id,
|
|
913
|
+
handlerCount: matchingHandlers.length
|
|
914
|
+
});
|
|
915
|
+
const results = await Promise.allSettled(
|
|
916
|
+
matchingHandlers.map((reg) => this.executeHandler(event, reg))
|
|
917
|
+
);
|
|
918
|
+
for (let i = 0; i < results.length; i++) {
|
|
919
|
+
const result = results[i];
|
|
920
|
+
if (result?.status === "rejected") {
|
|
921
|
+
this.logger.error(
|
|
922
|
+
`Handler failed for ${event.type}`,
|
|
923
|
+
result.reason,
|
|
924
|
+
{ eventId: event.id, pattern: matchingHandlers[i]?.pattern }
|
|
925
|
+
);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
/**
|
|
930
|
+
* Execute a single handler with retry logic
|
|
931
|
+
*/
|
|
932
|
+
async executeHandler(event, registration) {
|
|
933
|
+
const { handler, options } = registration;
|
|
934
|
+
const maxAttempts = options.retries + 1;
|
|
935
|
+
let lastError;
|
|
936
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
937
|
+
try {
|
|
938
|
+
const context = {
|
|
939
|
+
logger: this.logger.child({
|
|
940
|
+
eventId: event.id,
|
|
941
|
+
pattern: registration.pattern,
|
|
942
|
+
attempt
|
|
943
|
+
}),
|
|
944
|
+
attempt,
|
|
945
|
+
maxAttempts,
|
|
946
|
+
isRetry: attempt > 1
|
|
947
|
+
};
|
|
948
|
+
if (event.parstracecontext) {
|
|
949
|
+
const traceCtx = parseTraceContext(event.parstracecontext);
|
|
950
|
+
if (traceCtx) {
|
|
951
|
+
context.traceContext = traceCtx;
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
await handler(event, context);
|
|
955
|
+
return;
|
|
956
|
+
} catch (error) {
|
|
957
|
+
lastError = error;
|
|
958
|
+
if (attempt < maxAttempts) {
|
|
959
|
+
const delay = this.calculateBackoff(attempt, options);
|
|
960
|
+
this.logger.warn(
|
|
961
|
+
`Handler failed, retrying in ${delay}ms`,
|
|
962
|
+
{ eventId: event.id, attempt, maxAttempts }
|
|
963
|
+
);
|
|
964
|
+
await sleep2(delay);
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
await this.handleExhausted(event, registration, lastError);
|
|
969
|
+
}
|
|
970
|
+
/**
|
|
971
|
+
* Calculate backoff delay
|
|
972
|
+
*/
|
|
973
|
+
calculateBackoff(attempt, options) {
|
|
974
|
+
const baseDelay = 100;
|
|
975
|
+
if (options.backoff === "exponential") {
|
|
976
|
+
return Math.min(baseDelay * Math.pow(2, attempt - 1), options.maxDelay);
|
|
977
|
+
}
|
|
978
|
+
return Math.min(baseDelay * attempt, options.maxDelay);
|
|
979
|
+
}
|
|
980
|
+
/**
|
|
981
|
+
* Handle exhausted retries
|
|
982
|
+
*/
|
|
983
|
+
async handleExhausted(event, registration, error) {
|
|
984
|
+
const { options } = registration;
|
|
985
|
+
if (options.deadLetter && this.deadLetterQueue) {
|
|
986
|
+
await this.deadLetterQueue.add({
|
|
987
|
+
event,
|
|
988
|
+
error: error.message,
|
|
989
|
+
pattern: registration.pattern,
|
|
990
|
+
attempts: options.retries + 1
|
|
991
|
+
});
|
|
992
|
+
}
|
|
993
|
+
switch (options.onExhausted) {
|
|
994
|
+
case "alert":
|
|
995
|
+
this.logger.error(
|
|
996
|
+
`[ALERT] Event handler exhausted all retries`,
|
|
997
|
+
error,
|
|
998
|
+
{
|
|
999
|
+
eventId: event.id,
|
|
1000
|
+
eventType: event.type,
|
|
1001
|
+
pattern: registration.pattern
|
|
1002
|
+
}
|
|
1003
|
+
);
|
|
1004
|
+
break;
|
|
1005
|
+
case "discard":
|
|
1006
|
+
this.logger.debug(`Event discarded after exhausted retries`, {
|
|
1007
|
+
eventId: event.id
|
|
1008
|
+
});
|
|
1009
|
+
break;
|
|
1010
|
+
case "log":
|
|
1011
|
+
default:
|
|
1012
|
+
this.logger.warn(`Event handler exhausted all retries`, {
|
|
1013
|
+
eventId: event.id,
|
|
1014
|
+
error: error.message
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
/**
|
|
1019
|
+
* Get handlers matching an event type
|
|
1020
|
+
*/
|
|
1021
|
+
getMatchingHandlers(eventType) {
|
|
1022
|
+
const matching = [];
|
|
1023
|
+
for (const [pattern, handlers] of this.handlers) {
|
|
1024
|
+
if (matchEventType(eventType, pattern)) {
|
|
1025
|
+
matching.push(...handlers);
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
return matching;
|
|
1029
|
+
}
|
|
1030
|
+
/**
|
|
1031
|
+
* Get all registered patterns
|
|
1032
|
+
*/
|
|
1033
|
+
getPatterns() {
|
|
1034
|
+
return Array.from(this.handlers.keys());
|
|
1035
|
+
}
|
|
1036
|
+
/**
|
|
1037
|
+
* Check if a pattern has handlers
|
|
1038
|
+
*/
|
|
1039
|
+
hasHandlers(pattern) {
|
|
1040
|
+
return this.handlers.has(pattern);
|
|
1041
|
+
}
|
|
1042
|
+
/**
|
|
1043
|
+
* Clear all handlers
|
|
1044
|
+
*/
|
|
1045
|
+
clear() {
|
|
1046
|
+
this.handlers.clear();
|
|
1047
|
+
}
|
|
1048
|
+
};
|
|
1049
|
+
function sleep2(ms) {
|
|
1050
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1051
|
+
}
|
|
1052
|
+
function parseTraceContext(traceparent) {
|
|
1053
|
+
const parts = traceparent.split("-");
|
|
1054
|
+
if (parts.length !== 4) return void 0;
|
|
1055
|
+
const [, traceId, spanId, flags] = parts;
|
|
1056
|
+
if (!traceId || !spanId || !flags) return void 0;
|
|
1057
|
+
return {
|
|
1058
|
+
traceId,
|
|
1059
|
+
spanId,
|
|
1060
|
+
traceFlags: parseInt(flags, 16)
|
|
1061
|
+
};
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
// src/events/transports/memory.ts
|
|
1065
|
+
var MemoryEventTransport = class {
|
|
1066
|
+
name = "memory";
|
|
1067
|
+
registry;
|
|
1068
|
+
logger;
|
|
1069
|
+
sync;
|
|
1070
|
+
pendingEvents = [];
|
|
1071
|
+
processing = false;
|
|
1072
|
+
constructor(options = {}) {
|
|
1073
|
+
this.logger = options.logger ?? createLogger2({ name: "memory-transport" });
|
|
1074
|
+
this.sync = options.sync ?? false;
|
|
1075
|
+
const registryOptions = {
|
|
1076
|
+
logger: this.logger
|
|
1077
|
+
};
|
|
1078
|
+
if (options.defaultHandlerOptions) {
|
|
1079
|
+
registryOptions.defaultOptions = options.defaultHandlerOptions;
|
|
1080
|
+
}
|
|
1081
|
+
this.registry = new EventHandlerRegistry(registryOptions);
|
|
1082
|
+
}
|
|
1083
|
+
/**
|
|
1084
|
+
* Emit an event
|
|
1085
|
+
*/
|
|
1086
|
+
async emit(event) {
|
|
1087
|
+
this.logger.debug(`Event emitted: ${event.type}`, {
|
|
1088
|
+
eventId: event.id,
|
|
1089
|
+
tenantId: event.parstenantid
|
|
1090
|
+
});
|
|
1091
|
+
if (this.sync) {
|
|
1092
|
+
await this.registry.handle(event);
|
|
1093
|
+
} else {
|
|
1094
|
+
this.pendingEvents.push(event);
|
|
1095
|
+
this.processQueue();
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
/**
|
|
1099
|
+
* Subscribe to events
|
|
1100
|
+
*/
|
|
1101
|
+
subscribe(eventType, handler, options) {
|
|
1102
|
+
return this.registry.register(eventType, handler, options);
|
|
1103
|
+
}
|
|
1104
|
+
/**
|
|
1105
|
+
* Process pending events asynchronously
|
|
1106
|
+
*/
|
|
1107
|
+
async processQueue() {
|
|
1108
|
+
if (this.processing) return;
|
|
1109
|
+
this.processing = true;
|
|
1110
|
+
try {
|
|
1111
|
+
while (this.pendingEvents.length > 0) {
|
|
1112
|
+
const event = this.pendingEvents.shift();
|
|
1113
|
+
if (event) {
|
|
1114
|
+
await this.registry.handle(event);
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
} finally {
|
|
1118
|
+
this.processing = false;
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
/**
|
|
1122
|
+
* Wait for all pending events to be processed
|
|
1123
|
+
*/
|
|
1124
|
+
async flush() {
|
|
1125
|
+
while (this.pendingEvents.length > 0 || this.processing) {
|
|
1126
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
/**
|
|
1130
|
+
* Get pending event count
|
|
1131
|
+
*/
|
|
1132
|
+
get pendingCount() {
|
|
1133
|
+
return this.pendingEvents.length;
|
|
1134
|
+
}
|
|
1135
|
+
/**
|
|
1136
|
+
* Get registered patterns
|
|
1137
|
+
*/
|
|
1138
|
+
getPatterns() {
|
|
1139
|
+
return this.registry.getPatterns();
|
|
1140
|
+
}
|
|
1141
|
+
/**
|
|
1142
|
+
* Clear all subscriptions
|
|
1143
|
+
*/
|
|
1144
|
+
clear() {
|
|
1145
|
+
this.registry.clear();
|
|
1146
|
+
this.pendingEvents.length = 0;
|
|
1147
|
+
}
|
|
1148
|
+
/**
|
|
1149
|
+
* Close the transport
|
|
1150
|
+
*/
|
|
1151
|
+
async close() {
|
|
1152
|
+
await this.flush();
|
|
1153
|
+
this.clear();
|
|
1154
|
+
}
|
|
1155
|
+
};
|
|
1156
|
+
function createMemoryEventTransport(options) {
|
|
1157
|
+
return new MemoryEventTransport(options);
|
|
1158
|
+
}
|
|
1159
|
+
var GlobalEventBus = class _GlobalEventBus {
|
|
1160
|
+
static instance = null;
|
|
1161
|
+
transports = /* @__PURE__ */ new Map();
|
|
1162
|
+
logger;
|
|
1163
|
+
constructor() {
|
|
1164
|
+
this.logger = createLogger2({ name: "global-event-bus" });
|
|
1165
|
+
}
|
|
1166
|
+
static getInstance() {
|
|
1167
|
+
if (!_GlobalEventBus.instance) {
|
|
1168
|
+
_GlobalEventBus.instance = new _GlobalEventBus();
|
|
1169
|
+
}
|
|
1170
|
+
return _GlobalEventBus.instance;
|
|
1171
|
+
}
|
|
1172
|
+
/**
|
|
1173
|
+
* Register a service's event transport
|
|
1174
|
+
*/
|
|
1175
|
+
register(serviceName, transport) {
|
|
1176
|
+
if (this.transports.has(serviceName)) {
|
|
1177
|
+
throw new Error(`Service already registered: ${serviceName}`);
|
|
1178
|
+
}
|
|
1179
|
+
this.transports.set(serviceName, transport);
|
|
1180
|
+
this.logger.debug(`Service registered: ${serviceName}`);
|
|
1181
|
+
}
|
|
1182
|
+
/**
|
|
1183
|
+
* Unregister a service
|
|
1184
|
+
*/
|
|
1185
|
+
unregister(serviceName) {
|
|
1186
|
+
const deleted = this.transports.delete(serviceName);
|
|
1187
|
+
if (deleted) {
|
|
1188
|
+
this.logger.debug(`Service unregistered: ${serviceName}`);
|
|
1189
|
+
}
|
|
1190
|
+
return deleted;
|
|
1191
|
+
}
|
|
1192
|
+
/**
|
|
1193
|
+
* Broadcast an event to all services (except source)
|
|
1194
|
+
*/
|
|
1195
|
+
async broadcast(event, excludeSource) {
|
|
1196
|
+
const promises = [];
|
|
1197
|
+
for (const [name, transport] of this.transports) {
|
|
1198
|
+
if (name !== excludeSource) {
|
|
1199
|
+
promises.push(transport.emit(event));
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
await Promise.allSettled(promises);
|
|
1203
|
+
}
|
|
1204
|
+
/**
|
|
1205
|
+
* Send an event to a specific service
|
|
1206
|
+
*/
|
|
1207
|
+
async send(serviceName, event) {
|
|
1208
|
+
const transport = this.transports.get(serviceName);
|
|
1209
|
+
if (!transport) {
|
|
1210
|
+
this.logger.warn(`Target service not found: ${serviceName}`, {
|
|
1211
|
+
eventId: event.id
|
|
1212
|
+
});
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
await transport.emit(event);
|
|
1216
|
+
}
|
|
1217
|
+
/**
|
|
1218
|
+
* Get all registered service names
|
|
1219
|
+
*/
|
|
1220
|
+
getServices() {
|
|
1221
|
+
return Array.from(this.transports.keys());
|
|
1222
|
+
}
|
|
1223
|
+
/**
|
|
1224
|
+
* Clear all registrations
|
|
1225
|
+
*/
|
|
1226
|
+
clear() {
|
|
1227
|
+
this.transports.clear();
|
|
1228
|
+
}
|
|
1229
|
+
/**
|
|
1230
|
+
* Reset singleton (for testing)
|
|
1231
|
+
*/
|
|
1232
|
+
static reset() {
|
|
1233
|
+
_GlobalEventBus.instance = null;
|
|
1234
|
+
}
|
|
1235
|
+
};
|
|
1236
|
+
function getGlobalEventBus() {
|
|
1237
|
+
return GlobalEventBus.getInstance();
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
// src/tracing/tracer.ts
|
|
1241
|
+
import { createLogger as createLogger4 } from "@parsrun/core";
|
|
1242
|
+
|
|
1243
|
+
// src/tracing/exporters.ts
|
|
1244
|
+
import { createLogger as createLogger3 } from "@parsrun/core";
|
|
1245
|
+
|
|
1246
|
+
// src/tracing/tracer.ts
|
|
1247
|
+
var globalTracer = null;
|
|
1248
|
+
function getGlobalTracer() {
|
|
1249
|
+
return globalTracer;
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
// src/client.ts
|
|
1253
|
+
var ServiceClientImpl = class {
|
|
1254
|
+
name;
|
|
1255
|
+
rpcClient;
|
|
1256
|
+
eventTransport;
|
|
1257
|
+
config;
|
|
1258
|
+
tracer;
|
|
1259
|
+
constructor(definition, rpcTransport, eventTransport, config, _logger) {
|
|
1260
|
+
this.name = definition.name;
|
|
1261
|
+
this.config = mergeConfig(config);
|
|
1262
|
+
this.tracer = getGlobalTracer();
|
|
1263
|
+
this.rpcClient = new RpcClient({
|
|
1264
|
+
service: definition.name,
|
|
1265
|
+
transport: rpcTransport,
|
|
1266
|
+
config: this.config
|
|
1267
|
+
});
|
|
1268
|
+
this.eventTransport = eventTransport;
|
|
1269
|
+
}
|
|
1270
|
+
/**
|
|
1271
|
+
* Execute a query
|
|
1272
|
+
*/
|
|
1273
|
+
async query(method, input) {
|
|
1274
|
+
const methodName = String(method);
|
|
1275
|
+
const traceContext = this.tracer?.currentContext();
|
|
1276
|
+
if (this.tracer && traceContext) {
|
|
1277
|
+
return this.tracer.trace(
|
|
1278
|
+
`rpc.${this.name}.${methodName}`,
|
|
1279
|
+
async () => {
|
|
1280
|
+
return this.rpcClient.query(methodName, input, {
|
|
1281
|
+
traceContext
|
|
1282
|
+
});
|
|
1283
|
+
},
|
|
1284
|
+
{ kind: "client" }
|
|
1285
|
+
);
|
|
1286
|
+
}
|
|
1287
|
+
return this.rpcClient.query(methodName, input);
|
|
1288
|
+
}
|
|
1289
|
+
/**
|
|
1290
|
+
* Execute a mutation
|
|
1291
|
+
*/
|
|
1292
|
+
async mutate(method, input) {
|
|
1293
|
+
const methodName = String(method);
|
|
1294
|
+
const traceContext = this.tracer?.currentContext();
|
|
1295
|
+
if (this.tracer && traceContext) {
|
|
1296
|
+
return this.tracer.trace(
|
|
1297
|
+
`rpc.${this.name}.${methodName}`,
|
|
1298
|
+
async () => {
|
|
1299
|
+
return this.rpcClient.mutate(methodName, input, {
|
|
1300
|
+
traceContext
|
|
1301
|
+
});
|
|
1302
|
+
},
|
|
1303
|
+
{ kind: "client" }
|
|
1304
|
+
);
|
|
1305
|
+
}
|
|
1306
|
+
return this.rpcClient.mutate(methodName, input);
|
|
1307
|
+
}
|
|
1308
|
+
/**
|
|
1309
|
+
* Emit an event
|
|
1310
|
+
*/
|
|
1311
|
+
async emit(eventType, data) {
|
|
1312
|
+
const type = String(eventType);
|
|
1313
|
+
const traceContext = this.tracer?.currentContext();
|
|
1314
|
+
const event = {
|
|
1315
|
+
specversion: "1.0",
|
|
1316
|
+
type,
|
|
1317
|
+
source: this.name,
|
|
1318
|
+
id: generateId3(),
|
|
1319
|
+
time: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1320
|
+
data
|
|
1321
|
+
};
|
|
1322
|
+
if (traceContext) {
|
|
1323
|
+
event.parstracecontext = `00-${traceContext.traceId}-${traceContext.spanId}-01`;
|
|
1324
|
+
}
|
|
1325
|
+
await this.eventTransport.emit(event);
|
|
1326
|
+
}
|
|
1327
|
+
/**
|
|
1328
|
+
* Subscribe to events
|
|
1329
|
+
*/
|
|
1330
|
+
on(eventType, handler, options) {
|
|
1331
|
+
return this.eventTransport.subscribe(eventType, handler, options);
|
|
1332
|
+
}
|
|
1333
|
+
/**
|
|
1334
|
+
* Get circuit breaker state
|
|
1335
|
+
*/
|
|
1336
|
+
getCircuitState() {
|
|
1337
|
+
return this.rpcClient.getCircuitState();
|
|
1338
|
+
}
|
|
1339
|
+
/**
|
|
1340
|
+
* Close the client
|
|
1341
|
+
*/
|
|
1342
|
+
async close() {
|
|
1343
|
+
await this.rpcClient.close();
|
|
1344
|
+
await this.eventTransport.close?.();
|
|
1345
|
+
}
|
|
1346
|
+
};
|
|
1347
|
+
function useService(serviceName, options = {}) {
|
|
1348
|
+
const mode = options.mode ?? "embedded";
|
|
1349
|
+
const config = options.config ?? {};
|
|
1350
|
+
let rpcTransport;
|
|
1351
|
+
let eventTransport;
|
|
1352
|
+
switch (mode) {
|
|
1353
|
+
case "embedded": {
|
|
1354
|
+
const registry = getEmbeddedRegistry();
|
|
1355
|
+
if (!registry.has(serviceName)) {
|
|
1356
|
+
throw new Error(
|
|
1357
|
+
`Service not found in embedded registry: ${serviceName}. Make sure the service is registered before using it.`
|
|
1358
|
+
);
|
|
1359
|
+
}
|
|
1360
|
+
rpcTransport = registry.createTransport(serviceName);
|
|
1361
|
+
const eventBus = getGlobalEventBus();
|
|
1362
|
+
const services = eventBus.getServices();
|
|
1363
|
+
if (services.includes(serviceName)) {
|
|
1364
|
+
eventTransport = createMemoryEventTransport();
|
|
1365
|
+
} else {
|
|
1366
|
+
eventTransport = createMemoryEventTransport();
|
|
1367
|
+
}
|
|
1368
|
+
break;
|
|
1369
|
+
}
|
|
1370
|
+
case "http": {
|
|
1371
|
+
if (!options.baseUrl) {
|
|
1372
|
+
throw new Error("baseUrl is required for HTTP mode");
|
|
1373
|
+
}
|
|
1374
|
+
rpcTransport = createHttpTransport({
|
|
1375
|
+
baseUrl: options.baseUrl
|
|
1376
|
+
});
|
|
1377
|
+
eventTransport = createMemoryEventTransport();
|
|
1378
|
+
break;
|
|
1379
|
+
}
|
|
1380
|
+
case "binding": {
|
|
1381
|
+
if (!options.binding) {
|
|
1382
|
+
throw new Error("binding is required for binding mode");
|
|
1383
|
+
}
|
|
1384
|
+
rpcTransport = createBindingTransport(serviceName, options.binding);
|
|
1385
|
+
eventTransport = createMemoryEventTransport();
|
|
1386
|
+
break;
|
|
1387
|
+
}
|
|
1388
|
+
default:
|
|
1389
|
+
throw new Error(`Unknown service client mode: ${mode}`);
|
|
1390
|
+
}
|
|
1391
|
+
if (options.rpcTransport) {
|
|
1392
|
+
rpcTransport = options.rpcTransport;
|
|
1393
|
+
}
|
|
1394
|
+
if (options.eventTransport) {
|
|
1395
|
+
eventTransport = options.eventTransport;
|
|
1396
|
+
}
|
|
1397
|
+
const definition = {
|
|
1398
|
+
name: serviceName,
|
|
1399
|
+
version: "1.x"
|
|
1400
|
+
};
|
|
1401
|
+
return new ServiceClientImpl(
|
|
1402
|
+
definition,
|
|
1403
|
+
rpcTransport,
|
|
1404
|
+
eventTransport,
|
|
1405
|
+
config
|
|
1406
|
+
);
|
|
1407
|
+
}
|
|
1408
|
+
function useTypedService(definition, options = {}) {
|
|
1409
|
+
return useService(definition.name, options);
|
|
1410
|
+
}
|
|
1411
|
+
function createBindingTransport(_serviceName, binding) {
|
|
1412
|
+
return {
|
|
1413
|
+
name: "binding",
|
|
1414
|
+
async call(request) {
|
|
1415
|
+
const response = await binding.fetch("http://internal/rpc", {
|
|
1416
|
+
method: "POST",
|
|
1417
|
+
headers: {
|
|
1418
|
+
"Content-Type": "application/json",
|
|
1419
|
+
"X-Request-ID": request.id,
|
|
1420
|
+
"X-Service": request.service,
|
|
1421
|
+
"X-Method": request.method
|
|
1422
|
+
},
|
|
1423
|
+
body: JSON.stringify(request)
|
|
1424
|
+
});
|
|
1425
|
+
return response.json();
|
|
1426
|
+
},
|
|
1427
|
+
async close() {
|
|
1428
|
+
}
|
|
1429
|
+
};
|
|
1430
|
+
}
|
|
1431
|
+
var ServiceRegistry = class {
|
|
1432
|
+
clients = /* @__PURE__ */ new Map();
|
|
1433
|
+
config;
|
|
1434
|
+
constructor(config) {
|
|
1435
|
+
this.config = config ?? {};
|
|
1436
|
+
}
|
|
1437
|
+
/**
|
|
1438
|
+
* Get or create a service client
|
|
1439
|
+
*/
|
|
1440
|
+
get(serviceName, options) {
|
|
1441
|
+
let client = this.clients.get(serviceName);
|
|
1442
|
+
if (!client) {
|
|
1443
|
+
client = useService(serviceName, {
|
|
1444
|
+
...options,
|
|
1445
|
+
config: { ...this.config, ...options?.config }
|
|
1446
|
+
});
|
|
1447
|
+
this.clients.set(serviceName, client);
|
|
1448
|
+
}
|
|
1449
|
+
return client;
|
|
1450
|
+
}
|
|
1451
|
+
/**
|
|
1452
|
+
* Close all clients
|
|
1453
|
+
*/
|
|
1454
|
+
async closeAll() {
|
|
1455
|
+
const closePromises = Array.from(this.clients.values()).map((client) => {
|
|
1456
|
+
if ("close" in client && typeof client.close === "function") {
|
|
1457
|
+
return client.close();
|
|
1458
|
+
}
|
|
1459
|
+
return Promise.resolve();
|
|
1460
|
+
});
|
|
1461
|
+
await Promise.all(closePromises);
|
|
1462
|
+
this.clients.clear();
|
|
1463
|
+
}
|
|
1464
|
+
};
|
|
1465
|
+
function createServiceRegistry(config) {
|
|
1466
|
+
return new ServiceRegistry(config);
|
|
1467
|
+
}
|
|
1468
|
+
export {
|
|
1469
|
+
ServiceRegistry,
|
|
1470
|
+
createServiceRegistry,
|
|
1471
|
+
useService,
|
|
1472
|
+
useTypedService
|
|
1473
|
+
};
|
|
1474
|
+
//# sourceMappingURL=client.js.map
|