@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.
Files changed (145) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +135 -19
  3. package/dist/client/client.d.ts.map +1 -1
  4. package/dist/client/client.js +6 -28
  5. package/dist/client/client.js.map +1 -1
  6. package/dist/contracts/contract-builder.d.ts +7 -0
  7. package/dist/contracts/contract-builder.d.ts.map +1 -1
  8. package/dist/contracts/contract-builder.js +22 -0
  9. package/dist/contracts/contract-builder.js.map +1 -1
  10. package/dist/contracts/contract-group.d.ts +7 -0
  11. package/dist/contracts/contract-group.d.ts.map +1 -1
  12. package/dist/contracts/contract-group.js +14 -0
  13. package/dist/contracts/contract-group.js.map +1 -1
  14. package/dist/contracts/index.d.ts +4 -0
  15. package/dist/contracts/index.d.ts.map +1 -1
  16. package/dist/contracts/index.js +4 -0
  17. package/dist/contracts/index.js.map +1 -1
  18. package/dist/contracts/lifecycle.d.ts +40 -0
  19. package/dist/contracts/lifecycle.d.ts.map +1 -0
  20. package/dist/contracts/lifecycle.js +174 -0
  21. package/dist/contracts/lifecycle.js.map +1 -0
  22. package/dist/contracts/types.d.ts +5 -0
  23. package/dist/contracts/types.d.ts.map +1 -1
  24. package/dist/contracts/types.js.map +1 -1
  25. package/dist/error-reporting/index.d.ts +42 -1
  26. package/dist/error-reporting/index.d.ts.map +1 -1
  27. package/dist/error-reporting/index.js +60 -0
  28. package/dist/error-reporting/index.js.map +1 -1
  29. package/dist/error-reporting/internal.d.ts +11 -0
  30. package/dist/error-reporting/internal.d.ts.map +1 -0
  31. package/dist/error-reporting/internal.js +36 -0
  32. package/dist/error-reporting/internal.js.map +1 -0
  33. package/dist/events/index.d.ts +9 -4
  34. package/dist/events/index.d.ts.map +1 -1
  35. package/dist/events/index.js +5 -3
  36. package/dist/events/index.js.map +1 -1
  37. package/dist/idempotency/index.d.ts +25 -2
  38. package/dist/idempotency/index.d.ts.map +1 -1
  39. package/dist/idempotency/index.js +35 -5
  40. package/dist/idempotency/index.js.map +1 -1
  41. package/dist/jobs/index.d.ts +26 -4
  42. package/dist/jobs/index.d.ts.map +1 -1
  43. package/dist/jobs/index.js +51 -6
  44. package/dist/jobs/index.js.map +1 -1
  45. package/dist/locks/index.d.ts +19 -1
  46. package/dist/locks/index.d.ts.map +1 -1
  47. package/dist/locks/index.js +27 -12
  48. package/dist/locks/index.js.map +1 -1
  49. package/dist/openapi/index.d.ts +3 -1
  50. package/dist/openapi/index.d.ts.map +1 -1
  51. package/dist/openapi/index.js +14 -2
  52. package/dist/openapi/index.js.map +1 -1
  53. package/dist/outbox/index.d.ts +23 -4
  54. package/dist/outbox/index.d.ts.map +1 -1
  55. package/dist/outbox/index.js +71 -23
  56. package/dist/outbox/index.js.map +1 -1
  57. package/dist/ports/events.d.ts +5 -5
  58. package/dist/ports/events.d.ts.map +1 -1
  59. package/dist/ports/index.d.ts +4 -4
  60. package/dist/ports/unbound.js +1 -1
  61. package/dist/ports/unbound.js.map +1 -1
  62. package/dist/ports/unit-of-work.d.ts +4 -1
  63. package/dist/ports/unit-of-work.d.ts.map +1 -1
  64. package/dist/ports/unit-of-work.js +4 -3
  65. package/dist/ports/unit-of-work.js.map +1 -1
  66. package/dist/query-codec.d.ts +3 -0
  67. package/dist/query-codec.d.ts.map +1 -0
  68. package/dist/query-codec.js +110 -0
  69. package/dist/query-codec.js.map +1 -0
  70. package/dist/server/hooks/cors.d.ts +3 -2
  71. package/dist/server/hooks/cors.d.ts.map +1 -1
  72. package/dist/server/hooks/cors.js +7 -3
  73. package/dist/server/hooks/cors.js.map +1 -1
  74. package/dist/server/hooks/error-reporting.d.ts +8 -0
  75. package/dist/server/hooks/error-reporting.d.ts.map +1 -1
  76. package/dist/server/hooks/error-reporting.js +55 -25
  77. package/dist/server/hooks/error-reporting.js.map +1 -1
  78. package/dist/server/hooks/idempotency.d.ts +1 -1
  79. package/dist/server/hooks/idempotency.d.ts.map +1 -1
  80. package/dist/server/hooks/idempotency.js +30 -9
  81. package/dist/server/hooks/idempotency.js.map +1 -1
  82. package/dist/server/request-executor.d.ts.map +1 -1
  83. package/dist/server/request-executor.js +7 -12
  84. package/dist/server/request-executor.js.map +1 -1
  85. package/dist/server/request-preparation.d.ts.map +1 -1
  86. package/dist/server/request-preparation.js +3 -1
  87. package/dist/server/request-preparation.js.map +1 -1
  88. package/dist/server/response-finalization.d.ts +5 -1
  89. package/dist/server/response-finalization.d.ts.map +1 -1
  90. package/dist/server/response-finalization.js +66 -2
  91. package/dist/server/response-finalization.js.map +1 -1
  92. package/dist/server/server.d.ts.map +1 -1
  93. package/dist/server/server.js +10 -1
  94. package/dist/server/server.js.map +1 -1
  95. package/dist/testing/index.d.ts +5 -5
  96. package/dist/testing/index.d.ts.map +1 -1
  97. package/dist/testing/index.js +9 -4
  98. package/dist/testing/index.js.map +1 -1
  99. package/dist/tracing/index.d.ts +27 -0
  100. package/dist/tracing/index.d.ts.map +1 -1
  101. package/dist/tracing/index.js +96 -0
  102. package/dist/tracing/index.js.map +1 -1
  103. package/dist/uploads/index.d.ts.map +1 -1
  104. package/dist/uploads/index.js +39 -6
  105. package/dist/uploads/index.js.map +1 -1
  106. package/dist/webhooks/index.d.ts +2 -1
  107. package/dist/webhooks/index.d.ts.map +1 -1
  108. package/dist/webhooks/index.js +25 -1
  109. package/dist/webhooks/index.js.map +1 -1
  110. package/package.json +1 -1
  111. package/skills/app-architecture/SKILL.md +19 -1
  112. package/src/client/client.ts +6 -31
  113. package/src/contracts/contract-builder.ts +38 -0
  114. package/src/contracts/contract-group.ts +25 -0
  115. package/src/contracts/index.ts +8 -0
  116. package/src/contracts/lifecycle.ts +236 -0
  117. package/src/contracts/types.ts +5 -0
  118. package/src/error-reporting/index.ts +111 -0
  119. package/src/error-reporting/internal.ts +47 -0
  120. package/src/events/index.ts +20 -4
  121. package/src/idempotency/index.ts +81 -7
  122. package/src/jobs/index.ts +85 -6
  123. package/src/locks/index.ts +65 -16
  124. package/src/openapi/index.ts +22 -2
  125. package/src/outbox/index.ts +119 -27
  126. package/src/ports/events.ts +8 -1
  127. package/src/ports/index.ts +4 -4
  128. package/src/ports/unbound.ts +1 -1
  129. package/src/ports/unit-of-work.ts +10 -3
  130. package/src/query-codec.ts +130 -0
  131. package/src/server/hooks/cors.ts +10 -3
  132. package/src/server/hooks/error-reporting.ts +79 -23
  133. package/src/server/hooks/idempotency.ts +36 -10
  134. package/src/server/request-executor.ts +10 -13
  135. package/src/server/request-preparation.ts +4 -2
  136. package/src/server/response-finalization.ts +94 -5
  137. package/src/server/server.ts +15 -1
  138. package/src/testing/index.ts +25 -14
  139. package/src/tracing/index.ts +120 -0
  140. package/src/uploads/index.ts +44 -6
  141. package/src/webhooks/index.ts +34 -2
  142. package/dist/domain/events.d.ts +0 -44
  143. package/dist/domain/events.d.ts.map +0 -1
  144. package/dist/domain/events.js +0 -24
  145. package/dist/domain/events.js.map +0 -1
@@ -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
- ): BufferedDomainEventRecorder {
1315
+ ): DomainEventRecorderPort {
1276
1316
  return {
1277
- async record(event, payload) {
1278
- await enqueueEvent(outbox, event, payload, options);
1279
- },
1280
- entries() {
1281
- return [];
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, options);
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(event, payload);
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
- await deliverOutboxMessage(options, message);
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
- await options.outbox.markFailed({
1496
- id: message.id,
1497
- claimToken: message.claimToken,
1498
- error,
1499
- deadLetter,
1500
- now,
1501
- retryAt: deadLetter
1502
- ? undefined
1503
- : new Date(now.getTime() + retryDelayMs),
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,
@@ -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: (payload: InferEventPayload<E>) => Promise<void> | void,
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
  }
@@ -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 appPorts`.
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 appPorts = definePorts({
89
+ * const initialPorts = definePorts({
90
90
  * db: dbAdapter,
91
91
  * mailer: mailerAdapter,
92
92
  * });
93
- * export type AppPorts = typeof appPorts;
93
+ * export type AppPorts = typeof initialPorts;
94
94
  *
95
95
  * // Deferred form: providers contribute the rest at startup.
96
- * export const appPorts = definePorts<AppPorts>()({
96
+ * export const initialPorts = definePorts<AppPorts>()({
97
97
  * bound: { gate },
98
98
  * deferred: ["db", "mailer", "storage"],
99
99
  * });
@@ -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/app-ports.ts."
16
+ "(server/providers.ts) or bind it in infra/port-wiring.ts."
17
17
  );
18
18
  }
19
19
 
@@ -1,4 +1,7 @@
1
- import { parseEventPayload } from "../events/index.js";
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
+ }
@@ -104,8 +104,9 @@ export function applyCorsHeaders(
104
104
  /**
105
105
  * Create CORS hooks for preflight and regular responses.
106
106
  *
107
- * `OPTIONS` requests short-circuit with a 204 response. All other responses are
108
- * decorated in `beforeSend`.
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 (req.method !== "OPTIONS") return undefined;
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 {