@beignet/core 0.0.50 → 0.0.52
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 +24 -0
- package/README.md +99 -32
- package/dist/application/index.d.ts +5 -5
- package/dist/application/index.d.ts.map +1 -1
- package/dist/application/index.js +8 -4
- package/dist/application/index.js.map +1 -1
- package/dist/events/index.d.ts +26 -2
- package/dist/events/index.d.ts.map +1 -1
- package/dist/events/index.js +84 -7
- package/dist/events/index.js.map +1 -1
- package/dist/events/payload-state.d.ts +16 -0
- package/dist/events/payload-state.d.ts.map +1 -0
- package/dist/events/payload-state.js +123 -0
- package/dist/events/payload-state.js.map +1 -0
- 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/locks/index.d.ts.map +1 -1
- package/dist/locks/index.js +0 -4
- package/dist/locks/index.js.map +1 -1
- 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 +163 -15
- package/dist/outbox/index.d.ts.map +1 -1
- package/dist/outbox/index.js +1009 -155
- package/dist/outbox/index.js.map +1 -1
- package/dist/payments/index.d.ts.map +1 -1
- package/dist/payments/index.js +0 -4
- package/dist/payments/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/index.d.ts +1 -1
- package/dist/ports/index.d.ts.map +1 -1
- package/dist/ports/index.js.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 +16 -2
- package/dist/ports/testing.d.ts.map +1 -1
- package/dist/ports/testing.js +19 -4
- package/dist/ports/testing.js.map +1 -1
- package/dist/ports/unit-of-work.d.ts +9 -7
- package/dist/ports/unit-of-work.d.ts.map +1 -1
- package/dist/ports/unit-of-work.js +16 -7
- package/dist/ports/unit-of-work.js.map +1 -1
- package/dist/providers/index.d.ts +1 -1
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/provider.d.ts +0 -38
- package/dist/providers/provider.d.ts.map +1 -1
- package/dist/providers/provider.js.map +1 -1
- package/dist/search/index.d.ts.map +1 -1
- package/dist/search/index.js +2 -6
- package/dist/search/index.js.map +1 -1
- package/dist/server/hooks/cors.d.ts +5 -0
- package/dist/server/hooks/cors.d.ts.map +1 -1
- package/dist/server/hooks/cors.js +29 -1
- package/dist/server/hooks/cors.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/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 +35 -12
- package/dist/server/server.js.map +1 -1
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +12 -6
- package/dist/testing/index.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 +2 -2
- package/skills/app-architecture/SKILL.md +16 -0
- package/src/application/index.ts +39 -7
- package/src/events/index.ts +137 -7
- package/src/events/payload-state.ts +223 -0
- package/src/events/transport.ts +242 -0
- package/src/locks/index.ts +0 -4
- package/src/openapi/index.ts +41 -3
- package/src/outbox/index.ts +1393 -178
- package/src/payments/index.ts +0 -4
- package/src/ports/events.ts +4 -1
- package/src/ports/index.ts +3 -0
- package/src/ports/storage.ts +10 -0
- package/src/ports/testing.ts +42 -4
- package/src/ports/unit-of-work.ts +33 -15
- package/src/providers/index.ts +0 -1
- package/src/providers/provider.ts +0 -40
- package/src/search/index.ts +2 -6
- package/src/server/hooks/cors.ts +42 -0
- package/src/server/instrumentation.ts +10 -8
- package/src/server/response-finalization.ts +33 -15
- package/src/server/server.ts +61 -27
- package/src/testing/index.ts +20 -9
- package/src/uploads/index.ts +34 -16
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import type { EventPublishOptions } from "./index.js";
|
|
2
|
+
import type { EventTransportValue } from "./transport.js";
|
|
3
|
+
|
|
4
|
+
const PARSED_EVENT_PAYLOAD = Symbol.for("beignet.eventPayload.parsed");
|
|
5
|
+
const TRANSPORT_STABLE_EVENT_PAYLOAD = Symbol.for(
|
|
6
|
+
"beignet.eventPayload.transportStable",
|
|
7
|
+
);
|
|
8
|
+
|
|
9
|
+
type ParsedEventPublishOptions = EventPublishOptions & {
|
|
10
|
+
[PARSED_EVENT_PAYLOAD]?: object;
|
|
11
|
+
[TRANSPORT_STABLE_EVENT_PAYLOAD]?: object;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
interface ParsedPayloadState {
|
|
15
|
+
readonly eventName: string;
|
|
16
|
+
readonly payloadSchema: object;
|
|
17
|
+
readonly validate: unknown;
|
|
18
|
+
readonly payload: unknown;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type PayloadSnapshot =
|
|
22
|
+
| { readonly kind: "primitive"; readonly value: unknown }
|
|
23
|
+
| {
|
|
24
|
+
readonly kind: "array";
|
|
25
|
+
readonly value: object;
|
|
26
|
+
readonly prototype: object | null;
|
|
27
|
+
readonly items: readonly PayloadSnapshot[];
|
|
28
|
+
}
|
|
29
|
+
| {
|
|
30
|
+
readonly kind: "object";
|
|
31
|
+
readonly value: object;
|
|
32
|
+
readonly prototype: object | null;
|
|
33
|
+
readonly entries: readonly (readonly [string, PayloadSnapshot])[];
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
interface TransportStablePayloadState {
|
|
37
|
+
readonly eventName: string;
|
|
38
|
+
readonly payloadSchema: object;
|
|
39
|
+
readonly validate: unknown;
|
|
40
|
+
readonly payload: PayloadSnapshot;
|
|
41
|
+
readonly fingerprint: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface EventPayloadIdentity {
|
|
45
|
+
readonly name: string;
|
|
46
|
+
readonly payload: {
|
|
47
|
+
readonly "~standard": {
|
|
48
|
+
readonly validate: unknown;
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const transportStablePayloadStates = new WeakMap<
|
|
54
|
+
object,
|
|
55
|
+
TransportStablePayloadState
|
|
56
|
+
>();
|
|
57
|
+
const parsedPayloadStates = new WeakMap<object, ParsedPayloadState>();
|
|
58
|
+
|
|
59
|
+
function transportFingerprint(value: EventTransportValue): string {
|
|
60
|
+
return JSON.stringify(value);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function capturePayloadSnapshot(value: unknown): PayloadSnapshot {
|
|
64
|
+
if (value === null || typeof value !== "object") {
|
|
65
|
+
return { kind: "primitive", value };
|
|
66
|
+
}
|
|
67
|
+
if (Array.isArray(value)) {
|
|
68
|
+
const items: PayloadSnapshot[] = [];
|
|
69
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
70
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
|
|
71
|
+
if (!descriptor || !("value" in descriptor)) {
|
|
72
|
+
throw new TypeError(
|
|
73
|
+
"Transport-stable array state must contain data properties.",
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
items.push(capturePayloadSnapshot(descriptor.value));
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
kind: "array",
|
|
80
|
+
value,
|
|
81
|
+
prototype: Object.getPrototypeOf(value),
|
|
82
|
+
items,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
kind: "object",
|
|
87
|
+
value,
|
|
88
|
+
prototype: Object.getPrototypeOf(value),
|
|
89
|
+
entries: Reflect.ownKeys(value).map((key) => {
|
|
90
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
91
|
+
if (typeof key !== "string" || !descriptor || !("value" in descriptor)) {
|
|
92
|
+
throw new TypeError(
|
|
93
|
+
"Transport-stable payload state must contain data properties.",
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
return [key, capturePayloadSnapshot(descriptor.value)] as const;
|
|
97
|
+
}),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function matchesPayloadSnapshot(
|
|
102
|
+
snapshot: PayloadSnapshot,
|
|
103
|
+
value: unknown,
|
|
104
|
+
): boolean {
|
|
105
|
+
if (snapshot.kind === "primitive") return Object.is(snapshot.value, value);
|
|
106
|
+
if (snapshot.value !== value || value === null || typeof value !== "object") {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
if (Object.getPrototypeOf(value) !== snapshot.prototype) return false;
|
|
110
|
+
if (snapshot.kind === "array") {
|
|
111
|
+
return (
|
|
112
|
+
Array.isArray(value) &&
|
|
113
|
+
value.length === snapshot.items.length &&
|
|
114
|
+
snapshot.items.every((item, index) => {
|
|
115
|
+
const descriptor = Object.getOwnPropertyDescriptor(
|
|
116
|
+
value,
|
|
117
|
+
String(index),
|
|
118
|
+
);
|
|
119
|
+
return (
|
|
120
|
+
descriptor !== undefined &&
|
|
121
|
+
"value" in descriptor &&
|
|
122
|
+
matchesPayloadSnapshot(item, descriptor.value)
|
|
123
|
+
);
|
|
124
|
+
})
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
if (Array.isArray(value)) return false;
|
|
128
|
+
const keys = Reflect.ownKeys(value);
|
|
129
|
+
return (
|
|
130
|
+
keys.length === snapshot.entries.length &&
|
|
131
|
+
snapshot.entries.every(([key, item], index) => {
|
|
132
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
133
|
+
return (
|
|
134
|
+
keys[index] === key &&
|
|
135
|
+
descriptor !== undefined &&
|
|
136
|
+
"value" in descriptor &&
|
|
137
|
+
matchesPayloadSnapshot(item, descriptor.value)
|
|
138
|
+
);
|
|
139
|
+
})
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function markEventPayloadParsed(
|
|
144
|
+
event: EventPayloadIdentity,
|
|
145
|
+
payload: unknown,
|
|
146
|
+
options?: EventPublishOptions,
|
|
147
|
+
): EventPublishOptions {
|
|
148
|
+
const token = {};
|
|
149
|
+
parsedPayloadStates.set(token, {
|
|
150
|
+
eventName: event.name,
|
|
151
|
+
payloadSchema: event.payload,
|
|
152
|
+
validate: event.payload["~standard"].validate,
|
|
153
|
+
payload,
|
|
154
|
+
});
|
|
155
|
+
return {
|
|
156
|
+
...options,
|
|
157
|
+
[PARSED_EVENT_PAYLOAD]: token,
|
|
158
|
+
} as ParsedEventPublishOptions;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function isEventPayloadParsed(
|
|
162
|
+
event: EventPayloadIdentity,
|
|
163
|
+
payload: unknown,
|
|
164
|
+
options?: EventPublishOptions,
|
|
165
|
+
): boolean {
|
|
166
|
+
const token = (options as ParsedEventPublishOptions | undefined)?.[
|
|
167
|
+
PARSED_EVENT_PAYLOAD
|
|
168
|
+
];
|
|
169
|
+
const state = token ? parsedPayloadStates.get(token) : undefined;
|
|
170
|
+
return (
|
|
171
|
+
state?.eventName === event.name &&
|
|
172
|
+
state.payloadSchema === event.payload &&
|
|
173
|
+
state.validate === event.payload["~standard"].validate &&
|
|
174
|
+
Object.is(state.payload, payload)
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function markEventPayloadTransportStable(
|
|
179
|
+
event: EventPayloadIdentity,
|
|
180
|
+
payload: unknown,
|
|
181
|
+
transportValue: EventTransportValue,
|
|
182
|
+
options?: EventPublishOptions,
|
|
183
|
+
): EventPublishOptions {
|
|
184
|
+
const parsedToken = {};
|
|
185
|
+
parsedPayloadStates.set(parsedToken, {
|
|
186
|
+
eventName: event.name,
|
|
187
|
+
payloadSchema: event.payload,
|
|
188
|
+
validate: event.payload["~standard"].validate,
|
|
189
|
+
payload,
|
|
190
|
+
});
|
|
191
|
+
const stableToken = {};
|
|
192
|
+
transportStablePayloadStates.set(stableToken, {
|
|
193
|
+
eventName: event.name,
|
|
194
|
+
payloadSchema: event.payload,
|
|
195
|
+
validate: event.payload["~standard"].validate,
|
|
196
|
+
payload: capturePayloadSnapshot(payload),
|
|
197
|
+
fingerprint: transportFingerprint(transportValue),
|
|
198
|
+
});
|
|
199
|
+
return {
|
|
200
|
+
...options,
|
|
201
|
+
[PARSED_EVENT_PAYLOAD]: parsedToken,
|
|
202
|
+
[TRANSPORT_STABLE_EVENT_PAYLOAD]: stableToken,
|
|
203
|
+
} as ParsedEventPublishOptions;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function isEventPayloadTransportStable(
|
|
207
|
+
event: EventPayloadIdentity,
|
|
208
|
+
payload: unknown,
|
|
209
|
+
transportValue: EventTransportValue,
|
|
210
|
+
options?: EventPublishOptions,
|
|
211
|
+
): boolean {
|
|
212
|
+
const token = (options as ParsedEventPublishOptions | undefined)?.[
|
|
213
|
+
TRANSPORT_STABLE_EVENT_PAYLOAD
|
|
214
|
+
];
|
|
215
|
+
const state = token ? transportStablePayloadStates.get(token) : undefined;
|
|
216
|
+
return (
|
|
217
|
+
state?.eventName === event.name &&
|
|
218
|
+
state.payloadSchema === event.payload &&
|
|
219
|
+
state.validate === event.payload["~standard"].validate &&
|
|
220
|
+
matchesPayloadSnapshot(state.payload, payload) &&
|
|
221
|
+
state.fingerprint === transportFingerprint(transportValue)
|
|
222
|
+
);
|
|
223
|
+
}
|
|
@@ -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/locks/index.ts
CHANGED
|
@@ -285,10 +285,6 @@ export function createMemoryLocksProvider(
|
|
|
285
285
|
|
|
286
286
|
return createProvider({
|
|
287
287
|
name,
|
|
288
|
-
metadata: {
|
|
289
|
-
ports: ["locks"],
|
|
290
|
-
watchers: ["locks"],
|
|
291
|
-
},
|
|
292
288
|
setup({ ports }) {
|
|
293
289
|
const instrumentation = createProviderInstrumentation(ports, {
|
|
294
290
|
providerName: name,
|
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
|
}
|