@beignet/core 0.0.51 → 0.0.53
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 +27 -0
- package/README.md +112 -20
- package/dist/application/index.d.ts +1 -1
- package/dist/application/index.d.ts.map +1 -1
- package/dist/application/index.js +5 -2
- package/dist/application/index.js.map +1 -1
- package/dist/encryption/index.d.ts +40 -0
- package/dist/encryption/index.d.ts.map +1 -0
- package/dist/encryption/index.js +134 -0
- package/dist/encryption/index.js.map +1 -0
- package/dist/events/index.d.ts +26 -2
- package/dist/events/index.d.ts.map +1 -1
- package/dist/events/index.js +84 -10
- package/dist/events/index.js.map +1 -1
- package/dist/events/payload-state.d.ts +14 -2
- package/dist/events/payload-state.d.ts.map +1 -1
- package/dist/events/payload-state.js +116 -4
- package/dist/events/payload-state.js.map +1 -1
- package/dist/events/transport.d.ts +25 -0
- package/dist/events/transport.d.ts.map +1 -0
- package/dist/events/transport.js +192 -0
- package/dist/events/transport.js.map +1 -0
- package/dist/openapi/index.d.ts.map +1 -1
- package/dist/openapi/index.js +27 -4
- package/dist/openapi/index.js.map +1 -1
- package/dist/outbox/index.d.ts.map +1 -1
- package/dist/outbox/index.js +10 -13
- package/dist/outbox/index.js.map +1 -1
- package/dist/ports/events.d.ts +4 -1
- package/dist/ports/events.d.ts.map +1 -1
- package/dist/ports/storage.d.ts +7 -0
- package/dist/ports/storage.d.ts.map +1 -1
- package/dist/ports/storage.js +4 -0
- package/dist/ports/storage.js.map +1 -1
- package/dist/ports/testing.d.ts +3 -2
- package/dist/ports/testing.d.ts.map +1 -1
- package/dist/ports/testing.js +7 -4
- package/dist/ports/testing.js.map +1 -1
- package/dist/ports/unit-of-work.d.ts +4 -4
- package/dist/ports/unit-of-work.d.ts.map +1 -1
- package/dist/ports/unit-of-work.js +8 -9
- package/dist/ports/unit-of-work.js.map +1 -1
- package/dist/search/index.js +2 -2
- package/dist/search/index.js.map +1 -1
- package/dist/server/instrumentation.d.ts +5 -5
- package/dist/server/instrumentation.d.ts.map +1 -1
- package/dist/server/instrumentation.js +3 -4
- package/dist/server/instrumentation.js.map +1 -1
- package/dist/server/request-executor.d.ts.map +1 -1
- package/dist/server/request-executor.js +13 -0
- package/dist/server/request-executor.js.map +1 -1
- package/dist/server/response-finalization.d.ts.map +1 -1
- package/dist/server/response-finalization.js +25 -13
- package/dist/server/response-finalization.js.map +1 -1
- package/dist/server/server.d.ts +8 -5
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +1 -3
- package/dist/server/server.js.map +1 -1
- package/dist/server/use-case-route.d.ts +82 -17
- package/dist/server/use-case-route.d.ts.map +1 -1
- package/dist/server/use-case-route.js +40 -4
- package/dist/server/use-case-route.js.map +1 -1
- package/dist/uploads/index.d.ts.map +1 -1
- package/dist/uploads/index.js +37 -16
- package/dist/uploads/index.js.map +1 -1
- package/package.json +6 -2
- package/skills/app-architecture/SKILL.md +29 -6
- package/src/application/index.ts +24 -3
- package/src/encryption/index.ts +198 -0
- package/src/events/index.ts +137 -10
- package/src/events/payload-state.ts +205 -6
- package/src/events/transport.ts +242 -0
- package/src/openapi/index.ts +41 -3
- package/src/outbox/index.ts +26 -18
- package/src/ports/events.ts +4 -1
- package/src/ports/storage.ts +10 -0
- package/src/ports/testing.ts +11 -4
- package/src/ports/unit-of-work.ts +20 -16
- package/src/search/index.ts +2 -2
- package/src/server/instrumentation.ts +10 -8
- package/src/server/request-executor.ts +19 -0
- package/src/server/response-finalization.ts +33 -15
- package/src/server/server.ts +9 -8
- package/src/server/use-case-route.ts +234 -27
- package/src/uploads/index.ts +34 -16
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/** Canonical JSON value that can cross an event transport boundary. */
|
|
2
|
+
export type EventTransportValue =
|
|
3
|
+
| null
|
|
4
|
+
| string
|
|
5
|
+
| number
|
|
6
|
+
| boolean
|
|
7
|
+
| readonly EventTransportValue[]
|
|
8
|
+
| { readonly [key: string]: EventTransportValue };
|
|
9
|
+
|
|
10
|
+
/** Why an event payload cannot safely cross a serialized transport. */
|
|
11
|
+
export type EventTransportErrorReason = "not-json-safe" | "not-stable";
|
|
12
|
+
|
|
13
|
+
/** Error thrown when an event payload cannot survive canonical JSON transport. */
|
|
14
|
+
export class EventTransportError extends Error {
|
|
15
|
+
/** Stable event name. */
|
|
16
|
+
readonly eventName: string;
|
|
17
|
+
/** Whether the value is not JSON-safe or changes under repeated parsing. */
|
|
18
|
+
readonly reason: EventTransportErrorReason;
|
|
19
|
+
/** Payload path associated with a JSON-safety failure. */
|
|
20
|
+
readonly path?: string;
|
|
21
|
+
|
|
22
|
+
constructor(args: {
|
|
23
|
+
eventName: string;
|
|
24
|
+
reason: EventTransportErrorReason;
|
|
25
|
+
message: string;
|
|
26
|
+
path?: string;
|
|
27
|
+
cause?: unknown;
|
|
28
|
+
}) {
|
|
29
|
+
super(`Event "${args.eventName}" payload ${args.message}`, {
|
|
30
|
+
cause: args.cause,
|
|
31
|
+
});
|
|
32
|
+
this.name = "EventTransportError";
|
|
33
|
+
this.eventName = args.eventName;
|
|
34
|
+
this.reason = args.reason;
|
|
35
|
+
this.path = args.path;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function childTransportPath(path: string, key: string): string {
|
|
40
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key)
|
|
41
|
+
? `${path}.${key}`
|
|
42
|
+
: `${path}[${JSON.stringify(key)}]`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function throwJsonSafetyError(args: {
|
|
46
|
+
eventName: string;
|
|
47
|
+
path: string;
|
|
48
|
+
message: string;
|
|
49
|
+
}): never {
|
|
50
|
+
throw new EventTransportError({
|
|
51
|
+
eventName: args.eventName,
|
|
52
|
+
reason: "not-json-safe",
|
|
53
|
+
path: args.path,
|
|
54
|
+
message: `is not transport-safe at ${args.path}: ${args.message}`,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function toEventTransportValue(
|
|
59
|
+
eventName: string,
|
|
60
|
+
value: unknown,
|
|
61
|
+
path = "payload",
|
|
62
|
+
seen: WeakSet<object> = new WeakSet(),
|
|
63
|
+
): EventTransportValue {
|
|
64
|
+
if (value === null) return null;
|
|
65
|
+
|
|
66
|
+
if (typeof value === "string" || typeof value === "boolean") return value;
|
|
67
|
+
if (typeof value === "number") {
|
|
68
|
+
if (!Number.isFinite(value)) {
|
|
69
|
+
return throwJsonSafetyError({
|
|
70
|
+
eventName,
|
|
71
|
+
path,
|
|
72
|
+
message: "numbers must be finite.",
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
return Object.is(value, -0) ? 0 : value;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (typeof value !== "object") {
|
|
79
|
+
return throwJsonSafetyError({
|
|
80
|
+
eventName,
|
|
81
|
+
path,
|
|
82
|
+
message: `received ${typeof value}; use null, strings, finite numbers, booleans, arrays, or plain objects.`,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
if (value instanceof Date) {
|
|
88
|
+
return throwJsonSafetyError({
|
|
89
|
+
eventName,
|
|
90
|
+
path,
|
|
91
|
+
message: "Date values are not supported; use an ISO string or number.",
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (seen.has(value)) {
|
|
96
|
+
return throwJsonSafetyError({
|
|
97
|
+
eventName,
|
|
98
|
+
path,
|
|
99
|
+
message: "circular references are not supported.",
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
seen.add(value);
|
|
104
|
+
if (Array.isArray(value)) {
|
|
105
|
+
const output: EventTransportValue[] = [];
|
|
106
|
+
const ownKeys = Reflect.ownKeys(value);
|
|
107
|
+
const hasUnsupportedKey = ownKeys.some((key) => {
|
|
108
|
+
if (key === "length") return false;
|
|
109
|
+
if (typeof key !== "string") return true;
|
|
110
|
+
const index = Number(key);
|
|
111
|
+
return (
|
|
112
|
+
!Number.isInteger(index) ||
|
|
113
|
+
index < 0 ||
|
|
114
|
+
index >= value.length ||
|
|
115
|
+
String(index) !== key
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
if (hasUnsupportedKey) {
|
|
119
|
+
return throwJsonSafetyError({
|
|
120
|
+
eventName,
|
|
121
|
+
path,
|
|
122
|
+
message: "arrays cannot contain symbol or custom properties.",
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
if (ownKeys.length !== value.length + 1) {
|
|
126
|
+
return throwJsonSafetyError({
|
|
127
|
+
eventName,
|
|
128
|
+
path,
|
|
129
|
+
message: "sparse arrays are not supported.",
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
133
|
+
const key = String(index);
|
|
134
|
+
const itemPath = `${path}[${index}]`;
|
|
135
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
136
|
+
if (!descriptor) {
|
|
137
|
+
return throwJsonSafetyError({
|
|
138
|
+
eventName,
|
|
139
|
+
path: itemPath,
|
|
140
|
+
message: "sparse arrays are not supported.",
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
if (!("value" in descriptor) || !descriptor.enumerable) {
|
|
144
|
+
return throwJsonSafetyError({
|
|
145
|
+
eventName,
|
|
146
|
+
path: itemPath,
|
|
147
|
+
message: "array items must be enumerable data properties.",
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
output.push(
|
|
151
|
+
toEventTransportValue(eventName, descriptor.value, itemPath, seen),
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
return output;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const prototype = Object.getPrototypeOf(value);
|
|
158
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
159
|
+
return throwJsonSafetyError({
|
|
160
|
+
eventName,
|
|
161
|
+
path,
|
|
162
|
+
message: "objects must be plain objects.",
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const output = {} as Record<string, EventTransportValue>;
|
|
167
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
168
|
+
if (typeof key !== "string") {
|
|
169
|
+
return throwJsonSafetyError({
|
|
170
|
+
eventName,
|
|
171
|
+
path,
|
|
172
|
+
message: "symbol properties are not supported.",
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
const propertyPath = childTransportPath(path, key);
|
|
176
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
177
|
+
if (!descriptor || !("value" in descriptor) || !descriptor.enumerable) {
|
|
178
|
+
return throwJsonSafetyError({
|
|
179
|
+
eventName,
|
|
180
|
+
path: propertyPath,
|
|
181
|
+
message: "properties must be enumerable data properties.",
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
Object.defineProperty(output, key, {
|
|
185
|
+
configurable: true,
|
|
186
|
+
enumerable: true,
|
|
187
|
+
writable: true,
|
|
188
|
+
value: toEventTransportValue(
|
|
189
|
+
eventName,
|
|
190
|
+
descriptor.value,
|
|
191
|
+
propertyPath,
|
|
192
|
+
seen,
|
|
193
|
+
),
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
return output;
|
|
197
|
+
} catch (error) {
|
|
198
|
+
if (error instanceof EventTransportError) throw error;
|
|
199
|
+
throw new EventTransportError({
|
|
200
|
+
eventName,
|
|
201
|
+
reason: "not-json-safe",
|
|
202
|
+
path,
|
|
203
|
+
message: `could not be inspected as transport data at ${path}.`,
|
|
204
|
+
cause: error,
|
|
205
|
+
});
|
|
206
|
+
} finally {
|
|
207
|
+
seen.delete(value);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function eventTransportValuesEqual(
|
|
212
|
+
left: EventTransportValue,
|
|
213
|
+
right: EventTransportValue,
|
|
214
|
+
): boolean {
|
|
215
|
+
if (left === right) return true;
|
|
216
|
+
if (left === null || right === null || typeof left !== typeof right) {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
if (Array.isArray(left) || Array.isArray(right)) {
|
|
220
|
+
if (!Array.isArray(left) || !Array.isArray(right)) return false;
|
|
221
|
+
return (
|
|
222
|
+
left.length === right.length &&
|
|
223
|
+
left.every((value, index) =>
|
|
224
|
+
eventTransportValuesEqual(value, right[index]),
|
|
225
|
+
)
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
if (typeof left !== "object" || typeof right !== "object") return false;
|
|
229
|
+
|
|
230
|
+
const leftRecord = left as Readonly<Record<string, EventTransportValue>>;
|
|
231
|
+
const rightRecord = right as Readonly<Record<string, EventTransportValue>>;
|
|
232
|
+
const leftKeys = Object.keys(leftRecord).sort();
|
|
233
|
+
const rightKeys = Object.keys(rightRecord).sort();
|
|
234
|
+
return (
|
|
235
|
+
leftKeys.length === rightKeys.length &&
|
|
236
|
+
leftKeys.every(
|
|
237
|
+
(key, index) =>
|
|
238
|
+
key === rightKeys[index] &&
|
|
239
|
+
eventTransportValuesEqual(leftRecord[key], rightRecord[key]),
|
|
240
|
+
)
|
|
241
|
+
);
|
|
242
|
+
}
|
package/src/openapi/index.ts
CHANGED
|
@@ -436,20 +436,58 @@ export function contractsToOpenAPI(
|
|
|
436
436
|
],
|
|
437
437
|
};
|
|
438
438
|
const operationIds = new Map<string, string>();
|
|
439
|
+
const operations = new Map<
|
|
440
|
+
string,
|
|
441
|
+
{ readonly contractName: string; readonly route: string }
|
|
442
|
+
>();
|
|
443
|
+
const pathShapes = new Map<
|
|
444
|
+
string,
|
|
445
|
+
{
|
|
446
|
+
readonly contractName: string;
|
|
447
|
+
readonly pathKey: string;
|
|
448
|
+
readonly route: string;
|
|
449
|
+
}
|
|
450
|
+
>();
|
|
439
451
|
|
|
440
452
|
for (const contract of contracts) {
|
|
441
453
|
const config = resolveContract(contract);
|
|
442
454
|
assertValidContractLifecycle(config);
|
|
455
|
+
const pathTemplate = parsePathTemplate(config.path);
|
|
456
|
+
const pathKey = pathTemplate.openApiPath;
|
|
457
|
+
const route = `${config.method.toUpperCase()} ${config.path}`;
|
|
458
|
+
const operationRoute = `${config.method.toUpperCase()} ${pathKey}`;
|
|
459
|
+
const conflictingOperation = operations.get(operationRoute);
|
|
460
|
+
if (conflictingOperation) {
|
|
461
|
+
throw new Error(
|
|
462
|
+
`Duplicate OpenAPI operation: ${operationRoute} is produced by both contract "${conflictingOperation.contractName}" (${conflictingOperation.route}) and contract "${config.name}" (${route}). Each method + normalized path combination must be unique within an OpenAPI document.`,
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
const conflictingPathShape = pathShapes.get(pathTemplate.shapeKey);
|
|
466
|
+
if (conflictingPathShape && conflictingPathShape.pathKey !== pathKey) {
|
|
467
|
+
throw new Error(
|
|
468
|
+
`Ambiguous OpenAPI path: ${pathKey} from contract "${config.name}" (${route}) conflicts with ${conflictingPathShape.pathKey} from contract "${conflictingPathShape.contractName}" (${conflictingPathShape.route}). Templated paths with the same hierarchy must use the same parameter names within an OpenAPI document.`,
|
|
469
|
+
);
|
|
470
|
+
}
|
|
443
471
|
const operationId = getContractOperationId(config);
|
|
444
|
-
const route = `${config.method} ${config.path}`;
|
|
445
472
|
const conflictingRoute = operationIds.get(operationId);
|
|
446
473
|
if (conflictingRoute) {
|
|
447
474
|
throw new Error(
|
|
448
475
|
`Duplicate OpenAPI operationId: "${operationId}" is used by both ${conflictingRoute} and ${route}. Operation IDs must be unique within an OpenAPI document.`,
|
|
449
476
|
);
|
|
450
477
|
}
|
|
478
|
+
operations.set(operationRoute, {
|
|
479
|
+
contractName: config.name,
|
|
480
|
+
route,
|
|
481
|
+
});
|
|
482
|
+
if (!conflictingPathShape) {
|
|
483
|
+
pathShapes.set(pathTemplate.shapeKey, {
|
|
484
|
+
contractName: config.name,
|
|
485
|
+
pathKey,
|
|
486
|
+
route,
|
|
487
|
+
});
|
|
488
|
+
}
|
|
451
489
|
operationIds.set(operationId, route);
|
|
452
|
-
addContractToPaths(config, paths, state);
|
|
490
|
+
addContractToPaths(config, pathKey, paths, state);
|
|
453
491
|
}
|
|
454
492
|
|
|
455
493
|
const openapi: OpenAPIObject = {
|
|
@@ -481,10 +519,10 @@ export function contractsToOpenAPI(
|
|
|
481
519
|
*/
|
|
482
520
|
function addContractToPaths(
|
|
483
521
|
contract: AnyContract,
|
|
522
|
+
pathKey: string,
|
|
484
523
|
paths: PathsObject,
|
|
485
524
|
state: GeneratorState,
|
|
486
525
|
): void {
|
|
487
|
-
const pathKey = parsePathTemplate(contract.path).openApiPath;
|
|
488
526
|
if (!paths[pathKey]) {
|
|
489
527
|
paths[pathKey] = {};
|
|
490
528
|
}
|
package/src/outbox/index.ts
CHANGED
|
@@ -8,13 +8,10 @@
|
|
|
8
8
|
import {
|
|
9
9
|
type EventPayloadDef,
|
|
10
10
|
type EventPublishOptions,
|
|
11
|
+
type EventTransportValue,
|
|
11
12
|
type InferEventPayload,
|
|
12
|
-
|
|
13
|
+
prepareEventPayloadForTransport,
|
|
13
14
|
} from "../events/index.js";
|
|
14
|
-
import {
|
|
15
|
-
isEventPayloadParsed,
|
|
16
|
-
markEventPayloadParsed,
|
|
17
|
-
} from "../events/payload-state.js";
|
|
18
15
|
import {
|
|
19
16
|
getJobRetryDelayMs,
|
|
20
17
|
getJobRetryMaxAttempts,
|
|
@@ -1508,14 +1505,19 @@ export async function enqueueEvent<E extends EventPayloadDef>(
|
|
|
1508
1505
|
payload: InferEventPayload<E>,
|
|
1509
1506
|
options: EnqueueTypedOutboxOptions = {},
|
|
1510
1507
|
): Promise<OutboxMessage> {
|
|
1511
|
-
const
|
|
1512
|
-
return await
|
|
1508
|
+
const prepared = await prepareEventPayloadForTransport(event, payload);
|
|
1509
|
+
return await enqueueTransportEvent(
|
|
1510
|
+
outbox,
|
|
1511
|
+
event,
|
|
1512
|
+
prepared.transportValue,
|
|
1513
|
+
options,
|
|
1514
|
+
);
|
|
1513
1515
|
}
|
|
1514
1516
|
|
|
1515
|
-
async function
|
|
1517
|
+
async function enqueueTransportEvent<E extends EventPayloadDef>(
|
|
1516
1518
|
outbox: OutboxPort,
|
|
1517
1519
|
event: E,
|
|
1518
|
-
payload:
|
|
1520
|
+
payload: EventTransportValue,
|
|
1519
1521
|
options: EnqueueTypedOutboxOptions,
|
|
1520
1522
|
): Promise<OutboxMessage> {
|
|
1521
1523
|
const trace =
|
|
@@ -1524,7 +1526,7 @@ async function enqueueParsedEvent<E extends EventPayloadDef>(
|
|
|
1524
1526
|
id: options.id,
|
|
1525
1527
|
kind: "event",
|
|
1526
1528
|
name: event.name,
|
|
1527
|
-
payload
|
|
1529
|
+
payload,
|
|
1528
1530
|
trace,
|
|
1529
1531
|
availableAt: options.availableAt,
|
|
1530
1532
|
maxAttempts: options.maxAttempts,
|
|
@@ -1563,12 +1565,14 @@ export function createOutboxEventRecorder(
|
|
|
1563
1565
|
): DomainEventRecorderPort {
|
|
1564
1566
|
return {
|
|
1565
1567
|
async record(event, payload, publishOptions) {
|
|
1566
|
-
const
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1568
|
+
const prepared = await prepareEventPayloadForTransport(
|
|
1569
|
+
event,
|
|
1570
|
+
payload,
|
|
1571
|
+
publishOptions,
|
|
1572
|
+
);
|
|
1573
|
+
await enqueueTransportEvent(outbox, event, prepared.transportValue, {
|
|
1570
1574
|
...options,
|
|
1571
|
-
trace: publishOptions
|
|
1575
|
+
trace: prepared.publishOptions.trace ?? options.trace,
|
|
1572
1576
|
});
|
|
1573
1577
|
},
|
|
1574
1578
|
};
|
|
@@ -1673,11 +1677,15 @@ async function deliverOutboxMessage(
|
|
|
1673
1677
|
);
|
|
1674
1678
|
}
|
|
1675
1679
|
|
|
1676
|
-
const
|
|
1680
|
+
const prepared = await prepareEventPayloadForTransport(
|
|
1681
|
+
event,
|
|
1682
|
+
message.payload,
|
|
1683
|
+
trace ? { trace } : undefined,
|
|
1684
|
+
);
|
|
1677
1685
|
await options.eventBus.publish(
|
|
1678
1686
|
event,
|
|
1679
|
-
payload,
|
|
1680
|
-
|
|
1687
|
+
prepared.payload,
|
|
1688
|
+
prepared.publishOptions,
|
|
1681
1689
|
);
|
|
1682
1690
|
return;
|
|
1683
1691
|
}
|
package/src/ports/events.ts
CHANGED
|
@@ -53,7 +53,10 @@ export type InferJobPayload<J extends JobDef> = InferContractJobPayload<J>;
|
|
|
53
53
|
* An EventBus port for publishing and subscribing to domain events.
|
|
54
54
|
*
|
|
55
55
|
* This interface defines a framework-agnostic contract for event-driven
|
|
56
|
-
* communication within your application.
|
|
56
|
+
* communication within your application. Implementations must prepare
|
|
57
|
+
* producer payloads with `prepareEventPayloadForTransport(...)` from
|
|
58
|
+
* `@beignet/core/events` so direct publication and provider swaps preserve the
|
|
59
|
+
* same canonical JSON semantics.
|
|
57
60
|
*
|
|
58
61
|
* @example
|
|
59
62
|
* ```ts
|
package/src/ports/storage.ts
CHANGED
|
@@ -82,6 +82,13 @@ export interface StorageObjectBody extends StorageObject {
|
|
|
82
82
|
* buffering them.
|
|
83
83
|
*/
|
|
84
84
|
readonly bodyUsed: boolean;
|
|
85
|
+
/**
|
|
86
|
+
* Discard an unread body and release any resources held by its provider.
|
|
87
|
+
*
|
|
88
|
+
* Calling this after consumption has started is a no-op. Callers that only
|
|
89
|
+
* inspect object metadata should cancel the body in a `finally` block.
|
|
90
|
+
*/
|
|
91
|
+
cancel(reason?: unknown): Promise<void>;
|
|
85
92
|
/**
|
|
86
93
|
* Consume the object as a readable byte stream.
|
|
87
94
|
*/
|
|
@@ -376,6 +383,9 @@ function createObjectBody(entry: MemoryStorageEntry): StorageObjectBody {
|
|
|
376
383
|
get bodyUsed() {
|
|
377
384
|
return bodyUsed;
|
|
378
385
|
},
|
|
386
|
+
async cancel() {
|
|
387
|
+
if (!bodyUsed) bodyUsed = true;
|
|
388
|
+
},
|
|
379
389
|
stream() {
|
|
380
390
|
return bytesToStream(consumeBytes());
|
|
381
391
|
},
|
package/src/ports/testing.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { prepareEventPayloadForTransport } from "../events/index.js";
|
|
1
2
|
import type {
|
|
2
3
|
MemoryIdempotencyEntry,
|
|
3
4
|
MemoryIdempotencyStore,
|
|
@@ -81,8 +82,9 @@ export interface RecordedEventExpectation {
|
|
|
81
82
|
/**
|
|
82
83
|
* Create a recording event bus for testing.
|
|
83
84
|
*
|
|
84
|
-
* This bus
|
|
85
|
-
*
|
|
85
|
+
* This bus validates canonical transport output and records published events
|
|
86
|
+
* asynchronously for later assertion. Await `publish(...)` before reading the
|
|
87
|
+
* captured log. Subscription is not supported and throws when called.
|
|
86
88
|
*
|
|
87
89
|
* @example
|
|
88
90
|
* ```ts
|
|
@@ -104,8 +106,13 @@ export function createRecordingEventBus(): {
|
|
|
104
106
|
const events: RecordedEvent[] = [];
|
|
105
107
|
|
|
106
108
|
const bus: EventBusPort = {
|
|
107
|
-
publish(event, payload) {
|
|
108
|
-
|
|
109
|
+
async publish(event, payload, options) {
|
|
110
|
+
const prepared = await prepareEventPayloadForTransport(
|
|
111
|
+
event,
|
|
112
|
+
payload,
|
|
113
|
+
options,
|
|
114
|
+
);
|
|
115
|
+
events.push({ name: event.name, payload: prepared.payload });
|
|
109
116
|
},
|
|
110
117
|
subscribe() {
|
|
111
118
|
throw new Error("Not implemented for recording bus");
|
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type EventPublishOptions,
|
|
3
|
-
|
|
3
|
+
prepareEventPayloadForTransport,
|
|
4
4
|
} from "../events/index.js";
|
|
5
|
-
import {
|
|
6
|
-
isEventPayloadParsed,
|
|
7
|
-
markEventPayloadParsed,
|
|
8
|
-
} from "../events/payload-state.js";
|
|
5
|
+
import { isEventPayloadParsed } from "../events/payload-state.js";
|
|
9
6
|
import type {
|
|
10
7
|
DomainEventDef,
|
|
11
8
|
EventBusPort,
|
|
@@ -93,8 +90,8 @@ export interface RecordedDomainEvent {
|
|
|
93
90
|
*/
|
|
94
91
|
readonly eventName: string;
|
|
95
92
|
/**
|
|
96
|
-
* Recorded payload. Use-case helpers store
|
|
97
|
-
* recorder calls are validated when the buffer is flushed.
|
|
93
|
+
* Recorded payload. Use-case helpers store canonical transport-stable schema
|
|
94
|
+
* output; direct recorder calls are validated when the buffer is flushed.
|
|
98
95
|
*/
|
|
99
96
|
readonly payload: unknown;
|
|
100
97
|
/** Optional metadata propagated when the event is flushed. */
|
|
@@ -132,8 +129,8 @@ export interface BufferedDomainEventRecorder extends DomainEventRecorderPort {
|
|
|
132
129
|
*/
|
|
133
130
|
clear(): void;
|
|
134
131
|
/**
|
|
135
|
-
* Publish recorded events in FIFO order,
|
|
136
|
-
* directly without the use-case event helper.
|
|
132
|
+
* Publish recorded events in FIFO order, proving transport stability for
|
|
133
|
+
* entries recorded directly without the use-case event helper.
|
|
137
134
|
*/
|
|
138
135
|
flush(eventBus: EventBusPort): Promise<void>;
|
|
139
136
|
}
|
|
@@ -229,7 +226,10 @@ export function createObservedUnitOfWork<TxPorts>(
|
|
|
229
226
|
*/
|
|
230
227
|
export function createDomainEventRecorder(): BufferedDomainEventRecorder {
|
|
231
228
|
const records: RecordedDomainEvent[] = [];
|
|
232
|
-
const
|
|
229
|
+
const validationOptions = new WeakMap<
|
|
230
|
+
RecordedDomainEvent,
|
|
231
|
+
EventPublishOptions
|
|
232
|
+
>();
|
|
233
233
|
|
|
234
234
|
return {
|
|
235
235
|
record(event, payload, options) {
|
|
@@ -239,7 +239,9 @@ export function createDomainEventRecorder(): BufferedDomainEventRecorder {
|
|
|
239
239
|
payload,
|
|
240
240
|
...(options?.trace ? { options: { trace: options.trace } } : {}),
|
|
241
241
|
};
|
|
242
|
-
if (isEventPayloadParsed(options))
|
|
242
|
+
if (isEventPayloadParsed(event, payload, options) && options) {
|
|
243
|
+
validationOptions.set(record, { ...options });
|
|
244
|
+
}
|
|
243
245
|
records.push(record);
|
|
244
246
|
},
|
|
245
247
|
|
|
@@ -257,13 +259,15 @@ export function createDomainEventRecorder(): BufferedDomainEventRecorder {
|
|
|
257
259
|
async flush(eventBus) {
|
|
258
260
|
while (records.length > 0) {
|
|
259
261
|
const record = records[0];
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
262
|
+
const prepared = await prepareEventPayloadForTransport(
|
|
263
|
+
record.event,
|
|
264
|
+
record.payload,
|
|
265
|
+
validationOptions.get(record) ?? record.options,
|
|
266
|
+
);
|
|
263
267
|
await eventBus.publish(
|
|
264
268
|
record.event,
|
|
265
|
-
payload as never,
|
|
266
|
-
|
|
269
|
+
prepared.payload as never,
|
|
270
|
+
prepared.publishOptions,
|
|
267
271
|
);
|
|
268
272
|
records.shift();
|
|
269
273
|
}
|
package/src/search/index.ts
CHANGED
|
@@ -435,10 +435,10 @@ function instrumentSearch(
|
|
|
435
435
|
instrumentation.custom({
|
|
436
436
|
name: "search.query",
|
|
437
437
|
label: "Search query",
|
|
438
|
-
summary: `${index.name}: ${result.
|
|
438
|
+
summary: `${index.name}: ${result.hits.length} hits`,
|
|
439
439
|
details: {
|
|
440
440
|
index: index.name,
|
|
441
|
-
|
|
441
|
+
queryLength: result.query.length,
|
|
442
442
|
hits: result.hits.length,
|
|
443
443
|
total: result.page.total,
|
|
444
444
|
durationMs: Date.now() - startedAt,
|
|
@@ -61,8 +61,8 @@ export interface ServerInstrumentationOptions<Ctx = unknown> {
|
|
|
61
61
|
traceContextHeader?: string | false;
|
|
62
62
|
|
|
63
63
|
/**
|
|
64
|
-
* Request path prefixes that should not
|
|
65
|
-
*
|
|
64
|
+
* Request path prefixes that should not record instrumentation events.
|
|
65
|
+
* Ambient correlation still runs, and enabled response headers are written.
|
|
66
66
|
*
|
|
67
67
|
* Defaults to the devtools dashboard prefix so its polling traffic does not
|
|
68
68
|
* fill the event timeline.
|
|
@@ -125,10 +125,10 @@ export interface ServerInstrumentationRuntime<Ctx> {
|
|
|
125
125
|
*/
|
|
126
126
|
createServiceCorrelation(): RequestCorrelation;
|
|
127
127
|
/**
|
|
128
|
-
* Pipeline hook installed before user hooks
|
|
129
|
-
*
|
|
128
|
+
* Pipeline hook installed before user hooks. It always owns ambient
|
|
129
|
+
* correlation; response headers and event recording remain configurable.
|
|
130
130
|
*/
|
|
131
|
-
hook
|
|
131
|
+
hook: ServerHook<Ctx, AnyPorts>;
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
type TraceContextFields = {
|
|
@@ -350,7 +350,6 @@ export function createServerInstrumentation<Ctx>(
|
|
|
350
350
|
req: HttpRequestLike;
|
|
351
351
|
ctx?: unknown;
|
|
352
352
|
}) => {
|
|
353
|
-
if (isIgnoredPath(getPathname(args.req), ignorePaths)) return;
|
|
354
353
|
const trace = resolveTraceContext(args);
|
|
355
354
|
enterActiveRequestContext({
|
|
356
355
|
requestId: resolveRequestId(args),
|
|
@@ -377,7 +376,10 @@ export function createServerInstrumentation<Ctx>(
|
|
|
377
376
|
return undefined;
|
|
378
377
|
},
|
|
379
378
|
beforeSend: ({ req, ctx, response }) => {
|
|
380
|
-
if (
|
|
379
|
+
if (
|
|
380
|
+
!enabled ||
|
|
381
|
+
(requestIdHeader === false && traceContextHeader === false)
|
|
382
|
+
) {
|
|
381
383
|
return undefined;
|
|
382
384
|
}
|
|
383
385
|
|
|
@@ -502,6 +504,6 @@ export function createServerInstrumentation<Ctx>(
|
|
|
502
504
|
requestId: createRequestId(),
|
|
503
505
|
trace: tracing?.current() ?? createTraceContext(),
|
|
504
506
|
}),
|
|
505
|
-
hook
|
|
507
|
+
hook,
|
|
506
508
|
};
|
|
507
509
|
}
|
|
@@ -83,6 +83,7 @@ import {
|
|
|
83
83
|
} from "./route-matching.js";
|
|
84
84
|
import type { TrustedRequestInfo } from "./trusted-proxy.js";
|
|
85
85
|
import { InvalidRequestUrlError } from "./trusted-proxy-internal.js";
|
|
86
|
+
import { UseCaseRouteInputValidationError } from "./use-case-route.js";
|
|
86
87
|
|
|
87
88
|
function withoutHeadResponseBody(
|
|
88
89
|
response: HttpResponse,
|
|
@@ -282,6 +283,24 @@ export function createRequestExecutor<
|
|
|
282
283
|
};
|
|
283
284
|
}
|
|
284
285
|
|
|
286
|
+
if (currentError instanceof UseCaseRouteInputValidationError) {
|
|
287
|
+
return {
|
|
288
|
+
ctx,
|
|
289
|
+
response: errorResponse(
|
|
290
|
+
500,
|
|
291
|
+
currentError.code,
|
|
292
|
+
currentError.message,
|
|
293
|
+
{
|
|
294
|
+
contractName: currentError.contractName,
|
|
295
|
+
useCaseName: currentError.useCaseName,
|
|
296
|
+
location: "useCaseInput",
|
|
297
|
+
},
|
|
298
|
+
),
|
|
299
|
+
error: currentError,
|
|
300
|
+
owner: "framework",
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
285
304
|
if (currentError instanceof InvalidRequestUrlError) {
|
|
286
305
|
return {
|
|
287
306
|
ctx,
|