@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/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