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