@beignet/core 0.0.50 → 0.0.51

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 (70) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +53 -26
  3. package/dist/application/index.d.ts +5 -5
  4. package/dist/application/index.d.ts.map +1 -1
  5. package/dist/application/index.js +5 -4
  6. package/dist/application/index.js.map +1 -1
  7. package/dist/events/index.d.ts.map +1 -1
  8. package/dist/events/index.js +6 -3
  9. package/dist/events/index.js.map +1 -1
  10. package/dist/events/payload-state.d.ts +4 -0
  11. package/dist/events/payload-state.d.ts.map +1 -0
  12. package/dist/events/payload-state.js +11 -0
  13. package/dist/events/payload-state.js.map +1 -0
  14. package/dist/locks/index.d.ts.map +1 -1
  15. package/dist/locks/index.js +0 -4
  16. package/dist/locks/index.js.map +1 -1
  17. package/dist/outbox/index.d.ts +163 -15
  18. package/dist/outbox/index.d.ts.map +1 -1
  19. package/dist/outbox/index.js +1009 -152
  20. package/dist/outbox/index.js.map +1 -1
  21. package/dist/payments/index.d.ts.map +1 -1
  22. package/dist/payments/index.js +0 -4
  23. package/dist/payments/index.js.map +1 -1
  24. package/dist/ports/index.d.ts +1 -1
  25. package/dist/ports/index.d.ts.map +1 -1
  26. package/dist/ports/index.js.map +1 -1
  27. package/dist/ports/testing.d.ts +13 -0
  28. package/dist/ports/testing.d.ts.map +1 -1
  29. package/dist/ports/testing.js +12 -0
  30. package/dist/ports/testing.js.map +1 -1
  31. package/dist/ports/unit-of-work.d.ts +9 -7
  32. package/dist/ports/unit-of-work.d.ts.map +1 -1
  33. package/dist/ports/unit-of-work.js +16 -6
  34. package/dist/ports/unit-of-work.js.map +1 -1
  35. package/dist/providers/index.d.ts +1 -1
  36. package/dist/providers/index.d.ts.map +1 -1
  37. package/dist/providers/index.js.map +1 -1
  38. package/dist/providers/provider.d.ts +0 -38
  39. package/dist/providers/provider.d.ts.map +1 -1
  40. package/dist/providers/provider.js.map +1 -1
  41. package/dist/search/index.d.ts.map +1 -1
  42. package/dist/search/index.js +0 -4
  43. package/dist/search/index.js.map +1 -1
  44. package/dist/server/hooks/cors.d.ts +5 -0
  45. package/dist/server/hooks/cors.d.ts.map +1 -1
  46. package/dist/server/hooks/cors.js +29 -1
  47. package/dist/server/hooks/cors.js.map +1 -1
  48. package/dist/server/server.d.ts.map +1 -1
  49. package/dist/server/server.js +34 -9
  50. package/dist/server/server.js.map +1 -1
  51. package/dist/testing/index.d.ts.map +1 -1
  52. package/dist/testing/index.js +12 -6
  53. package/dist/testing/index.js.map +1 -1
  54. package/package.json +1 -1
  55. package/skills/app-architecture/SKILL.md +14 -0
  56. package/src/application/index.ts +18 -7
  57. package/src/events/index.ts +9 -6
  58. package/src/events/payload-state.ts +24 -0
  59. package/src/locks/index.ts +0 -4
  60. package/src/outbox/index.ts +1382 -175
  61. package/src/payments/index.ts +0 -4
  62. package/src/ports/index.ts +3 -0
  63. package/src/ports/testing.ts +31 -0
  64. package/src/ports/unit-of-work.ts +28 -14
  65. package/src/providers/index.ts +0 -1
  66. package/src/providers/provider.ts +0 -40
  67. package/src/search/index.ts +0 -4
  68. package/src/server/hooks/cors.ts +42 -0
  69. package/src/server/server.ts +52 -19
  70. package/src/testing/index.ts +20 -9
@@ -624,10 +624,6 @@ export function createMemoryPaymentsProvider(
624
624
 
625
625
  return createProvider({
626
626
  name,
627
- metadata: {
628
- ports: ["payments"],
629
- watchers: ["payments"],
630
- },
631
627
  setup({ ports }) {
632
628
  const instrumentation = createProviderInstrumentation(ports, {
633
629
  providerName: name,
@@ -283,6 +283,7 @@ export type {
283
283
  ClaimedOutboxMessage,
284
284
  OutboxAdminPort,
285
285
  OutboxClaimBatchOptions,
286
+ OutboxClaimBatchResult,
286
287
  OutboxCountMessagesOptions,
287
288
  OutboxDeleteResult,
288
289
  OutboxEnqueueInput,
@@ -296,6 +297,8 @@ export type {
296
297
  OutboxPort,
297
298
  OutboxPruneDeliveredInput,
298
299
  OutboxPurgeDeadLetteredInput,
300
+ OutboxRenewClaimInput,
301
+ OutboxRenewClaimResult,
299
302
  OutboxRequeueMessageInput,
300
303
  } from "../outbox/index.js";
301
304
  /**
@@ -953,6 +953,19 @@ export interface OutboxDrainResultExpectation {
953
953
  * Expected dead-lettered count.
954
954
  */
955
955
  deadLettered?: number;
956
+ /**
957
+ * Expected count of eligible messages dead-lettered after exhausting their
958
+ * claim attempt budget.
959
+ */
960
+ abandonedDeadLettered?: number;
961
+ /**
962
+ * Expected count of messages whose durable settlement remained unknown.
963
+ */
964
+ settlementFailed?: number;
965
+ /**
966
+ * Expected count of messages whose active claim could not be confirmed.
967
+ */
968
+ leaseLost?: number;
956
969
  }
957
970
 
958
971
  /**
@@ -2421,6 +2434,24 @@ function outboxDrainResultMatches(
2421
2434
  ) {
2422
2435
  return false;
2423
2436
  }
2437
+ if (
2438
+ expectation.abandonedDeadLettered !== undefined &&
2439
+ result.abandonedDeadLettered !== expectation.abandonedDeadLettered
2440
+ ) {
2441
+ return false;
2442
+ }
2443
+ if (
2444
+ expectation.settlementFailed !== undefined &&
2445
+ result.settlementFailed !== expectation.settlementFailed
2446
+ ) {
2447
+ return false;
2448
+ }
2449
+ if (
2450
+ expectation.leaseLost !== undefined &&
2451
+ result.leaseLost !== expectation.leaseLost
2452
+ ) {
2453
+ return false;
2454
+ }
2424
2455
 
2425
2456
  return true;
2426
2457
  }
@@ -2,6 +2,10 @@ import {
2
2
  type EventPublishOptions,
3
3
  parseEventPayload,
4
4
  } from "../events/index.js";
5
+ import {
6
+ isEventPayloadParsed,
7
+ markEventPayloadParsed,
8
+ } from "../events/payload-state.js";
5
9
  import type {
6
10
  DomainEventDef,
7
11
  EventBusPort,
@@ -83,17 +87,18 @@ export interface RecordedDomainEvent {
83
87
  /**
84
88
  * Event definition used to validate the payload before publishing.
85
89
  */
86
- event: DomainEventDef;
90
+ readonly event: DomainEventDef;
87
91
  /**
88
92
  * Stable event name.
89
93
  */
90
- eventName: string;
94
+ readonly eventName: string;
91
95
  /**
92
- * Unparsed payload recorded during the transaction.
96
+ * Recorded payload. Use-case helpers store parsed schema output; direct
97
+ * recorder calls are validated when the buffer is flushed.
93
98
  */
94
- payload: unknown;
99
+ readonly payload: unknown;
95
100
  /** Optional metadata propagated when the event is flushed. */
96
- options?: EventPublishOptions;
101
+ readonly options?: EventPublishOptions;
97
102
  }
98
103
 
99
104
  /**
@@ -119,7 +124,7 @@ export interface DomainEventRecorderPort {
119
124
  */
120
125
  export interface BufferedDomainEventRecorder extends DomainEventRecorderPort {
121
126
  /**
122
- * Return recorded events without clearing them.
127
+ * Return a snapshot of recorded events without clearing them.
123
128
  */
124
129
  entries(): readonly RecordedDomainEvent[];
125
130
  /**
@@ -127,7 +132,8 @@ export interface BufferedDomainEventRecorder extends DomainEventRecorderPort {
127
132
  */
128
133
  clear(): void;
129
134
  /**
130
- * Validate and publish all recorded events to an event bus in FIFO order.
135
+ * Publish recorded events in FIFO order, validating entries recorded
136
+ * directly without the use-case event helper.
131
137
  */
132
138
  flush(eventBus: EventBusPort): Promise<void>;
133
139
  }
@@ -223,19 +229,25 @@ export function createObservedUnitOfWork<TxPorts>(
223
229
  */
224
230
  export function createDomainEventRecorder(): BufferedDomainEventRecorder {
225
231
  const records: RecordedDomainEvent[] = [];
232
+ const parsedRecords = new WeakSet<RecordedDomainEvent>();
226
233
 
227
234
  return {
228
235
  record(event, payload, options) {
229
- records.push({
236
+ const record: RecordedDomainEvent = {
230
237
  event,
231
238
  eventName: event.name,
232
239
  payload,
233
- ...(options ? { options } : {}),
234
- });
240
+ ...(options?.trace ? { options: { trace: options.trace } } : {}),
241
+ };
242
+ if (isEventPayloadParsed(options)) parsedRecords.add(record);
243
+ records.push(record);
235
244
  },
236
245
 
237
246
  entries() {
238
- return records;
247
+ return records.map((record) => ({
248
+ ...record,
249
+ ...(record.options ? { options: { ...record.options } } : {}),
250
+ }));
239
251
  },
240
252
 
241
253
  clear() {
@@ -245,11 +257,13 @@ export function createDomainEventRecorder(): BufferedDomainEventRecorder {
245
257
  async flush(eventBus) {
246
258
  while (records.length > 0) {
247
259
  const record = records[0];
248
- await parseEventPayload(record.event, record.payload);
260
+ const payload = parsedRecords.has(record)
261
+ ? record.payload
262
+ : await parseEventPayload(record.event, record.payload);
249
263
  await eventBus.publish(
250
264
  record.event,
251
- record.payload as never,
252
- record.options,
265
+ payload as never,
266
+ markEventPayloadParsed(record.options),
253
267
  );
254
268
  records.shift();
255
269
  }
@@ -51,5 +51,4 @@ export {
51
51
  type ProviderServiceContextFactory,
52
52
  type ProviderSetupResult,
53
53
  type ServiceProvider,
54
- type ServiceProviderMetadata,
55
54
  } from "./provider.js";
@@ -126,41 +126,6 @@ export type ProviderSetupResult<
126
126
  ): MaybePromise<void>;
127
127
  };
128
128
 
129
- /**
130
- * Static provider metadata used by docs and app-local tooling.
131
- *
132
- * Metadata is descriptive. It does not change provider setup, ordering, or
133
- * runtime port merging behavior.
134
- *
135
- * Reusable provider packages should also declare package-owned
136
- * `beignet.provider` metadata in package.json so external tooling can inspect
137
- * provider facts without importing runtime code.
138
- */
139
- export interface ServiceProviderMetadata {
140
- /**
141
- * Package that exports this provider, when it comes from a reusable package.
142
- */
143
- packageName?: string;
144
- /**
145
- * App port keys this provider contributes or replaces.
146
- */
147
- ports?: readonly string[];
148
- /**
149
- * App port keys this provider expects previous providers or base app ports to
150
- * have installed before setup runs.
151
- */
152
- requires?: readonly string[];
153
- /**
154
- * Environment variables this provider reads directly or via config loading.
155
- */
156
- env?: readonly string[];
157
- /**
158
- * Devtools watcher names this provider can emit through provider
159
- * instrumentation.
160
- */
161
- watchers?: readonly string[];
162
- }
163
-
164
129
  /**
165
130
  * A service provider that can extend or replace ports during app initialization.
166
131
  *
@@ -205,11 +170,6 @@ export interface ServiceProvider<
205
170
  */
206
171
  name: string;
207
172
 
208
- /**
209
- * Optional static metadata for docs and diagnostics.
210
- */
211
- metadata?: ServiceProviderMetadata;
212
-
213
173
  /**
214
174
  * Optional configuration definition.
215
175
  * If provided, the config will be loaded and validated before calling setup.
@@ -351,10 +351,6 @@ export function createMemorySearchProvider(
351
351
 
352
352
  return createProvider({
353
353
  name,
354
- metadata: {
355
- ports: ["search"],
356
- watchers: ["search"],
357
- },
358
354
  setup({ ports }) {
359
355
  const instrumentation = createProviderInstrumentation(ports, {
360
356
  providerName: name,
@@ -2,6 +2,7 @@
2
2
  * CORS hook utilities for @beignet/core/server
3
3
  */
4
4
 
5
+ import { BEIGNET_ERROR_OWNER_HEADER } from "../../contracts/types.js";
5
6
  import type {
6
7
  HttpRequestLike,
7
8
  HttpResponseHeaders,
@@ -25,6 +26,11 @@ export interface CorsConfig {
25
26
  * Allowed request headers.
26
27
  */
27
28
  headers?: string[];
29
+ /**
30
+ * Additional response headers browser JavaScript may read. Beignet always
31
+ * exposes its framework error-ownership header.
32
+ */
33
+ exposedHeaders?: string[];
28
34
  /**
29
35
  * Whether credentialed requests are allowed.
30
36
  */
@@ -35,6 +41,7 @@ const DEFAULT_CORS: Required<CorsConfig> = {
35
41
  origins: "*",
36
42
  methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
37
43
  headers: ["Content-Type", "Authorization"],
44
+ exposedHeaders: [],
38
45
  credentials: false,
39
46
  };
40
47
 
@@ -43,6 +50,7 @@ function resolveCorsConfig(config: CorsConfig): Required<CorsConfig> {
43
50
  origins: config.origins ?? DEFAULT_CORS.origins,
44
51
  methods: config.methods ?? DEFAULT_CORS.methods,
45
52
  headers: config.headers ?? DEFAULT_CORS.headers,
53
+ exposedHeaders: config.exposedHeaders ?? DEFAULT_CORS.exposedHeaders,
46
54
  credentials: config.credentials ?? DEFAULT_CORS.credentials,
47
55
  };
48
56
 
@@ -71,6 +79,35 @@ function appendVaryOrigin(headers: HttpResponseHeaders): void {
71
79
  headers[varyKey] = current ? `${current}, Origin` : "Origin";
72
80
  }
73
81
 
82
+ function appendCommaSeparatedHeader(
83
+ headers: HttpResponseHeaders,
84
+ name: string,
85
+ appended: readonly string[],
86
+ ): void {
87
+ const key =
88
+ Object.keys(headers).find(
89
+ (candidate) => candidate.toLowerCase() === name.toLowerCase(),
90
+ ) ?? name;
91
+ const existing = headers[key];
92
+ const values = [
93
+ ...(typeof existing === "string"
94
+ ? existing.split(",")
95
+ : (existing ?? []).flatMap((value) => value.split(","))),
96
+ ...appended,
97
+ ]
98
+ .map((value) => value.trim())
99
+ .filter(Boolean);
100
+ const seen = new Set<string>();
101
+ const unique = values.filter((value) => {
102
+ const normalized = value.toLowerCase();
103
+ if (seen.has(normalized)) return false;
104
+ seen.add(normalized);
105
+ return true;
106
+ });
107
+
108
+ headers[key] = unique.join(", ");
109
+ }
110
+
74
111
  /**
75
112
  * Apply CORS response headers to a mutable header record.
76
113
  *
@@ -86,6 +123,7 @@ export function applyCorsHeaders(
86
123
  origins,
87
124
  methods,
88
125
  headers: allowedHeaders,
126
+ exposedHeaders,
89
127
  credentials,
90
128
  } = resolveCorsConfig(corsConfig);
91
129
 
@@ -101,6 +139,10 @@ export function applyCorsHeaders(
101
139
 
102
140
  headers["Access-Control-Allow-Methods"] = methods.join(", ");
103
141
  headers["Access-Control-Allow-Headers"] = allowedHeaders.join(", ");
142
+ appendCommaSeparatedHeader(headers, "Access-Control-Expose-Headers", [
143
+ BEIGNET_ERROR_OWNER_HEADER,
144
+ ...exposedHeaders,
145
+ ]);
104
146
 
105
147
  if (credentials) {
106
148
  headers["Access-Control-Allow-Credentials"] = "true";
@@ -669,24 +669,6 @@ export async function createServer<
669
669
  registryNeedsSort = true;
670
670
  };
671
671
 
672
- const createBuilder = <C extends HttpContractConfig>(
673
- contract: C,
674
- shouldRegister: boolean,
675
- ): RouteBuilder<Ctx, C> => ({
676
- handle: (fn) => {
677
- const wrapped = buildHandler(
678
- options,
679
- finalPorts,
680
- contextRuntime,
681
- contract,
682
- fn,
683
- hooks,
684
- );
685
- if (shouldRegister) registerRoute(contract, fn);
686
- return wrapped;
687
- },
688
- });
689
-
690
672
  if (options.routes) {
691
673
  try {
692
674
  for (const route of options.routes) {
@@ -917,7 +899,58 @@ export async function createServer<
917
899
  api,
918
900
  route: (contractLike) => {
919
901
  const contract = resolveContract(contractLike);
920
- return createBuilder(contract, true);
902
+ return {
903
+ handle: (fn) => {
904
+ const wrapped = buildHandler(
905
+ options,
906
+ finalPorts,
907
+ contextRuntime,
908
+ contract,
909
+ fn,
910
+ hooks,
911
+ );
912
+ registerRoute(contract, fn);
913
+ const compiled = compilePath(contract.path);
914
+
915
+ return async (req: HttpRequestLike) => {
916
+ const method = req.method.toUpperCase();
917
+ let url: URL;
918
+ try {
919
+ url = parseHttpRequestUrl(req.url);
920
+ } catch (error) {
921
+ return await executeFallback(
922
+ {
923
+ contract: fallbackContract("invalidRequestUrl", method, "/"),
924
+ handler: async () => {
925
+ throw error;
926
+ },
927
+ },
928
+ req,
929
+ {},
930
+ );
931
+ }
932
+
933
+ const requestMethod = method;
934
+ const contractMethod = contract.method.toUpperCase();
935
+ const methodMatches =
936
+ requestMethod === contractMethod ||
937
+ (requestMethod === "HEAD" && contractMethod === "GET");
938
+ if (!methodMatches || !compiled.pattern.test(url.pathname)) {
939
+ const pathname = url.pathname || "/";
940
+ return await executeFallback(
941
+ {
942
+ contract: fallbackContract("notFound", method, pathname),
943
+ handler: notFoundHandler,
944
+ },
945
+ req,
946
+ {},
947
+ );
948
+ }
949
+
950
+ return await wrapped(req);
951
+ };
952
+ },
953
+ };
921
954
  },
922
955
  rawRoute: (init) => ({
923
956
  handle: (fn) => {
@@ -15,7 +15,7 @@ import {
15
15
  } from "../notifications/index.js";
16
16
  import {
17
17
  createMemoryOutbox,
18
- enqueueEvent,
18
+ createOutboxEventRecorder,
19
19
  type MemoryOutboxPort,
20
20
  } from "../outbox/index.js";
21
21
  import {
@@ -489,10 +489,17 @@ export function createTestPorts<
489
489
  outbox: MemoryOutboxPort;
490
490
  }
491
491
  ).outbox;
492
- for (const entry of events.entries()) {
493
- await enqueueEvent(outbox, entry.event, entry.payload);
494
- }
495
- events.clear();
492
+ const outboxRecorder = createOutboxEventRecorder(outbox);
493
+ await events.flush({
494
+ publish(event, payload, options) {
495
+ return outboxRecorder.record(event, payload, options);
496
+ },
497
+ subscribe() {
498
+ throw new Error(
499
+ "The test outbox event publisher does not support subscriptions.",
500
+ );
501
+ },
502
+ });
496
503
  }
497
504
  await transactionOptions?.afterCommit?.(tx);
498
505
  },
@@ -619,18 +626,19 @@ function completeTestPortOverrides<Ports extends AnyPorts>(
619
626
 
620
627
  function resolveBufferedTransactionEvents(
621
628
  tx: unknown,
622
- ): Pick<BufferedDomainEventRecorder, "entries" | "clear"> {
629
+ ): Pick<BufferedDomainEventRecorder, "entries" | "clear" | "flush"> {
623
630
  const events =
624
631
  tx && typeof tx === "object"
625
632
  ? (tx as { events?: unknown }).events
626
633
  : undefined;
627
634
  const recorder = events as
628
- | Partial<Pick<BufferedDomainEventRecorder, "entries" | "clear">>
635
+ | Partial<Pick<BufferedDomainEventRecorder, "entries" | "clear" | "flush">>
629
636
  | undefined;
630
637
  if (
631
638
  !recorder ||
632
639
  typeof recorder.entries !== "function" ||
633
- typeof recorder.clear !== "function"
640
+ typeof recorder.clear !== "function" ||
641
+ typeof recorder.flush !== "function"
634
642
  ) {
635
643
  throw new Error(
636
644
  "createTestPorts transaction.outbox requires tx.events to be a buffered domain event recorder. " +
@@ -638,7 +646,10 @@ function resolveBufferedTransactionEvents(
638
646
  );
639
647
  }
640
648
 
641
- return recorder as Pick<BufferedDomainEventRecorder, "entries" | "clear">;
649
+ return recorder as Pick<
650
+ BufferedDomainEventRecorder,
651
+ "entries" | "clear" | "flush"
652
+ >;
642
653
  }
643
654
 
644
655
  /**