@beignet/core 0.0.37 → 0.0.38
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/CHANGELOG.md +13 -0
- package/README.md +108 -19
- package/dist/client/client.d.ts.map +1 -1
- package/dist/client/client.js +6 -28
- package/dist/client/client.js.map +1 -1
- package/dist/error-reporting/index.d.ts +42 -1
- package/dist/error-reporting/index.d.ts.map +1 -1
- package/dist/error-reporting/index.js +60 -0
- package/dist/error-reporting/index.js.map +1 -1
- package/dist/error-reporting/internal.d.ts +11 -0
- package/dist/error-reporting/internal.d.ts.map +1 -0
- package/dist/error-reporting/internal.js +36 -0
- package/dist/error-reporting/internal.js.map +1 -0
- package/dist/events/index.d.ts +9 -4
- package/dist/events/index.d.ts.map +1 -1
- package/dist/events/index.js +5 -3
- package/dist/events/index.js.map +1 -1
- package/dist/idempotency/index.d.ts +25 -2
- package/dist/idempotency/index.d.ts.map +1 -1
- package/dist/idempotency/index.js +35 -5
- package/dist/idempotency/index.js.map +1 -1
- package/dist/jobs/index.d.ts +26 -4
- package/dist/jobs/index.d.ts.map +1 -1
- package/dist/jobs/index.js +51 -6
- package/dist/jobs/index.js.map +1 -1
- package/dist/locks/index.d.ts +19 -1
- package/dist/locks/index.d.ts.map +1 -1
- package/dist/locks/index.js +27 -12
- package/dist/locks/index.js.map +1 -1
- package/dist/outbox/index.d.ts +23 -4
- package/dist/outbox/index.d.ts.map +1 -1
- package/dist/outbox/index.js +71 -23
- package/dist/outbox/index.js.map +1 -1
- package/dist/ports/events.d.ts +5 -5
- package/dist/ports/events.d.ts.map +1 -1
- package/dist/ports/index.d.ts +4 -4
- package/dist/ports/unbound.js +1 -1
- package/dist/ports/unbound.js.map +1 -1
- package/dist/ports/unit-of-work.d.ts +4 -1
- package/dist/ports/unit-of-work.d.ts.map +1 -1
- package/dist/ports/unit-of-work.js +4 -3
- package/dist/ports/unit-of-work.js.map +1 -1
- package/dist/query-codec.d.ts +3 -0
- package/dist/query-codec.d.ts.map +1 -0
- package/dist/query-codec.js +110 -0
- package/dist/query-codec.js.map +1 -0
- package/dist/server/hooks/cors.d.ts +3 -2
- package/dist/server/hooks/cors.d.ts.map +1 -1
- package/dist/server/hooks/cors.js +7 -3
- package/dist/server/hooks/cors.js.map +1 -1
- package/dist/server/hooks/error-reporting.d.ts +8 -0
- package/dist/server/hooks/error-reporting.d.ts.map +1 -1
- package/dist/server/hooks/error-reporting.js +55 -25
- package/dist/server/hooks/error-reporting.js.map +1 -1
- package/dist/server/hooks/idempotency.d.ts +1 -1
- package/dist/server/hooks/idempotency.d.ts.map +1 -1
- package/dist/server/hooks/idempotency.js +30 -9
- package/dist/server/hooks/idempotency.js.map +1 -1
- package/dist/server/request-executor.d.ts.map +1 -1
- package/dist/server/request-executor.js +4 -10
- package/dist/server/request-executor.js.map +1 -1
- package/dist/server/request-preparation.d.ts.map +1 -1
- package/dist/server/request-preparation.js +3 -1
- package/dist/server/request-preparation.js.map +1 -1
- package/dist/server/response-finalization.d.ts +3 -1
- package/dist/server/response-finalization.d.ts.map +1 -1
- package/dist/server/response-finalization.js +24 -2
- package/dist/server/response-finalization.js.map +1 -1
- package/dist/server/server.js +1 -1
- package/dist/server/server.js.map +1 -1
- package/dist/testing/index.d.ts +5 -5
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +9 -4
- package/dist/testing/index.js.map +1 -1
- package/dist/tracing/index.d.ts +27 -0
- package/dist/tracing/index.d.ts.map +1 -1
- package/dist/tracing/index.js +96 -0
- package/dist/tracing/index.js.map +1 -1
- package/dist/uploads/index.d.ts.map +1 -1
- package/dist/uploads/index.js +39 -6
- package/dist/uploads/index.js.map +1 -1
- package/dist/webhooks/index.d.ts +2 -1
- package/dist/webhooks/index.d.ts.map +1 -1
- package/dist/webhooks/index.js +25 -1
- package/dist/webhooks/index.js.map +1 -1
- package/package.json +1 -1
- package/skills/app-architecture/SKILL.md +19 -1
- package/src/client/client.ts +6 -31
- package/src/error-reporting/index.ts +111 -0
- package/src/error-reporting/internal.ts +47 -0
- package/src/events/index.ts +20 -4
- package/src/idempotency/index.ts +81 -7
- package/src/jobs/index.ts +85 -6
- package/src/locks/index.ts +65 -16
- package/src/outbox/index.ts +119 -27
- package/src/ports/events.ts +8 -1
- package/src/ports/index.ts +4 -4
- package/src/ports/unbound.ts +1 -1
- package/src/ports/unit-of-work.ts +10 -3
- package/src/query-codec.ts +130 -0
- package/src/server/hooks/cors.ts +10 -3
- package/src/server/hooks/error-reporting.ts +79 -23
- package/src/server/hooks/idempotency.ts +36 -10
- package/src/server/request-executor.ts +4 -12
- package/src/server/request-preparation.ts +4 -2
- package/src/server/response-finalization.ts +36 -5
- package/src/server/server.ts +1 -1
- package/src/testing/index.ts +25 -14
- package/src/tracing/index.ts +120 -0
- package/src/uploads/index.ts +44 -6
- package/src/webhooks/index.ts +34 -2
package/src/locks/index.ts
CHANGED
|
@@ -64,6 +64,25 @@ export type LeaseRenewOptions = {
|
|
|
64
64
|
ttlMs?: number;
|
|
65
65
|
};
|
|
66
66
|
|
|
67
|
+
/**
|
|
68
|
+
* State required to restore a lease handle in another runtime invocation.
|
|
69
|
+
*/
|
|
70
|
+
export type LeaseRestoreOptions = {
|
|
71
|
+
/**
|
|
72
|
+
* TTL to use when `renew()` is called without an explicit override.
|
|
73
|
+
*/
|
|
74
|
+
ttlMs: number;
|
|
75
|
+
/**
|
|
76
|
+
* Persisted expiry when the caller has it. Omitted values stay unknown.
|
|
77
|
+
*/
|
|
78
|
+
expiresAt?: Date;
|
|
79
|
+
/**
|
|
80
|
+
* Persisted fencing token when the caller has it. Omitted values stay
|
|
81
|
+
* unknown rather than being fabricated.
|
|
82
|
+
*/
|
|
83
|
+
fencingToken?: string | number;
|
|
84
|
+
};
|
|
85
|
+
|
|
67
86
|
/**
|
|
68
87
|
* Active lease handle returned by a lock provider.
|
|
69
88
|
*/
|
|
@@ -100,7 +119,11 @@ export type LocksPort = {
|
|
|
100
119
|
options: LeaseAcquireOptions,
|
|
101
120
|
fn: (ctx: { lease: LeaseHandle }) => MaybePromise<T>,
|
|
102
121
|
): Promise<T | undefined>;
|
|
103
|
-
restore(
|
|
122
|
+
restore(
|
|
123
|
+
key: string,
|
|
124
|
+
ownerToken: string,
|
|
125
|
+
options: LeaseRestoreOptions,
|
|
126
|
+
): LeaseHandle;
|
|
104
127
|
forceRelease(key: string): Promise<boolean>;
|
|
105
128
|
};
|
|
106
129
|
|
|
@@ -220,20 +243,21 @@ export function createMemoryLocks(
|
|
|
220
243
|
await result.lease.release();
|
|
221
244
|
}
|
|
222
245
|
},
|
|
223
|
-
restore(key, ownerToken) {
|
|
246
|
+
restore(key, ownerToken, restoreOptions) {
|
|
224
247
|
if (!key) throw new LeaseOptionsError("Lease key is required.");
|
|
225
248
|
if (!ownerToken) {
|
|
226
249
|
throw new LeaseOptionsError("Lease owner token is required.");
|
|
227
250
|
}
|
|
251
|
+
validateRestoreOptions(restoreOptions);
|
|
228
252
|
|
|
229
253
|
return createMemoryLease(
|
|
230
254
|
port,
|
|
231
255
|
{
|
|
232
256
|
key,
|
|
233
257
|
ownerToken,
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
fencingToken:
|
|
258
|
+
ttlMs: restoreOptions.ttlMs,
|
|
259
|
+
expiresAt: restoreOptions.expiresAt,
|
|
260
|
+
fencingToken: restoreOptions.fencingToken,
|
|
237
261
|
},
|
|
238
262
|
now,
|
|
239
263
|
);
|
|
@@ -306,32 +330,40 @@ export function withLease<T>(
|
|
|
306
330
|
|
|
307
331
|
function createMemoryLease(
|
|
308
332
|
port: MemoryLocksPort,
|
|
309
|
-
record:
|
|
333
|
+
record: {
|
|
334
|
+
key: string;
|
|
335
|
+
ownerToken: string;
|
|
336
|
+
ttlMs: number;
|
|
337
|
+
expiresAt?: Date;
|
|
338
|
+
fencingToken?: string | number;
|
|
339
|
+
},
|
|
310
340
|
now: () => Date,
|
|
311
341
|
): LeaseHandle {
|
|
312
342
|
const lease: LeaseHandle = {
|
|
313
343
|
key: record.key,
|
|
314
344
|
ownerToken: record.ownerToken,
|
|
315
|
-
expiresAt: record.expiresAt,
|
|
316
|
-
|
|
345
|
+
...(record.expiresAt ? { expiresAt: record.expiresAt } : {}),
|
|
346
|
+
...(record.fencingToken !== undefined
|
|
347
|
+
? { fencingToken: record.fencingToken }
|
|
348
|
+
: {}),
|
|
317
349
|
async renew(options) {
|
|
318
350
|
const active = port.leases.get(record.key);
|
|
319
|
-
if (
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
) {
|
|
351
|
+
if (!active || active.ownerToken !== record.ownerToken) {
|
|
352
|
+
return false;
|
|
353
|
+
}
|
|
354
|
+
if (active.expiresAt.getTime() <= now().getTime()) {
|
|
324
355
|
port.leases.delete(record.key);
|
|
325
356
|
return false;
|
|
326
357
|
}
|
|
327
358
|
|
|
328
|
-
const ttlMs = options?.ttlMs ??
|
|
359
|
+
const ttlMs = options?.ttlMs ?? record.ttlMs;
|
|
329
360
|
if (!Number.isFinite(ttlMs) || ttlMs <= 0) {
|
|
330
361
|
throw new LeaseOptionsError("Lease ttlMs must be a positive number.");
|
|
331
362
|
}
|
|
332
363
|
|
|
333
364
|
active.ttlMs = ttlMs;
|
|
334
365
|
active.expiresAt = new Date(now().getTime() + ttlMs);
|
|
366
|
+
record.ttlMs = ttlMs;
|
|
335
367
|
lease.expiresAt = active.expiresAt;
|
|
336
368
|
return true;
|
|
337
369
|
},
|
|
@@ -352,6 +384,23 @@ function createMemoryLease(
|
|
|
352
384
|
return lease;
|
|
353
385
|
}
|
|
354
386
|
|
|
387
|
+
function validateRestoreOptions(options: LeaseRestoreOptions): void {
|
|
388
|
+
if (!options || !Number.isFinite(options.ttlMs) || options.ttlMs <= 0) {
|
|
389
|
+
throw new LeaseOptionsError(
|
|
390
|
+
"Lease restore ttlMs must be a positive number.",
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
if (
|
|
394
|
+
options.expiresAt !== undefined &&
|
|
395
|
+
(!(options.expiresAt instanceof Date) ||
|
|
396
|
+
!Number.isFinite(options.expiresAt.getTime()))
|
|
397
|
+
) {
|
|
398
|
+
throw new LeaseOptionsError(
|
|
399
|
+
"Lease restore expiresAt must be a valid Date.",
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
355
404
|
function instrumentLocks(
|
|
356
405
|
locks: LocksPort,
|
|
357
406
|
instrumentation: ReturnType<typeof createProviderInstrumentation>,
|
|
@@ -387,8 +436,8 @@ function instrumentLocks(
|
|
|
387
436
|
await result.lease.release();
|
|
388
437
|
}
|
|
389
438
|
},
|
|
390
|
-
restore(key, ownerToken) {
|
|
391
|
-
return locks.restore(key, ownerToken);
|
|
439
|
+
restore(key, ownerToken, options) {
|
|
440
|
+
return locks.restore(key, ownerToken, options);
|
|
392
441
|
},
|
|
393
442
|
async forceRelease(key) {
|
|
394
443
|
const released = await locks.forceRelease(key);
|
package/src/outbox/index.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
9
|
type EventPayloadDef,
|
|
10
|
+
type EventPublishOptions,
|
|
10
11
|
type InferEventPayload,
|
|
11
12
|
parseEventPayload,
|
|
12
13
|
} from "../events/index.js";
|
|
@@ -21,15 +22,20 @@ import {
|
|
|
21
22
|
shouldRetryJob,
|
|
22
23
|
} from "../jobs/index.js";
|
|
23
24
|
import type { JobDispatcherPort } from "../ports/events.js";
|
|
24
|
-
import type {
|
|
25
|
-
BufferedDomainEventRecorder,
|
|
26
|
-
DomainEventRecorderPort,
|
|
27
|
-
} from "../ports/unit-of-work.js";
|
|
25
|
+
import type { DomainEventRecorderPort } from "../ports/unit-of-work.js";
|
|
28
26
|
import {
|
|
29
27
|
type BaseProviderInstrumentationEvent,
|
|
30
28
|
createProviderInstrumentation,
|
|
31
29
|
type ProviderInstrumentationTarget,
|
|
32
30
|
} from "../providers/index.js";
|
|
31
|
+
import {
|
|
32
|
+
captureTraceCarrier,
|
|
33
|
+
parseTraceCarrier,
|
|
34
|
+
resolveTracingPort,
|
|
35
|
+
runWithTracing,
|
|
36
|
+
type TraceCarrier,
|
|
37
|
+
type TracingPort,
|
|
38
|
+
} from "../tracing/index.js";
|
|
33
39
|
|
|
34
40
|
/**
|
|
35
41
|
* Value or promise of that value.
|
|
@@ -113,6 +119,8 @@ export interface OutboxEnqueueInput {
|
|
|
113
119
|
* JSON-serializable payload.
|
|
114
120
|
*/
|
|
115
121
|
payload: OutboxJsonValue;
|
|
122
|
+
/** Versioned trace context captured when the message was recorded. */
|
|
123
|
+
trace?: TraceCarrier;
|
|
116
124
|
/**
|
|
117
125
|
* Earliest time the message may be claimed.
|
|
118
126
|
*/
|
|
@@ -143,6 +151,8 @@ export interface OutboxMessage {
|
|
|
143
151
|
* JSON-serializable payload.
|
|
144
152
|
*/
|
|
145
153
|
payload: OutboxJsonValue;
|
|
154
|
+
/** Versioned trace context captured when the message was recorded. */
|
|
155
|
+
trace?: TraceCarrier;
|
|
146
156
|
/**
|
|
147
157
|
* Current delivery status.
|
|
148
158
|
*/
|
|
@@ -490,6 +500,10 @@ export interface EnqueueTypedOutboxOptions {
|
|
|
490
500
|
* Maximum delivery attempts before dead-lettering.
|
|
491
501
|
*/
|
|
492
502
|
maxAttempts?: number;
|
|
503
|
+
/** Explicit trace context to persist with this message. */
|
|
504
|
+
trace?: TraceCarrier;
|
|
505
|
+
/** Tracing port used to capture the active context at enqueue time. */
|
|
506
|
+
tracing?: TracingPort;
|
|
493
507
|
}
|
|
494
508
|
|
|
495
509
|
/**
|
|
@@ -547,6 +561,7 @@ export interface DrainOutboxOptions {
|
|
|
547
561
|
publish<E extends EventPayloadDef>(
|
|
548
562
|
event: E,
|
|
549
563
|
payload: InferEventPayload<E>,
|
|
564
|
+
options?: EventPublishOptions,
|
|
550
565
|
): MaybePromise<void>;
|
|
551
566
|
};
|
|
552
567
|
/**
|
|
@@ -592,6 +607,23 @@ export interface DrainOutboxOptions {
|
|
|
592
607
|
error: unknown,
|
|
593
608
|
message: ClaimedOutboxMessage,
|
|
594
609
|
) => MaybePromise<void>;
|
|
610
|
+
/**
|
|
611
|
+
* Observer called after a failed delivery is successfully moved to the dead
|
|
612
|
+
* letter state. Observer failures are ignored.
|
|
613
|
+
*/
|
|
614
|
+
onDeadLetter?: (
|
|
615
|
+
error: unknown,
|
|
616
|
+
message: ClaimedOutboxMessage,
|
|
617
|
+
) => MaybePromise<void>;
|
|
618
|
+
/**
|
|
619
|
+
* Observer called when a failed delivery cannot be settled as retryable or
|
|
620
|
+
* dead-lettered. Observer failures are ignored.
|
|
621
|
+
*/
|
|
622
|
+
onSettlementError?: (
|
|
623
|
+
settlementError: unknown,
|
|
624
|
+
message: ClaimedOutboxMessage,
|
|
625
|
+
deliveryError: unknown,
|
|
626
|
+
) => MaybePromise<void>;
|
|
595
627
|
}
|
|
596
628
|
|
|
597
629
|
/**
|
|
@@ -921,11 +953,13 @@ export function createOutboxMessage(
|
|
|
921
953
|
}
|
|
922
954
|
|
|
923
955
|
const now = options.now ?? new Date();
|
|
956
|
+
const trace = parseTraceCarrier(input.trace);
|
|
924
957
|
return {
|
|
925
958
|
id: options.id ?? input.id ?? options.createId?.() ?? createId(),
|
|
926
959
|
kind: input.kind,
|
|
927
960
|
name: input.name,
|
|
928
961
|
payload: toOutboxJsonValue(input.payload),
|
|
962
|
+
...(trace ? { trace } : {}),
|
|
929
963
|
status: "pending",
|
|
930
964
|
attempts: 0,
|
|
931
965
|
maxAttempts: input.maxAttempts ?? DEFAULT_OUTBOX_MAX_ATTEMPTS,
|
|
@@ -1236,11 +1270,14 @@ export async function enqueueEvent<E extends EventPayloadDef>(
|
|
|
1236
1270
|
options: EnqueueTypedOutboxOptions = {},
|
|
1237
1271
|
): Promise<OutboxMessage> {
|
|
1238
1272
|
const parsed = await parseEventPayload(event, payload);
|
|
1273
|
+
const trace =
|
|
1274
|
+
parseTraceCarrier(options.trace) ?? captureTraceCarrier(options.tracing);
|
|
1239
1275
|
return outbox.enqueue({
|
|
1240
1276
|
id: options.id,
|
|
1241
1277
|
kind: "event",
|
|
1242
1278
|
name: event.name,
|
|
1243
1279
|
payload: toOutboxJsonValue(parsed),
|
|
1280
|
+
trace,
|
|
1244
1281
|
availableAt: options.availableAt,
|
|
1245
1282
|
maxAttempts: options.maxAttempts,
|
|
1246
1283
|
});
|
|
@@ -1256,11 +1293,14 @@ export async function enqueueJob<J extends JobDef>(
|
|
|
1256
1293
|
options: EnqueueTypedOutboxOptions = {},
|
|
1257
1294
|
): Promise<OutboxMessage> {
|
|
1258
1295
|
const parsed = await parseJobPayload(job, payload);
|
|
1296
|
+
const trace =
|
|
1297
|
+
parseTraceCarrier(options.trace) ?? captureTraceCarrier(options.tracing);
|
|
1259
1298
|
return outbox.enqueue({
|
|
1260
1299
|
id: options.id,
|
|
1261
1300
|
kind: "job",
|
|
1262
1301
|
name: job.name,
|
|
1263
1302
|
payload: toOutboxJsonValue(parsed),
|
|
1303
|
+
trace,
|
|
1264
1304
|
availableAt: options.availableAt,
|
|
1265
1305
|
maxAttempts: options.maxAttempts ?? getJobRetryMaxAttempts(job.retry),
|
|
1266
1306
|
});
|
|
@@ -1272,16 +1312,14 @@ export async function enqueueJob<J extends JobDef>(
|
|
|
1272
1312
|
export function createOutboxEventRecorder(
|
|
1273
1313
|
outbox: OutboxPort,
|
|
1274
1314
|
options: EnqueueTypedOutboxOptions = {},
|
|
1275
|
-
):
|
|
1315
|
+
): DomainEventRecorderPort {
|
|
1276
1316
|
return {
|
|
1277
|
-
async record(event, payload) {
|
|
1278
|
-
await enqueueEvent(outbox, event, payload,
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1317
|
+
async record(event, payload, publishOptions) {
|
|
1318
|
+
await enqueueEvent(outbox, event, payload, {
|
|
1319
|
+
...options,
|
|
1320
|
+
trace: publishOptions?.trace ?? options.trace,
|
|
1321
|
+
});
|
|
1282
1322
|
},
|
|
1283
|
-
clear() {},
|
|
1284
|
-
async flush() {},
|
|
1285
1323
|
};
|
|
1286
1324
|
}
|
|
1287
1325
|
|
|
@@ -1293,8 +1331,11 @@ export function createOutboxJobDispatcher(
|
|
|
1293
1331
|
options: EnqueueTypedOutboxOptions = {},
|
|
1294
1332
|
): JobDispatcherPort {
|
|
1295
1333
|
return {
|
|
1296
|
-
async dispatch(job, payload) {
|
|
1297
|
-
await enqueueJob(outbox, job, payload,
|
|
1334
|
+
async dispatch(job, payload, dispatchOptions) {
|
|
1335
|
+
await enqueueJob(outbox, job, payload, {
|
|
1336
|
+
...options,
|
|
1337
|
+
trace: dispatchOptions?.trace ?? options.trace,
|
|
1338
|
+
});
|
|
1298
1339
|
},
|
|
1299
1340
|
};
|
|
1300
1341
|
}
|
|
@@ -1365,6 +1406,7 @@ function outboxInstrumentationDetails(
|
|
|
1365
1406
|
async function deliverOutboxMessage(
|
|
1366
1407
|
options: DrainOutboxOptions,
|
|
1367
1408
|
message: ClaimedOutboxMessage,
|
|
1409
|
+
trace?: TraceCarrier,
|
|
1368
1410
|
): Promise<void> {
|
|
1369
1411
|
if (message.kind === "event") {
|
|
1370
1412
|
if (!options.eventBus) {
|
|
@@ -1381,7 +1423,11 @@ async function deliverOutboxMessage(
|
|
|
1381
1423
|
}
|
|
1382
1424
|
|
|
1383
1425
|
const payload = await parseEventPayload(event, message.payload);
|
|
1384
|
-
await options.eventBus.publish(
|
|
1426
|
+
await options.eventBus.publish(
|
|
1427
|
+
event,
|
|
1428
|
+
payload,
|
|
1429
|
+
trace ? { trace } : undefined,
|
|
1430
|
+
);
|
|
1385
1431
|
return;
|
|
1386
1432
|
}
|
|
1387
1433
|
|
|
@@ -1414,11 +1460,12 @@ async function deliverOutboxMessage(
|
|
|
1414
1460
|
await singleAttempt(job, payload, {
|
|
1415
1461
|
attempt: message.attempts,
|
|
1416
1462
|
maxAttempts: message.maxAttempts,
|
|
1463
|
+
trace,
|
|
1417
1464
|
});
|
|
1418
1465
|
return;
|
|
1419
1466
|
}
|
|
1420
1467
|
|
|
1421
|
-
await options.jobs.dispatch(job, payload);
|
|
1468
|
+
await options.jobs.dispatch(job, payload, trace ? { trace } : undefined);
|
|
1422
1469
|
}
|
|
1423
1470
|
|
|
1424
1471
|
/**
|
|
@@ -1448,6 +1495,7 @@ export async function drainOutbox(
|
|
|
1448
1495
|
watcher: "jobs",
|
|
1449
1496
|
},
|
|
1450
1497
|
);
|
|
1498
|
+
const tracing = resolveTracingPort(options.instrumentation);
|
|
1451
1499
|
|
|
1452
1500
|
const now = options.now ?? new Date();
|
|
1453
1501
|
const messages = await options.outbox.claimBatch({
|
|
@@ -1464,7 +1512,28 @@ export async function drainOutbox(
|
|
|
1464
1512
|
|
|
1465
1513
|
for (const message of messages) {
|
|
1466
1514
|
try {
|
|
1467
|
-
|
|
1515
|
+
const parentTrace = parseTraceCarrier(message.trace);
|
|
1516
|
+
const traceAttributes = {
|
|
1517
|
+
"beignet.outbox.message_kind": message.kind,
|
|
1518
|
+
"beignet.outbox.message_name": message.name,
|
|
1519
|
+
} as const;
|
|
1520
|
+
await runWithTracing(
|
|
1521
|
+
tracing,
|
|
1522
|
+
{
|
|
1523
|
+
name: `beignet.outbox deliver ${message.name}`,
|
|
1524
|
+
type: "outbox",
|
|
1525
|
+
kind: "consumer",
|
|
1526
|
+
parent: parentTrace,
|
|
1527
|
+
attributes: traceAttributes,
|
|
1528
|
+
metricAttributes: traceAttributes,
|
|
1529
|
+
},
|
|
1530
|
+
(span) =>
|
|
1531
|
+
deliverOutboxMessage(
|
|
1532
|
+
options,
|
|
1533
|
+
message,
|
|
1534
|
+
captureTraceCarrier(span?.context ?? parentTrace),
|
|
1535
|
+
),
|
|
1536
|
+
);
|
|
1468
1537
|
await options.outbox.markDelivered({
|
|
1469
1538
|
id: message.id,
|
|
1470
1539
|
claimToken: message.claimToken,
|
|
@@ -1492,18 +1561,41 @@ export async function drainOutbox(
|
|
|
1492
1561
|
const retryDelayMs = deadLetter
|
|
1493
1562
|
? 0
|
|
1494
1563
|
: resolveRetryDelayMs(options, message, error, now);
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1564
|
+
try {
|
|
1565
|
+
await options.outbox.markFailed({
|
|
1566
|
+
id: message.id,
|
|
1567
|
+
claimToken: message.claimToken,
|
|
1568
|
+
error,
|
|
1569
|
+
deadLetter,
|
|
1570
|
+
now,
|
|
1571
|
+
retryAt: deadLetter
|
|
1572
|
+
? undefined
|
|
1573
|
+
: new Date(now.getTime() + retryDelayMs),
|
|
1574
|
+
});
|
|
1575
|
+
} catch (settlementError) {
|
|
1576
|
+
try {
|
|
1577
|
+
await options.onSettlementError?.(settlementError, message, error);
|
|
1578
|
+
} catch {
|
|
1579
|
+
// Preserve the settlement failure when its observer also fails.
|
|
1580
|
+
}
|
|
1581
|
+
instrumentation.custom({
|
|
1582
|
+
name: "outbox.settlement.failed",
|
|
1583
|
+
label: "Outbox settlement failed",
|
|
1584
|
+
summary: `Could not settle failed ${message.kind} "${message.name}"`,
|
|
1585
|
+
details: outboxInstrumentationDetails(message, {
|
|
1586
|
+
deliveryError: serializeOutboxError(error),
|
|
1587
|
+
settlementError: serializeOutboxError(settlementError),
|
|
1588
|
+
}),
|
|
1589
|
+
});
|
|
1590
|
+
continue;
|
|
1591
|
+
}
|
|
1505
1592
|
|
|
1506
1593
|
if (deadLetter) {
|
|
1594
|
+
try {
|
|
1595
|
+
await options.onDeadLetter?.(error, message);
|
|
1596
|
+
} catch {
|
|
1597
|
+
// Dead-letter observers must not change the settled message state.
|
|
1598
|
+
}
|
|
1507
1599
|
instrumentation.record({
|
|
1508
1600
|
type: "outbox",
|
|
1509
1601
|
...options.instrumentationContext,
|
package/src/ports/events.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
EventPayloadDef,
|
|
3
|
+
EventPublishOptions,
|
|
3
4
|
InferEventPayload as InferContractEventPayload,
|
|
4
5
|
StandardSchema,
|
|
5
6
|
} from "../events/index.js";
|
|
6
7
|
import type {
|
|
7
8
|
JobDef as ContractJobDef,
|
|
8
9
|
InferJobPayload as InferContractJobPayload,
|
|
10
|
+
JobDispatchOptions,
|
|
9
11
|
} from "../jobs/index.js";
|
|
10
12
|
|
|
11
13
|
/**
|
|
@@ -74,6 +76,7 @@ export interface EventBusPort {
|
|
|
74
76
|
publish<E extends DomainEventDef>(
|
|
75
77
|
event: E,
|
|
76
78
|
payload: InferEventPayload<E>,
|
|
79
|
+
options?: EventPublishOptions,
|
|
77
80
|
): Promise<void> | void;
|
|
78
81
|
|
|
79
82
|
/**
|
|
@@ -81,7 +84,10 @@ export interface EventBusPort {
|
|
|
81
84
|
*/
|
|
82
85
|
subscribe<E extends DomainEventDef>(
|
|
83
86
|
event: E,
|
|
84
|
-
handler: (
|
|
87
|
+
handler: (
|
|
88
|
+
payload: InferEventPayload<E>,
|
|
89
|
+
options?: EventPublishOptions,
|
|
90
|
+
) => Promise<void> | void,
|
|
85
91
|
): () => void;
|
|
86
92
|
}
|
|
87
93
|
|
|
@@ -96,5 +102,6 @@ export interface JobDispatcherPort {
|
|
|
96
102
|
dispatch<J extends JobDef>(
|
|
97
103
|
job: J,
|
|
98
104
|
payload: InferJobPayload<J>,
|
|
105
|
+
options?: JobDispatchOptions,
|
|
99
106
|
): Promise<void> | void;
|
|
100
107
|
}
|
package/src/ports/index.ts
CHANGED
|
@@ -75,7 +75,7 @@ export type DeferredPortsDefinition<
|
|
|
75
75
|
* Define the set of ports (outbound dependencies) for your application.
|
|
76
76
|
*
|
|
77
77
|
* The identity form captures the exact shape of the provided `ports` object
|
|
78
|
-
* so you can export `type AppPorts = typeof
|
|
78
|
+
* so you can export `type AppPorts = typeof initialPorts`.
|
|
79
79
|
*
|
|
80
80
|
* The curried form, `definePorts<AppPorts>()({ bound, deferred })`, declares
|
|
81
81
|
* which port keys providers contribute at server startup. Deferred keys boot
|
|
@@ -86,14 +86,14 @@ export type DeferredPortsDefinition<
|
|
|
86
86
|
* @example
|
|
87
87
|
* ```ts
|
|
88
88
|
* // Identity form: every port is bound directly.
|
|
89
|
-
* const
|
|
89
|
+
* const initialPorts = definePorts({
|
|
90
90
|
* db: dbAdapter,
|
|
91
91
|
* mailer: mailerAdapter,
|
|
92
92
|
* });
|
|
93
|
-
* export type AppPorts = typeof
|
|
93
|
+
* export type AppPorts = typeof initialPorts;
|
|
94
94
|
*
|
|
95
95
|
* // Deferred form: providers contribute the rest at startup.
|
|
96
|
-
* export const
|
|
96
|
+
* export const initialPorts = definePorts<AppPorts>()({
|
|
97
97
|
* bound: { gate },
|
|
98
98
|
* deferred: ["db", "mailer", "storage"],
|
|
99
99
|
* });
|
package/src/ports/unbound.ts
CHANGED
|
@@ -13,7 +13,7 @@ function unboundPortMessage(portName: string): string {
|
|
|
13
13
|
return (
|
|
14
14
|
`Port "${portName}" is not bound. "${portName}" is declared as deferred ` +
|
|
15
15
|
"in definePorts(...). Register a provider that contributes it " +
|
|
16
|
-
"(server/providers.ts) or bind it in infra/
|
|
16
|
+
"(server/providers.ts) or bind it in infra/port-wiring.ts."
|
|
17
17
|
);
|
|
18
18
|
}
|
|
19
19
|
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
type EventPublishOptions,
|
|
3
|
+
parseEventPayload,
|
|
4
|
+
} from "../events/index.js";
|
|
2
5
|
import type {
|
|
3
6
|
DomainEventDef,
|
|
4
7
|
EventBusPort,
|
|
@@ -71,6 +74,8 @@ export interface RecordedDomainEvent {
|
|
|
71
74
|
* Unparsed payload recorded during the transaction.
|
|
72
75
|
*/
|
|
73
76
|
payload: unknown;
|
|
77
|
+
/** Optional metadata propagated when the event is flushed. */
|
|
78
|
+
options?: EventPublishOptions;
|
|
74
79
|
}
|
|
75
80
|
|
|
76
81
|
/**
|
|
@@ -87,6 +92,7 @@ export interface DomainEventRecorderPort {
|
|
|
87
92
|
record<E extends DomainEventDef>(
|
|
88
93
|
event: E,
|
|
89
94
|
payload: InferEventPayload<E>,
|
|
95
|
+
options?: EventPublishOptions,
|
|
90
96
|
): Promise<void> | void;
|
|
91
97
|
}
|
|
92
98
|
|
|
@@ -167,11 +173,12 @@ export function createDomainEventRecorder(): BufferedDomainEventRecorder {
|
|
|
167
173
|
const records: RecordedDomainEvent[] = [];
|
|
168
174
|
|
|
169
175
|
return {
|
|
170
|
-
record(event, payload) {
|
|
176
|
+
record(event, payload, options) {
|
|
171
177
|
records.push({
|
|
172
178
|
event,
|
|
173
179
|
eventName: event.name,
|
|
174
180
|
payload,
|
|
181
|
+
...(options ? { options } : {}),
|
|
175
182
|
});
|
|
176
183
|
},
|
|
177
184
|
|
|
@@ -187,7 +194,7 @@ export function createDomainEventRecorder(): BufferedDomainEventRecorder {
|
|
|
187
194
|
while (records.length > 0) {
|
|
188
195
|
const record = records[0];
|
|
189
196
|
const payload = await parseEventPayload(record.event, record.payload);
|
|
190
|
-
await eventBus.publish(record.event, payload);
|
|
197
|
+
await eventBus.publish(record.event, payload, record.options);
|
|
191
198
|
records.shift();
|
|
192
199
|
}
|
|
193
200
|
},
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
const QUERY_VALUE_PREFIX = "~beignet-query:";
|
|
2
|
+
|
|
3
|
+
type EncodedQueryValue =
|
|
4
|
+
| { type: "null" }
|
|
5
|
+
| { type: "string"; value: string }
|
|
6
|
+
| { type: "number"; value: number }
|
|
7
|
+
| { type: "boolean"; value: boolean }
|
|
8
|
+
| { type: "date"; value: string }
|
|
9
|
+
| { type: "array"; value: EncodedQueryValue[] }
|
|
10
|
+
| { type: "object"; value: Record<string, EncodedQueryValue> };
|
|
11
|
+
|
|
12
|
+
function encodeStructuredValue(
|
|
13
|
+
value: unknown,
|
|
14
|
+
ancestors: Set<object>,
|
|
15
|
+
): EncodedQueryValue {
|
|
16
|
+
if (value === null) return { type: "null" };
|
|
17
|
+
if (typeof value === "string") return { type: "string", value };
|
|
18
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
19
|
+
return { type: "number", value };
|
|
20
|
+
}
|
|
21
|
+
if (typeof value === "boolean") return { type: "boolean", value };
|
|
22
|
+
if (value instanceof Date && Number.isFinite(value.getTime())) {
|
|
23
|
+
return { type: "date", value: value.toISOString() };
|
|
24
|
+
}
|
|
25
|
+
if (typeof value !== "object" || value === null) {
|
|
26
|
+
throw new TypeError("Query value is not serializable.");
|
|
27
|
+
}
|
|
28
|
+
if (ancestors.has(value)) {
|
|
29
|
+
throw new TypeError("Query value contains a circular reference.");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
ancestors.add(value);
|
|
33
|
+
try {
|
|
34
|
+
if (Array.isArray(value)) {
|
|
35
|
+
return {
|
|
36
|
+
type: "array",
|
|
37
|
+
value: value.map((entry) => encodeStructuredValue(entry, ancestors)),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const prototype = Object.getPrototypeOf(value);
|
|
42
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
43
|
+
throw new TypeError("Query value contains a non-plain object.");
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
type: "object",
|
|
47
|
+
value: Object.fromEntries(
|
|
48
|
+
Object.entries(value)
|
|
49
|
+
.filter(([, entry]) => entry !== undefined)
|
|
50
|
+
.map(([key, entry]) => [
|
|
51
|
+
key,
|
|
52
|
+
encodeStructuredValue(entry, ancestors),
|
|
53
|
+
]),
|
|
54
|
+
),
|
|
55
|
+
};
|
|
56
|
+
} finally {
|
|
57
|
+
ancestors.delete(value);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function decodeStructuredValue(value: unknown): unknown {
|
|
62
|
+
if (typeof value !== "object" || value === null || !("type" in value)) {
|
|
63
|
+
throw new TypeError("Invalid encoded query value.");
|
|
64
|
+
}
|
|
65
|
+
const encoded = value as { type: unknown; value?: unknown };
|
|
66
|
+
|
|
67
|
+
switch (encoded.type) {
|
|
68
|
+
case "null":
|
|
69
|
+
return null;
|
|
70
|
+
case "string":
|
|
71
|
+
if (typeof encoded.value === "string") return encoded.value;
|
|
72
|
+
break;
|
|
73
|
+
case "number":
|
|
74
|
+
if (typeof encoded.value === "number" && Number.isFinite(encoded.value)) {
|
|
75
|
+
return encoded.value;
|
|
76
|
+
}
|
|
77
|
+
break;
|
|
78
|
+
case "boolean":
|
|
79
|
+
if (typeof encoded.value === "boolean") return encoded.value;
|
|
80
|
+
break;
|
|
81
|
+
case "date": {
|
|
82
|
+
if (typeof encoded.value !== "string") break;
|
|
83
|
+
const date = new Date(encoded.value);
|
|
84
|
+
if (Number.isFinite(date.getTime())) return date;
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
case "array":
|
|
88
|
+
if (Array.isArray(encoded.value)) {
|
|
89
|
+
return encoded.value.map(decodeStructuredValue);
|
|
90
|
+
}
|
|
91
|
+
break;
|
|
92
|
+
case "object":
|
|
93
|
+
if (
|
|
94
|
+
typeof encoded.value === "object" &&
|
|
95
|
+
encoded.value !== null &&
|
|
96
|
+
!Array.isArray(encoded.value)
|
|
97
|
+
) {
|
|
98
|
+
return Object.fromEntries(
|
|
99
|
+
Object.entries(encoded.value).map(([key, entry]) => [
|
|
100
|
+
key,
|
|
101
|
+
decodeStructuredValue(entry),
|
|
102
|
+
]),
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
throw new TypeError("Invalid encoded query value.");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function encodeQueryValue(value: unknown): string {
|
|
112
|
+
if (typeof value === "string" && !value.startsWith(QUERY_VALUE_PREFIX)) {
|
|
113
|
+
return value;
|
|
114
|
+
}
|
|
115
|
+
return `${QUERY_VALUE_PREFIX}${JSON.stringify(
|
|
116
|
+
encodeStructuredValue(value, new Set()),
|
|
117
|
+
)}`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function decodeQueryValue(value: string): unknown {
|
|
121
|
+
if (!value.startsWith(QUERY_VALUE_PREFIX)) return value;
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
return decodeStructuredValue(
|
|
125
|
+
JSON.parse(value.slice(QUERY_VALUE_PREFIX.length)) as unknown,
|
|
126
|
+
);
|
|
127
|
+
} catch {
|
|
128
|
+
return value;
|
|
129
|
+
}
|
|
130
|
+
}
|