@beignet/core 0.0.36 → 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 +19 -0
- package/README.md +135 -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/contracts/contract-builder.d.ts +7 -0
- package/dist/contracts/contract-builder.d.ts.map +1 -1
- package/dist/contracts/contract-builder.js +22 -0
- package/dist/contracts/contract-builder.js.map +1 -1
- package/dist/contracts/contract-group.d.ts +7 -0
- package/dist/contracts/contract-group.d.ts.map +1 -1
- package/dist/contracts/contract-group.js +14 -0
- package/dist/contracts/contract-group.js.map +1 -1
- package/dist/contracts/index.d.ts +4 -0
- package/dist/contracts/index.d.ts.map +1 -1
- package/dist/contracts/index.js +4 -0
- package/dist/contracts/index.js.map +1 -1
- package/dist/contracts/lifecycle.d.ts +40 -0
- package/dist/contracts/lifecycle.d.ts.map +1 -0
- package/dist/contracts/lifecycle.js +174 -0
- package/dist/contracts/lifecycle.js.map +1 -0
- package/dist/contracts/types.d.ts +5 -0
- package/dist/contracts/types.d.ts.map +1 -1
- package/dist/contracts/types.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/openapi/index.d.ts +3 -1
- package/dist/openapi/index.d.ts.map +1 -1
- package/dist/openapi/index.js +14 -2
- package/dist/openapi/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 +7 -12
- 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 +5 -1
- package/dist/server/response-finalization.d.ts.map +1 -1
- package/dist/server/response-finalization.js +66 -2
- package/dist/server/response-finalization.js.map +1 -1
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +10 -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/contracts/contract-builder.ts +38 -0
- package/src/contracts/contract-group.ts +25 -0
- package/src/contracts/index.ts +8 -0
- package/src/contracts/lifecycle.ts +236 -0
- package/src/contracts/types.ts +5 -0
- 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/openapi/index.ts +22 -2
- 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 +10 -13
- package/src/server/request-preparation.ts +4 -2
- package/src/server/response-finalization.ts +94 -5
- package/src/server/server.ts +15 -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/dist/domain/events.d.ts +0 -44
- package/dist/domain/events.d.ts.map +0 -1
- package/dist/domain/events.js +0 -24
- package/dist/domain/events.js.map +0 -1
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
|
+
}
|
package/src/server/hooks/cors.ts
CHANGED
|
@@ -104,8 +104,9 @@ export function applyCorsHeaders(
|
|
|
104
104
|
/**
|
|
105
105
|
* Create CORS hooks for preflight and regular responses.
|
|
106
106
|
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
107
|
+
* CORS preflight requests short-circuit with a 204 response. Explicit
|
|
108
|
+
* `OPTIONS` routes without `Access-Control-Request-Method` continue through the
|
|
109
|
+
* normal route pipeline. All responses are decorated in `beforeSend`.
|
|
109
110
|
*/
|
|
110
111
|
export function createCorsHooks<Ctx>(config: CorsConfig): ServerHook<Ctx> {
|
|
111
112
|
const corsConfig = resolveCorsConfig(config);
|
|
@@ -113,7 +114,13 @@ export function createCorsHooks<Ctx>(config: CorsConfig): ServerHook<Ctx> {
|
|
|
113
114
|
return {
|
|
114
115
|
name: "cors",
|
|
115
116
|
onRequest: ({ req }) => {
|
|
116
|
-
if (
|
|
117
|
+
if (
|
|
118
|
+
req.method !== "OPTIONS" ||
|
|
119
|
+
!req.headers.get("Origin") ||
|
|
120
|
+
!req.headers.get("Access-Control-Request-Method")
|
|
121
|
+
) {
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
117
124
|
const headers: Record<string, string> = {};
|
|
118
125
|
applyCorsHeaders(headers, req, corsConfig);
|
|
119
126
|
return {
|