@parsrun/service 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +375 -0
- package/dist/client.d.ts +55 -0
- package/dist/client.js +1474 -0
- package/dist/client.js.map +1 -0
- package/dist/define.d.ts +82 -0
- package/dist/define.js +120 -0
- package/dist/define.js.map +1 -0
- package/dist/events/index.d.ts +285 -0
- package/dist/events/index.js +853 -0
- package/dist/events/index.js.map +1 -0
- package/dist/handler-CmiDUWZv.d.ts +204 -0
- package/dist/index-CVOAoJjZ.d.ts +268 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.js +3589 -0
- package/dist/index.js.map +1 -0
- package/dist/resilience/index.d.ts +197 -0
- package/dist/resilience/index.js +387 -0
- package/dist/resilience/index.js.map +1 -0
- package/dist/rpc/index.d.ts +5 -0
- package/dist/rpc/index.js +1175 -0
- package/dist/rpc/index.js.map +1 -0
- package/dist/serialization/index.d.ts +37 -0
- package/dist/serialization/index.js +320 -0
- package/dist/serialization/index.js.map +1 -0
- package/dist/server-DFE8n2Sx.d.ts +106 -0
- package/dist/tracing/index.d.ts +406 -0
- package/dist/tracing/index.js +820 -0
- package/dist/tracing/index.js.map +1 -0
- package/dist/transports/cloudflare/index.d.ts +237 -0
- package/dist/transports/cloudflare/index.js +746 -0
- package/dist/transports/cloudflare/index.js.map +1 -0
- package/dist/types-n4LLSPQU.d.ts +473 -0
- package/package.json +91 -0
|
@@ -0,0 +1,746 @@
|
|
|
1
|
+
// src/transports/cloudflare/binding.ts
|
|
2
|
+
import { createLogger } from "@parsrun/core";
|
|
3
|
+
|
|
4
|
+
// src/serialization/index.ts
|
|
5
|
+
var jsonSerializer = {
|
|
6
|
+
encode(data) {
|
|
7
|
+
return JSON.stringify(data);
|
|
8
|
+
},
|
|
9
|
+
decode(raw) {
|
|
10
|
+
if (raw instanceof ArrayBuffer) {
|
|
11
|
+
const decoder = new TextDecoder();
|
|
12
|
+
return JSON.parse(decoder.decode(raw));
|
|
13
|
+
}
|
|
14
|
+
return JSON.parse(raw);
|
|
15
|
+
},
|
|
16
|
+
contentType: "application/json"
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// src/rpc/errors.ts
|
|
20
|
+
import { ParsError } from "@parsrun/core";
|
|
21
|
+
var RpcError = class extends ParsError {
|
|
22
|
+
retryable;
|
|
23
|
+
retryAfter;
|
|
24
|
+
constructor(message, code, statusCode = 500, options) {
|
|
25
|
+
super(message, code, statusCode, options?.details);
|
|
26
|
+
this.name = "RpcError";
|
|
27
|
+
this.retryable = options?.retryable ?? false;
|
|
28
|
+
if (options?.retryAfter !== void 0) {
|
|
29
|
+
this.retryAfter = options.retryAfter;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
var TransportError = class extends RpcError {
|
|
34
|
+
constructor(message, cause) {
|
|
35
|
+
const options = {
|
|
36
|
+
retryable: true
|
|
37
|
+
};
|
|
38
|
+
if (cause) {
|
|
39
|
+
options.details = { cause: cause.message };
|
|
40
|
+
}
|
|
41
|
+
super(message, "TRANSPORT_ERROR", 502, options);
|
|
42
|
+
this.name = "TransportError";
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
var SerializationError = class extends RpcError {
|
|
46
|
+
constructor(message, cause) {
|
|
47
|
+
const options = {
|
|
48
|
+
retryable: false
|
|
49
|
+
};
|
|
50
|
+
if (cause) {
|
|
51
|
+
options.details = { cause: cause.message };
|
|
52
|
+
}
|
|
53
|
+
super(message, "SERIALIZATION_ERROR", 400, options);
|
|
54
|
+
this.name = "SerializationError";
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// src/rpc/transports/http.ts
|
|
59
|
+
function parseTraceparent(header) {
|
|
60
|
+
const parts = header.split("-");
|
|
61
|
+
if (parts.length !== 4) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
const [version, traceId, spanId, flags] = parts;
|
|
65
|
+
if (version !== "00" || !traceId || !spanId || !flags) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
if (traceId.length !== 32 || spanId.length !== 16 || flags.length !== 2) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
traceId,
|
|
73
|
+
spanId,
|
|
74
|
+
traceFlags: parseInt(flags, 16)
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// src/transports/cloudflare/binding.ts
|
|
79
|
+
var ServiceBindingTransport = class {
|
|
80
|
+
name = "service-binding";
|
|
81
|
+
serviceName;
|
|
82
|
+
binding;
|
|
83
|
+
serializer;
|
|
84
|
+
logger;
|
|
85
|
+
constructor(options) {
|
|
86
|
+
this.serviceName = options.serviceName;
|
|
87
|
+
this.binding = options.binding;
|
|
88
|
+
this.serializer = options.serializer ?? jsonSerializer;
|
|
89
|
+
this.logger = options.logger ?? createLogger({ name: `binding:${options.serviceName}` });
|
|
90
|
+
}
|
|
91
|
+
async call(request) {
|
|
92
|
+
const startTime = Date.now();
|
|
93
|
+
try {
|
|
94
|
+
let body;
|
|
95
|
+
try {
|
|
96
|
+
body = this.serializer.encode(request);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
throw new SerializationError(
|
|
99
|
+
"Failed to serialize request",
|
|
100
|
+
error instanceof Error ? error : void 0
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
const headers = {
|
|
104
|
+
"Content-Type": this.serializer.contentType,
|
|
105
|
+
Accept: this.serializer.contentType,
|
|
106
|
+
"X-Request-ID": request.id,
|
|
107
|
+
"X-Service": request.service,
|
|
108
|
+
"X-Method": request.method,
|
|
109
|
+
"X-Method-Type": request.type
|
|
110
|
+
};
|
|
111
|
+
if (request.version) {
|
|
112
|
+
headers["X-Service-Version"] = request.version;
|
|
113
|
+
}
|
|
114
|
+
if (request.traceContext) {
|
|
115
|
+
headers["traceparent"] = formatTraceparent(request.traceContext);
|
|
116
|
+
if (request.traceContext.traceState) {
|
|
117
|
+
headers["tracestate"] = request.traceContext.traceState;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (request.metadata?.tenantId) {
|
|
121
|
+
headers["X-Tenant-ID"] = String(request.metadata.tenantId);
|
|
122
|
+
}
|
|
123
|
+
const response = await this.binding.fetch("http://internal/rpc", {
|
|
124
|
+
method: "POST",
|
|
125
|
+
headers,
|
|
126
|
+
body: typeof body === "string" ? body : body
|
|
127
|
+
});
|
|
128
|
+
let responseData;
|
|
129
|
+
try {
|
|
130
|
+
const contentType = response.headers.get("Content-Type") ?? "";
|
|
131
|
+
if (contentType.includes("msgpack")) {
|
|
132
|
+
const buffer = await response.arrayBuffer();
|
|
133
|
+
responseData = this.serializer.decode(buffer);
|
|
134
|
+
} else {
|
|
135
|
+
const text = await response.text();
|
|
136
|
+
responseData = this.serializer.decode(text);
|
|
137
|
+
}
|
|
138
|
+
} catch (error) {
|
|
139
|
+
throw new SerializationError(
|
|
140
|
+
"Failed to deserialize response",
|
|
141
|
+
error instanceof Error ? error : void 0
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
const duration = Date.now() - startTime;
|
|
145
|
+
this.logger.debug(`RPC call completed`, {
|
|
146
|
+
service: this.serviceName,
|
|
147
|
+
method: request.method,
|
|
148
|
+
durationMs: duration,
|
|
149
|
+
success: responseData.success
|
|
150
|
+
});
|
|
151
|
+
return responseData;
|
|
152
|
+
} catch (error) {
|
|
153
|
+
const duration = Date.now() - startTime;
|
|
154
|
+
if (error instanceof SerializationError) {
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
157
|
+
this.logger.error(`RPC call failed`, error, {
|
|
158
|
+
service: this.serviceName,
|
|
159
|
+
method: request.method,
|
|
160
|
+
durationMs: duration
|
|
161
|
+
});
|
|
162
|
+
throw new TransportError(
|
|
163
|
+
`Service binding call failed: ${error.message}`,
|
|
164
|
+
error
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
async close() {
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
function createServiceBindingTransport(options) {
|
|
172
|
+
return new ServiceBindingTransport(options);
|
|
173
|
+
}
|
|
174
|
+
function createServiceBindingHandler(server, options) {
|
|
175
|
+
const serializer = options?.serializer ?? jsonSerializer;
|
|
176
|
+
const logger = options?.logger ?? createLogger({ name: "binding-handler" });
|
|
177
|
+
return async (request) => {
|
|
178
|
+
const url = new URL(request.url);
|
|
179
|
+
if (request.method !== "POST" || url.pathname !== "/rpc") {
|
|
180
|
+
return new Response("Not Found", { status: 404 });
|
|
181
|
+
}
|
|
182
|
+
try {
|
|
183
|
+
const contentType = request.headers.get("Content-Type") ?? "";
|
|
184
|
+
let body;
|
|
185
|
+
if (contentType.includes("msgpack")) {
|
|
186
|
+
const buffer = await request.arrayBuffer();
|
|
187
|
+
body = serializer.decode(buffer);
|
|
188
|
+
} else {
|
|
189
|
+
const text = await request.text();
|
|
190
|
+
body = serializer.decode(text);
|
|
191
|
+
}
|
|
192
|
+
const traceparent = request.headers.get("traceparent");
|
|
193
|
+
if (traceparent) {
|
|
194
|
+
const parsedTrace = parseTraceparent(traceparent);
|
|
195
|
+
if (parsedTrace) {
|
|
196
|
+
body.traceContext = parsedTrace;
|
|
197
|
+
const tracestate = request.headers.get("tracestate");
|
|
198
|
+
if (tracestate) {
|
|
199
|
+
body.traceContext.traceState = tracestate;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
const tenantId = request.headers.get("X-Tenant-ID");
|
|
204
|
+
if (tenantId) {
|
|
205
|
+
body.metadata = { ...body.metadata, tenantId };
|
|
206
|
+
}
|
|
207
|
+
const response = await server.handle(body);
|
|
208
|
+
const responseBody = serializer.encode(response);
|
|
209
|
+
return new Response(
|
|
210
|
+
typeof responseBody === "string" ? responseBody : responseBody,
|
|
211
|
+
{
|
|
212
|
+
status: response.success ? 200 : getHttpStatus(response.error?.code),
|
|
213
|
+
headers: {
|
|
214
|
+
"Content-Type": serializer.contentType,
|
|
215
|
+
"X-Request-ID": body.id
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
);
|
|
219
|
+
} catch (error) {
|
|
220
|
+
logger.error("Handler error", error);
|
|
221
|
+
return new Response(
|
|
222
|
+
JSON.stringify({
|
|
223
|
+
success: false,
|
|
224
|
+
error: {
|
|
225
|
+
code: "INTERNAL_ERROR",
|
|
226
|
+
message: error.message
|
|
227
|
+
}
|
|
228
|
+
}),
|
|
229
|
+
{
|
|
230
|
+
status: 500,
|
|
231
|
+
headers: { "Content-Type": "application/json" }
|
|
232
|
+
}
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
function formatTraceparent(ctx) {
|
|
238
|
+
const flags = ctx.traceFlags.toString(16).padStart(2, "0");
|
|
239
|
+
return `00-${ctx.traceId}-${ctx.spanId}-${flags}`;
|
|
240
|
+
}
|
|
241
|
+
function getHttpStatus(code) {
|
|
242
|
+
switch (code) {
|
|
243
|
+
case "METHOD_NOT_FOUND":
|
|
244
|
+
case "SERVICE_NOT_FOUND":
|
|
245
|
+
return 404;
|
|
246
|
+
case "VERSION_MISMATCH":
|
|
247
|
+
case "VALIDATION_ERROR":
|
|
248
|
+
return 400;
|
|
249
|
+
case "UNAUTHORIZED":
|
|
250
|
+
return 401;
|
|
251
|
+
case "FORBIDDEN":
|
|
252
|
+
return 403;
|
|
253
|
+
case "TIMEOUT":
|
|
254
|
+
return 504;
|
|
255
|
+
case "CIRCUIT_OPEN":
|
|
256
|
+
case "BULKHEAD_REJECTED":
|
|
257
|
+
return 503;
|
|
258
|
+
default:
|
|
259
|
+
return 500;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// src/transports/cloudflare/queue.ts
|
|
264
|
+
import { createLogger as createLogger3 } from "@parsrun/core";
|
|
265
|
+
|
|
266
|
+
// src/events/format.ts
|
|
267
|
+
import { generateId } from "@parsrun/core";
|
|
268
|
+
function toCompactEvent(event) {
|
|
269
|
+
const compact = {
|
|
270
|
+
e: event.type,
|
|
271
|
+
s: event.source,
|
|
272
|
+
i: event.id,
|
|
273
|
+
t: new Date(event.time).getTime(),
|
|
274
|
+
d: event.data
|
|
275
|
+
};
|
|
276
|
+
if (event.parstracecontext) compact.ctx = event.parstracecontext;
|
|
277
|
+
if (event.parstenantid) compact.tid = event.parstenantid;
|
|
278
|
+
return compact;
|
|
279
|
+
}
|
|
280
|
+
function fromCompactEvent(compact, source) {
|
|
281
|
+
const event = {
|
|
282
|
+
specversion: "1.0",
|
|
283
|
+
type: compact.e,
|
|
284
|
+
source: source ?? compact.s,
|
|
285
|
+
id: compact.i,
|
|
286
|
+
time: new Date(compact.t).toISOString(),
|
|
287
|
+
datacontenttype: "application/json",
|
|
288
|
+
data: compact.d
|
|
289
|
+
};
|
|
290
|
+
if (compact.ctx) event.parstracecontext = compact.ctx;
|
|
291
|
+
if (compact.tid) event.parstenantid = compact.tid;
|
|
292
|
+
return event;
|
|
293
|
+
}
|
|
294
|
+
function matchEventType(type, pattern) {
|
|
295
|
+
if (pattern === "*" || pattern === "**") {
|
|
296
|
+
return true;
|
|
297
|
+
}
|
|
298
|
+
const typeParts = type.split(".");
|
|
299
|
+
const patternParts = pattern.split(".");
|
|
300
|
+
let ti = 0;
|
|
301
|
+
let pi = 0;
|
|
302
|
+
while (ti < typeParts.length && pi < patternParts.length) {
|
|
303
|
+
const pp = patternParts[pi];
|
|
304
|
+
if (pp === "**") {
|
|
305
|
+
if (pi === patternParts.length - 1) {
|
|
306
|
+
return true;
|
|
307
|
+
}
|
|
308
|
+
for (let i = ti; i <= typeParts.length; i++) {
|
|
309
|
+
const remaining = typeParts.slice(i).join(".");
|
|
310
|
+
const remainingPattern = patternParts.slice(pi + 1).join(".");
|
|
311
|
+
if (matchEventType(remaining, remainingPattern)) {
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return false;
|
|
316
|
+
}
|
|
317
|
+
if (pp === "*") {
|
|
318
|
+
ti++;
|
|
319
|
+
pi++;
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
if (pp !== typeParts[ti]) {
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
ti++;
|
|
326
|
+
pi++;
|
|
327
|
+
}
|
|
328
|
+
return ti === typeParts.length && pi === patternParts.length;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// src/events/handler.ts
|
|
332
|
+
import { createLogger as createLogger2 } from "@parsrun/core";
|
|
333
|
+
var EventHandlerRegistry = class {
|
|
334
|
+
handlers = /* @__PURE__ */ new Map();
|
|
335
|
+
logger;
|
|
336
|
+
deadLetterQueue;
|
|
337
|
+
defaultOptions;
|
|
338
|
+
constructor(options = {}) {
|
|
339
|
+
this.logger = options.logger ?? createLogger2({ name: "event-handler" });
|
|
340
|
+
if (options.deadLetterQueue) {
|
|
341
|
+
this.deadLetterQueue = options.deadLetterQueue;
|
|
342
|
+
}
|
|
343
|
+
const defaultOpts = {
|
|
344
|
+
retries: options.defaultOptions?.retries ?? 3,
|
|
345
|
+
backoff: options.defaultOptions?.backoff ?? "exponential",
|
|
346
|
+
maxDelay: options.defaultOptions?.maxDelay ?? 3e4,
|
|
347
|
+
onExhausted: options.defaultOptions?.onExhausted ?? "log"
|
|
348
|
+
};
|
|
349
|
+
if (options.defaultOptions?.deadLetter) {
|
|
350
|
+
defaultOpts.deadLetter = options.defaultOptions.deadLetter;
|
|
351
|
+
}
|
|
352
|
+
this.defaultOptions = defaultOpts;
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Register an event handler
|
|
356
|
+
*/
|
|
357
|
+
register(pattern, handler, options) {
|
|
358
|
+
const registration = {
|
|
359
|
+
pattern,
|
|
360
|
+
handler,
|
|
361
|
+
options: {
|
|
362
|
+
...this.defaultOptions,
|
|
363
|
+
...options
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
const handlers = this.handlers.get(pattern) ?? [];
|
|
367
|
+
handlers.push(registration);
|
|
368
|
+
this.handlers.set(pattern, handlers);
|
|
369
|
+
this.logger.debug(`Handler registered for pattern: ${pattern}`);
|
|
370
|
+
return () => {
|
|
371
|
+
const currentHandlers = this.handlers.get(pattern);
|
|
372
|
+
if (currentHandlers) {
|
|
373
|
+
const index = currentHandlers.indexOf(registration);
|
|
374
|
+
if (index !== -1) {
|
|
375
|
+
currentHandlers.splice(index, 1);
|
|
376
|
+
if (currentHandlers.length === 0) {
|
|
377
|
+
this.handlers.delete(pattern);
|
|
378
|
+
}
|
|
379
|
+
this.logger.debug(`Handler unregistered for pattern: ${pattern}`);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Handle an event
|
|
386
|
+
*/
|
|
387
|
+
async handle(event) {
|
|
388
|
+
const matchingHandlers = this.getMatchingHandlers(event.type);
|
|
389
|
+
if (matchingHandlers.length === 0) {
|
|
390
|
+
this.logger.debug(`No handlers for event type: ${event.type}`, {
|
|
391
|
+
eventId: event.id
|
|
392
|
+
});
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
this.logger.debug(`Handling event: ${event.type}`, {
|
|
396
|
+
eventId: event.id,
|
|
397
|
+
handlerCount: matchingHandlers.length
|
|
398
|
+
});
|
|
399
|
+
const results = await Promise.allSettled(
|
|
400
|
+
matchingHandlers.map((reg) => this.executeHandler(event, reg))
|
|
401
|
+
);
|
|
402
|
+
for (let i = 0; i < results.length; i++) {
|
|
403
|
+
const result = results[i];
|
|
404
|
+
if (result?.status === "rejected") {
|
|
405
|
+
this.logger.error(
|
|
406
|
+
`Handler failed for ${event.type}`,
|
|
407
|
+
result.reason,
|
|
408
|
+
{ eventId: event.id, pattern: matchingHandlers[i]?.pattern }
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Execute a single handler with retry logic
|
|
415
|
+
*/
|
|
416
|
+
async executeHandler(event, registration) {
|
|
417
|
+
const { handler, options } = registration;
|
|
418
|
+
const maxAttempts = options.retries + 1;
|
|
419
|
+
let lastError;
|
|
420
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
421
|
+
try {
|
|
422
|
+
const context = {
|
|
423
|
+
logger: this.logger.child({
|
|
424
|
+
eventId: event.id,
|
|
425
|
+
pattern: registration.pattern,
|
|
426
|
+
attempt
|
|
427
|
+
}),
|
|
428
|
+
attempt,
|
|
429
|
+
maxAttempts,
|
|
430
|
+
isRetry: attempt > 1
|
|
431
|
+
};
|
|
432
|
+
if (event.parstracecontext) {
|
|
433
|
+
const traceCtx = parseTraceContext(event.parstracecontext);
|
|
434
|
+
if (traceCtx) {
|
|
435
|
+
context.traceContext = traceCtx;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
await handler(event, context);
|
|
439
|
+
return;
|
|
440
|
+
} catch (error) {
|
|
441
|
+
lastError = error;
|
|
442
|
+
if (attempt < maxAttempts) {
|
|
443
|
+
const delay = this.calculateBackoff(attempt, options);
|
|
444
|
+
this.logger.warn(
|
|
445
|
+
`Handler failed, retrying in ${delay}ms`,
|
|
446
|
+
{ eventId: event.id, attempt, maxAttempts }
|
|
447
|
+
);
|
|
448
|
+
await sleep(delay);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
await this.handleExhausted(event, registration, lastError);
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Calculate backoff delay
|
|
456
|
+
*/
|
|
457
|
+
calculateBackoff(attempt, options) {
|
|
458
|
+
const baseDelay = 100;
|
|
459
|
+
if (options.backoff === "exponential") {
|
|
460
|
+
return Math.min(baseDelay * Math.pow(2, attempt - 1), options.maxDelay);
|
|
461
|
+
}
|
|
462
|
+
return Math.min(baseDelay * attempt, options.maxDelay);
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Handle exhausted retries
|
|
466
|
+
*/
|
|
467
|
+
async handleExhausted(event, registration, error) {
|
|
468
|
+
const { options } = registration;
|
|
469
|
+
if (options.deadLetter && this.deadLetterQueue) {
|
|
470
|
+
await this.deadLetterQueue.add({
|
|
471
|
+
event,
|
|
472
|
+
error: error.message,
|
|
473
|
+
pattern: registration.pattern,
|
|
474
|
+
attempts: options.retries + 1
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
switch (options.onExhausted) {
|
|
478
|
+
case "alert":
|
|
479
|
+
this.logger.error(
|
|
480
|
+
`[ALERT] Event handler exhausted all retries`,
|
|
481
|
+
error,
|
|
482
|
+
{
|
|
483
|
+
eventId: event.id,
|
|
484
|
+
eventType: event.type,
|
|
485
|
+
pattern: registration.pattern
|
|
486
|
+
}
|
|
487
|
+
);
|
|
488
|
+
break;
|
|
489
|
+
case "discard":
|
|
490
|
+
this.logger.debug(`Event discarded after exhausted retries`, {
|
|
491
|
+
eventId: event.id
|
|
492
|
+
});
|
|
493
|
+
break;
|
|
494
|
+
case "log":
|
|
495
|
+
default:
|
|
496
|
+
this.logger.warn(`Event handler exhausted all retries`, {
|
|
497
|
+
eventId: event.id,
|
|
498
|
+
error: error.message
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Get handlers matching an event type
|
|
504
|
+
*/
|
|
505
|
+
getMatchingHandlers(eventType) {
|
|
506
|
+
const matching = [];
|
|
507
|
+
for (const [pattern, handlers] of this.handlers) {
|
|
508
|
+
if (matchEventType(eventType, pattern)) {
|
|
509
|
+
matching.push(...handlers);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
return matching;
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* Get all registered patterns
|
|
516
|
+
*/
|
|
517
|
+
getPatterns() {
|
|
518
|
+
return Array.from(this.handlers.keys());
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Check if a pattern has handlers
|
|
522
|
+
*/
|
|
523
|
+
hasHandlers(pattern) {
|
|
524
|
+
return this.handlers.has(pattern);
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* Clear all handlers
|
|
528
|
+
*/
|
|
529
|
+
clear() {
|
|
530
|
+
this.handlers.clear();
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
function sleep(ms) {
|
|
534
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
535
|
+
}
|
|
536
|
+
function parseTraceContext(traceparent) {
|
|
537
|
+
const parts = traceparent.split("-");
|
|
538
|
+
if (parts.length !== 4) return void 0;
|
|
539
|
+
const [, traceId, spanId, flags] = parts;
|
|
540
|
+
if (!traceId || !spanId || !flags) return void 0;
|
|
541
|
+
return {
|
|
542
|
+
traceId,
|
|
543
|
+
spanId,
|
|
544
|
+
traceFlags: parseInt(flags, 16)
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// src/transports/cloudflare/queue.ts
|
|
549
|
+
var CloudflareQueueTransport = class {
|
|
550
|
+
name = "cloudflare-queue";
|
|
551
|
+
queue;
|
|
552
|
+
queueName;
|
|
553
|
+
compact;
|
|
554
|
+
logger;
|
|
555
|
+
batchSize;
|
|
556
|
+
flushInterval;
|
|
557
|
+
registry;
|
|
558
|
+
buffer = [];
|
|
559
|
+
flushTimer = null;
|
|
560
|
+
constructor(options) {
|
|
561
|
+
this.queue = options.queue;
|
|
562
|
+
this.queueName = options.queueName ?? "events";
|
|
563
|
+
this.compact = options.compact ?? true;
|
|
564
|
+
this.logger = options.logger ?? createLogger3({ name: `queue:${this.queueName}` });
|
|
565
|
+
this.batchSize = options.batchSize ?? 100;
|
|
566
|
+
this.flushInterval = options.flushInterval ?? 1e3;
|
|
567
|
+
this.registry = new EventHandlerRegistry({ logger: this.logger });
|
|
568
|
+
this.flushTimer = setInterval(() => this.flush(), this.flushInterval);
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Emit an event to the queue
|
|
572
|
+
*/
|
|
573
|
+
async emit(event) {
|
|
574
|
+
this.buffer.push(event);
|
|
575
|
+
if (this.buffer.length >= this.batchSize) {
|
|
576
|
+
await this.flush();
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Subscribe to events (for local handler registration)
|
|
581
|
+
*/
|
|
582
|
+
subscribe(eventType, handler, options) {
|
|
583
|
+
return this.registry.register(eventType, handler, options);
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* Flush buffered events to the queue
|
|
587
|
+
*/
|
|
588
|
+
async flush() {
|
|
589
|
+
if (this.buffer.length === 0) return;
|
|
590
|
+
const events = this.buffer.splice(0, this.batchSize);
|
|
591
|
+
try {
|
|
592
|
+
if (events.length === 1) {
|
|
593
|
+
const body = this.compact ? toCompactEvent(events[0]) : events[0];
|
|
594
|
+
await this.queue.send(body);
|
|
595
|
+
} else {
|
|
596
|
+
const messages = events.map((event) => ({
|
|
597
|
+
body: this.compact ? toCompactEvent(event) : event
|
|
598
|
+
}));
|
|
599
|
+
await this.queue.sendBatch(messages);
|
|
600
|
+
}
|
|
601
|
+
this.logger.debug(`Sent ${events.length} events to queue`, {
|
|
602
|
+
queue: this.queueName
|
|
603
|
+
});
|
|
604
|
+
} catch (error) {
|
|
605
|
+
this.buffer.unshift(...events);
|
|
606
|
+
this.logger.error("Failed to send events to queue", error);
|
|
607
|
+
throw error;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
/**
|
|
611
|
+
* Handle a queue message (called by queue consumer)
|
|
612
|
+
*/
|
|
613
|
+
async handleMessage(message) {
|
|
614
|
+
try {
|
|
615
|
+
const event = this.parseEvent(message.body);
|
|
616
|
+
await this.registry.handle(event);
|
|
617
|
+
message.ack();
|
|
618
|
+
} catch (error) {
|
|
619
|
+
this.logger.error("Failed to handle queue message", error, {
|
|
620
|
+
messageId: message.id
|
|
621
|
+
});
|
|
622
|
+
message.retry();
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* Handle a batch of queue messages
|
|
627
|
+
*/
|
|
628
|
+
async handleBatch(batch) {
|
|
629
|
+
const results = await Promise.allSettled(
|
|
630
|
+
batch.messages.map((msg) => this.handleMessage(msg))
|
|
631
|
+
);
|
|
632
|
+
const failures = results.filter((r) => r.status === "rejected");
|
|
633
|
+
if (failures.length > 0) {
|
|
634
|
+
this.logger.warn(`${failures.length}/${batch.messages.length} messages failed`);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Parse event from message body
|
|
639
|
+
*/
|
|
640
|
+
parseEvent(body) {
|
|
641
|
+
if (this.compact && isCompactEvent(body)) {
|
|
642
|
+
return fromCompactEvent(body);
|
|
643
|
+
}
|
|
644
|
+
return body;
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Close the transport
|
|
648
|
+
*/
|
|
649
|
+
async close() {
|
|
650
|
+
if (this.flushTimer) {
|
|
651
|
+
clearInterval(this.flushTimer);
|
|
652
|
+
this.flushTimer = null;
|
|
653
|
+
}
|
|
654
|
+
await this.flush();
|
|
655
|
+
this.registry.clear();
|
|
656
|
+
}
|
|
657
|
+
};
|
|
658
|
+
function createCloudflareQueueTransport(options) {
|
|
659
|
+
return new CloudflareQueueTransport(options);
|
|
660
|
+
}
|
|
661
|
+
function createQueueConsumer(registry, options) {
|
|
662
|
+
const compact = options?.compact ?? true;
|
|
663
|
+
const logger = options?.logger ?? createLogger3({ name: "queue-consumer" });
|
|
664
|
+
return async (batch) => {
|
|
665
|
+
logger.info(`Processing batch of ${batch.messages.length} messages`, {
|
|
666
|
+
queue: batch.queue
|
|
667
|
+
});
|
|
668
|
+
for (const message of batch.messages) {
|
|
669
|
+
try {
|
|
670
|
+
let event;
|
|
671
|
+
if (compact && isCompactEvent(message.body)) {
|
|
672
|
+
event = fromCompactEvent(message.body);
|
|
673
|
+
} else {
|
|
674
|
+
event = message.body;
|
|
675
|
+
}
|
|
676
|
+
await registry.handle(event);
|
|
677
|
+
message.ack();
|
|
678
|
+
} catch (error) {
|
|
679
|
+
logger.error("Failed to process message", error, {
|
|
680
|
+
messageId: message.id
|
|
681
|
+
});
|
|
682
|
+
message.retry();
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
function isCompactEvent(body) {
|
|
688
|
+
if (!body || typeof body !== "object") return false;
|
|
689
|
+
const obj = body;
|
|
690
|
+
return typeof obj["e"] === "string" && typeof obj["s"] === "string" && typeof obj["i"] === "string" && typeof obj["t"] === "number";
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// src/transports/cloudflare/durable-object.ts
|
|
694
|
+
import { createLogger as createLogger4 } from "@parsrun/core";
|
|
695
|
+
var DurableObjectTransport = class {
|
|
696
|
+
name = "durable-object";
|
|
697
|
+
namespace;
|
|
698
|
+
objectIdResolver;
|
|
699
|
+
serializer;
|
|
700
|
+
logger;
|
|
701
|
+
constructor(options) {
|
|
702
|
+
this.namespace = options.namespace;
|
|
703
|
+
this.objectIdResolver = typeof options.objectId === "function" ? options.objectId : () => options.objectId;
|
|
704
|
+
this.serializer = options.serializer ?? jsonSerializer;
|
|
705
|
+
this.logger = options.logger ?? createLogger4({ name: "durable-object" });
|
|
706
|
+
}
|
|
707
|
+
async call(request) {
|
|
708
|
+
try {
|
|
709
|
+
const objectIdName = this.objectIdResolver(request);
|
|
710
|
+
const id = this.namespace.idFromName(objectIdName);
|
|
711
|
+
const stub = this.namespace.get(id);
|
|
712
|
+
const body = this.serializer.encode(request);
|
|
713
|
+
const response = await stub.fetch("http://internal/rpc", {
|
|
714
|
+
method: "POST",
|
|
715
|
+
headers: {
|
|
716
|
+
"Content-Type": this.serializer.contentType
|
|
717
|
+
},
|
|
718
|
+
body: typeof body === "string" ? body : body
|
|
719
|
+
});
|
|
720
|
+
const text = await response.text();
|
|
721
|
+
return this.serializer.decode(text);
|
|
722
|
+
} catch (error) {
|
|
723
|
+
this.logger.error("Durable Object call failed", error);
|
|
724
|
+
throw new TransportError(
|
|
725
|
+
`Durable Object call failed: ${error.message}`,
|
|
726
|
+
error
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
async close() {
|
|
731
|
+
}
|
|
732
|
+
};
|
|
733
|
+
function createDurableObjectTransport(options) {
|
|
734
|
+
return new DurableObjectTransport(options);
|
|
735
|
+
}
|
|
736
|
+
export {
|
|
737
|
+
CloudflareQueueTransport,
|
|
738
|
+
DurableObjectTransport,
|
|
739
|
+
ServiceBindingTransport,
|
|
740
|
+
createCloudflareQueueTransport,
|
|
741
|
+
createDurableObjectTransport,
|
|
742
|
+
createQueueConsumer,
|
|
743
|
+
createServiceBindingHandler,
|
|
744
|
+
createServiceBindingTransport
|
|
745
|
+
};
|
|
746
|
+
//# sourceMappingURL=index.js.map
|