@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/index.js
ADDED
|
@@ -0,0 +1,3589 @@
|
|
|
1
|
+
// src/define.ts
|
|
2
|
+
function defineService(definition) {
|
|
3
|
+
validateServiceDefinition(definition);
|
|
4
|
+
return Object.freeze({
|
|
5
|
+
...definition,
|
|
6
|
+
queries: definition.queries ? Object.freeze({ ...definition.queries }) : void 0,
|
|
7
|
+
mutations: definition.mutations ? Object.freeze({ ...definition.mutations }) : void 0,
|
|
8
|
+
events: definition.events ? Object.freeze({
|
|
9
|
+
emits: definition.events.emits ? Object.freeze({ ...definition.events.emits }) : void 0,
|
|
10
|
+
handles: definition.events.handles ? Object.freeze([...definition.events.handles]) : void 0
|
|
11
|
+
}) : void 0
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
function validateServiceDefinition(definition) {
|
|
15
|
+
if (!definition.name) {
|
|
16
|
+
throw new Error("Service name is required");
|
|
17
|
+
}
|
|
18
|
+
if (!definition.version) {
|
|
19
|
+
throw new Error("Service version is required");
|
|
20
|
+
}
|
|
21
|
+
const versionRegex = /^\d+\.\d+\.\d+(-[a-zA-Z0-9.]+)?$/;
|
|
22
|
+
if (!versionRegex.test(definition.version)) {
|
|
23
|
+
throw new Error(`Invalid version format: ${definition.version}. Expected semver (e.g., 1.0.0)`);
|
|
24
|
+
}
|
|
25
|
+
const nameRegex = /^[a-zA-Z][a-zA-Z0-9_]*$/;
|
|
26
|
+
if (definition.queries) {
|
|
27
|
+
for (const name of Object.keys(definition.queries)) {
|
|
28
|
+
if (!nameRegex.test(name)) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
`Invalid query name: ${name}. Must start with letter and contain only alphanumeric and underscore`
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (definition.mutations) {
|
|
36
|
+
for (const name of Object.keys(definition.mutations)) {
|
|
37
|
+
if (!nameRegex.test(name)) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
`Invalid mutation name: ${name}. Must start with letter and contain only alphanumeric and underscore`
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const eventNameRegex = /^[a-zA-Z][a-zA-Z0-9_.]*$/;
|
|
45
|
+
if (definition.events?.emits) {
|
|
46
|
+
for (const name of Object.keys(definition.events.emits)) {
|
|
47
|
+
if (!eventNameRegex.test(name)) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
`Invalid event name: ${name}. Must start with letter and contain only alphanumeric, underscore, and dot`
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (definition.events?.handles) {
|
|
55
|
+
for (const name of definition.events.handles) {
|
|
56
|
+
if (!eventNameRegex.test(name)) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
`Invalid handled event name: ${name}. Must start with letter and contain only alphanumeric, underscore, and dot`
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function getServiceMethods(definition) {
|
|
65
|
+
return {
|
|
66
|
+
queries: definition.queries ? Object.keys(definition.queries) : [],
|
|
67
|
+
mutations: definition.mutations ? Object.keys(definition.mutations) : []
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function getServiceEvents(definition) {
|
|
71
|
+
return {
|
|
72
|
+
emits: definition.events?.emits ? Object.keys(definition.events.emits) : [],
|
|
73
|
+
handles: definition.events?.handles ?? []
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function satisfiesVersion(version, requirement) {
|
|
77
|
+
const versionParts = version.split(".").map((p) => parseInt(p, 10));
|
|
78
|
+
const requirementParts = requirement.split(".");
|
|
79
|
+
for (let i = 0; i < requirementParts.length; i++) {
|
|
80
|
+
const req = requirementParts[i];
|
|
81
|
+
if (req === "x" || req === "*") {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const reqNum = parseInt(req ?? "0", 10);
|
|
85
|
+
const verNum = versionParts[i] ?? 0;
|
|
86
|
+
if (verNum !== reqNum) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
function isMethodDeprecated(definition, methodName, type) {
|
|
93
|
+
const methods = type === "query" ? definition.queries : definition.mutations;
|
|
94
|
+
const method = methods?.[methodName];
|
|
95
|
+
if (!method?.deprecated) {
|
|
96
|
+
return { deprecated: false };
|
|
97
|
+
}
|
|
98
|
+
const result = {
|
|
99
|
+
deprecated: true,
|
|
100
|
+
since: method.deprecated
|
|
101
|
+
};
|
|
102
|
+
if (method.replacement) {
|
|
103
|
+
result.replacement = method.replacement;
|
|
104
|
+
}
|
|
105
|
+
return result;
|
|
106
|
+
}
|
|
107
|
+
function getMethodTimeout(definition, methodName, type, defaultTimeout) {
|
|
108
|
+
const methods = type === "query" ? definition.queries : definition.mutations;
|
|
109
|
+
const method = methods?.[methodName];
|
|
110
|
+
return method?.timeout ?? defaultTimeout;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// src/client.ts
|
|
114
|
+
import { generateId as generateId3 } from "@parsrun/core";
|
|
115
|
+
|
|
116
|
+
// src/config.ts
|
|
117
|
+
var DEFAULT_EVENT_CONFIG = {
|
|
118
|
+
format: "cloudevents",
|
|
119
|
+
internalCompact: true
|
|
120
|
+
};
|
|
121
|
+
var DEFAULT_SERIALIZATION_CONFIG = {
|
|
122
|
+
format: "json"
|
|
123
|
+
};
|
|
124
|
+
var DEFAULT_TRACING_CONFIG = {
|
|
125
|
+
enabled: true,
|
|
126
|
+
sampler: { ratio: 0.1 },
|
|
127
|
+
exporter: "console",
|
|
128
|
+
endpoint: "",
|
|
129
|
+
serviceName: "pars-service"
|
|
130
|
+
};
|
|
131
|
+
var DEFAULT_VERSIONING_CONFIG = {
|
|
132
|
+
strategy: "header",
|
|
133
|
+
defaultVersion: "1.x"
|
|
134
|
+
};
|
|
135
|
+
var DEFAULT_RESILIENCE_CONFIG = {
|
|
136
|
+
circuitBreaker: {
|
|
137
|
+
enabled: true,
|
|
138
|
+
failureThreshold: 5,
|
|
139
|
+
resetTimeout: 3e4,
|
|
140
|
+
successThreshold: 2
|
|
141
|
+
},
|
|
142
|
+
bulkhead: {
|
|
143
|
+
maxConcurrent: 100,
|
|
144
|
+
maxQueue: 50
|
|
145
|
+
},
|
|
146
|
+
timeout: 5e3,
|
|
147
|
+
retry: {
|
|
148
|
+
attempts: 3,
|
|
149
|
+
backoff: "exponential",
|
|
150
|
+
initialDelay: 100,
|
|
151
|
+
maxDelay: 1e4
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
var DEFAULT_DEAD_LETTER_CONFIG = {
|
|
155
|
+
enabled: true,
|
|
156
|
+
retention: "30d",
|
|
157
|
+
onFail: "alert",
|
|
158
|
+
alertThreshold: 10
|
|
159
|
+
};
|
|
160
|
+
var DEFAULT_SERVICE_CONFIG = {
|
|
161
|
+
events: DEFAULT_EVENT_CONFIG,
|
|
162
|
+
serialization: DEFAULT_SERIALIZATION_CONFIG,
|
|
163
|
+
tracing: DEFAULT_TRACING_CONFIG,
|
|
164
|
+
versioning: DEFAULT_VERSIONING_CONFIG,
|
|
165
|
+
resilience: DEFAULT_RESILIENCE_CONFIG,
|
|
166
|
+
deadLetter: DEFAULT_DEAD_LETTER_CONFIG
|
|
167
|
+
};
|
|
168
|
+
function mergeConfig(userConfig) {
|
|
169
|
+
if (!userConfig) {
|
|
170
|
+
return { ...DEFAULT_SERVICE_CONFIG };
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
events: {
|
|
174
|
+
...DEFAULT_EVENT_CONFIG,
|
|
175
|
+
...userConfig.events
|
|
176
|
+
},
|
|
177
|
+
serialization: {
|
|
178
|
+
...DEFAULT_SERIALIZATION_CONFIG,
|
|
179
|
+
...userConfig.serialization
|
|
180
|
+
},
|
|
181
|
+
tracing: {
|
|
182
|
+
...DEFAULT_TRACING_CONFIG,
|
|
183
|
+
...userConfig.tracing
|
|
184
|
+
},
|
|
185
|
+
versioning: {
|
|
186
|
+
...DEFAULT_VERSIONING_CONFIG,
|
|
187
|
+
...userConfig.versioning
|
|
188
|
+
},
|
|
189
|
+
resilience: {
|
|
190
|
+
...DEFAULT_RESILIENCE_CONFIG,
|
|
191
|
+
...userConfig.resilience,
|
|
192
|
+
circuitBreaker: {
|
|
193
|
+
...DEFAULT_RESILIENCE_CONFIG.circuitBreaker,
|
|
194
|
+
...userConfig.resilience?.circuitBreaker
|
|
195
|
+
},
|
|
196
|
+
bulkhead: {
|
|
197
|
+
...DEFAULT_RESILIENCE_CONFIG.bulkhead,
|
|
198
|
+
...userConfig.resilience?.bulkhead
|
|
199
|
+
},
|
|
200
|
+
retry: {
|
|
201
|
+
...DEFAULT_RESILIENCE_CONFIG.retry,
|
|
202
|
+
...userConfig.resilience?.retry
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
deadLetter: {
|
|
206
|
+
...DEFAULT_DEAD_LETTER_CONFIG,
|
|
207
|
+
...userConfig.deadLetter
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
function createDevConfig(overrides) {
|
|
212
|
+
return mergeConfig({
|
|
213
|
+
tracing: {
|
|
214
|
+
enabled: true,
|
|
215
|
+
sampler: "always",
|
|
216
|
+
exporter: "console"
|
|
217
|
+
},
|
|
218
|
+
resilience: {
|
|
219
|
+
circuitBreaker: { enabled: false },
|
|
220
|
+
timeout: 3e4
|
|
221
|
+
},
|
|
222
|
+
...overrides
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
function createProdConfig(overrides) {
|
|
226
|
+
return mergeConfig({
|
|
227
|
+
tracing: {
|
|
228
|
+
enabled: true,
|
|
229
|
+
sampler: { ratio: 0.1 },
|
|
230
|
+
exporter: "otlp"
|
|
231
|
+
},
|
|
232
|
+
resilience: {
|
|
233
|
+
circuitBreaker: { enabled: true },
|
|
234
|
+
timeout: 5e3
|
|
235
|
+
},
|
|
236
|
+
...overrides
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
function validateConfig(config) {
|
|
240
|
+
const errors = [];
|
|
241
|
+
if (config.tracing?.sampler && typeof config.tracing.sampler === "object") {
|
|
242
|
+
const ratio = config.tracing.sampler.ratio;
|
|
243
|
+
if (ratio < 0 || ratio > 1) {
|
|
244
|
+
errors.push("tracing.sampler.ratio must be between 0 and 1");
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if (config.resilience?.timeout !== void 0 && config.resilience.timeout < 0) {
|
|
248
|
+
errors.push("resilience.timeout must be non-negative");
|
|
249
|
+
}
|
|
250
|
+
if (config.resilience?.circuitBreaker?.failureThreshold !== void 0) {
|
|
251
|
+
if (config.resilience.circuitBreaker.failureThreshold < 1) {
|
|
252
|
+
errors.push("resilience.circuitBreaker.failureThreshold must be at least 1");
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (config.resilience?.bulkhead?.maxConcurrent !== void 0) {
|
|
256
|
+
if (config.resilience.bulkhead.maxConcurrent < 1) {
|
|
257
|
+
errors.push("resilience.bulkhead.maxConcurrent must be at least 1");
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (config.resilience?.retry?.attempts !== void 0) {
|
|
261
|
+
if (config.resilience.retry.attempts < 0) {
|
|
262
|
+
errors.push("resilience.retry.attempts must be non-negative");
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return {
|
|
266
|
+
valid: errors.length === 0,
|
|
267
|
+
errors
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// src/rpc/client.ts
|
|
272
|
+
import { generateId } from "@parsrun/core";
|
|
273
|
+
|
|
274
|
+
// src/rpc/errors.ts
|
|
275
|
+
import { ParsError } from "@parsrun/core";
|
|
276
|
+
var RpcError = class extends ParsError {
|
|
277
|
+
retryable;
|
|
278
|
+
retryAfter;
|
|
279
|
+
constructor(message, code, statusCode = 500, options) {
|
|
280
|
+
super(message, code, statusCode, options?.details);
|
|
281
|
+
this.name = "RpcError";
|
|
282
|
+
this.retryable = options?.retryable ?? false;
|
|
283
|
+
if (options?.retryAfter !== void 0) {
|
|
284
|
+
this.retryAfter = options.retryAfter;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
var ServiceNotFoundError = class extends RpcError {
|
|
289
|
+
constructor(serviceName) {
|
|
290
|
+
super(`Service not found: ${serviceName}`, "SERVICE_NOT_FOUND", 404, {
|
|
291
|
+
retryable: false,
|
|
292
|
+
details: { service: serviceName }
|
|
293
|
+
});
|
|
294
|
+
this.name = "ServiceNotFoundError";
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
var MethodNotFoundError = class extends RpcError {
|
|
298
|
+
constructor(serviceName, methodName) {
|
|
299
|
+
super(
|
|
300
|
+
`Method not found: ${serviceName}.${methodName}`,
|
|
301
|
+
"METHOD_NOT_FOUND",
|
|
302
|
+
404,
|
|
303
|
+
{
|
|
304
|
+
retryable: false,
|
|
305
|
+
details: { service: serviceName, method: methodName }
|
|
306
|
+
}
|
|
307
|
+
);
|
|
308
|
+
this.name = "MethodNotFoundError";
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
var VersionMismatchError = class extends RpcError {
|
|
312
|
+
constructor(serviceName, requested, available) {
|
|
313
|
+
super(
|
|
314
|
+
`Version mismatch for ${serviceName}: requested ${requested}, available ${available}`,
|
|
315
|
+
"VERSION_MISMATCH",
|
|
316
|
+
400,
|
|
317
|
+
{
|
|
318
|
+
retryable: false,
|
|
319
|
+
details: { service: serviceName, requested, available }
|
|
320
|
+
}
|
|
321
|
+
);
|
|
322
|
+
this.name = "VersionMismatchError";
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
var TimeoutError = class extends RpcError {
|
|
326
|
+
constructor(serviceName, methodName, timeoutMs) {
|
|
327
|
+
super(
|
|
328
|
+
`Request to ${serviceName}.${methodName} timed out after ${timeoutMs}ms`,
|
|
329
|
+
"TIMEOUT",
|
|
330
|
+
504,
|
|
331
|
+
{
|
|
332
|
+
retryable: true,
|
|
333
|
+
details: { service: serviceName, method: methodName, timeout: timeoutMs }
|
|
334
|
+
}
|
|
335
|
+
);
|
|
336
|
+
this.name = "TimeoutError";
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
var CircuitOpenError = class extends RpcError {
|
|
340
|
+
constructor(serviceName, resetAfterMs) {
|
|
341
|
+
super(
|
|
342
|
+
`Circuit breaker open for ${serviceName}`,
|
|
343
|
+
"CIRCUIT_OPEN",
|
|
344
|
+
503,
|
|
345
|
+
{
|
|
346
|
+
retryable: true,
|
|
347
|
+
retryAfter: Math.ceil(resetAfterMs / 1e3),
|
|
348
|
+
details: { service: serviceName, resetAfterMs }
|
|
349
|
+
}
|
|
350
|
+
);
|
|
351
|
+
this.name = "CircuitOpenError";
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
var BulkheadRejectedError = class extends RpcError {
|
|
355
|
+
constructor(serviceName) {
|
|
356
|
+
super(
|
|
357
|
+
`Request rejected by bulkhead for ${serviceName}: too many concurrent requests`,
|
|
358
|
+
"BULKHEAD_REJECTED",
|
|
359
|
+
503,
|
|
360
|
+
{
|
|
361
|
+
retryable: true,
|
|
362
|
+
retryAfter: 1,
|
|
363
|
+
details: { service: serviceName }
|
|
364
|
+
}
|
|
365
|
+
);
|
|
366
|
+
this.name = "BulkheadRejectedError";
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
var TransportError = class extends RpcError {
|
|
370
|
+
constructor(message, cause) {
|
|
371
|
+
const options = {
|
|
372
|
+
retryable: true
|
|
373
|
+
};
|
|
374
|
+
if (cause) {
|
|
375
|
+
options.details = { cause: cause.message };
|
|
376
|
+
}
|
|
377
|
+
super(message, "TRANSPORT_ERROR", 502, options);
|
|
378
|
+
this.name = "TransportError";
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
var SerializationError = class extends RpcError {
|
|
382
|
+
constructor(message, cause) {
|
|
383
|
+
const options = {
|
|
384
|
+
retryable: false
|
|
385
|
+
};
|
|
386
|
+
if (cause) {
|
|
387
|
+
options.details = { cause: cause.message };
|
|
388
|
+
}
|
|
389
|
+
super(message, "SERIALIZATION_ERROR", 400, options);
|
|
390
|
+
this.name = "SerializationError";
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
function toRpcError(error) {
|
|
394
|
+
if (error instanceof RpcError) {
|
|
395
|
+
return error;
|
|
396
|
+
}
|
|
397
|
+
if (error instanceof Error) {
|
|
398
|
+
return new RpcError(error.message, "INTERNAL_ERROR", 500, {
|
|
399
|
+
retryable: false,
|
|
400
|
+
details: { originalError: error.name }
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
return new RpcError(String(error), "UNKNOWN_ERROR", 500, {
|
|
404
|
+
retryable: false
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// src/resilience/circuit-breaker.ts
|
|
409
|
+
var CircuitBreaker = class {
|
|
410
|
+
_state = "closed";
|
|
411
|
+
failures = 0;
|
|
412
|
+
successes = 0;
|
|
413
|
+
lastFailureTime = 0;
|
|
414
|
+
options;
|
|
415
|
+
constructor(options) {
|
|
416
|
+
this.options = options;
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Get current state
|
|
420
|
+
*/
|
|
421
|
+
get state() {
|
|
422
|
+
if (this._state === "open") {
|
|
423
|
+
const timeSinceFailure = Date.now() - this.lastFailureTime;
|
|
424
|
+
if (timeSinceFailure >= this.options.resetTimeout) {
|
|
425
|
+
this.transitionTo("half-open");
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
return this._state;
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Execute a function with circuit breaker protection
|
|
432
|
+
*/
|
|
433
|
+
async execute(fn) {
|
|
434
|
+
const currentState = this.state;
|
|
435
|
+
if (currentState === "open") {
|
|
436
|
+
const resetAfter = this.options.resetTimeout - (Date.now() - this.lastFailureTime);
|
|
437
|
+
throw new CircuitOpenError("service", Math.max(0, resetAfter));
|
|
438
|
+
}
|
|
439
|
+
try {
|
|
440
|
+
const result = await fn();
|
|
441
|
+
this.onSuccess();
|
|
442
|
+
return result;
|
|
443
|
+
} catch (error) {
|
|
444
|
+
this.onFailure();
|
|
445
|
+
throw error;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Record a successful call
|
|
450
|
+
*/
|
|
451
|
+
onSuccess() {
|
|
452
|
+
if (this._state === "half-open") {
|
|
453
|
+
this.successes++;
|
|
454
|
+
if (this.successes >= this.options.successThreshold) {
|
|
455
|
+
this.transitionTo("closed");
|
|
456
|
+
}
|
|
457
|
+
} else if (this._state === "closed") {
|
|
458
|
+
this.failures = 0;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Record a failed call
|
|
463
|
+
*/
|
|
464
|
+
onFailure() {
|
|
465
|
+
this.lastFailureTime = Date.now();
|
|
466
|
+
if (this._state === "half-open") {
|
|
467
|
+
this.transitionTo("open");
|
|
468
|
+
} else if (this._state === "closed") {
|
|
469
|
+
this.failures++;
|
|
470
|
+
if (this.failures >= this.options.failureThreshold) {
|
|
471
|
+
this.transitionTo("open");
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Transition to a new state
|
|
477
|
+
*/
|
|
478
|
+
transitionTo(newState) {
|
|
479
|
+
const oldState = this._state;
|
|
480
|
+
this._state = newState;
|
|
481
|
+
if (newState === "closed") {
|
|
482
|
+
this.failures = 0;
|
|
483
|
+
this.successes = 0;
|
|
484
|
+
} else if (newState === "half-open") {
|
|
485
|
+
this.successes = 0;
|
|
486
|
+
}
|
|
487
|
+
this.options.onStateChange?.(oldState, newState);
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Manually reset the circuit breaker
|
|
491
|
+
*/
|
|
492
|
+
reset() {
|
|
493
|
+
this.transitionTo("closed");
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Get circuit breaker statistics
|
|
497
|
+
*/
|
|
498
|
+
getStats() {
|
|
499
|
+
return {
|
|
500
|
+
state: this.state,
|
|
501
|
+
failures: this.failures,
|
|
502
|
+
successes: this.successes,
|
|
503
|
+
lastFailureTime: this.lastFailureTime
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
// src/resilience/bulkhead.ts
|
|
509
|
+
var Bulkhead = class {
|
|
510
|
+
_concurrent = 0;
|
|
511
|
+
queue = [];
|
|
512
|
+
options;
|
|
513
|
+
constructor(options) {
|
|
514
|
+
this.options = options;
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Get current concurrent count
|
|
518
|
+
*/
|
|
519
|
+
get concurrent() {
|
|
520
|
+
return this._concurrent;
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Get current queue size
|
|
524
|
+
*/
|
|
525
|
+
get queued() {
|
|
526
|
+
return this.queue.length;
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Check if bulkhead is full
|
|
530
|
+
*/
|
|
531
|
+
get isFull() {
|
|
532
|
+
return this._concurrent >= this.options.maxConcurrent && this.queue.length >= this.options.maxQueue;
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Execute a function with bulkhead protection
|
|
536
|
+
*/
|
|
537
|
+
async execute(fn) {
|
|
538
|
+
if (this._concurrent < this.options.maxConcurrent) {
|
|
539
|
+
return this.doExecute(fn);
|
|
540
|
+
}
|
|
541
|
+
if (this.queue.length < this.options.maxQueue) {
|
|
542
|
+
return this.enqueue(fn);
|
|
543
|
+
}
|
|
544
|
+
this.options.onRejected?.();
|
|
545
|
+
throw new BulkheadRejectedError("service");
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* Execute immediately
|
|
549
|
+
*/
|
|
550
|
+
async doExecute(fn) {
|
|
551
|
+
this._concurrent++;
|
|
552
|
+
try {
|
|
553
|
+
return await fn();
|
|
554
|
+
} finally {
|
|
555
|
+
this._concurrent--;
|
|
556
|
+
this.processQueue();
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Add to queue
|
|
561
|
+
*/
|
|
562
|
+
enqueue(fn) {
|
|
563
|
+
return new Promise((resolve, reject) => {
|
|
564
|
+
this.queue.push({
|
|
565
|
+
fn,
|
|
566
|
+
resolve,
|
|
567
|
+
reject
|
|
568
|
+
});
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Process queued requests
|
|
573
|
+
*/
|
|
574
|
+
processQueue() {
|
|
575
|
+
if (this.queue.length === 0) return;
|
|
576
|
+
if (this._concurrent >= this.options.maxConcurrent) return;
|
|
577
|
+
const queued = this.queue.shift();
|
|
578
|
+
if (!queued) return;
|
|
579
|
+
this.doExecute(queued.fn).then(queued.resolve).catch(queued.reject);
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Get bulkhead statistics
|
|
583
|
+
*/
|
|
584
|
+
getStats() {
|
|
585
|
+
return {
|
|
586
|
+
concurrent: this._concurrent,
|
|
587
|
+
queued: this.queue.length,
|
|
588
|
+
maxConcurrent: this.options.maxConcurrent,
|
|
589
|
+
maxQueue: this.options.maxQueue
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* Clear the queue (reject all pending)
|
|
594
|
+
*/
|
|
595
|
+
clearQueue() {
|
|
596
|
+
const error = new BulkheadRejectedError("service");
|
|
597
|
+
while (this.queue.length > 0) {
|
|
598
|
+
const queued = this.queue.shift();
|
|
599
|
+
queued?.reject(error);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
};
|
|
603
|
+
|
|
604
|
+
// src/resilience/retry.ts
|
|
605
|
+
var defaultShouldRetry = (error) => {
|
|
606
|
+
if (error && typeof error === "object" && "retryable" in error) {
|
|
607
|
+
return error.retryable;
|
|
608
|
+
}
|
|
609
|
+
return false;
|
|
610
|
+
};
|
|
611
|
+
function calculateDelay(attempt, options) {
|
|
612
|
+
let delay;
|
|
613
|
+
if (options.backoff === "exponential") {
|
|
614
|
+
delay = options.initialDelay * Math.pow(2, attempt);
|
|
615
|
+
} else {
|
|
616
|
+
delay = options.initialDelay * (attempt + 1);
|
|
617
|
+
}
|
|
618
|
+
delay = Math.min(delay, options.maxDelay);
|
|
619
|
+
if (options.jitter && options.jitter > 0) {
|
|
620
|
+
const jitterRange = delay * options.jitter;
|
|
621
|
+
delay = delay - jitterRange / 2 + Math.random() * jitterRange;
|
|
622
|
+
}
|
|
623
|
+
return Math.round(delay);
|
|
624
|
+
}
|
|
625
|
+
function sleep(ms) {
|
|
626
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
627
|
+
}
|
|
628
|
+
function withRetry(fn, options) {
|
|
629
|
+
const shouldRetry = options.shouldRetry ?? defaultShouldRetry;
|
|
630
|
+
return async () => {
|
|
631
|
+
let lastError;
|
|
632
|
+
for (let attempt = 0; attempt <= options.attempts; attempt++) {
|
|
633
|
+
try {
|
|
634
|
+
return await fn();
|
|
635
|
+
} catch (error) {
|
|
636
|
+
lastError = error;
|
|
637
|
+
if (attempt >= options.attempts || !shouldRetry(error, attempt)) {
|
|
638
|
+
throw error;
|
|
639
|
+
}
|
|
640
|
+
const delay = calculateDelay(attempt, options);
|
|
641
|
+
options.onRetry?.(error, attempt + 1, delay);
|
|
642
|
+
await sleep(delay);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
throw lastError;
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
async function executeWithRetry(fn, options) {
|
|
649
|
+
return withRetry(fn, options)();
|
|
650
|
+
}
|
|
651
|
+
function createRetryWrapper(defaultOptions) {
|
|
652
|
+
const defaults = {
|
|
653
|
+
attempts: defaultOptions.attempts ?? 3,
|
|
654
|
+
backoff: defaultOptions.backoff ?? "exponential",
|
|
655
|
+
initialDelay: defaultOptions.initialDelay ?? 100,
|
|
656
|
+
maxDelay: defaultOptions.maxDelay ?? 1e4,
|
|
657
|
+
jitter: defaultOptions.jitter ?? 0.1
|
|
658
|
+
};
|
|
659
|
+
if (defaultOptions.shouldRetry) {
|
|
660
|
+
defaults.shouldRetry = defaultOptions.shouldRetry;
|
|
661
|
+
}
|
|
662
|
+
if (defaultOptions.onRetry) {
|
|
663
|
+
defaults.onRetry = defaultOptions.onRetry;
|
|
664
|
+
}
|
|
665
|
+
return async (fn, options) => {
|
|
666
|
+
return executeWithRetry(fn, { ...defaults, ...options });
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// src/resilience/timeout.ts
|
|
671
|
+
var TimeoutExceededError = class extends Error {
|
|
672
|
+
timeout;
|
|
673
|
+
constructor(timeout) {
|
|
674
|
+
super(`Operation timed out after ${timeout}ms`);
|
|
675
|
+
this.name = "TimeoutExceededError";
|
|
676
|
+
this.timeout = timeout;
|
|
677
|
+
}
|
|
678
|
+
};
|
|
679
|
+
function withTimeout(fn, timeoutMs, onTimeout) {
|
|
680
|
+
return async () => {
|
|
681
|
+
let timeoutId;
|
|
682
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
683
|
+
timeoutId = setTimeout(() => {
|
|
684
|
+
if (onTimeout) {
|
|
685
|
+
try {
|
|
686
|
+
onTimeout();
|
|
687
|
+
} catch (error) {
|
|
688
|
+
reject(error);
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
reject(new TimeoutExceededError(timeoutMs));
|
|
693
|
+
}, timeoutMs);
|
|
694
|
+
});
|
|
695
|
+
try {
|
|
696
|
+
return await Promise.race([fn(), timeoutPromise]);
|
|
697
|
+
} finally {
|
|
698
|
+
if (timeoutId !== void 0) {
|
|
699
|
+
clearTimeout(timeoutId);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
async function executeWithTimeout(fn, timeoutMs, onTimeout) {
|
|
705
|
+
return withTimeout(fn, timeoutMs, onTimeout)();
|
|
706
|
+
}
|
|
707
|
+
function createTimeoutWrapper(defaultTimeoutMs) {
|
|
708
|
+
return async (fn, timeoutMs) => {
|
|
709
|
+
return executeWithTimeout(fn, timeoutMs ?? defaultTimeoutMs);
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
async function raceWithTimeout(promises, timeoutMs) {
|
|
713
|
+
let timeoutId;
|
|
714
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
715
|
+
timeoutId = setTimeout(() => {
|
|
716
|
+
reject(new TimeoutExceededError(timeoutMs));
|
|
717
|
+
}, timeoutMs);
|
|
718
|
+
});
|
|
719
|
+
try {
|
|
720
|
+
return await Promise.race([...promises, timeoutPromise]);
|
|
721
|
+
} finally {
|
|
722
|
+
if (timeoutId !== void 0) {
|
|
723
|
+
clearTimeout(timeoutId);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
async function executeWithDeadline(fn, deadline) {
|
|
728
|
+
const now = Date.now();
|
|
729
|
+
const deadlineMs = deadline.getTime();
|
|
730
|
+
const timeoutMs = Math.max(0, deadlineMs - now);
|
|
731
|
+
if (timeoutMs === 0) {
|
|
732
|
+
throw new TimeoutExceededError(0);
|
|
733
|
+
}
|
|
734
|
+
return executeWithTimeout(fn, timeoutMs);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// src/rpc/client.ts
|
|
738
|
+
var RpcClient = class {
|
|
739
|
+
service;
|
|
740
|
+
transport;
|
|
741
|
+
config;
|
|
742
|
+
defaultMetadata;
|
|
743
|
+
circuitBreaker;
|
|
744
|
+
bulkhead;
|
|
745
|
+
constructor(options) {
|
|
746
|
+
this.service = options.service;
|
|
747
|
+
this.transport = options.transport;
|
|
748
|
+
this.config = mergeConfig(options.config);
|
|
749
|
+
this.defaultMetadata = options.defaultMetadata ?? {};
|
|
750
|
+
const cbConfig = this.config.resilience?.circuitBreaker;
|
|
751
|
+
if (cbConfig && cbConfig.enabled && cbConfig.failureThreshold !== void 0 && cbConfig.resetTimeout !== void 0 && cbConfig.successThreshold !== void 0) {
|
|
752
|
+
this.circuitBreaker = new CircuitBreaker({
|
|
753
|
+
failureThreshold: cbConfig.failureThreshold,
|
|
754
|
+
resetTimeout: cbConfig.resetTimeout,
|
|
755
|
+
successThreshold: cbConfig.successThreshold
|
|
756
|
+
});
|
|
757
|
+
} else {
|
|
758
|
+
this.circuitBreaker = null;
|
|
759
|
+
}
|
|
760
|
+
const bhConfig = this.config.resilience?.bulkhead;
|
|
761
|
+
if (bhConfig && bhConfig.maxConcurrent !== void 0 && bhConfig.maxQueue !== void 0) {
|
|
762
|
+
this.bulkhead = new Bulkhead({
|
|
763
|
+
maxConcurrent: bhConfig.maxConcurrent,
|
|
764
|
+
maxQueue: bhConfig.maxQueue
|
|
765
|
+
});
|
|
766
|
+
} else {
|
|
767
|
+
this.bulkhead = null;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
/**
|
|
771
|
+
* Execute a query
|
|
772
|
+
*/
|
|
773
|
+
async query(method, input, options) {
|
|
774
|
+
return this.call("query", method, input, options);
|
|
775
|
+
}
|
|
776
|
+
/**
|
|
777
|
+
* Execute a mutation
|
|
778
|
+
*/
|
|
779
|
+
async mutate(method, input, options) {
|
|
780
|
+
return this.call("mutation", method, input, options);
|
|
781
|
+
}
|
|
782
|
+
/**
|
|
783
|
+
* Internal call implementation
|
|
784
|
+
*/
|
|
785
|
+
async call(type, method, input, options) {
|
|
786
|
+
const request = {
|
|
787
|
+
id: generateId(),
|
|
788
|
+
service: this.service,
|
|
789
|
+
method,
|
|
790
|
+
type,
|
|
791
|
+
input,
|
|
792
|
+
metadata: {
|
|
793
|
+
...this.defaultMetadata,
|
|
794
|
+
...options?.metadata
|
|
795
|
+
}
|
|
796
|
+
};
|
|
797
|
+
const version = options?.version ?? this.config.versioning.defaultVersion;
|
|
798
|
+
if (version) {
|
|
799
|
+
request.version = version;
|
|
800
|
+
}
|
|
801
|
+
if (options?.traceContext) {
|
|
802
|
+
request.traceContext = options.traceContext;
|
|
803
|
+
}
|
|
804
|
+
const timeout = options?.timeout ?? this.config.resilience.timeout ?? 3e4;
|
|
805
|
+
const retryConfig = options?.retry ?? this.config.resilience.retry;
|
|
806
|
+
let execute = async () => {
|
|
807
|
+
const response = await this.transport.call(request);
|
|
808
|
+
if (!response.success) {
|
|
809
|
+
const error = toRpcError(
|
|
810
|
+
new Error(response.error?.message ?? "Unknown error")
|
|
811
|
+
);
|
|
812
|
+
throw error;
|
|
813
|
+
}
|
|
814
|
+
return response.output;
|
|
815
|
+
};
|
|
816
|
+
execute = withTimeout(execute, timeout, () => {
|
|
817
|
+
throw new TimeoutError(this.service, method, timeout);
|
|
818
|
+
});
|
|
819
|
+
const attempts = retryConfig?.attempts ?? 0;
|
|
820
|
+
if (attempts > 0) {
|
|
821
|
+
execute = withRetry(execute, {
|
|
822
|
+
attempts,
|
|
823
|
+
backoff: retryConfig?.backoff ?? "exponential",
|
|
824
|
+
initialDelay: retryConfig?.initialDelay ?? 100,
|
|
825
|
+
maxDelay: retryConfig?.maxDelay ?? 5e3,
|
|
826
|
+
shouldRetry: (error) => {
|
|
827
|
+
if (error instanceof Error && "retryable" in error) {
|
|
828
|
+
return error.retryable;
|
|
829
|
+
}
|
|
830
|
+
return false;
|
|
831
|
+
}
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
if (this.circuitBreaker) {
|
|
835
|
+
const cb = this.circuitBreaker;
|
|
836
|
+
const originalExecute = execute;
|
|
837
|
+
execute = async () => {
|
|
838
|
+
return cb.execute(originalExecute);
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
if (this.bulkhead) {
|
|
842
|
+
const bh = this.bulkhead;
|
|
843
|
+
const originalExecute = execute;
|
|
844
|
+
execute = async () => {
|
|
845
|
+
return bh.execute(originalExecute);
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
return execute();
|
|
849
|
+
}
|
|
850
|
+
/**
|
|
851
|
+
* Get circuit breaker state
|
|
852
|
+
*/
|
|
853
|
+
getCircuitState() {
|
|
854
|
+
return this.circuitBreaker?.state ?? null;
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
857
|
+
* Get bulkhead stats
|
|
858
|
+
*/
|
|
859
|
+
getBulkheadStats() {
|
|
860
|
+
if (!this.bulkhead) return null;
|
|
861
|
+
return {
|
|
862
|
+
concurrent: this.bulkhead.concurrent,
|
|
863
|
+
queued: this.bulkhead.queued
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* Close the client and release resources
|
|
868
|
+
*/
|
|
869
|
+
async close() {
|
|
870
|
+
await this.transport.close?.();
|
|
871
|
+
}
|
|
872
|
+
};
|
|
873
|
+
function createRpcClient(options) {
|
|
874
|
+
return new RpcClient(options);
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// src/rpc/transports/embedded.ts
|
|
878
|
+
var EmbeddedTransport = class {
|
|
879
|
+
name = "embedded";
|
|
880
|
+
server;
|
|
881
|
+
constructor(server) {
|
|
882
|
+
this.server = server;
|
|
883
|
+
}
|
|
884
|
+
async call(request) {
|
|
885
|
+
return this.server.handle(request);
|
|
886
|
+
}
|
|
887
|
+
async close() {
|
|
888
|
+
}
|
|
889
|
+
};
|
|
890
|
+
function createEmbeddedTransport(server) {
|
|
891
|
+
return new EmbeddedTransport(server);
|
|
892
|
+
}
|
|
893
|
+
var EmbeddedRegistry = class _EmbeddedRegistry {
|
|
894
|
+
static instance = null;
|
|
895
|
+
servers = /* @__PURE__ */ new Map();
|
|
896
|
+
constructor() {
|
|
897
|
+
}
|
|
898
|
+
static getInstance() {
|
|
899
|
+
if (!_EmbeddedRegistry.instance) {
|
|
900
|
+
_EmbeddedRegistry.instance = new _EmbeddedRegistry();
|
|
901
|
+
}
|
|
902
|
+
return _EmbeddedRegistry.instance;
|
|
903
|
+
}
|
|
904
|
+
/**
|
|
905
|
+
* Register a service
|
|
906
|
+
*/
|
|
907
|
+
register(name, server) {
|
|
908
|
+
if (this.servers.has(name)) {
|
|
909
|
+
throw new Error(`Service already registered: ${name}`);
|
|
910
|
+
}
|
|
911
|
+
this.servers.set(name, server);
|
|
912
|
+
}
|
|
913
|
+
/**
|
|
914
|
+
* Unregister a service
|
|
915
|
+
*/
|
|
916
|
+
unregister(name) {
|
|
917
|
+
return this.servers.delete(name);
|
|
918
|
+
}
|
|
919
|
+
/**
|
|
920
|
+
* Get a service by name
|
|
921
|
+
*/
|
|
922
|
+
get(name) {
|
|
923
|
+
return this.servers.get(name);
|
|
924
|
+
}
|
|
925
|
+
/**
|
|
926
|
+
* Check if a service is registered
|
|
927
|
+
*/
|
|
928
|
+
has(name) {
|
|
929
|
+
return this.servers.has(name);
|
|
930
|
+
}
|
|
931
|
+
/**
|
|
932
|
+
* Get all registered service names
|
|
933
|
+
*/
|
|
934
|
+
getServiceNames() {
|
|
935
|
+
return Array.from(this.servers.keys());
|
|
936
|
+
}
|
|
937
|
+
/**
|
|
938
|
+
* Create a transport for a registered service
|
|
939
|
+
*/
|
|
940
|
+
createTransport(name) {
|
|
941
|
+
const server = this.servers.get(name);
|
|
942
|
+
if (!server) {
|
|
943
|
+
throw new Error(`Service not found: ${name}`);
|
|
944
|
+
}
|
|
945
|
+
return new EmbeddedTransport(server);
|
|
946
|
+
}
|
|
947
|
+
/**
|
|
948
|
+
* Clear all registered services
|
|
949
|
+
*/
|
|
950
|
+
clear() {
|
|
951
|
+
this.servers.clear();
|
|
952
|
+
}
|
|
953
|
+
/**
|
|
954
|
+
* Reset the singleton instance (for testing)
|
|
955
|
+
*/
|
|
956
|
+
static reset() {
|
|
957
|
+
_EmbeddedRegistry.instance = null;
|
|
958
|
+
}
|
|
959
|
+
};
|
|
960
|
+
function getEmbeddedRegistry() {
|
|
961
|
+
return EmbeddedRegistry.getInstance();
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
// src/serialization/index.ts
|
|
965
|
+
var jsonSerializer = {
|
|
966
|
+
encode(data) {
|
|
967
|
+
return JSON.stringify(data);
|
|
968
|
+
},
|
|
969
|
+
decode(raw) {
|
|
970
|
+
if (raw instanceof ArrayBuffer) {
|
|
971
|
+
const decoder = new TextDecoder();
|
|
972
|
+
return JSON.parse(decoder.decode(raw));
|
|
973
|
+
}
|
|
974
|
+
return JSON.parse(raw);
|
|
975
|
+
},
|
|
976
|
+
contentType: "application/json"
|
|
977
|
+
};
|
|
978
|
+
function msgpackEncode(value) {
|
|
979
|
+
const parts = [];
|
|
980
|
+
function encode(val) {
|
|
981
|
+
if (val === null || val === void 0) {
|
|
982
|
+
parts.push(new Uint8Array([192]));
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
if (typeof val === "boolean") {
|
|
986
|
+
parts.push(new Uint8Array([val ? 195 : 194]));
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
if (typeof val === "number") {
|
|
990
|
+
if (Number.isInteger(val)) {
|
|
991
|
+
if (val >= 0 && val <= 127) {
|
|
992
|
+
parts.push(new Uint8Array([val]));
|
|
993
|
+
} else if (val < 0 && val >= -32) {
|
|
994
|
+
parts.push(new Uint8Array([val & 255]));
|
|
995
|
+
} else if (val >= 0 && val <= 255) {
|
|
996
|
+
parts.push(new Uint8Array([204, val]));
|
|
997
|
+
} else if (val >= 0 && val <= 65535) {
|
|
998
|
+
parts.push(new Uint8Array([205, val >> 8 & 255, val & 255]));
|
|
999
|
+
} else if (val >= 0 && val <= 4294967295) {
|
|
1000
|
+
parts.push(
|
|
1001
|
+
new Uint8Array([
|
|
1002
|
+
206,
|
|
1003
|
+
val >> 24 & 255,
|
|
1004
|
+
val >> 16 & 255,
|
|
1005
|
+
val >> 8 & 255,
|
|
1006
|
+
val & 255
|
|
1007
|
+
])
|
|
1008
|
+
);
|
|
1009
|
+
} else if (val >= -128 && val <= 127) {
|
|
1010
|
+
parts.push(new Uint8Array([208, val & 255]));
|
|
1011
|
+
} else if (val >= -32768 && val <= 32767) {
|
|
1012
|
+
parts.push(new Uint8Array([209, val >> 8 & 255, val & 255]));
|
|
1013
|
+
} else if (val >= -2147483648 && val <= 2147483647) {
|
|
1014
|
+
parts.push(
|
|
1015
|
+
new Uint8Array([
|
|
1016
|
+
210,
|
|
1017
|
+
val >> 24 & 255,
|
|
1018
|
+
val >> 16 & 255,
|
|
1019
|
+
val >> 8 & 255,
|
|
1020
|
+
val & 255
|
|
1021
|
+
])
|
|
1022
|
+
);
|
|
1023
|
+
} else {
|
|
1024
|
+
const buffer = new ArrayBuffer(9);
|
|
1025
|
+
const view = new DataView(buffer);
|
|
1026
|
+
view.setUint8(0, 203);
|
|
1027
|
+
view.setFloat64(1, val, false);
|
|
1028
|
+
parts.push(new Uint8Array(buffer));
|
|
1029
|
+
}
|
|
1030
|
+
} else {
|
|
1031
|
+
const buffer = new ArrayBuffer(9);
|
|
1032
|
+
const view = new DataView(buffer);
|
|
1033
|
+
view.setUint8(0, 203);
|
|
1034
|
+
view.setFloat64(1, val, false);
|
|
1035
|
+
parts.push(new Uint8Array(buffer));
|
|
1036
|
+
}
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
if (typeof val === "string") {
|
|
1040
|
+
const encoded = new TextEncoder().encode(val);
|
|
1041
|
+
const len = encoded.length;
|
|
1042
|
+
if (len <= 31) {
|
|
1043
|
+
parts.push(new Uint8Array([160 | len]));
|
|
1044
|
+
} else if (len <= 255) {
|
|
1045
|
+
parts.push(new Uint8Array([217, len]));
|
|
1046
|
+
} else if (len <= 65535) {
|
|
1047
|
+
parts.push(new Uint8Array([218, len >> 8 & 255, len & 255]));
|
|
1048
|
+
} else {
|
|
1049
|
+
parts.push(
|
|
1050
|
+
new Uint8Array([
|
|
1051
|
+
219,
|
|
1052
|
+
len >> 24 & 255,
|
|
1053
|
+
len >> 16 & 255,
|
|
1054
|
+
len >> 8 & 255,
|
|
1055
|
+
len & 255
|
|
1056
|
+
])
|
|
1057
|
+
);
|
|
1058
|
+
}
|
|
1059
|
+
parts.push(encoded);
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1062
|
+
if (Array.isArray(val)) {
|
|
1063
|
+
const len = val.length;
|
|
1064
|
+
if (len <= 15) {
|
|
1065
|
+
parts.push(new Uint8Array([144 | len]));
|
|
1066
|
+
} else if (len <= 65535) {
|
|
1067
|
+
parts.push(new Uint8Array([220, len >> 8 & 255, len & 255]));
|
|
1068
|
+
} else {
|
|
1069
|
+
parts.push(
|
|
1070
|
+
new Uint8Array([
|
|
1071
|
+
221,
|
|
1072
|
+
len >> 24 & 255,
|
|
1073
|
+
len >> 16 & 255,
|
|
1074
|
+
len >> 8 & 255,
|
|
1075
|
+
len & 255
|
|
1076
|
+
])
|
|
1077
|
+
);
|
|
1078
|
+
}
|
|
1079
|
+
for (const item of val) {
|
|
1080
|
+
encode(item);
|
|
1081
|
+
}
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
if (typeof val === "object") {
|
|
1085
|
+
const keys = Object.keys(val);
|
|
1086
|
+
const len = keys.length;
|
|
1087
|
+
if (len <= 15) {
|
|
1088
|
+
parts.push(new Uint8Array([128 | len]));
|
|
1089
|
+
} else if (len <= 65535) {
|
|
1090
|
+
parts.push(new Uint8Array([222, len >> 8 & 255, len & 255]));
|
|
1091
|
+
} else {
|
|
1092
|
+
parts.push(
|
|
1093
|
+
new Uint8Array([
|
|
1094
|
+
223,
|
|
1095
|
+
len >> 24 & 255,
|
|
1096
|
+
len >> 16 & 255,
|
|
1097
|
+
len >> 8 & 255,
|
|
1098
|
+
len & 255
|
|
1099
|
+
])
|
|
1100
|
+
);
|
|
1101
|
+
}
|
|
1102
|
+
for (const key of keys) {
|
|
1103
|
+
encode(key);
|
|
1104
|
+
encode(val[key]);
|
|
1105
|
+
}
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
parts.push(new Uint8Array([192]));
|
|
1109
|
+
}
|
|
1110
|
+
encode(value);
|
|
1111
|
+
const totalLength = parts.reduce((sum, p) => sum + p.length, 0);
|
|
1112
|
+
const result = new Uint8Array(totalLength);
|
|
1113
|
+
let offset = 0;
|
|
1114
|
+
for (const part of parts) {
|
|
1115
|
+
result.set(part, offset);
|
|
1116
|
+
offset += part.length;
|
|
1117
|
+
}
|
|
1118
|
+
return result;
|
|
1119
|
+
}
|
|
1120
|
+
function msgpackDecode(buffer) {
|
|
1121
|
+
let offset = 0;
|
|
1122
|
+
function decode() {
|
|
1123
|
+
if (offset >= buffer.length) {
|
|
1124
|
+
throw new Error("Unexpected end of buffer");
|
|
1125
|
+
}
|
|
1126
|
+
const byte = buffer[offset++];
|
|
1127
|
+
if (byte <= 127) {
|
|
1128
|
+
return byte;
|
|
1129
|
+
}
|
|
1130
|
+
if (byte >= 224) {
|
|
1131
|
+
return byte - 256;
|
|
1132
|
+
}
|
|
1133
|
+
if (byte >= 128 && byte <= 143) {
|
|
1134
|
+
const len = byte - 128;
|
|
1135
|
+
const result = {};
|
|
1136
|
+
for (let i = 0; i < len; i++) {
|
|
1137
|
+
const key = decode();
|
|
1138
|
+
result[key] = decode();
|
|
1139
|
+
}
|
|
1140
|
+
return result;
|
|
1141
|
+
}
|
|
1142
|
+
if (byte >= 144 && byte <= 159) {
|
|
1143
|
+
const len = byte - 144;
|
|
1144
|
+
const result = [];
|
|
1145
|
+
for (let i = 0; i < len; i++) {
|
|
1146
|
+
result.push(decode());
|
|
1147
|
+
}
|
|
1148
|
+
return result;
|
|
1149
|
+
}
|
|
1150
|
+
if (byte >= 160 && byte <= 191) {
|
|
1151
|
+
const len = byte - 160;
|
|
1152
|
+
const str = new TextDecoder().decode(buffer.subarray(offset, offset + len));
|
|
1153
|
+
offset += len;
|
|
1154
|
+
return str;
|
|
1155
|
+
}
|
|
1156
|
+
switch (byte) {
|
|
1157
|
+
case 192:
|
|
1158
|
+
return null;
|
|
1159
|
+
case 194:
|
|
1160
|
+
return false;
|
|
1161
|
+
case 195:
|
|
1162
|
+
return true;
|
|
1163
|
+
case 204:
|
|
1164
|
+
return buffer[offset++];
|
|
1165
|
+
case 205:
|
|
1166
|
+
return buffer[offset++] << 8 | buffer[offset++];
|
|
1167
|
+
case 206:
|
|
1168
|
+
return (buffer[offset++] << 24 >>> 0) + (buffer[offset++] << 16) + (buffer[offset++] << 8) + buffer[offset++];
|
|
1169
|
+
case 208: {
|
|
1170
|
+
const val = buffer[offset++];
|
|
1171
|
+
return val > 127 ? val - 256 : val;
|
|
1172
|
+
}
|
|
1173
|
+
case 209: {
|
|
1174
|
+
const val = buffer[offset++] << 8 | buffer[offset++];
|
|
1175
|
+
return val > 32767 ? val - 65536 : val;
|
|
1176
|
+
}
|
|
1177
|
+
case 210: {
|
|
1178
|
+
const val = buffer[offset++] << 24 | buffer[offset++] << 16 | buffer[offset++] << 8 | buffer[offset++];
|
|
1179
|
+
return val;
|
|
1180
|
+
}
|
|
1181
|
+
case 203: {
|
|
1182
|
+
const view = new DataView(buffer.buffer, buffer.byteOffset + offset, 8);
|
|
1183
|
+
offset += 8;
|
|
1184
|
+
return view.getFloat64(0, false);
|
|
1185
|
+
}
|
|
1186
|
+
case 217: {
|
|
1187
|
+
const len = buffer[offset++];
|
|
1188
|
+
const str = new TextDecoder().decode(buffer.subarray(offset, offset + len));
|
|
1189
|
+
offset += len;
|
|
1190
|
+
return str;
|
|
1191
|
+
}
|
|
1192
|
+
case 218: {
|
|
1193
|
+
const len = buffer[offset++] << 8 | buffer[offset++];
|
|
1194
|
+
const str = new TextDecoder().decode(buffer.subarray(offset, offset + len));
|
|
1195
|
+
offset += len;
|
|
1196
|
+
return str;
|
|
1197
|
+
}
|
|
1198
|
+
case 219: {
|
|
1199
|
+
const len = buffer[offset++] << 24 | buffer[offset++] << 16 | buffer[offset++] << 8 | buffer[offset++];
|
|
1200
|
+
const str = new TextDecoder().decode(buffer.subarray(offset, offset + len));
|
|
1201
|
+
offset += len;
|
|
1202
|
+
return str;
|
|
1203
|
+
}
|
|
1204
|
+
case 220: {
|
|
1205
|
+
const len = buffer[offset++] << 8 | buffer[offset++];
|
|
1206
|
+
const result = [];
|
|
1207
|
+
for (let i = 0; i < len; i++) {
|
|
1208
|
+
result.push(decode());
|
|
1209
|
+
}
|
|
1210
|
+
return result;
|
|
1211
|
+
}
|
|
1212
|
+
case 221: {
|
|
1213
|
+
const len = buffer[offset++] << 24 | buffer[offset++] << 16 | buffer[offset++] << 8 | buffer[offset++];
|
|
1214
|
+
const result = [];
|
|
1215
|
+
for (let i = 0; i < len; i++) {
|
|
1216
|
+
result.push(decode());
|
|
1217
|
+
}
|
|
1218
|
+
return result;
|
|
1219
|
+
}
|
|
1220
|
+
case 222: {
|
|
1221
|
+
const len = buffer[offset++] << 8 | buffer[offset++];
|
|
1222
|
+
const result = {};
|
|
1223
|
+
for (let i = 0; i < len; i++) {
|
|
1224
|
+
const key = decode();
|
|
1225
|
+
result[key] = decode();
|
|
1226
|
+
}
|
|
1227
|
+
return result;
|
|
1228
|
+
}
|
|
1229
|
+
case 223: {
|
|
1230
|
+
const len = buffer[offset++] << 24 | buffer[offset++] << 16 | buffer[offset++] << 8 | buffer[offset++];
|
|
1231
|
+
const result = {};
|
|
1232
|
+
for (let i = 0; i < len; i++) {
|
|
1233
|
+
const key = decode();
|
|
1234
|
+
result[key] = decode();
|
|
1235
|
+
}
|
|
1236
|
+
return result;
|
|
1237
|
+
}
|
|
1238
|
+
default:
|
|
1239
|
+
throw new Error(`Unknown MessagePack type: 0x${byte.toString(16)}`);
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
return decode();
|
|
1243
|
+
}
|
|
1244
|
+
var msgpackSerializer = {
|
|
1245
|
+
encode(data) {
|
|
1246
|
+
const encoded = msgpackEncode(data);
|
|
1247
|
+
const buffer = new ArrayBuffer(encoded.byteLength);
|
|
1248
|
+
new Uint8Array(buffer).set(encoded);
|
|
1249
|
+
return buffer;
|
|
1250
|
+
},
|
|
1251
|
+
decode(raw) {
|
|
1252
|
+
if (typeof raw === "string") {
|
|
1253
|
+
const binary = atob(raw);
|
|
1254
|
+
const bytes = new Uint8Array(binary.length);
|
|
1255
|
+
for (let i = 0; i < binary.length; i++) {
|
|
1256
|
+
bytes[i] = binary.charCodeAt(i);
|
|
1257
|
+
}
|
|
1258
|
+
return msgpackDecode(bytes);
|
|
1259
|
+
}
|
|
1260
|
+
return msgpackDecode(new Uint8Array(raw));
|
|
1261
|
+
},
|
|
1262
|
+
contentType: "application/msgpack"
|
|
1263
|
+
};
|
|
1264
|
+
function getSerializer(format) {
|
|
1265
|
+
switch (format) {
|
|
1266
|
+
case "json":
|
|
1267
|
+
return jsonSerializer;
|
|
1268
|
+
case "msgpack":
|
|
1269
|
+
return msgpackSerializer;
|
|
1270
|
+
default:
|
|
1271
|
+
return jsonSerializer;
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
function createSerializer(options) {
|
|
1275
|
+
return options;
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
// src/rpc/transports/http.ts
|
|
1279
|
+
var HttpTransport = class {
|
|
1280
|
+
name = "http";
|
|
1281
|
+
baseUrl;
|
|
1282
|
+
serializer;
|
|
1283
|
+
headers;
|
|
1284
|
+
fetchFn;
|
|
1285
|
+
timeout;
|
|
1286
|
+
constructor(options) {
|
|
1287
|
+
this.baseUrl = options.baseUrl.replace(/\/$/, "");
|
|
1288
|
+
this.serializer = options.serializer ?? jsonSerializer;
|
|
1289
|
+
this.headers = options.headers ?? {};
|
|
1290
|
+
this.fetchFn = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
1291
|
+
this.timeout = options.timeout ?? 3e4;
|
|
1292
|
+
}
|
|
1293
|
+
async call(request) {
|
|
1294
|
+
const url = `${this.baseUrl}/rpc`;
|
|
1295
|
+
const controller = new AbortController();
|
|
1296
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
1297
|
+
try {
|
|
1298
|
+
let body;
|
|
1299
|
+
try {
|
|
1300
|
+
body = this.serializer.encode(request);
|
|
1301
|
+
} catch (error) {
|
|
1302
|
+
throw new SerializationError(
|
|
1303
|
+
"Failed to serialize request",
|
|
1304
|
+
error instanceof Error ? error : void 0
|
|
1305
|
+
);
|
|
1306
|
+
}
|
|
1307
|
+
const response = await this.fetchFn(url, {
|
|
1308
|
+
method: "POST",
|
|
1309
|
+
headers: {
|
|
1310
|
+
"Content-Type": this.serializer.contentType,
|
|
1311
|
+
Accept: this.serializer.contentType,
|
|
1312
|
+
"X-Request-ID": request.id,
|
|
1313
|
+
"X-Service": request.service,
|
|
1314
|
+
"X-Method": request.method,
|
|
1315
|
+
"X-Method-Type": request.type,
|
|
1316
|
+
...request.version ? { "X-Service-Version": request.version } : {},
|
|
1317
|
+
...request.traceContext ? {
|
|
1318
|
+
traceparent: formatTraceparent(request.traceContext),
|
|
1319
|
+
...request.traceContext.traceState ? { tracestate: request.traceContext.traceState } : {}
|
|
1320
|
+
} : {},
|
|
1321
|
+
...this.headers
|
|
1322
|
+
},
|
|
1323
|
+
body: body instanceof ArrayBuffer ? body : body,
|
|
1324
|
+
signal: controller.signal
|
|
1325
|
+
});
|
|
1326
|
+
let responseData;
|
|
1327
|
+
try {
|
|
1328
|
+
const contentType = response.headers.get("Content-Type") ?? "";
|
|
1329
|
+
if (contentType.includes("msgpack")) {
|
|
1330
|
+
const buffer = await response.arrayBuffer();
|
|
1331
|
+
responseData = this.serializer.decode(buffer);
|
|
1332
|
+
} else {
|
|
1333
|
+
const text = await response.text();
|
|
1334
|
+
responseData = this.serializer.decode(text);
|
|
1335
|
+
}
|
|
1336
|
+
} catch (error) {
|
|
1337
|
+
throw new SerializationError(
|
|
1338
|
+
"Failed to deserialize response",
|
|
1339
|
+
error instanceof Error ? error : void 0
|
|
1340
|
+
);
|
|
1341
|
+
}
|
|
1342
|
+
return responseData;
|
|
1343
|
+
} catch (error) {
|
|
1344
|
+
if (error instanceof SerializationError) {
|
|
1345
|
+
throw error;
|
|
1346
|
+
}
|
|
1347
|
+
if (error instanceof Error) {
|
|
1348
|
+
if (error.name === "AbortError") {
|
|
1349
|
+
throw new TransportError(`Request timeout after ${this.timeout}ms`);
|
|
1350
|
+
}
|
|
1351
|
+
throw new TransportError(`HTTP request failed: ${error.message}`, error);
|
|
1352
|
+
}
|
|
1353
|
+
throw new TransportError("Unknown transport error");
|
|
1354
|
+
} finally {
|
|
1355
|
+
clearTimeout(timeoutId);
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
async close() {
|
|
1359
|
+
}
|
|
1360
|
+
};
|
|
1361
|
+
function createHttpTransport(options) {
|
|
1362
|
+
return new HttpTransport(options);
|
|
1363
|
+
}
|
|
1364
|
+
function formatTraceparent(ctx) {
|
|
1365
|
+
const flags = ctx.traceFlags.toString(16).padStart(2, "0");
|
|
1366
|
+
return `00-${ctx.traceId}-${ctx.spanId}-${flags}`;
|
|
1367
|
+
}
|
|
1368
|
+
function parseTraceparent(header) {
|
|
1369
|
+
const parts = header.split("-");
|
|
1370
|
+
if (parts.length !== 4) {
|
|
1371
|
+
return null;
|
|
1372
|
+
}
|
|
1373
|
+
const [version, traceId, spanId, flags] = parts;
|
|
1374
|
+
if (version !== "00" || !traceId || !spanId || !flags) {
|
|
1375
|
+
return null;
|
|
1376
|
+
}
|
|
1377
|
+
if (traceId.length !== 32 || spanId.length !== 16 || flags.length !== 2) {
|
|
1378
|
+
return null;
|
|
1379
|
+
}
|
|
1380
|
+
return {
|
|
1381
|
+
traceId,
|
|
1382
|
+
spanId,
|
|
1383
|
+
traceFlags: parseInt(flags, 16)
|
|
1384
|
+
};
|
|
1385
|
+
}
|
|
1386
|
+
function createHttpHandler(server) {
|
|
1387
|
+
return async (request) => {
|
|
1388
|
+
try {
|
|
1389
|
+
const contentType = request.headers.get("Content-Type") ?? "application/json";
|
|
1390
|
+
let body;
|
|
1391
|
+
if (contentType.includes("msgpack")) {
|
|
1392
|
+
const buffer = await request.arrayBuffer();
|
|
1393
|
+
body = JSON.parse(new TextDecoder().decode(buffer));
|
|
1394
|
+
} else {
|
|
1395
|
+
body = await request.json();
|
|
1396
|
+
}
|
|
1397
|
+
const rpcRequest = body;
|
|
1398
|
+
const traceparent = request.headers.get("traceparent");
|
|
1399
|
+
if (traceparent) {
|
|
1400
|
+
const traceContext = parseTraceparent(traceparent);
|
|
1401
|
+
if (traceContext) {
|
|
1402
|
+
const tracestate = request.headers.get("tracestate");
|
|
1403
|
+
if (tracestate) {
|
|
1404
|
+
traceContext.traceState = tracestate;
|
|
1405
|
+
}
|
|
1406
|
+
rpcRequest.traceContext = traceContext;
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
const response = await server.handle(rpcRequest);
|
|
1410
|
+
return new Response(JSON.stringify(response), {
|
|
1411
|
+
status: response.success ? 200 : getHttpStatus(response.error?.code),
|
|
1412
|
+
headers: {
|
|
1413
|
+
"Content-Type": "application/json",
|
|
1414
|
+
"X-Request-ID": rpcRequest.id
|
|
1415
|
+
}
|
|
1416
|
+
});
|
|
1417
|
+
} catch (error) {
|
|
1418
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1419
|
+
return new Response(
|
|
1420
|
+
JSON.stringify({
|
|
1421
|
+
success: false,
|
|
1422
|
+
error: {
|
|
1423
|
+
code: "INTERNAL_ERROR",
|
|
1424
|
+
message
|
|
1425
|
+
}
|
|
1426
|
+
}),
|
|
1427
|
+
{
|
|
1428
|
+
status: 500,
|
|
1429
|
+
headers: { "Content-Type": "application/json" }
|
|
1430
|
+
}
|
|
1431
|
+
);
|
|
1432
|
+
}
|
|
1433
|
+
};
|
|
1434
|
+
}
|
|
1435
|
+
function getHttpStatus(code) {
|
|
1436
|
+
switch (code) {
|
|
1437
|
+
case "METHOD_NOT_FOUND":
|
|
1438
|
+
case "SERVICE_NOT_FOUND":
|
|
1439
|
+
return 404;
|
|
1440
|
+
case "VERSION_MISMATCH":
|
|
1441
|
+
case "VALIDATION_ERROR":
|
|
1442
|
+
case "SERIALIZATION_ERROR":
|
|
1443
|
+
return 400;
|
|
1444
|
+
case "UNAUTHORIZED":
|
|
1445
|
+
return 401;
|
|
1446
|
+
case "FORBIDDEN":
|
|
1447
|
+
return 403;
|
|
1448
|
+
case "TIMEOUT":
|
|
1449
|
+
return 504;
|
|
1450
|
+
case "CIRCUIT_OPEN":
|
|
1451
|
+
case "BULKHEAD_REJECTED":
|
|
1452
|
+
return 503;
|
|
1453
|
+
default:
|
|
1454
|
+
return 500;
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
// src/events/transports/memory.ts
|
|
1459
|
+
import { createLogger as createLogger2 } from "@parsrun/core";
|
|
1460
|
+
|
|
1461
|
+
// src/events/handler.ts
|
|
1462
|
+
import { createLogger } from "@parsrun/core";
|
|
1463
|
+
|
|
1464
|
+
// src/events/format.ts
|
|
1465
|
+
import { generateId as generateId2 } from "@parsrun/core";
|
|
1466
|
+
function createEvent(options) {
|
|
1467
|
+
const event = {
|
|
1468
|
+
specversion: "1.0",
|
|
1469
|
+
type: options.type,
|
|
1470
|
+
source: options.source,
|
|
1471
|
+
id: options.id ?? generateId2(),
|
|
1472
|
+
time: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1473
|
+
datacontenttype: "application/json",
|
|
1474
|
+
data: options.data
|
|
1475
|
+
};
|
|
1476
|
+
if (options.subject) event.subject = options.subject;
|
|
1477
|
+
if (options.tenantId) event.parstenantid = options.tenantId;
|
|
1478
|
+
if (options.requestId) event.parsrequestid = options.requestId;
|
|
1479
|
+
if (options.traceContext) event.parstracecontext = formatTraceContext(options.traceContext);
|
|
1480
|
+
if (options.delivery) event.parsdelivery = options.delivery;
|
|
1481
|
+
return event;
|
|
1482
|
+
}
|
|
1483
|
+
function toCloudEvent(event) {
|
|
1484
|
+
return { ...event };
|
|
1485
|
+
}
|
|
1486
|
+
function toCompactEvent(event) {
|
|
1487
|
+
const compact = {
|
|
1488
|
+
e: event.type,
|
|
1489
|
+
s: event.source,
|
|
1490
|
+
i: event.id,
|
|
1491
|
+
t: new Date(event.time).getTime(),
|
|
1492
|
+
d: event.data
|
|
1493
|
+
};
|
|
1494
|
+
if (event.parstracecontext) compact.ctx = event.parstracecontext;
|
|
1495
|
+
if (event.parstenantid) compact.tid = event.parstenantid;
|
|
1496
|
+
return compact;
|
|
1497
|
+
}
|
|
1498
|
+
function fromCompactEvent(compact, source) {
|
|
1499
|
+
const event = {
|
|
1500
|
+
specversion: "1.0",
|
|
1501
|
+
type: compact.e,
|
|
1502
|
+
source: source ?? compact.s,
|
|
1503
|
+
id: compact.i,
|
|
1504
|
+
time: new Date(compact.t).toISOString(),
|
|
1505
|
+
datacontenttype: "application/json",
|
|
1506
|
+
data: compact.d
|
|
1507
|
+
};
|
|
1508
|
+
if (compact.ctx) event.parstracecontext = compact.ctx;
|
|
1509
|
+
if (compact.tid) event.parstenantid = compact.tid;
|
|
1510
|
+
return event;
|
|
1511
|
+
}
|
|
1512
|
+
function formatEventType(source, type) {
|
|
1513
|
+
return `com.pars.${source}.${type}`;
|
|
1514
|
+
}
|
|
1515
|
+
function parseEventType(fullType) {
|
|
1516
|
+
const prefix = "com.pars.";
|
|
1517
|
+
if (!fullType.startsWith(prefix)) {
|
|
1518
|
+
const parts = fullType.split(".");
|
|
1519
|
+
if (parts.length >= 2) {
|
|
1520
|
+
const [source, ...rest] = parts;
|
|
1521
|
+
return { source, type: rest.join(".") };
|
|
1522
|
+
}
|
|
1523
|
+
return null;
|
|
1524
|
+
}
|
|
1525
|
+
const withoutPrefix = fullType.slice(prefix.length);
|
|
1526
|
+
const dotIndex = withoutPrefix.indexOf(".");
|
|
1527
|
+
if (dotIndex === -1) {
|
|
1528
|
+
return null;
|
|
1529
|
+
}
|
|
1530
|
+
return {
|
|
1531
|
+
source: withoutPrefix.slice(0, dotIndex),
|
|
1532
|
+
type: withoutPrefix.slice(dotIndex + 1)
|
|
1533
|
+
};
|
|
1534
|
+
}
|
|
1535
|
+
function matchEventType(type, pattern) {
|
|
1536
|
+
if (pattern === "*" || pattern === "**") {
|
|
1537
|
+
return true;
|
|
1538
|
+
}
|
|
1539
|
+
const typeParts = type.split(".");
|
|
1540
|
+
const patternParts = pattern.split(".");
|
|
1541
|
+
let ti = 0;
|
|
1542
|
+
let pi = 0;
|
|
1543
|
+
while (ti < typeParts.length && pi < patternParts.length) {
|
|
1544
|
+
const pp = patternParts[pi];
|
|
1545
|
+
if (pp === "**") {
|
|
1546
|
+
if (pi === patternParts.length - 1) {
|
|
1547
|
+
return true;
|
|
1548
|
+
}
|
|
1549
|
+
for (let i = ti; i <= typeParts.length; i++) {
|
|
1550
|
+
const remaining = typeParts.slice(i).join(".");
|
|
1551
|
+
const remainingPattern = patternParts.slice(pi + 1).join(".");
|
|
1552
|
+
if (matchEventType(remaining, remainingPattern)) {
|
|
1553
|
+
return true;
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
return false;
|
|
1557
|
+
}
|
|
1558
|
+
if (pp === "*") {
|
|
1559
|
+
ti++;
|
|
1560
|
+
pi++;
|
|
1561
|
+
continue;
|
|
1562
|
+
}
|
|
1563
|
+
if (pp !== typeParts[ti]) {
|
|
1564
|
+
return false;
|
|
1565
|
+
}
|
|
1566
|
+
ti++;
|
|
1567
|
+
pi++;
|
|
1568
|
+
}
|
|
1569
|
+
return ti === typeParts.length && pi === patternParts.length;
|
|
1570
|
+
}
|
|
1571
|
+
function formatTraceContext(ctx) {
|
|
1572
|
+
const flags = ctx.traceFlags.toString(16).padStart(2, "0");
|
|
1573
|
+
return `00-${ctx.traceId}-${ctx.spanId}-${flags}`;
|
|
1574
|
+
}
|
|
1575
|
+
function validateEvent(event) {
|
|
1576
|
+
if (!event || typeof event !== "object") {
|
|
1577
|
+
return false;
|
|
1578
|
+
}
|
|
1579
|
+
const e = event;
|
|
1580
|
+
if (e["specversion"] !== "1.0") return false;
|
|
1581
|
+
if (typeof e["type"] !== "string" || e["type"].length === 0) return false;
|
|
1582
|
+
if (typeof e["source"] !== "string" || e["source"].length === 0) return false;
|
|
1583
|
+
if (typeof e["id"] !== "string" || e["id"].length === 0) return false;
|
|
1584
|
+
if (typeof e["time"] !== "string") return false;
|
|
1585
|
+
return true;
|
|
1586
|
+
}
|
|
1587
|
+
function validateCompactEvent(event) {
|
|
1588
|
+
if (!event || typeof event !== "object") {
|
|
1589
|
+
return false;
|
|
1590
|
+
}
|
|
1591
|
+
const e = event;
|
|
1592
|
+
if (typeof e["e"] !== "string" || e["e"].length === 0) return false;
|
|
1593
|
+
if (typeof e["s"] !== "string" || e["s"].length === 0) return false;
|
|
1594
|
+
if (typeof e["i"] !== "string" || e["i"].length === 0) return false;
|
|
1595
|
+
if (typeof e["t"] !== "number") return false;
|
|
1596
|
+
return true;
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
// src/events/handler.ts
|
|
1600
|
+
var EventHandlerRegistry = class {
|
|
1601
|
+
handlers = /* @__PURE__ */ new Map();
|
|
1602
|
+
logger;
|
|
1603
|
+
deadLetterQueue;
|
|
1604
|
+
defaultOptions;
|
|
1605
|
+
constructor(options = {}) {
|
|
1606
|
+
this.logger = options.logger ?? createLogger({ name: "event-handler" });
|
|
1607
|
+
if (options.deadLetterQueue) {
|
|
1608
|
+
this.deadLetterQueue = options.deadLetterQueue;
|
|
1609
|
+
}
|
|
1610
|
+
const defaultOpts = {
|
|
1611
|
+
retries: options.defaultOptions?.retries ?? 3,
|
|
1612
|
+
backoff: options.defaultOptions?.backoff ?? "exponential",
|
|
1613
|
+
maxDelay: options.defaultOptions?.maxDelay ?? 3e4,
|
|
1614
|
+
onExhausted: options.defaultOptions?.onExhausted ?? "log"
|
|
1615
|
+
};
|
|
1616
|
+
if (options.defaultOptions?.deadLetter) {
|
|
1617
|
+
defaultOpts.deadLetter = options.defaultOptions.deadLetter;
|
|
1618
|
+
}
|
|
1619
|
+
this.defaultOptions = defaultOpts;
|
|
1620
|
+
}
|
|
1621
|
+
/**
|
|
1622
|
+
* Register an event handler
|
|
1623
|
+
*/
|
|
1624
|
+
register(pattern, handler, options) {
|
|
1625
|
+
const registration = {
|
|
1626
|
+
pattern,
|
|
1627
|
+
handler,
|
|
1628
|
+
options: {
|
|
1629
|
+
...this.defaultOptions,
|
|
1630
|
+
...options
|
|
1631
|
+
}
|
|
1632
|
+
};
|
|
1633
|
+
const handlers = this.handlers.get(pattern) ?? [];
|
|
1634
|
+
handlers.push(registration);
|
|
1635
|
+
this.handlers.set(pattern, handlers);
|
|
1636
|
+
this.logger.debug(`Handler registered for pattern: ${pattern}`);
|
|
1637
|
+
return () => {
|
|
1638
|
+
const currentHandlers = this.handlers.get(pattern);
|
|
1639
|
+
if (currentHandlers) {
|
|
1640
|
+
const index = currentHandlers.indexOf(registration);
|
|
1641
|
+
if (index !== -1) {
|
|
1642
|
+
currentHandlers.splice(index, 1);
|
|
1643
|
+
if (currentHandlers.length === 0) {
|
|
1644
|
+
this.handlers.delete(pattern);
|
|
1645
|
+
}
|
|
1646
|
+
this.logger.debug(`Handler unregistered for pattern: ${pattern}`);
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
};
|
|
1650
|
+
}
|
|
1651
|
+
/**
|
|
1652
|
+
* Handle an event
|
|
1653
|
+
*/
|
|
1654
|
+
async handle(event) {
|
|
1655
|
+
const matchingHandlers = this.getMatchingHandlers(event.type);
|
|
1656
|
+
if (matchingHandlers.length === 0) {
|
|
1657
|
+
this.logger.debug(`No handlers for event type: ${event.type}`, {
|
|
1658
|
+
eventId: event.id
|
|
1659
|
+
});
|
|
1660
|
+
return;
|
|
1661
|
+
}
|
|
1662
|
+
this.logger.debug(`Handling event: ${event.type}`, {
|
|
1663
|
+
eventId: event.id,
|
|
1664
|
+
handlerCount: matchingHandlers.length
|
|
1665
|
+
});
|
|
1666
|
+
const results = await Promise.allSettled(
|
|
1667
|
+
matchingHandlers.map((reg) => this.executeHandler(event, reg))
|
|
1668
|
+
);
|
|
1669
|
+
for (let i = 0; i < results.length; i++) {
|
|
1670
|
+
const result = results[i];
|
|
1671
|
+
if (result?.status === "rejected") {
|
|
1672
|
+
this.logger.error(
|
|
1673
|
+
`Handler failed for ${event.type}`,
|
|
1674
|
+
result.reason,
|
|
1675
|
+
{ eventId: event.id, pattern: matchingHandlers[i]?.pattern }
|
|
1676
|
+
);
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
/**
|
|
1681
|
+
* Execute a single handler with retry logic
|
|
1682
|
+
*/
|
|
1683
|
+
async executeHandler(event, registration) {
|
|
1684
|
+
const { handler, options } = registration;
|
|
1685
|
+
const maxAttempts = options.retries + 1;
|
|
1686
|
+
let lastError;
|
|
1687
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
1688
|
+
try {
|
|
1689
|
+
const context = {
|
|
1690
|
+
logger: this.logger.child({
|
|
1691
|
+
eventId: event.id,
|
|
1692
|
+
pattern: registration.pattern,
|
|
1693
|
+
attempt
|
|
1694
|
+
}),
|
|
1695
|
+
attempt,
|
|
1696
|
+
maxAttempts,
|
|
1697
|
+
isRetry: attempt > 1
|
|
1698
|
+
};
|
|
1699
|
+
if (event.parstracecontext) {
|
|
1700
|
+
const traceCtx = parseTraceContext(event.parstracecontext);
|
|
1701
|
+
if (traceCtx) {
|
|
1702
|
+
context.traceContext = traceCtx;
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
await handler(event, context);
|
|
1706
|
+
return;
|
|
1707
|
+
} catch (error) {
|
|
1708
|
+
lastError = error;
|
|
1709
|
+
if (attempt < maxAttempts) {
|
|
1710
|
+
const delay = this.calculateBackoff(attempt, options);
|
|
1711
|
+
this.logger.warn(
|
|
1712
|
+
`Handler failed, retrying in ${delay}ms`,
|
|
1713
|
+
{ eventId: event.id, attempt, maxAttempts }
|
|
1714
|
+
);
|
|
1715
|
+
await sleep2(delay);
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
await this.handleExhausted(event, registration, lastError);
|
|
1720
|
+
}
|
|
1721
|
+
/**
|
|
1722
|
+
* Calculate backoff delay
|
|
1723
|
+
*/
|
|
1724
|
+
calculateBackoff(attempt, options) {
|
|
1725
|
+
const baseDelay = 100;
|
|
1726
|
+
if (options.backoff === "exponential") {
|
|
1727
|
+
return Math.min(baseDelay * Math.pow(2, attempt - 1), options.maxDelay);
|
|
1728
|
+
}
|
|
1729
|
+
return Math.min(baseDelay * attempt, options.maxDelay);
|
|
1730
|
+
}
|
|
1731
|
+
/**
|
|
1732
|
+
* Handle exhausted retries
|
|
1733
|
+
*/
|
|
1734
|
+
async handleExhausted(event, registration, error) {
|
|
1735
|
+
const { options } = registration;
|
|
1736
|
+
if (options.deadLetter && this.deadLetterQueue) {
|
|
1737
|
+
await this.deadLetterQueue.add({
|
|
1738
|
+
event,
|
|
1739
|
+
error: error.message,
|
|
1740
|
+
pattern: registration.pattern,
|
|
1741
|
+
attempts: options.retries + 1
|
|
1742
|
+
});
|
|
1743
|
+
}
|
|
1744
|
+
switch (options.onExhausted) {
|
|
1745
|
+
case "alert":
|
|
1746
|
+
this.logger.error(
|
|
1747
|
+
`[ALERT] Event handler exhausted all retries`,
|
|
1748
|
+
error,
|
|
1749
|
+
{
|
|
1750
|
+
eventId: event.id,
|
|
1751
|
+
eventType: event.type,
|
|
1752
|
+
pattern: registration.pattern
|
|
1753
|
+
}
|
|
1754
|
+
);
|
|
1755
|
+
break;
|
|
1756
|
+
case "discard":
|
|
1757
|
+
this.logger.debug(`Event discarded after exhausted retries`, {
|
|
1758
|
+
eventId: event.id
|
|
1759
|
+
});
|
|
1760
|
+
break;
|
|
1761
|
+
case "log":
|
|
1762
|
+
default:
|
|
1763
|
+
this.logger.warn(`Event handler exhausted all retries`, {
|
|
1764
|
+
eventId: event.id,
|
|
1765
|
+
error: error.message
|
|
1766
|
+
});
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
/**
|
|
1770
|
+
* Get handlers matching an event type
|
|
1771
|
+
*/
|
|
1772
|
+
getMatchingHandlers(eventType) {
|
|
1773
|
+
const matching = [];
|
|
1774
|
+
for (const [pattern, handlers] of this.handlers) {
|
|
1775
|
+
if (matchEventType(eventType, pattern)) {
|
|
1776
|
+
matching.push(...handlers);
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
return matching;
|
|
1780
|
+
}
|
|
1781
|
+
/**
|
|
1782
|
+
* Get all registered patterns
|
|
1783
|
+
*/
|
|
1784
|
+
getPatterns() {
|
|
1785
|
+
return Array.from(this.handlers.keys());
|
|
1786
|
+
}
|
|
1787
|
+
/**
|
|
1788
|
+
* Check if a pattern has handlers
|
|
1789
|
+
*/
|
|
1790
|
+
hasHandlers(pattern) {
|
|
1791
|
+
return this.handlers.has(pattern);
|
|
1792
|
+
}
|
|
1793
|
+
/**
|
|
1794
|
+
* Clear all handlers
|
|
1795
|
+
*/
|
|
1796
|
+
clear() {
|
|
1797
|
+
this.handlers.clear();
|
|
1798
|
+
}
|
|
1799
|
+
};
|
|
1800
|
+
function createEventHandlerRegistry(options) {
|
|
1801
|
+
return new EventHandlerRegistry(options);
|
|
1802
|
+
}
|
|
1803
|
+
function sleep2(ms) {
|
|
1804
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1805
|
+
}
|
|
1806
|
+
function parseTraceContext(traceparent) {
|
|
1807
|
+
const parts = traceparent.split("-");
|
|
1808
|
+
if (parts.length !== 4) return void 0;
|
|
1809
|
+
const [, traceId, spanId, flags] = parts;
|
|
1810
|
+
if (!traceId || !spanId || !flags) return void 0;
|
|
1811
|
+
return {
|
|
1812
|
+
traceId,
|
|
1813
|
+
spanId,
|
|
1814
|
+
traceFlags: parseInt(flags, 16)
|
|
1815
|
+
};
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
// src/events/transports/memory.ts
|
|
1819
|
+
var MemoryEventTransport = class {
|
|
1820
|
+
name = "memory";
|
|
1821
|
+
registry;
|
|
1822
|
+
logger;
|
|
1823
|
+
sync;
|
|
1824
|
+
pendingEvents = [];
|
|
1825
|
+
processing = false;
|
|
1826
|
+
constructor(options = {}) {
|
|
1827
|
+
this.logger = options.logger ?? createLogger2({ name: "memory-transport" });
|
|
1828
|
+
this.sync = options.sync ?? false;
|
|
1829
|
+
const registryOptions = {
|
|
1830
|
+
logger: this.logger
|
|
1831
|
+
};
|
|
1832
|
+
if (options.defaultHandlerOptions) {
|
|
1833
|
+
registryOptions.defaultOptions = options.defaultHandlerOptions;
|
|
1834
|
+
}
|
|
1835
|
+
this.registry = new EventHandlerRegistry(registryOptions);
|
|
1836
|
+
}
|
|
1837
|
+
/**
|
|
1838
|
+
* Emit an event
|
|
1839
|
+
*/
|
|
1840
|
+
async emit(event) {
|
|
1841
|
+
this.logger.debug(`Event emitted: ${event.type}`, {
|
|
1842
|
+
eventId: event.id,
|
|
1843
|
+
tenantId: event.parstenantid
|
|
1844
|
+
});
|
|
1845
|
+
if (this.sync) {
|
|
1846
|
+
await this.registry.handle(event);
|
|
1847
|
+
} else {
|
|
1848
|
+
this.pendingEvents.push(event);
|
|
1849
|
+
this.processQueue();
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
/**
|
|
1853
|
+
* Subscribe to events
|
|
1854
|
+
*/
|
|
1855
|
+
subscribe(eventType, handler, options) {
|
|
1856
|
+
return this.registry.register(eventType, handler, options);
|
|
1857
|
+
}
|
|
1858
|
+
/**
|
|
1859
|
+
* Process pending events asynchronously
|
|
1860
|
+
*/
|
|
1861
|
+
async processQueue() {
|
|
1862
|
+
if (this.processing) return;
|
|
1863
|
+
this.processing = true;
|
|
1864
|
+
try {
|
|
1865
|
+
while (this.pendingEvents.length > 0) {
|
|
1866
|
+
const event = this.pendingEvents.shift();
|
|
1867
|
+
if (event) {
|
|
1868
|
+
await this.registry.handle(event);
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
} finally {
|
|
1872
|
+
this.processing = false;
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
/**
|
|
1876
|
+
* Wait for all pending events to be processed
|
|
1877
|
+
*/
|
|
1878
|
+
async flush() {
|
|
1879
|
+
while (this.pendingEvents.length > 0 || this.processing) {
|
|
1880
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
/**
|
|
1884
|
+
* Get pending event count
|
|
1885
|
+
*/
|
|
1886
|
+
get pendingCount() {
|
|
1887
|
+
return this.pendingEvents.length;
|
|
1888
|
+
}
|
|
1889
|
+
/**
|
|
1890
|
+
* Get registered patterns
|
|
1891
|
+
*/
|
|
1892
|
+
getPatterns() {
|
|
1893
|
+
return this.registry.getPatterns();
|
|
1894
|
+
}
|
|
1895
|
+
/**
|
|
1896
|
+
* Clear all subscriptions
|
|
1897
|
+
*/
|
|
1898
|
+
clear() {
|
|
1899
|
+
this.registry.clear();
|
|
1900
|
+
this.pendingEvents.length = 0;
|
|
1901
|
+
}
|
|
1902
|
+
/**
|
|
1903
|
+
* Close the transport
|
|
1904
|
+
*/
|
|
1905
|
+
async close() {
|
|
1906
|
+
await this.flush();
|
|
1907
|
+
this.clear();
|
|
1908
|
+
}
|
|
1909
|
+
};
|
|
1910
|
+
function createMemoryEventTransport(options) {
|
|
1911
|
+
return new MemoryEventTransport(options);
|
|
1912
|
+
}
|
|
1913
|
+
var GlobalEventBus = class _GlobalEventBus {
|
|
1914
|
+
static instance = null;
|
|
1915
|
+
transports = /* @__PURE__ */ new Map();
|
|
1916
|
+
logger;
|
|
1917
|
+
constructor() {
|
|
1918
|
+
this.logger = createLogger2({ name: "global-event-bus" });
|
|
1919
|
+
}
|
|
1920
|
+
static getInstance() {
|
|
1921
|
+
if (!_GlobalEventBus.instance) {
|
|
1922
|
+
_GlobalEventBus.instance = new _GlobalEventBus();
|
|
1923
|
+
}
|
|
1924
|
+
return _GlobalEventBus.instance;
|
|
1925
|
+
}
|
|
1926
|
+
/**
|
|
1927
|
+
* Register a service's event transport
|
|
1928
|
+
*/
|
|
1929
|
+
register(serviceName, transport) {
|
|
1930
|
+
if (this.transports.has(serviceName)) {
|
|
1931
|
+
throw new Error(`Service already registered: ${serviceName}`);
|
|
1932
|
+
}
|
|
1933
|
+
this.transports.set(serviceName, transport);
|
|
1934
|
+
this.logger.debug(`Service registered: ${serviceName}`);
|
|
1935
|
+
}
|
|
1936
|
+
/**
|
|
1937
|
+
* Unregister a service
|
|
1938
|
+
*/
|
|
1939
|
+
unregister(serviceName) {
|
|
1940
|
+
const deleted = this.transports.delete(serviceName);
|
|
1941
|
+
if (deleted) {
|
|
1942
|
+
this.logger.debug(`Service unregistered: ${serviceName}`);
|
|
1943
|
+
}
|
|
1944
|
+
return deleted;
|
|
1945
|
+
}
|
|
1946
|
+
/**
|
|
1947
|
+
* Broadcast an event to all services (except source)
|
|
1948
|
+
*/
|
|
1949
|
+
async broadcast(event, excludeSource) {
|
|
1950
|
+
const promises = [];
|
|
1951
|
+
for (const [name, transport] of this.transports) {
|
|
1952
|
+
if (name !== excludeSource) {
|
|
1953
|
+
promises.push(transport.emit(event));
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
await Promise.allSettled(promises);
|
|
1957
|
+
}
|
|
1958
|
+
/**
|
|
1959
|
+
* Send an event to a specific service
|
|
1960
|
+
*/
|
|
1961
|
+
async send(serviceName, event) {
|
|
1962
|
+
const transport = this.transports.get(serviceName);
|
|
1963
|
+
if (!transport) {
|
|
1964
|
+
this.logger.warn(`Target service not found: ${serviceName}`, {
|
|
1965
|
+
eventId: event.id
|
|
1966
|
+
});
|
|
1967
|
+
return;
|
|
1968
|
+
}
|
|
1969
|
+
await transport.emit(event);
|
|
1970
|
+
}
|
|
1971
|
+
/**
|
|
1972
|
+
* Get all registered service names
|
|
1973
|
+
*/
|
|
1974
|
+
getServices() {
|
|
1975
|
+
return Array.from(this.transports.keys());
|
|
1976
|
+
}
|
|
1977
|
+
/**
|
|
1978
|
+
* Clear all registrations
|
|
1979
|
+
*/
|
|
1980
|
+
clear() {
|
|
1981
|
+
this.transports.clear();
|
|
1982
|
+
}
|
|
1983
|
+
/**
|
|
1984
|
+
* Reset singleton (for testing)
|
|
1985
|
+
*/
|
|
1986
|
+
static reset() {
|
|
1987
|
+
_GlobalEventBus.instance = null;
|
|
1988
|
+
}
|
|
1989
|
+
};
|
|
1990
|
+
function getGlobalEventBus() {
|
|
1991
|
+
return GlobalEventBus.getInstance();
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
// src/tracing/tracer.ts
|
|
1995
|
+
import { createLogger as createLogger4 } from "@parsrun/core";
|
|
1996
|
+
|
|
1997
|
+
// src/tracing/context.ts
|
|
1998
|
+
function generateTraceId() {
|
|
1999
|
+
const bytes = new Uint8Array(16);
|
|
2000
|
+
crypto.getRandomValues(bytes);
|
|
2001
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
2002
|
+
}
|
|
2003
|
+
function generateSpanId() {
|
|
2004
|
+
const bytes = new Uint8Array(8);
|
|
2005
|
+
crypto.getRandomValues(bytes);
|
|
2006
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
2007
|
+
}
|
|
2008
|
+
function createTraceContext(options) {
|
|
2009
|
+
const ctx = {
|
|
2010
|
+
traceId: options?.traceId ?? generateTraceId(),
|
|
2011
|
+
spanId: options?.spanId ?? generateSpanId(),
|
|
2012
|
+
traceFlags: options?.traceFlags ?? 1
|
|
2013
|
+
// Default: sampled
|
|
2014
|
+
};
|
|
2015
|
+
if (options?.traceState) {
|
|
2016
|
+
ctx.traceState = options.traceState;
|
|
2017
|
+
}
|
|
2018
|
+
return ctx;
|
|
2019
|
+
}
|
|
2020
|
+
function createChildContext(parent) {
|
|
2021
|
+
const ctx = {
|
|
2022
|
+
traceId: parent.traceId,
|
|
2023
|
+
spanId: generateSpanId(),
|
|
2024
|
+
traceFlags: parent.traceFlags
|
|
2025
|
+
};
|
|
2026
|
+
if (parent.traceState) {
|
|
2027
|
+
ctx.traceState = parent.traceState;
|
|
2028
|
+
}
|
|
2029
|
+
return ctx;
|
|
2030
|
+
}
|
|
2031
|
+
function formatTraceparent2(ctx) {
|
|
2032
|
+
const version = "00";
|
|
2033
|
+
const flags = ctx.traceFlags.toString(16).padStart(2, "0");
|
|
2034
|
+
return `${version}-${ctx.traceId}-${ctx.spanId}-${flags}`;
|
|
2035
|
+
}
|
|
2036
|
+
function parseTraceparent2(header) {
|
|
2037
|
+
const parts = header.trim().split("-");
|
|
2038
|
+
if (parts.length !== 4) {
|
|
2039
|
+
return null;
|
|
2040
|
+
}
|
|
2041
|
+
const [version, traceId, spanId, flags] = parts;
|
|
2042
|
+
if (version !== "00") {
|
|
2043
|
+
return null;
|
|
2044
|
+
}
|
|
2045
|
+
if (!traceId || traceId.length !== 32 || !/^[0-9a-f]+$/i.test(traceId)) {
|
|
2046
|
+
return null;
|
|
2047
|
+
}
|
|
2048
|
+
if (!spanId || spanId.length !== 16 || !/^[0-9a-f]+$/i.test(spanId)) {
|
|
2049
|
+
return null;
|
|
2050
|
+
}
|
|
2051
|
+
if (!flags || flags.length !== 2 || !/^[0-9a-f]+$/i.test(flags)) {
|
|
2052
|
+
return null;
|
|
2053
|
+
}
|
|
2054
|
+
return {
|
|
2055
|
+
traceId: traceId.toLowerCase(),
|
|
2056
|
+
spanId: spanId.toLowerCase(),
|
|
2057
|
+
traceFlags: parseInt(flags, 16)
|
|
2058
|
+
};
|
|
2059
|
+
}
|
|
2060
|
+
function formatTracestate(state) {
|
|
2061
|
+
return Object.entries(state).map(([key, value]) => `${key}=${value}`).join(",");
|
|
2062
|
+
}
|
|
2063
|
+
function parseTracestate(header) {
|
|
2064
|
+
const state = {};
|
|
2065
|
+
for (const pair of header.split(",")) {
|
|
2066
|
+
const [key, value] = pair.trim().split("=");
|
|
2067
|
+
if (key && value) {
|
|
2068
|
+
state[key] = value;
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
return state;
|
|
2072
|
+
}
|
|
2073
|
+
var TraceContextManager = class {
|
|
2074
|
+
stack = [];
|
|
2075
|
+
/**
|
|
2076
|
+
* Get current trace context
|
|
2077
|
+
*/
|
|
2078
|
+
current() {
|
|
2079
|
+
return this.stack[this.stack.length - 1];
|
|
2080
|
+
}
|
|
2081
|
+
/**
|
|
2082
|
+
* Run a function with a trace context
|
|
2083
|
+
*/
|
|
2084
|
+
async run(ctx, fn) {
|
|
2085
|
+
this.stack.push(ctx);
|
|
2086
|
+
try {
|
|
2087
|
+
return await fn();
|
|
2088
|
+
} finally {
|
|
2089
|
+
this.stack.pop();
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
/**
|
|
2093
|
+
* Run a function with a new child context
|
|
2094
|
+
*/
|
|
2095
|
+
async runChild(fn) {
|
|
2096
|
+
const parent = this.current();
|
|
2097
|
+
const child = parent ? createChildContext(parent) : createTraceContext();
|
|
2098
|
+
return this.run(child, fn);
|
|
2099
|
+
}
|
|
2100
|
+
/**
|
|
2101
|
+
* Create context from incoming request headers
|
|
2102
|
+
*/
|
|
2103
|
+
fromHeaders(headers) {
|
|
2104
|
+
const traceparent = headers instanceof Headers ? headers.get("traceparent") : headers["traceparent"];
|
|
2105
|
+
if (!traceparent) {
|
|
2106
|
+
return void 0;
|
|
2107
|
+
}
|
|
2108
|
+
const ctx = parseTraceparent2(traceparent);
|
|
2109
|
+
if (!ctx) {
|
|
2110
|
+
return void 0;
|
|
2111
|
+
}
|
|
2112
|
+
const tracestate = headers instanceof Headers ? headers.get("tracestate") : headers["tracestate"];
|
|
2113
|
+
if (tracestate) {
|
|
2114
|
+
ctx.traceState = tracestate;
|
|
2115
|
+
}
|
|
2116
|
+
return ctx;
|
|
2117
|
+
}
|
|
2118
|
+
/**
|
|
2119
|
+
* Add trace context to outgoing request headers
|
|
2120
|
+
*/
|
|
2121
|
+
toHeaders(ctx) {
|
|
2122
|
+
const headers = {
|
|
2123
|
+
traceparent: formatTraceparent2(ctx)
|
|
2124
|
+
};
|
|
2125
|
+
if (ctx.traceState) {
|
|
2126
|
+
headers["tracestate"] = ctx.traceState;
|
|
2127
|
+
}
|
|
2128
|
+
return headers;
|
|
2129
|
+
}
|
|
2130
|
+
/**
|
|
2131
|
+
* Check if current context is sampled
|
|
2132
|
+
*/
|
|
2133
|
+
isSampled() {
|
|
2134
|
+
const ctx = this.current();
|
|
2135
|
+
return ctx ? (ctx.traceFlags & 1) === 1 : false;
|
|
2136
|
+
}
|
|
2137
|
+
/**
|
|
2138
|
+
* Clear all contexts (for testing)
|
|
2139
|
+
*/
|
|
2140
|
+
clear() {
|
|
2141
|
+
this.stack.length = 0;
|
|
2142
|
+
}
|
|
2143
|
+
};
|
|
2144
|
+
function shouldSample(sampler, traceId) {
|
|
2145
|
+
if (sampler === "always") {
|
|
2146
|
+
return true;
|
|
2147
|
+
}
|
|
2148
|
+
if (sampler === "never") {
|
|
2149
|
+
return false;
|
|
2150
|
+
}
|
|
2151
|
+
if (traceId) {
|
|
2152
|
+
const hash = parseInt(traceId.slice(-8), 16);
|
|
2153
|
+
const threshold = Math.floor(sampler.ratio * 4294967295);
|
|
2154
|
+
return hash < threshold;
|
|
2155
|
+
}
|
|
2156
|
+
return Math.random() < sampler.ratio;
|
|
2157
|
+
}
|
|
2158
|
+
|
|
2159
|
+
// src/tracing/spans.ts
|
|
2160
|
+
function createSpan(options) {
|
|
2161
|
+
let traceContext;
|
|
2162
|
+
if (options.parent) {
|
|
2163
|
+
traceContext = {
|
|
2164
|
+
traceId: options.parent.traceId,
|
|
2165
|
+
spanId: generateSpanId(),
|
|
2166
|
+
traceFlags: options.parent.traceFlags
|
|
2167
|
+
};
|
|
2168
|
+
if (options.parent.traceState) {
|
|
2169
|
+
traceContext.traceState = options.parent.traceState;
|
|
2170
|
+
}
|
|
2171
|
+
} else {
|
|
2172
|
+
traceContext = createTraceContext();
|
|
2173
|
+
}
|
|
2174
|
+
const span = {
|
|
2175
|
+
name: options.name,
|
|
2176
|
+
kind: options.kind ?? "internal",
|
|
2177
|
+
traceContext,
|
|
2178
|
+
startTime: options.startTime ?? Date.now(),
|
|
2179
|
+
status: "unset",
|
|
2180
|
+
attributes: options.attributes ?? {},
|
|
2181
|
+
events: []
|
|
2182
|
+
};
|
|
2183
|
+
if (options.parent?.spanId) {
|
|
2184
|
+
span.parentSpanId = options.parent.spanId;
|
|
2185
|
+
}
|
|
2186
|
+
return span;
|
|
2187
|
+
}
|
|
2188
|
+
var SpanManager = class {
|
|
2189
|
+
spans = /* @__PURE__ */ new Map();
|
|
2190
|
+
/**
|
|
2191
|
+
* Start a new span
|
|
2192
|
+
*/
|
|
2193
|
+
startSpan(options) {
|
|
2194
|
+
const span = createSpan(options);
|
|
2195
|
+
this.spans.set(span.traceContext.spanId, span);
|
|
2196
|
+
return span;
|
|
2197
|
+
}
|
|
2198
|
+
/**
|
|
2199
|
+
* End a span
|
|
2200
|
+
*/
|
|
2201
|
+
endSpan(span, status) {
|
|
2202
|
+
span.endTime = Date.now();
|
|
2203
|
+
if (status) {
|
|
2204
|
+
span.status = status;
|
|
2205
|
+
} else if (span.status === "unset") {
|
|
2206
|
+
span.status = "ok";
|
|
2207
|
+
}
|
|
2208
|
+
return span;
|
|
2209
|
+
}
|
|
2210
|
+
/**
|
|
2211
|
+
* Set span attribute
|
|
2212
|
+
*/
|
|
2213
|
+
setAttribute(span, key, value) {
|
|
2214
|
+
span.attributes[key] = value;
|
|
2215
|
+
}
|
|
2216
|
+
/**
|
|
2217
|
+
* Set multiple span attributes
|
|
2218
|
+
*/
|
|
2219
|
+
setAttributes(span, attributes) {
|
|
2220
|
+
Object.assign(span.attributes, attributes);
|
|
2221
|
+
}
|
|
2222
|
+
/**
|
|
2223
|
+
* Add span event
|
|
2224
|
+
*/
|
|
2225
|
+
addEvent(span, name, attributes) {
|
|
2226
|
+
const event = {
|
|
2227
|
+
name,
|
|
2228
|
+
time: Date.now()
|
|
2229
|
+
};
|
|
2230
|
+
if (attributes) {
|
|
2231
|
+
event.attributes = attributes;
|
|
2232
|
+
}
|
|
2233
|
+
span.events.push(event);
|
|
2234
|
+
}
|
|
2235
|
+
/**
|
|
2236
|
+
* Set span status
|
|
2237
|
+
*/
|
|
2238
|
+
setStatus(span, status) {
|
|
2239
|
+
span.status = status;
|
|
2240
|
+
}
|
|
2241
|
+
/**
|
|
2242
|
+
* Record exception on span
|
|
2243
|
+
*/
|
|
2244
|
+
recordException(span, error) {
|
|
2245
|
+
span.status = "error";
|
|
2246
|
+
this.addEvent(span, "exception", {
|
|
2247
|
+
"exception.type": error.name,
|
|
2248
|
+
"exception.message": error.message,
|
|
2249
|
+
"exception.stacktrace": error.stack ?? ""
|
|
2250
|
+
});
|
|
2251
|
+
}
|
|
2252
|
+
/**
|
|
2253
|
+
* Get span by ID
|
|
2254
|
+
*/
|
|
2255
|
+
getSpan(spanId) {
|
|
2256
|
+
return this.spans.get(spanId);
|
|
2257
|
+
}
|
|
2258
|
+
/**
|
|
2259
|
+
* Get all completed spans and clear
|
|
2260
|
+
*/
|
|
2261
|
+
flush() {
|
|
2262
|
+
const completed = Array.from(this.spans.values()).filter((s) => s.endTime);
|
|
2263
|
+
for (const span of completed) {
|
|
2264
|
+
this.spans.delete(span.traceContext.spanId);
|
|
2265
|
+
}
|
|
2266
|
+
return completed;
|
|
2267
|
+
}
|
|
2268
|
+
/**
|
|
2269
|
+
* Clear all spans
|
|
2270
|
+
*/
|
|
2271
|
+
clear() {
|
|
2272
|
+
this.spans.clear();
|
|
2273
|
+
}
|
|
2274
|
+
};
|
|
2275
|
+
function getSpanDuration(span) {
|
|
2276
|
+
if (!span.endTime) return void 0;
|
|
2277
|
+
return span.endTime - span.startTime;
|
|
2278
|
+
}
|
|
2279
|
+
function isSpanCompleted(span) {
|
|
2280
|
+
return span.endTime !== void 0;
|
|
2281
|
+
}
|
|
2282
|
+
function spanToLogObject(span) {
|
|
2283
|
+
return {
|
|
2284
|
+
traceId: span.traceContext.traceId,
|
|
2285
|
+
spanId: span.traceContext.spanId,
|
|
2286
|
+
parentSpanId: span.parentSpanId,
|
|
2287
|
+
name: span.name,
|
|
2288
|
+
kind: span.kind,
|
|
2289
|
+
status: span.status,
|
|
2290
|
+
startTime: new Date(span.startTime).toISOString(),
|
|
2291
|
+
endTime: span.endTime ? new Date(span.endTime).toISOString() : void 0,
|
|
2292
|
+
durationMs: getSpanDuration(span),
|
|
2293
|
+
attributes: span.attributes,
|
|
2294
|
+
events: span.events.map((e) => ({
|
|
2295
|
+
name: e.name,
|
|
2296
|
+
time: new Date(e.time).toISOString(),
|
|
2297
|
+
attributes: e.attributes
|
|
2298
|
+
}))
|
|
2299
|
+
};
|
|
2300
|
+
}
|
|
2301
|
+
var SpanAttributes = {
|
|
2302
|
+
// HTTP
|
|
2303
|
+
HTTP_METHOD: "http.method",
|
|
2304
|
+
HTTP_URL: "http.url",
|
|
2305
|
+
HTTP_STATUS_CODE: "http.status_code",
|
|
2306
|
+
HTTP_REQUEST_CONTENT_LENGTH: "http.request_content_length",
|
|
2307
|
+
HTTP_RESPONSE_CONTENT_LENGTH: "http.response_content_length",
|
|
2308
|
+
// RPC
|
|
2309
|
+
RPC_SYSTEM: "rpc.system",
|
|
2310
|
+
RPC_SERVICE: "rpc.service",
|
|
2311
|
+
RPC_METHOD: "rpc.method",
|
|
2312
|
+
// Database
|
|
2313
|
+
DB_SYSTEM: "db.system",
|
|
2314
|
+
DB_NAME: "db.name",
|
|
2315
|
+
DB_OPERATION: "db.operation",
|
|
2316
|
+
DB_STATEMENT: "db.statement",
|
|
2317
|
+
// Messaging
|
|
2318
|
+
MESSAGING_SYSTEM: "messaging.system",
|
|
2319
|
+
MESSAGING_DESTINATION: "messaging.destination",
|
|
2320
|
+
MESSAGING_MESSAGE_ID: "messaging.message_id",
|
|
2321
|
+
// Service
|
|
2322
|
+
SERVICE_NAME: "service.name",
|
|
2323
|
+
SERVICE_VERSION: "service.version",
|
|
2324
|
+
// Error
|
|
2325
|
+
EXCEPTION_TYPE: "exception.type",
|
|
2326
|
+
EXCEPTION_MESSAGE: "exception.message",
|
|
2327
|
+
EXCEPTION_STACKTRACE: "exception.stacktrace",
|
|
2328
|
+
// Custom Pars attributes
|
|
2329
|
+
PARS_TENANT_ID: "pars.tenant_id",
|
|
2330
|
+
PARS_USER_ID: "pars.user_id",
|
|
2331
|
+
PARS_REQUEST_ID: "pars.request_id"
|
|
2332
|
+
};
|
|
2333
|
+
|
|
2334
|
+
// src/tracing/exporters.ts
|
|
2335
|
+
import { createLogger as createLogger3 } from "@parsrun/core";
|
|
2336
|
+
var ConsoleExporter = class {
|
|
2337
|
+
name = "console";
|
|
2338
|
+
logger;
|
|
2339
|
+
pretty;
|
|
2340
|
+
includeAttributes;
|
|
2341
|
+
includeEvents;
|
|
2342
|
+
constructor(options = {}) {
|
|
2343
|
+
this.logger = options.logger ?? createLogger3({ name: "trace-exporter" });
|
|
2344
|
+
this.pretty = options.pretty ?? true;
|
|
2345
|
+
this.includeAttributes = options.includeAttributes ?? true;
|
|
2346
|
+
this.includeEvents = options.includeEvents ?? true;
|
|
2347
|
+
}
|
|
2348
|
+
async export(spans) {
|
|
2349
|
+
for (const span of spans) {
|
|
2350
|
+
const duration = getSpanDuration(span);
|
|
2351
|
+
const status = span.status === "error" ? "ERROR" : span.status === "ok" ? "OK" : "UNSET";
|
|
2352
|
+
if (this.pretty) {
|
|
2353
|
+
const indent = span.parentSpanId ? " \u2514\u2500" : "\u2500\u2500";
|
|
2354
|
+
const statusIcon = span.status === "error" ? "\u2717" : span.status === "ok" ? "\u2713" : "\u25CB";
|
|
2355
|
+
const durationStr = duration !== void 0 ? `${duration}ms` : "?ms";
|
|
2356
|
+
console.log(
|
|
2357
|
+
`${indent} ${statusIcon} [${span.kind}] ${span.name} (${durationStr}) trace=${span.traceContext.traceId.slice(0, 8)}`
|
|
2358
|
+
);
|
|
2359
|
+
if (this.includeAttributes && Object.keys(span.attributes).length > 0) {
|
|
2360
|
+
console.log(` attributes:`, span.attributes);
|
|
2361
|
+
}
|
|
2362
|
+
if (this.includeEvents && span.events.length > 0) {
|
|
2363
|
+
for (const event of span.events) {
|
|
2364
|
+
console.log(` event: ${event.name}`, event.attributes ?? "");
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
} else {
|
|
2368
|
+
const logObj = spanToLogObject(span);
|
|
2369
|
+
this.logger.info(`Span: ${span.name}`, {
|
|
2370
|
+
...logObj,
|
|
2371
|
+
status,
|
|
2372
|
+
durationMs: duration
|
|
2373
|
+
});
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2377
|
+
async shutdown() {
|
|
2378
|
+
}
|
|
2379
|
+
};
|
|
2380
|
+
function createConsoleExporter(options) {
|
|
2381
|
+
return new ConsoleExporter(options);
|
|
2382
|
+
}
|
|
2383
|
+
var OtlpExporter = class {
|
|
2384
|
+
name = "otlp";
|
|
2385
|
+
endpoint;
|
|
2386
|
+
serviceName;
|
|
2387
|
+
serviceVersion;
|
|
2388
|
+
headers;
|
|
2389
|
+
timeout;
|
|
2390
|
+
batchSize;
|
|
2391
|
+
flushInterval;
|
|
2392
|
+
logger;
|
|
2393
|
+
buffer = [];
|
|
2394
|
+
flushTimer = null;
|
|
2395
|
+
constructor(options) {
|
|
2396
|
+
this.endpoint = options.endpoint.replace(/\/$/, "");
|
|
2397
|
+
this.serviceName = options.serviceName;
|
|
2398
|
+
this.serviceVersion = options.serviceVersion ?? "1.0.0";
|
|
2399
|
+
this.headers = options.headers ?? {};
|
|
2400
|
+
this.timeout = options.timeout ?? 1e4;
|
|
2401
|
+
this.batchSize = options.batchSize ?? 100;
|
|
2402
|
+
this.flushInterval = options.flushInterval ?? 5e3;
|
|
2403
|
+
this.logger = options.logger ?? createLogger3({ name: "otlp-exporter" });
|
|
2404
|
+
this.flushTimer = setInterval(() => this.flush(), this.flushInterval);
|
|
2405
|
+
}
|
|
2406
|
+
async export(spans) {
|
|
2407
|
+
this.buffer.push(...spans);
|
|
2408
|
+
if (this.buffer.length >= this.batchSize) {
|
|
2409
|
+
await this.flush();
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2412
|
+
async flush() {
|
|
2413
|
+
if (this.buffer.length === 0) return;
|
|
2414
|
+
const spansToExport = this.buffer.splice(0, this.batchSize);
|
|
2415
|
+
try {
|
|
2416
|
+
const payload = this.buildOtlpPayload(spansToExport);
|
|
2417
|
+
const controller = new AbortController();
|
|
2418
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
2419
|
+
try {
|
|
2420
|
+
const response = await fetch(`${this.endpoint}/v1/traces`, {
|
|
2421
|
+
method: "POST",
|
|
2422
|
+
headers: {
|
|
2423
|
+
"Content-Type": "application/json",
|
|
2424
|
+
...this.headers
|
|
2425
|
+
},
|
|
2426
|
+
body: JSON.stringify(payload),
|
|
2427
|
+
signal: controller.signal
|
|
2428
|
+
});
|
|
2429
|
+
if (!response.ok) {
|
|
2430
|
+
throw new Error(`OTLP export failed: ${response.status} ${response.statusText}`);
|
|
2431
|
+
}
|
|
2432
|
+
this.logger.debug(`Exported ${spansToExport.length} spans to OTLP`);
|
|
2433
|
+
} finally {
|
|
2434
|
+
clearTimeout(timeoutId);
|
|
2435
|
+
}
|
|
2436
|
+
} catch (error) {
|
|
2437
|
+
this.logger.error(`Failed to export spans to OTLP`, error);
|
|
2438
|
+
this.buffer.unshift(...spansToExport);
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
buildOtlpPayload(spans) {
|
|
2442
|
+
return {
|
|
2443
|
+
resourceSpans: [
|
|
2444
|
+
{
|
|
2445
|
+
resource: {
|
|
2446
|
+
attributes: [
|
|
2447
|
+
{ key: "service.name", value: { stringValue: this.serviceName } },
|
|
2448
|
+
{ key: "service.version", value: { stringValue: this.serviceVersion } }
|
|
2449
|
+
]
|
|
2450
|
+
},
|
|
2451
|
+
scopeSpans: [
|
|
2452
|
+
{
|
|
2453
|
+
scope: {
|
|
2454
|
+
name: "@parsrun/service",
|
|
2455
|
+
version: "0.1.0"
|
|
2456
|
+
},
|
|
2457
|
+
spans: spans.map((span) => this.convertSpan(span))
|
|
2458
|
+
}
|
|
2459
|
+
]
|
|
2460
|
+
}
|
|
2461
|
+
]
|
|
2462
|
+
};
|
|
2463
|
+
}
|
|
2464
|
+
convertSpan(span) {
|
|
2465
|
+
const otlpSpan = {
|
|
2466
|
+
traceId: span.traceContext.traceId,
|
|
2467
|
+
spanId: span.traceContext.spanId,
|
|
2468
|
+
name: span.name,
|
|
2469
|
+
kind: this.convertSpanKind(span.kind),
|
|
2470
|
+
startTimeUnixNano: String(span.startTime * 1e6),
|
|
2471
|
+
attributes: Object.entries(span.attributes).map(([key, value]) => ({
|
|
2472
|
+
key,
|
|
2473
|
+
value: this.convertAttributeValue(value)
|
|
2474
|
+
})),
|
|
2475
|
+
events: span.events.map((event) => ({
|
|
2476
|
+
name: event.name,
|
|
2477
|
+
timeUnixNano: String(event.time * 1e6),
|
|
2478
|
+
attributes: event.attributes ? Object.entries(event.attributes).map(([key, value]) => ({
|
|
2479
|
+
key,
|
|
2480
|
+
value: this.convertAttributeValue(value)
|
|
2481
|
+
})) : []
|
|
2482
|
+
})),
|
|
2483
|
+
status: {
|
|
2484
|
+
code: span.status === "error" ? 2 : span.status === "ok" ? 1 : 0
|
|
2485
|
+
}
|
|
2486
|
+
};
|
|
2487
|
+
if (span.parentSpanId) {
|
|
2488
|
+
otlpSpan.parentSpanId = span.parentSpanId;
|
|
2489
|
+
}
|
|
2490
|
+
if (span.endTime) {
|
|
2491
|
+
otlpSpan.endTimeUnixNano = String(span.endTime * 1e6);
|
|
2492
|
+
}
|
|
2493
|
+
return otlpSpan;
|
|
2494
|
+
}
|
|
2495
|
+
convertSpanKind(kind) {
|
|
2496
|
+
switch (kind) {
|
|
2497
|
+
case "internal":
|
|
2498
|
+
return 1;
|
|
2499
|
+
case "server":
|
|
2500
|
+
return 2;
|
|
2501
|
+
case "client":
|
|
2502
|
+
return 3;
|
|
2503
|
+
case "producer":
|
|
2504
|
+
return 4;
|
|
2505
|
+
case "consumer":
|
|
2506
|
+
return 5;
|
|
2507
|
+
default:
|
|
2508
|
+
return 0;
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2511
|
+
convertAttributeValue(value) {
|
|
2512
|
+
if (typeof value === "string") {
|
|
2513
|
+
return { stringValue: value };
|
|
2514
|
+
}
|
|
2515
|
+
if (typeof value === "number") {
|
|
2516
|
+
if (Number.isInteger(value)) {
|
|
2517
|
+
return { intValue: String(value) };
|
|
2518
|
+
}
|
|
2519
|
+
return { doubleValue: value };
|
|
2520
|
+
}
|
|
2521
|
+
if (typeof value === "boolean") {
|
|
2522
|
+
return { boolValue: value };
|
|
2523
|
+
}
|
|
2524
|
+
if (Array.isArray(value)) {
|
|
2525
|
+
return {
|
|
2526
|
+
arrayValue: {
|
|
2527
|
+
values: value.map((v) => this.convertAttributeValue(v))
|
|
2528
|
+
}
|
|
2529
|
+
};
|
|
2530
|
+
}
|
|
2531
|
+
return { stringValue: String(value) };
|
|
2532
|
+
}
|
|
2533
|
+
async shutdown() {
|
|
2534
|
+
if (this.flushTimer) {
|
|
2535
|
+
clearInterval(this.flushTimer);
|
|
2536
|
+
this.flushTimer = null;
|
|
2537
|
+
}
|
|
2538
|
+
await this.flush();
|
|
2539
|
+
}
|
|
2540
|
+
};
|
|
2541
|
+
function createOtlpExporter(options) {
|
|
2542
|
+
return new OtlpExporter(options);
|
|
2543
|
+
}
|
|
2544
|
+
|
|
2545
|
+
// src/tracing/tracer.ts
|
|
2546
|
+
var Tracer = class {
|
|
2547
|
+
serviceName;
|
|
2548
|
+
serviceVersion;
|
|
2549
|
+
sampler;
|
|
2550
|
+
exporter;
|
|
2551
|
+
logger;
|
|
2552
|
+
contextManager;
|
|
2553
|
+
spanManager;
|
|
2554
|
+
enabled;
|
|
2555
|
+
flushTimer = null;
|
|
2556
|
+
constructor(options) {
|
|
2557
|
+
this.serviceName = options.serviceName;
|
|
2558
|
+
this.serviceVersion = options.serviceVersion ?? "1.0.0";
|
|
2559
|
+
this.enabled = options.config?.enabled ?? true;
|
|
2560
|
+
this.sampler = options.config?.sampler ?? { ratio: 0.1 };
|
|
2561
|
+
this.exporter = options.exporter ?? new ConsoleExporter();
|
|
2562
|
+
this.logger = options.logger ?? createLogger4({ name: `tracer:${options.serviceName}` });
|
|
2563
|
+
this.contextManager = new TraceContextManager();
|
|
2564
|
+
this.spanManager = new SpanManager();
|
|
2565
|
+
if (this.enabled) {
|
|
2566
|
+
this.flushTimer = setInterval(() => this.flush(), 5e3);
|
|
2567
|
+
}
|
|
2568
|
+
}
|
|
2569
|
+
/**
|
|
2570
|
+
* Start a new span
|
|
2571
|
+
*/
|
|
2572
|
+
startSpan(name, options) {
|
|
2573
|
+
if (!this.enabled) return null;
|
|
2574
|
+
const parent = options?.parent ?? this.contextManager.current();
|
|
2575
|
+
if (!parent && !shouldSample(this.sampler)) {
|
|
2576
|
+
return null;
|
|
2577
|
+
}
|
|
2578
|
+
const spanOptions = {
|
|
2579
|
+
name,
|
|
2580
|
+
kind: options?.kind ?? "internal",
|
|
2581
|
+
attributes: {
|
|
2582
|
+
[SpanAttributes.SERVICE_NAME]: this.serviceName,
|
|
2583
|
+
[SpanAttributes.SERVICE_VERSION]: this.serviceVersion,
|
|
2584
|
+
...options?.attributes
|
|
2585
|
+
}
|
|
2586
|
+
};
|
|
2587
|
+
if (parent) {
|
|
2588
|
+
spanOptions.parent = parent;
|
|
2589
|
+
}
|
|
2590
|
+
const span = this.spanManager.startSpan(spanOptions);
|
|
2591
|
+
return span;
|
|
2592
|
+
}
|
|
2593
|
+
/**
|
|
2594
|
+
* End a span
|
|
2595
|
+
*/
|
|
2596
|
+
endSpan(span, error) {
|
|
2597
|
+
if (!span) return;
|
|
2598
|
+
if (error) {
|
|
2599
|
+
this.spanManager.recordException(span, error);
|
|
2600
|
+
}
|
|
2601
|
+
this.spanManager.endSpan(span, error ? "error" : "ok");
|
|
2602
|
+
}
|
|
2603
|
+
/**
|
|
2604
|
+
* Run a function with automatic span creation
|
|
2605
|
+
*/
|
|
2606
|
+
async trace(name, fn, options) {
|
|
2607
|
+
const span = this.startSpan(name, options);
|
|
2608
|
+
if (!span) {
|
|
2609
|
+
return fn(null);
|
|
2610
|
+
}
|
|
2611
|
+
try {
|
|
2612
|
+
const result = await this.contextManager.run(span.traceContext, () => fn(span));
|
|
2613
|
+
this.endSpan(span);
|
|
2614
|
+
return result;
|
|
2615
|
+
} catch (error) {
|
|
2616
|
+
this.endSpan(span, error);
|
|
2617
|
+
throw error;
|
|
2618
|
+
}
|
|
2619
|
+
}
|
|
2620
|
+
/**
|
|
2621
|
+
* Add attribute to current span
|
|
2622
|
+
*/
|
|
2623
|
+
setAttribute(key, value) {
|
|
2624
|
+
const ctx = this.contextManager.current();
|
|
2625
|
+
if (!ctx) return;
|
|
2626
|
+
const span = this.spanManager.getSpan(ctx.spanId);
|
|
2627
|
+
if (span) {
|
|
2628
|
+
this.spanManager.setAttribute(span, key, value);
|
|
2629
|
+
}
|
|
2630
|
+
}
|
|
2631
|
+
/**
|
|
2632
|
+
* Add event to current span
|
|
2633
|
+
*/
|
|
2634
|
+
addEvent(name, attributes) {
|
|
2635
|
+
const ctx = this.contextManager.current();
|
|
2636
|
+
if (!ctx) return;
|
|
2637
|
+
const span = this.spanManager.getSpan(ctx.spanId);
|
|
2638
|
+
if (span) {
|
|
2639
|
+
this.spanManager.addEvent(span, name, attributes);
|
|
2640
|
+
}
|
|
2641
|
+
}
|
|
2642
|
+
/**
|
|
2643
|
+
* Get current trace context
|
|
2644
|
+
*/
|
|
2645
|
+
currentContext() {
|
|
2646
|
+
return this.contextManager.current();
|
|
2647
|
+
}
|
|
2648
|
+
/**
|
|
2649
|
+
* Get context manager for advanced use
|
|
2650
|
+
*/
|
|
2651
|
+
getContextManager() {
|
|
2652
|
+
return this.contextManager;
|
|
2653
|
+
}
|
|
2654
|
+
/**
|
|
2655
|
+
* Extract trace context from incoming request
|
|
2656
|
+
*/
|
|
2657
|
+
extract(headers) {
|
|
2658
|
+
return this.contextManager.fromHeaders(headers);
|
|
2659
|
+
}
|
|
2660
|
+
/**
|
|
2661
|
+
* Inject trace context into outgoing request headers
|
|
2662
|
+
*/
|
|
2663
|
+
inject(ctx) {
|
|
2664
|
+
const context = ctx ?? this.contextManager.current();
|
|
2665
|
+
if (!context) return {};
|
|
2666
|
+
return this.contextManager.toHeaders(context);
|
|
2667
|
+
}
|
|
2668
|
+
/**
|
|
2669
|
+
* Flush completed spans to exporter
|
|
2670
|
+
*/
|
|
2671
|
+
async flush() {
|
|
2672
|
+
const spans = this.spanManager.flush();
|
|
2673
|
+
if (spans.length > 0) {
|
|
2674
|
+
try {
|
|
2675
|
+
await this.exporter.export(spans);
|
|
2676
|
+
} catch (error) {
|
|
2677
|
+
this.logger.error("Failed to export spans", error);
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2680
|
+
}
|
|
2681
|
+
/**
|
|
2682
|
+
* Shutdown tracer
|
|
2683
|
+
*/
|
|
2684
|
+
async shutdown() {
|
|
2685
|
+
if (this.flushTimer) {
|
|
2686
|
+
clearInterval(this.flushTimer);
|
|
2687
|
+
this.flushTimer = null;
|
|
2688
|
+
}
|
|
2689
|
+
await this.flush();
|
|
2690
|
+
await this.exporter.shutdown();
|
|
2691
|
+
this.spanManager.clear();
|
|
2692
|
+
this.contextManager.clear();
|
|
2693
|
+
}
|
|
2694
|
+
};
|
|
2695
|
+
function createTracer(options) {
|
|
2696
|
+
return new Tracer(options);
|
|
2697
|
+
}
|
|
2698
|
+
var globalTracer = null;
|
|
2699
|
+
function getGlobalTracer() {
|
|
2700
|
+
return globalTracer;
|
|
2701
|
+
}
|
|
2702
|
+
function setGlobalTracer(tracer) {
|
|
2703
|
+
globalTracer = tracer;
|
|
2704
|
+
}
|
|
2705
|
+
function resetGlobalTracer() {
|
|
2706
|
+
globalTracer = null;
|
|
2707
|
+
}
|
|
2708
|
+
function createTracingMiddleware(tracer) {
|
|
2709
|
+
return async (request, next) => {
|
|
2710
|
+
const parentCtx = tracer.extract(request.headers);
|
|
2711
|
+
const url = new URL(request.url);
|
|
2712
|
+
const spanOpts = {
|
|
2713
|
+
kind: "server",
|
|
2714
|
+
attributes: {
|
|
2715
|
+
[SpanAttributes.HTTP_METHOD]: request.method,
|
|
2716
|
+
[SpanAttributes.HTTP_URL]: request.url
|
|
2717
|
+
}
|
|
2718
|
+
};
|
|
2719
|
+
if (parentCtx) {
|
|
2720
|
+
spanOpts.parent = parentCtx;
|
|
2721
|
+
}
|
|
2722
|
+
const span = tracer.startSpan(`${request.method} ${url.pathname}`, spanOpts);
|
|
2723
|
+
if (!span) {
|
|
2724
|
+
return next();
|
|
2725
|
+
}
|
|
2726
|
+
try {
|
|
2727
|
+
const response = await tracer.getContextManager().run(span.traceContext, next);
|
|
2728
|
+
tracer.endSpan(span);
|
|
2729
|
+
span.attributes[SpanAttributes.HTTP_STATUS_CODE] = response.status;
|
|
2730
|
+
return response;
|
|
2731
|
+
} catch (error) {
|
|
2732
|
+
tracer.endSpan(span, error);
|
|
2733
|
+
throw error;
|
|
2734
|
+
}
|
|
2735
|
+
};
|
|
2736
|
+
}
|
|
2737
|
+
function createRpcTracing(tracer) {
|
|
2738
|
+
return {
|
|
2739
|
+
/**
|
|
2740
|
+
* Trace an outgoing RPC call
|
|
2741
|
+
*/
|
|
2742
|
+
async traceCall(service, method, fn) {
|
|
2743
|
+
return tracer.trace(
|
|
2744
|
+
`rpc.${service}.${method}`,
|
|
2745
|
+
fn,
|
|
2746
|
+
{
|
|
2747
|
+
kind: "client",
|
|
2748
|
+
attributes: {
|
|
2749
|
+
[SpanAttributes.RPC_SYSTEM]: "pars",
|
|
2750
|
+
[SpanAttributes.RPC_SERVICE]: service,
|
|
2751
|
+
[SpanAttributes.RPC_METHOD]: method
|
|
2752
|
+
}
|
|
2753
|
+
}
|
|
2754
|
+
);
|
|
2755
|
+
},
|
|
2756
|
+
/**
|
|
2757
|
+
* Trace an incoming RPC request
|
|
2758
|
+
*/
|
|
2759
|
+
async traceHandler(service, method, fn, parentCtx) {
|
|
2760
|
+
const handlerOpts = {
|
|
2761
|
+
kind: "server",
|
|
2762
|
+
attributes: {
|
|
2763
|
+
[SpanAttributes.RPC_SYSTEM]: "pars",
|
|
2764
|
+
[SpanAttributes.RPC_SERVICE]: service,
|
|
2765
|
+
[SpanAttributes.RPC_METHOD]: method
|
|
2766
|
+
}
|
|
2767
|
+
};
|
|
2768
|
+
if (parentCtx) {
|
|
2769
|
+
handlerOpts.parent = parentCtx;
|
|
2770
|
+
}
|
|
2771
|
+
const span = tracer.startSpan(`rpc.${service}.${method}`, handlerOpts);
|
|
2772
|
+
if (!span) {
|
|
2773
|
+
return fn();
|
|
2774
|
+
}
|
|
2775
|
+
try {
|
|
2776
|
+
const result = await tracer.getContextManager().run(span.traceContext, fn);
|
|
2777
|
+
tracer.endSpan(span);
|
|
2778
|
+
return result;
|
|
2779
|
+
} catch (error) {
|
|
2780
|
+
tracer.endSpan(span, error);
|
|
2781
|
+
throw error;
|
|
2782
|
+
}
|
|
2783
|
+
}
|
|
2784
|
+
};
|
|
2785
|
+
}
|
|
2786
|
+
|
|
2787
|
+
// src/client.ts
|
|
2788
|
+
var ServiceClientImpl = class {
|
|
2789
|
+
name;
|
|
2790
|
+
rpcClient;
|
|
2791
|
+
eventTransport;
|
|
2792
|
+
config;
|
|
2793
|
+
tracer;
|
|
2794
|
+
constructor(definition, rpcTransport, eventTransport, config, _logger) {
|
|
2795
|
+
this.name = definition.name;
|
|
2796
|
+
this.config = mergeConfig(config);
|
|
2797
|
+
this.tracer = getGlobalTracer();
|
|
2798
|
+
this.rpcClient = new RpcClient({
|
|
2799
|
+
service: definition.name,
|
|
2800
|
+
transport: rpcTransport,
|
|
2801
|
+
config: this.config
|
|
2802
|
+
});
|
|
2803
|
+
this.eventTransport = eventTransport;
|
|
2804
|
+
}
|
|
2805
|
+
/**
|
|
2806
|
+
* Execute a query
|
|
2807
|
+
*/
|
|
2808
|
+
async query(method, input) {
|
|
2809
|
+
const methodName = String(method);
|
|
2810
|
+
const traceContext = this.tracer?.currentContext();
|
|
2811
|
+
if (this.tracer && traceContext) {
|
|
2812
|
+
return this.tracer.trace(
|
|
2813
|
+
`rpc.${this.name}.${methodName}`,
|
|
2814
|
+
async () => {
|
|
2815
|
+
return this.rpcClient.query(methodName, input, {
|
|
2816
|
+
traceContext
|
|
2817
|
+
});
|
|
2818
|
+
},
|
|
2819
|
+
{ kind: "client" }
|
|
2820
|
+
);
|
|
2821
|
+
}
|
|
2822
|
+
return this.rpcClient.query(methodName, input);
|
|
2823
|
+
}
|
|
2824
|
+
/**
|
|
2825
|
+
* Execute a mutation
|
|
2826
|
+
*/
|
|
2827
|
+
async mutate(method, input) {
|
|
2828
|
+
const methodName = String(method);
|
|
2829
|
+
const traceContext = this.tracer?.currentContext();
|
|
2830
|
+
if (this.tracer && traceContext) {
|
|
2831
|
+
return this.tracer.trace(
|
|
2832
|
+
`rpc.${this.name}.${methodName}`,
|
|
2833
|
+
async () => {
|
|
2834
|
+
return this.rpcClient.mutate(methodName, input, {
|
|
2835
|
+
traceContext
|
|
2836
|
+
});
|
|
2837
|
+
},
|
|
2838
|
+
{ kind: "client" }
|
|
2839
|
+
);
|
|
2840
|
+
}
|
|
2841
|
+
return this.rpcClient.mutate(methodName, input);
|
|
2842
|
+
}
|
|
2843
|
+
/**
|
|
2844
|
+
* Emit an event
|
|
2845
|
+
*/
|
|
2846
|
+
async emit(eventType, data) {
|
|
2847
|
+
const type = String(eventType);
|
|
2848
|
+
const traceContext = this.tracer?.currentContext();
|
|
2849
|
+
const event = {
|
|
2850
|
+
specversion: "1.0",
|
|
2851
|
+
type,
|
|
2852
|
+
source: this.name,
|
|
2853
|
+
id: generateId3(),
|
|
2854
|
+
time: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2855
|
+
data
|
|
2856
|
+
};
|
|
2857
|
+
if (traceContext) {
|
|
2858
|
+
event.parstracecontext = `00-${traceContext.traceId}-${traceContext.spanId}-01`;
|
|
2859
|
+
}
|
|
2860
|
+
await this.eventTransport.emit(event);
|
|
2861
|
+
}
|
|
2862
|
+
/**
|
|
2863
|
+
* Subscribe to events
|
|
2864
|
+
*/
|
|
2865
|
+
on(eventType, handler, options) {
|
|
2866
|
+
return this.eventTransport.subscribe(eventType, handler, options);
|
|
2867
|
+
}
|
|
2868
|
+
/**
|
|
2869
|
+
* Get circuit breaker state
|
|
2870
|
+
*/
|
|
2871
|
+
getCircuitState() {
|
|
2872
|
+
return this.rpcClient.getCircuitState();
|
|
2873
|
+
}
|
|
2874
|
+
/**
|
|
2875
|
+
* Close the client
|
|
2876
|
+
*/
|
|
2877
|
+
async close() {
|
|
2878
|
+
await this.rpcClient.close();
|
|
2879
|
+
await this.eventTransport.close?.();
|
|
2880
|
+
}
|
|
2881
|
+
};
|
|
2882
|
+
function useService(serviceName, options = {}) {
|
|
2883
|
+
const mode = options.mode ?? "embedded";
|
|
2884
|
+
const config = options.config ?? {};
|
|
2885
|
+
let rpcTransport;
|
|
2886
|
+
let eventTransport;
|
|
2887
|
+
switch (mode) {
|
|
2888
|
+
case "embedded": {
|
|
2889
|
+
const registry = getEmbeddedRegistry();
|
|
2890
|
+
if (!registry.has(serviceName)) {
|
|
2891
|
+
throw new Error(
|
|
2892
|
+
`Service not found in embedded registry: ${serviceName}. Make sure the service is registered before using it.`
|
|
2893
|
+
);
|
|
2894
|
+
}
|
|
2895
|
+
rpcTransport = registry.createTransport(serviceName);
|
|
2896
|
+
const eventBus = getGlobalEventBus();
|
|
2897
|
+
const services = eventBus.getServices();
|
|
2898
|
+
if (services.includes(serviceName)) {
|
|
2899
|
+
eventTransport = createMemoryEventTransport();
|
|
2900
|
+
} else {
|
|
2901
|
+
eventTransport = createMemoryEventTransport();
|
|
2902
|
+
}
|
|
2903
|
+
break;
|
|
2904
|
+
}
|
|
2905
|
+
case "http": {
|
|
2906
|
+
if (!options.baseUrl) {
|
|
2907
|
+
throw new Error("baseUrl is required for HTTP mode");
|
|
2908
|
+
}
|
|
2909
|
+
rpcTransport = createHttpTransport({
|
|
2910
|
+
baseUrl: options.baseUrl
|
|
2911
|
+
});
|
|
2912
|
+
eventTransport = createMemoryEventTransport();
|
|
2913
|
+
break;
|
|
2914
|
+
}
|
|
2915
|
+
case "binding": {
|
|
2916
|
+
if (!options.binding) {
|
|
2917
|
+
throw new Error("binding is required for binding mode");
|
|
2918
|
+
}
|
|
2919
|
+
rpcTransport = createBindingTransport(serviceName, options.binding);
|
|
2920
|
+
eventTransport = createMemoryEventTransport();
|
|
2921
|
+
break;
|
|
2922
|
+
}
|
|
2923
|
+
default:
|
|
2924
|
+
throw new Error(`Unknown service client mode: ${mode}`);
|
|
2925
|
+
}
|
|
2926
|
+
if (options.rpcTransport) {
|
|
2927
|
+
rpcTransport = options.rpcTransport;
|
|
2928
|
+
}
|
|
2929
|
+
if (options.eventTransport) {
|
|
2930
|
+
eventTransport = options.eventTransport;
|
|
2931
|
+
}
|
|
2932
|
+
const definition = {
|
|
2933
|
+
name: serviceName,
|
|
2934
|
+
version: "1.x"
|
|
2935
|
+
};
|
|
2936
|
+
return new ServiceClientImpl(
|
|
2937
|
+
definition,
|
|
2938
|
+
rpcTransport,
|
|
2939
|
+
eventTransport,
|
|
2940
|
+
config
|
|
2941
|
+
);
|
|
2942
|
+
}
|
|
2943
|
+
function useTypedService(definition, options = {}) {
|
|
2944
|
+
return useService(definition.name, options);
|
|
2945
|
+
}
|
|
2946
|
+
function createBindingTransport(_serviceName, binding) {
|
|
2947
|
+
return {
|
|
2948
|
+
name: "binding",
|
|
2949
|
+
async call(request) {
|
|
2950
|
+
const response = await binding.fetch("http://internal/rpc", {
|
|
2951
|
+
method: "POST",
|
|
2952
|
+
headers: {
|
|
2953
|
+
"Content-Type": "application/json",
|
|
2954
|
+
"X-Request-ID": request.id,
|
|
2955
|
+
"X-Service": request.service,
|
|
2956
|
+
"X-Method": request.method
|
|
2957
|
+
},
|
|
2958
|
+
body: JSON.stringify(request)
|
|
2959
|
+
});
|
|
2960
|
+
return response.json();
|
|
2961
|
+
},
|
|
2962
|
+
async close() {
|
|
2963
|
+
}
|
|
2964
|
+
};
|
|
2965
|
+
}
|
|
2966
|
+
var ServiceRegistry = class {
|
|
2967
|
+
clients = /* @__PURE__ */ new Map();
|
|
2968
|
+
config;
|
|
2969
|
+
constructor(config) {
|
|
2970
|
+
this.config = config ?? {};
|
|
2971
|
+
}
|
|
2972
|
+
/**
|
|
2973
|
+
* Get or create a service client
|
|
2974
|
+
*/
|
|
2975
|
+
get(serviceName, options) {
|
|
2976
|
+
let client = this.clients.get(serviceName);
|
|
2977
|
+
if (!client) {
|
|
2978
|
+
client = useService(serviceName, {
|
|
2979
|
+
...options,
|
|
2980
|
+
config: { ...this.config, ...options?.config }
|
|
2981
|
+
});
|
|
2982
|
+
this.clients.set(serviceName, client);
|
|
2983
|
+
}
|
|
2984
|
+
return client;
|
|
2985
|
+
}
|
|
2986
|
+
/**
|
|
2987
|
+
* Close all clients
|
|
2988
|
+
*/
|
|
2989
|
+
async closeAll() {
|
|
2990
|
+
const closePromises = Array.from(this.clients.values()).map((client) => {
|
|
2991
|
+
if ("close" in client && typeof client.close === "function") {
|
|
2992
|
+
return client.close();
|
|
2993
|
+
}
|
|
2994
|
+
return Promise.resolve();
|
|
2995
|
+
});
|
|
2996
|
+
await Promise.all(closePromises);
|
|
2997
|
+
this.clients.clear();
|
|
2998
|
+
}
|
|
2999
|
+
};
|
|
3000
|
+
function createServiceRegistry(config) {
|
|
3001
|
+
return new ServiceRegistry(config);
|
|
3002
|
+
}
|
|
3003
|
+
|
|
3004
|
+
// src/rpc/server.ts
|
|
3005
|
+
import { createLogger as createLogger5 } from "@parsrun/core";
|
|
3006
|
+
var RpcServer = class {
|
|
3007
|
+
definition;
|
|
3008
|
+
handlers;
|
|
3009
|
+
logger;
|
|
3010
|
+
defaultTimeout;
|
|
3011
|
+
middleware;
|
|
3012
|
+
constructor(options) {
|
|
3013
|
+
this.definition = options.definition;
|
|
3014
|
+
this.handlers = options.handlers;
|
|
3015
|
+
this.logger = options.logger ?? createLogger5({ name: `rpc:${options.definition.name}` });
|
|
3016
|
+
this.defaultTimeout = options.defaultTimeout ?? 3e4;
|
|
3017
|
+
this.middleware = options.middleware ?? [];
|
|
3018
|
+
}
|
|
3019
|
+
/**
|
|
3020
|
+
* Handle an RPC request
|
|
3021
|
+
*/
|
|
3022
|
+
async handle(request) {
|
|
3023
|
+
const startTime = Date.now();
|
|
3024
|
+
const context = {
|
|
3025
|
+
requestId: request.id,
|
|
3026
|
+
service: request.service,
|
|
3027
|
+
method: request.method,
|
|
3028
|
+
type: request.type,
|
|
3029
|
+
metadata: request.metadata ?? {},
|
|
3030
|
+
logger: this.logger.child({ requestId: request.id, method: request.method })
|
|
3031
|
+
};
|
|
3032
|
+
if (request.traceContext) {
|
|
3033
|
+
context.traceContext = request.traceContext;
|
|
3034
|
+
}
|
|
3035
|
+
try {
|
|
3036
|
+
if (request.version && !satisfiesVersion(this.definition.version, request.version)) {
|
|
3037
|
+
throw new VersionMismatchError(
|
|
3038
|
+
this.definition.name,
|
|
3039
|
+
request.version,
|
|
3040
|
+
this.definition.version
|
|
3041
|
+
);
|
|
3042
|
+
}
|
|
3043
|
+
const handler = this.getHandler(request.method, request.type);
|
|
3044
|
+
if (!handler) {
|
|
3045
|
+
throw new MethodNotFoundError(this.definition.name, request.method);
|
|
3046
|
+
}
|
|
3047
|
+
const deprecation = isMethodDeprecated(this.definition, request.method, request.type);
|
|
3048
|
+
if (deprecation.deprecated) {
|
|
3049
|
+
context.logger.warn(`Method ${request.method} is deprecated`, {
|
|
3050
|
+
since: deprecation.since,
|
|
3051
|
+
replacement: deprecation.replacement
|
|
3052
|
+
});
|
|
3053
|
+
}
|
|
3054
|
+
const chain = this.buildMiddlewareChain(request, context, handler);
|
|
3055
|
+
const timeout = getMethodTimeout(
|
|
3056
|
+
this.definition,
|
|
3057
|
+
request.method,
|
|
3058
|
+
request.type,
|
|
3059
|
+
this.defaultTimeout
|
|
3060
|
+
);
|
|
3061
|
+
const output = await Promise.race([
|
|
3062
|
+
chain(),
|
|
3063
|
+
new Promise(
|
|
3064
|
+
(_, reject) => setTimeout(() => reject(new Error("Handler timeout")), timeout)
|
|
3065
|
+
)
|
|
3066
|
+
]);
|
|
3067
|
+
const duration = Date.now() - startTime;
|
|
3068
|
+
context.logger.info(`${request.type} ${request.method} completed`, { durationMs: duration });
|
|
3069
|
+
const successResponse = {
|
|
3070
|
+
id: request.id,
|
|
3071
|
+
success: true,
|
|
3072
|
+
version: this.definition.version,
|
|
3073
|
+
output
|
|
3074
|
+
};
|
|
3075
|
+
if (request.traceContext) {
|
|
3076
|
+
successResponse.traceContext = request.traceContext;
|
|
3077
|
+
}
|
|
3078
|
+
return successResponse;
|
|
3079
|
+
} catch (error) {
|
|
3080
|
+
const duration = Date.now() - startTime;
|
|
3081
|
+
const rpcError = toRpcError(error);
|
|
3082
|
+
context.logger.error(`${request.type} ${request.method} failed`, error, {
|
|
3083
|
+
durationMs: duration,
|
|
3084
|
+
errorCode: rpcError.code
|
|
3085
|
+
});
|
|
3086
|
+
const errorData = {
|
|
3087
|
+
code: rpcError.code,
|
|
3088
|
+
message: rpcError.message,
|
|
3089
|
+
retryable: rpcError.retryable
|
|
3090
|
+
};
|
|
3091
|
+
if (rpcError.details) {
|
|
3092
|
+
errorData.details = rpcError.details;
|
|
3093
|
+
}
|
|
3094
|
+
if (rpcError.retryAfter !== void 0) {
|
|
3095
|
+
errorData.retryAfter = rpcError.retryAfter;
|
|
3096
|
+
}
|
|
3097
|
+
const errorResponse = {
|
|
3098
|
+
id: request.id,
|
|
3099
|
+
success: false,
|
|
3100
|
+
version: this.definition.version,
|
|
3101
|
+
error: errorData
|
|
3102
|
+
};
|
|
3103
|
+
if (request.traceContext) {
|
|
3104
|
+
errorResponse.traceContext = request.traceContext;
|
|
3105
|
+
}
|
|
3106
|
+
return errorResponse;
|
|
3107
|
+
}
|
|
3108
|
+
}
|
|
3109
|
+
/**
|
|
3110
|
+
* Get handler for a method
|
|
3111
|
+
*/
|
|
3112
|
+
getHandler(method, type) {
|
|
3113
|
+
const handlers = type === "query" ? this.handlers.queries : this.handlers.mutations;
|
|
3114
|
+
return handlers?.[method];
|
|
3115
|
+
}
|
|
3116
|
+
/**
|
|
3117
|
+
* Build middleware chain
|
|
3118
|
+
*/
|
|
3119
|
+
buildMiddlewareChain(request, context, handler) {
|
|
3120
|
+
let index = -1;
|
|
3121
|
+
const dispatch = async (i) => {
|
|
3122
|
+
if (i <= index) {
|
|
3123
|
+
throw new Error("next() called multiple times");
|
|
3124
|
+
}
|
|
3125
|
+
index = i;
|
|
3126
|
+
if (i < this.middleware.length) {
|
|
3127
|
+
const mw = this.middleware[i];
|
|
3128
|
+
return mw(request, context, () => dispatch(i + 1));
|
|
3129
|
+
}
|
|
3130
|
+
return handler(request.input, context);
|
|
3131
|
+
};
|
|
3132
|
+
return () => dispatch(0);
|
|
3133
|
+
}
|
|
3134
|
+
/**
|
|
3135
|
+
* Get service definition
|
|
3136
|
+
*/
|
|
3137
|
+
getDefinition() {
|
|
3138
|
+
return this.definition;
|
|
3139
|
+
}
|
|
3140
|
+
/**
|
|
3141
|
+
* Get registered methods
|
|
3142
|
+
*/
|
|
3143
|
+
getMethods() {
|
|
3144
|
+
return {
|
|
3145
|
+
queries: Object.keys(this.handlers.queries ?? {}),
|
|
3146
|
+
mutations: Object.keys(this.handlers.mutations ?? {})
|
|
3147
|
+
};
|
|
3148
|
+
}
|
|
3149
|
+
};
|
|
3150
|
+
function createRpcServer(options) {
|
|
3151
|
+
return new RpcServer(options);
|
|
3152
|
+
}
|
|
3153
|
+
function loggingMiddleware() {
|
|
3154
|
+
return async (request, context, next) => {
|
|
3155
|
+
context.logger.debug(`Handling ${request.type} ${request.method}`, {
|
|
3156
|
+
inputKeys: Object.keys(request.input)
|
|
3157
|
+
});
|
|
3158
|
+
const result = await next();
|
|
3159
|
+
context.logger.debug(`Completed ${request.type} ${request.method}`);
|
|
3160
|
+
return result;
|
|
3161
|
+
};
|
|
3162
|
+
}
|
|
3163
|
+
function validationMiddleware(validators) {
|
|
3164
|
+
return async (request, _context, next) => {
|
|
3165
|
+
const validator = validators[request.method];
|
|
3166
|
+
if (validator) {
|
|
3167
|
+
request.input = validator(request.input);
|
|
3168
|
+
}
|
|
3169
|
+
return next();
|
|
3170
|
+
};
|
|
3171
|
+
}
|
|
3172
|
+
function tenantMiddleware() {
|
|
3173
|
+
return async (_request, context, next) => {
|
|
3174
|
+
const tenantId = context.metadata["tenantId"];
|
|
3175
|
+
if (tenantId) {
|
|
3176
|
+
context.logger = context.logger.child({ tenantId });
|
|
3177
|
+
}
|
|
3178
|
+
return next();
|
|
3179
|
+
};
|
|
3180
|
+
}
|
|
3181
|
+
|
|
3182
|
+
// src/events/emitter.ts
|
|
3183
|
+
import { createLogger as createLogger6 } from "@parsrun/core";
|
|
3184
|
+
var EventEmitter = class {
|
|
3185
|
+
service;
|
|
3186
|
+
definition;
|
|
3187
|
+
transport;
|
|
3188
|
+
logger;
|
|
3189
|
+
defaultTenantId;
|
|
3190
|
+
validateEvents;
|
|
3191
|
+
constructor(options) {
|
|
3192
|
+
this.service = options.service;
|
|
3193
|
+
if (options.definition) {
|
|
3194
|
+
this.definition = options.definition;
|
|
3195
|
+
}
|
|
3196
|
+
this.transport = options.transport;
|
|
3197
|
+
this.logger = options.logger ?? createLogger6({ name: `events:${options.service}` });
|
|
3198
|
+
if (options.defaultTenantId) {
|
|
3199
|
+
this.defaultTenantId = options.defaultTenantId;
|
|
3200
|
+
}
|
|
3201
|
+
this.validateEvents = options.validateEvents ?? true;
|
|
3202
|
+
}
|
|
3203
|
+
/**
|
|
3204
|
+
* Emit an event
|
|
3205
|
+
*/
|
|
3206
|
+
async emit(type, data, options) {
|
|
3207
|
+
if (this.validateEvents && this.definition?.events?.emits) {
|
|
3208
|
+
const emits = this.definition.events.emits;
|
|
3209
|
+
if (!(type in emits)) {
|
|
3210
|
+
this.logger.warn(`Event type not declared in service definition: ${type}`);
|
|
3211
|
+
}
|
|
3212
|
+
}
|
|
3213
|
+
let delivery;
|
|
3214
|
+
if (this.definition?.events?.emits?.[type]) {
|
|
3215
|
+
delivery = this.definition.events.emits[type].delivery;
|
|
3216
|
+
}
|
|
3217
|
+
const eventOptions = {
|
|
3218
|
+
type,
|
|
3219
|
+
source: this.service,
|
|
3220
|
+
data
|
|
3221
|
+
};
|
|
3222
|
+
if (options?.eventId) eventOptions.id = options.eventId;
|
|
3223
|
+
if (options?.subject) eventOptions.subject = options.subject;
|
|
3224
|
+
const tenantId = options?.tenantId ?? this.defaultTenantId;
|
|
3225
|
+
if (tenantId) eventOptions.tenantId = tenantId;
|
|
3226
|
+
if (options?.requestId) eventOptions.requestId = options.requestId;
|
|
3227
|
+
if (options?.traceContext) eventOptions.traceContext = options.traceContext;
|
|
3228
|
+
const eventDelivery = options?.delivery ?? delivery;
|
|
3229
|
+
if (eventDelivery) eventOptions.delivery = eventDelivery;
|
|
3230
|
+
const event = createEvent(eventOptions);
|
|
3231
|
+
try {
|
|
3232
|
+
await this.transport.emit(event);
|
|
3233
|
+
this.logger.debug(`Event emitted: ${type}`, {
|
|
3234
|
+
eventId: event.id,
|
|
3235
|
+
tenantId: event.parstenantid
|
|
3236
|
+
});
|
|
3237
|
+
return event.id;
|
|
3238
|
+
} catch (error) {
|
|
3239
|
+
this.logger.error(`Failed to emit event: ${type}`, error, {
|
|
3240
|
+
eventId: event.id
|
|
3241
|
+
});
|
|
3242
|
+
throw error;
|
|
3243
|
+
}
|
|
3244
|
+
}
|
|
3245
|
+
/**
|
|
3246
|
+
* Emit multiple events
|
|
3247
|
+
*/
|
|
3248
|
+
async emitBatch(events) {
|
|
3249
|
+
const results = [];
|
|
3250
|
+
for (const { type, data, options } of events) {
|
|
3251
|
+
const eventId = await this.emit(type, data, options);
|
|
3252
|
+
results.push(eventId);
|
|
3253
|
+
}
|
|
3254
|
+
return results;
|
|
3255
|
+
}
|
|
3256
|
+
/**
|
|
3257
|
+
* Create a scoped emitter with preset options
|
|
3258
|
+
*/
|
|
3259
|
+
scoped(options) {
|
|
3260
|
+
return new ScopedEmitter(this, options);
|
|
3261
|
+
}
|
|
3262
|
+
/**
|
|
3263
|
+
* Get service name
|
|
3264
|
+
*/
|
|
3265
|
+
get serviceName() {
|
|
3266
|
+
return this.service;
|
|
3267
|
+
}
|
|
3268
|
+
};
|
|
3269
|
+
var ScopedEmitter = class {
|
|
3270
|
+
emitter;
|
|
3271
|
+
defaultOptions;
|
|
3272
|
+
constructor(emitter, defaultOptions) {
|
|
3273
|
+
this.emitter = emitter;
|
|
3274
|
+
this.defaultOptions = defaultOptions;
|
|
3275
|
+
}
|
|
3276
|
+
async emit(type, data, options) {
|
|
3277
|
+
return this.emitter.emit(type, data, {
|
|
3278
|
+
...this.defaultOptions,
|
|
3279
|
+
...options
|
|
3280
|
+
});
|
|
3281
|
+
}
|
|
3282
|
+
};
|
|
3283
|
+
function createEventEmitter(options) {
|
|
3284
|
+
return new EventEmitter(options);
|
|
3285
|
+
}
|
|
3286
|
+
function createTypedEmitter(definition, options) {
|
|
3287
|
+
const emitter = new EventEmitter({
|
|
3288
|
+
...options,
|
|
3289
|
+
service: definition.name,
|
|
3290
|
+
definition
|
|
3291
|
+
});
|
|
3292
|
+
return emitter;
|
|
3293
|
+
}
|
|
3294
|
+
|
|
3295
|
+
// src/events/dead-letter.ts
|
|
3296
|
+
import { createLogger as createLogger7, generateId as generateId4 } from "@parsrun/core";
|
|
3297
|
+
var DeadLetterQueue = class {
|
|
3298
|
+
entries = /* @__PURE__ */ new Map();
|
|
3299
|
+
resolvedOptions;
|
|
3300
|
+
logger;
|
|
3301
|
+
cleanupTimer = null;
|
|
3302
|
+
constructor(options = {}) {
|
|
3303
|
+
this.resolvedOptions = {
|
|
3304
|
+
maxSize: options.maxSize ?? 1e3,
|
|
3305
|
+
retentionMs: options.retentionMs ?? 30 * 24 * 60 * 60 * 1e3,
|
|
3306
|
+
// 30 days
|
|
3307
|
+
alertThreshold: options.alertThreshold ?? 10
|
|
3308
|
+
};
|
|
3309
|
+
if (options.onAdd) this.resolvedOptions.onAdd = options.onAdd;
|
|
3310
|
+
if (options.onThreshold) this.resolvedOptions.onThreshold = options.onThreshold;
|
|
3311
|
+
if (options.logger) this.resolvedOptions.logger = options.logger;
|
|
3312
|
+
this.logger = options.logger ?? createLogger7({ name: "dlq" });
|
|
3313
|
+
this.cleanupTimer = setInterval(() => this.cleanup(), 60 * 60 * 1e3);
|
|
3314
|
+
}
|
|
3315
|
+
/**
|
|
3316
|
+
* Add an entry to the DLQ
|
|
3317
|
+
*/
|
|
3318
|
+
async add(options) {
|
|
3319
|
+
const entry = {
|
|
3320
|
+
id: generateId4(),
|
|
3321
|
+
event: options.event,
|
|
3322
|
+
error: options.error,
|
|
3323
|
+
pattern: options.pattern,
|
|
3324
|
+
attempts: options.attempts,
|
|
3325
|
+
addedAt: /* @__PURE__ */ new Date()
|
|
3326
|
+
};
|
|
3327
|
+
if (options.metadata) {
|
|
3328
|
+
entry.metadata = options.metadata;
|
|
3329
|
+
}
|
|
3330
|
+
if (this.entries.size >= this.resolvedOptions.maxSize) {
|
|
3331
|
+
const oldest = this.getOldest();
|
|
3332
|
+
if (oldest) {
|
|
3333
|
+
this.entries.delete(oldest.id);
|
|
3334
|
+
this.logger.debug(`DLQ: Removed oldest entry to make room`, {
|
|
3335
|
+
removedId: oldest.id
|
|
3336
|
+
});
|
|
3337
|
+
}
|
|
3338
|
+
}
|
|
3339
|
+
this.entries.set(entry.id, entry);
|
|
3340
|
+
this.logger.warn(`DLQ: Entry added`, {
|
|
3341
|
+
id: entry.id,
|
|
3342
|
+
eventId: entry.event.id,
|
|
3343
|
+
eventType: entry.event.type,
|
|
3344
|
+
error: entry.error
|
|
3345
|
+
});
|
|
3346
|
+
this.resolvedOptions.onAdd?.(entry);
|
|
3347
|
+
if (this.entries.size >= this.resolvedOptions.alertThreshold) {
|
|
3348
|
+
this.resolvedOptions.onThreshold?.(this.entries.size);
|
|
3349
|
+
}
|
|
3350
|
+
return entry.id;
|
|
3351
|
+
}
|
|
3352
|
+
/**
|
|
3353
|
+
* Get an entry by ID
|
|
3354
|
+
*/
|
|
3355
|
+
get(id) {
|
|
3356
|
+
return this.entries.get(id);
|
|
3357
|
+
}
|
|
3358
|
+
/**
|
|
3359
|
+
* Get all entries
|
|
3360
|
+
*/
|
|
3361
|
+
getAll() {
|
|
3362
|
+
return Array.from(this.entries.values());
|
|
3363
|
+
}
|
|
3364
|
+
/**
|
|
3365
|
+
* Get entries by event type
|
|
3366
|
+
*/
|
|
3367
|
+
getByEventType(eventType) {
|
|
3368
|
+
return Array.from(this.entries.values()).filter(
|
|
3369
|
+
(e) => e.event.type === eventType
|
|
3370
|
+
);
|
|
3371
|
+
}
|
|
3372
|
+
/**
|
|
3373
|
+
* Get entries by pattern
|
|
3374
|
+
*/
|
|
3375
|
+
getByPattern(pattern) {
|
|
3376
|
+
return Array.from(this.entries.values()).filter(
|
|
3377
|
+
(e) => e.pattern === pattern
|
|
3378
|
+
);
|
|
3379
|
+
}
|
|
3380
|
+
/**
|
|
3381
|
+
* Remove an entry
|
|
3382
|
+
*/
|
|
3383
|
+
remove(id) {
|
|
3384
|
+
const deleted = this.entries.delete(id);
|
|
3385
|
+
if (deleted) {
|
|
3386
|
+
this.logger.debug(`DLQ: Entry removed`, { id });
|
|
3387
|
+
}
|
|
3388
|
+
return deleted;
|
|
3389
|
+
}
|
|
3390
|
+
/**
|
|
3391
|
+
* Retry an entry (remove from DLQ and return event)
|
|
3392
|
+
*/
|
|
3393
|
+
retry(id) {
|
|
3394
|
+
const entry = this.entries.get(id);
|
|
3395
|
+
if (!entry) return void 0;
|
|
3396
|
+
this.entries.delete(id);
|
|
3397
|
+
this.logger.info(`DLQ: Entry removed for retry`, {
|
|
3398
|
+
id,
|
|
3399
|
+
eventId: entry.event.id
|
|
3400
|
+
});
|
|
3401
|
+
return entry.event;
|
|
3402
|
+
}
|
|
3403
|
+
/**
|
|
3404
|
+
* Get count
|
|
3405
|
+
*/
|
|
3406
|
+
get size() {
|
|
3407
|
+
return this.entries.size;
|
|
3408
|
+
}
|
|
3409
|
+
/**
|
|
3410
|
+
* Clear all entries
|
|
3411
|
+
*/
|
|
3412
|
+
clear() {
|
|
3413
|
+
this.entries.clear();
|
|
3414
|
+
this.logger.info(`DLQ: Cleared all entries`);
|
|
3415
|
+
}
|
|
3416
|
+
/**
|
|
3417
|
+
* Cleanup expired entries
|
|
3418
|
+
*/
|
|
3419
|
+
cleanup() {
|
|
3420
|
+
const now = Date.now();
|
|
3421
|
+
let removed = 0;
|
|
3422
|
+
for (const [id, entry] of this.entries) {
|
|
3423
|
+
const age = now - entry.addedAt.getTime();
|
|
3424
|
+
if (age > this.resolvedOptions.retentionMs) {
|
|
3425
|
+
this.entries.delete(id);
|
|
3426
|
+
removed++;
|
|
3427
|
+
}
|
|
3428
|
+
}
|
|
3429
|
+
if (removed > 0) {
|
|
3430
|
+
this.logger.debug(`DLQ: Cleaned up ${removed} expired entries`);
|
|
3431
|
+
}
|
|
3432
|
+
}
|
|
3433
|
+
/**
|
|
3434
|
+
* Get oldest entry
|
|
3435
|
+
*/
|
|
3436
|
+
getOldest() {
|
|
3437
|
+
let oldest;
|
|
3438
|
+
for (const entry of this.entries.values()) {
|
|
3439
|
+
if (!oldest || entry.addedAt < oldest.addedAt) {
|
|
3440
|
+
oldest = entry;
|
|
3441
|
+
}
|
|
3442
|
+
}
|
|
3443
|
+
return oldest;
|
|
3444
|
+
}
|
|
3445
|
+
/**
|
|
3446
|
+
* Stop cleanup timer
|
|
3447
|
+
*/
|
|
3448
|
+
close() {
|
|
3449
|
+
if (this.cleanupTimer) {
|
|
3450
|
+
clearInterval(this.cleanupTimer);
|
|
3451
|
+
this.cleanupTimer = null;
|
|
3452
|
+
}
|
|
3453
|
+
}
|
|
3454
|
+
/**
|
|
3455
|
+
* Export entries for persistence
|
|
3456
|
+
*/
|
|
3457
|
+
export() {
|
|
3458
|
+
return Array.from(this.entries.values()).map((e) => ({
|
|
3459
|
+
...e,
|
|
3460
|
+
addedAt: e.addedAt
|
|
3461
|
+
}));
|
|
3462
|
+
}
|
|
3463
|
+
/**
|
|
3464
|
+
* Import entries from persistence
|
|
3465
|
+
*/
|
|
3466
|
+
import(entries) {
|
|
3467
|
+
for (const entry of entries) {
|
|
3468
|
+
this.entries.set(entry.id, {
|
|
3469
|
+
...entry,
|
|
3470
|
+
addedAt: new Date(entry.addedAt)
|
|
3471
|
+
});
|
|
3472
|
+
}
|
|
3473
|
+
this.logger.info(`DLQ: Imported ${entries.length} entries`);
|
|
3474
|
+
}
|
|
3475
|
+
};
|
|
3476
|
+
function createDeadLetterQueue(options) {
|
|
3477
|
+
return new DeadLetterQueue(options);
|
|
3478
|
+
}
|
|
3479
|
+
export {
|
|
3480
|
+
Bulkhead,
|
|
3481
|
+
BulkheadRejectedError,
|
|
3482
|
+
CircuitBreaker,
|
|
3483
|
+
CircuitOpenError,
|
|
3484
|
+
ConsoleExporter,
|
|
3485
|
+
DEFAULT_DEAD_LETTER_CONFIG,
|
|
3486
|
+
DEFAULT_EVENT_CONFIG,
|
|
3487
|
+
DEFAULT_RESILIENCE_CONFIG,
|
|
3488
|
+
DEFAULT_SERIALIZATION_CONFIG,
|
|
3489
|
+
DEFAULT_SERVICE_CONFIG,
|
|
3490
|
+
DEFAULT_TRACING_CONFIG,
|
|
3491
|
+
DEFAULT_VERSIONING_CONFIG,
|
|
3492
|
+
DeadLetterQueue,
|
|
3493
|
+
EmbeddedRegistry,
|
|
3494
|
+
EmbeddedTransport,
|
|
3495
|
+
EventEmitter,
|
|
3496
|
+
EventHandlerRegistry,
|
|
3497
|
+
GlobalEventBus,
|
|
3498
|
+
HttpTransport,
|
|
3499
|
+
MemoryEventTransport,
|
|
3500
|
+
MethodNotFoundError,
|
|
3501
|
+
OtlpExporter,
|
|
3502
|
+
RpcClient,
|
|
3503
|
+
RpcError,
|
|
3504
|
+
RpcServer,
|
|
3505
|
+
ScopedEmitter,
|
|
3506
|
+
SerializationError,
|
|
3507
|
+
ServiceNotFoundError,
|
|
3508
|
+
ServiceRegistry,
|
|
3509
|
+
SpanAttributes,
|
|
3510
|
+
SpanManager,
|
|
3511
|
+
TimeoutError,
|
|
3512
|
+
TimeoutExceededError,
|
|
3513
|
+
TraceContextManager,
|
|
3514
|
+
Tracer,
|
|
3515
|
+
TransportError,
|
|
3516
|
+
VersionMismatchError,
|
|
3517
|
+
createChildContext,
|
|
3518
|
+
createConsoleExporter,
|
|
3519
|
+
createDeadLetterQueue,
|
|
3520
|
+
createDevConfig,
|
|
3521
|
+
createEmbeddedTransport,
|
|
3522
|
+
createEvent,
|
|
3523
|
+
createEventEmitter,
|
|
3524
|
+
createEventHandlerRegistry,
|
|
3525
|
+
createHttpHandler,
|
|
3526
|
+
createHttpTransport,
|
|
3527
|
+
createMemoryEventTransport,
|
|
3528
|
+
createOtlpExporter,
|
|
3529
|
+
createProdConfig,
|
|
3530
|
+
createRetryWrapper,
|
|
3531
|
+
createRpcClient,
|
|
3532
|
+
createRpcServer,
|
|
3533
|
+
createRpcTracing,
|
|
3534
|
+
createSerializer,
|
|
3535
|
+
createServiceRegistry,
|
|
3536
|
+
createSpan,
|
|
3537
|
+
createTimeoutWrapper,
|
|
3538
|
+
createTraceContext,
|
|
3539
|
+
createTracer,
|
|
3540
|
+
createTracingMiddleware,
|
|
3541
|
+
createTypedEmitter,
|
|
3542
|
+
defineService,
|
|
3543
|
+
executeWithDeadline,
|
|
3544
|
+
executeWithRetry,
|
|
3545
|
+
executeWithTimeout,
|
|
3546
|
+
formatEventType,
|
|
3547
|
+
formatTraceparent2 as formatTraceparent,
|
|
3548
|
+
formatTracestate,
|
|
3549
|
+
fromCompactEvent,
|
|
3550
|
+
generateSpanId,
|
|
3551
|
+
generateTraceId,
|
|
3552
|
+
getEmbeddedRegistry,
|
|
3553
|
+
getGlobalEventBus,
|
|
3554
|
+
getGlobalTracer,
|
|
3555
|
+
getMethodTimeout,
|
|
3556
|
+
getSerializer,
|
|
3557
|
+
getServiceEvents,
|
|
3558
|
+
getServiceMethods,
|
|
3559
|
+
getSpanDuration,
|
|
3560
|
+
isMethodDeprecated,
|
|
3561
|
+
isSpanCompleted,
|
|
3562
|
+
jsonSerializer,
|
|
3563
|
+
loggingMiddleware,
|
|
3564
|
+
matchEventType,
|
|
3565
|
+
mergeConfig,
|
|
3566
|
+
msgpackSerializer,
|
|
3567
|
+
parseEventType,
|
|
3568
|
+
parseTraceparent,
|
|
3569
|
+
parseTracestate,
|
|
3570
|
+
raceWithTimeout,
|
|
3571
|
+
resetGlobalTracer,
|
|
3572
|
+
satisfiesVersion,
|
|
3573
|
+
setGlobalTracer,
|
|
3574
|
+
shouldSample,
|
|
3575
|
+
spanToLogObject,
|
|
3576
|
+
tenantMiddleware,
|
|
3577
|
+
toCloudEvent,
|
|
3578
|
+
toCompactEvent,
|
|
3579
|
+
toRpcError,
|
|
3580
|
+
useService,
|
|
3581
|
+
useTypedService,
|
|
3582
|
+
validateCompactEvent,
|
|
3583
|
+
validateConfig,
|
|
3584
|
+
validateEvent,
|
|
3585
|
+
validationMiddleware,
|
|
3586
|
+
withRetry,
|
|
3587
|
+
withTimeout
|
|
3588
|
+
};
|
|
3589
|
+
//# sourceMappingURL=index.js.map
|