@beignet/core 0.0.37 → 0.0.38
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/README.md +108 -19
- package/dist/client/client.d.ts.map +1 -1
- package/dist/client/client.js +6 -28
- package/dist/client/client.js.map +1 -1
- package/dist/error-reporting/index.d.ts +42 -1
- package/dist/error-reporting/index.d.ts.map +1 -1
- package/dist/error-reporting/index.js +60 -0
- package/dist/error-reporting/index.js.map +1 -1
- package/dist/error-reporting/internal.d.ts +11 -0
- package/dist/error-reporting/internal.d.ts.map +1 -0
- package/dist/error-reporting/internal.js +36 -0
- package/dist/error-reporting/internal.js.map +1 -0
- package/dist/events/index.d.ts +9 -4
- package/dist/events/index.d.ts.map +1 -1
- package/dist/events/index.js +5 -3
- package/dist/events/index.js.map +1 -1
- package/dist/idempotency/index.d.ts +25 -2
- package/dist/idempotency/index.d.ts.map +1 -1
- package/dist/idempotency/index.js +35 -5
- package/dist/idempotency/index.js.map +1 -1
- package/dist/jobs/index.d.ts +26 -4
- package/dist/jobs/index.d.ts.map +1 -1
- package/dist/jobs/index.js +51 -6
- package/dist/jobs/index.js.map +1 -1
- package/dist/locks/index.d.ts +19 -1
- package/dist/locks/index.d.ts.map +1 -1
- package/dist/locks/index.js +27 -12
- package/dist/locks/index.js.map +1 -1
- package/dist/outbox/index.d.ts +23 -4
- package/dist/outbox/index.d.ts.map +1 -1
- package/dist/outbox/index.js +71 -23
- package/dist/outbox/index.js.map +1 -1
- package/dist/ports/events.d.ts +5 -5
- package/dist/ports/events.d.ts.map +1 -1
- package/dist/ports/index.d.ts +4 -4
- package/dist/ports/unbound.js +1 -1
- package/dist/ports/unbound.js.map +1 -1
- package/dist/ports/unit-of-work.d.ts +4 -1
- package/dist/ports/unit-of-work.d.ts.map +1 -1
- package/dist/ports/unit-of-work.js +4 -3
- package/dist/ports/unit-of-work.js.map +1 -1
- package/dist/query-codec.d.ts +3 -0
- package/dist/query-codec.d.ts.map +1 -0
- package/dist/query-codec.js +110 -0
- package/dist/query-codec.js.map +1 -0
- package/dist/server/hooks/cors.d.ts +3 -2
- package/dist/server/hooks/cors.d.ts.map +1 -1
- package/dist/server/hooks/cors.js +7 -3
- package/dist/server/hooks/cors.js.map +1 -1
- package/dist/server/hooks/error-reporting.d.ts +8 -0
- package/dist/server/hooks/error-reporting.d.ts.map +1 -1
- package/dist/server/hooks/error-reporting.js +55 -25
- package/dist/server/hooks/error-reporting.js.map +1 -1
- package/dist/server/hooks/idempotency.d.ts +1 -1
- package/dist/server/hooks/idempotency.d.ts.map +1 -1
- package/dist/server/hooks/idempotency.js +30 -9
- package/dist/server/hooks/idempotency.js.map +1 -1
- package/dist/server/request-executor.d.ts.map +1 -1
- package/dist/server/request-executor.js +4 -10
- package/dist/server/request-executor.js.map +1 -1
- package/dist/server/request-preparation.d.ts.map +1 -1
- package/dist/server/request-preparation.js +3 -1
- package/dist/server/request-preparation.js.map +1 -1
- package/dist/server/response-finalization.d.ts +3 -1
- package/dist/server/response-finalization.d.ts.map +1 -1
- package/dist/server/response-finalization.js +24 -2
- package/dist/server/response-finalization.js.map +1 -1
- package/dist/server/server.js +1 -1
- package/dist/server/server.js.map +1 -1
- package/dist/testing/index.d.ts +5 -5
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +9 -4
- package/dist/testing/index.js.map +1 -1
- package/dist/tracing/index.d.ts +27 -0
- package/dist/tracing/index.d.ts.map +1 -1
- package/dist/tracing/index.js +96 -0
- package/dist/tracing/index.js.map +1 -1
- package/dist/uploads/index.d.ts.map +1 -1
- package/dist/uploads/index.js +39 -6
- package/dist/uploads/index.js.map +1 -1
- package/dist/webhooks/index.d.ts +2 -1
- package/dist/webhooks/index.d.ts.map +1 -1
- package/dist/webhooks/index.js +25 -1
- package/dist/webhooks/index.js.map +1 -1
- package/package.json +1 -1
- package/skills/app-architecture/SKILL.md +19 -1
- package/src/client/client.ts +6 -31
- package/src/error-reporting/index.ts +111 -0
- package/src/error-reporting/internal.ts +47 -0
- package/src/events/index.ts +20 -4
- package/src/idempotency/index.ts +81 -7
- package/src/jobs/index.ts +85 -6
- package/src/locks/index.ts +65 -16
- package/src/outbox/index.ts +119 -27
- package/src/ports/events.ts +8 -1
- package/src/ports/index.ts +4 -4
- package/src/ports/unbound.ts +1 -1
- package/src/ports/unit-of-work.ts +10 -3
- package/src/query-codec.ts +130 -0
- package/src/server/hooks/cors.ts +10 -3
- package/src/server/hooks/error-reporting.ts +79 -23
- package/src/server/hooks/idempotency.ts +36 -10
- package/src/server/request-executor.ts +4 -12
- package/src/server/request-preparation.ts +4 -2
- package/src/server/response-finalization.ts +36 -5
- package/src/server/server.ts +1 -1
- package/src/testing/index.ts +25 -14
- package/src/tracing/index.ts +120 -0
- package/src/uploads/index.ts +44 -6
- package/src/webhooks/index.ts +34 -2
|
@@ -4,6 +4,18 @@
|
|
|
4
4
|
* Provider-neutral error reporting primitives for Beignet applications.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { type RedactionOptions, redactValue } from "../ports/redaction.js";
|
|
8
|
+
import {
|
|
9
|
+
DEFAULT_ERROR_REPORTING_TIMEOUT_MS,
|
|
10
|
+
errorReportingObserverTimeout,
|
|
11
|
+
runErrorReportingOperation,
|
|
12
|
+
} from "./internal.js";
|
|
13
|
+
|
|
14
|
+
export {
|
|
15
|
+
DEFAULT_ERROR_REPORTING_TIMEOUT_MS,
|
|
16
|
+
ErrorReportingTimeoutError,
|
|
17
|
+
} from "./internal.js";
|
|
18
|
+
|
|
7
19
|
type MaybePromise<T> = T | Promise<T>;
|
|
8
20
|
|
|
9
21
|
/**
|
|
@@ -67,6 +79,33 @@ export type ErrorReportOptions = {
|
|
|
67
79
|
traceparent?: string;
|
|
68
80
|
};
|
|
69
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Arguments accepted by `tryReportException(...)`.
|
|
84
|
+
*/
|
|
85
|
+
export interface TryReportExceptionOptions {
|
|
86
|
+
/** Reporter that owns the capture. Omit it to make reporting a no-op. */
|
|
87
|
+
reporter?: ErrorReporterPort | ErrorReporterResolver;
|
|
88
|
+
/** Original application or infrastructure error. */
|
|
89
|
+
error: unknown;
|
|
90
|
+
/** Structured metadata attached to the report. */
|
|
91
|
+
reportOptions?: ErrorReportOptions;
|
|
92
|
+
/**
|
|
93
|
+
* Maximum time allowed for capture and, separately, the failure observer.
|
|
94
|
+
* Set to `false` only when the reporting implementation is intentionally
|
|
95
|
+
* unbounded.
|
|
96
|
+
*
|
|
97
|
+
* @default 1000
|
|
98
|
+
*/
|
|
99
|
+
timeoutMs?: number | false;
|
|
100
|
+
/**
|
|
101
|
+
* Observer for reporter failures. Observer failures are also isolated.
|
|
102
|
+
*/
|
|
103
|
+
onReporterError?: (args: {
|
|
104
|
+
error: unknown;
|
|
105
|
+
reportingError: unknown;
|
|
106
|
+
}) => MaybePromise<void>;
|
|
107
|
+
}
|
|
108
|
+
|
|
70
109
|
/**
|
|
71
110
|
* Result returned by a reporting provider after capture.
|
|
72
111
|
*/
|
|
@@ -102,6 +141,11 @@ export type ErrorReporterPort = {
|
|
|
102
141
|
flush(options?: ErrorReporterFlushOptions): Promise<boolean>;
|
|
103
142
|
};
|
|
104
143
|
|
|
144
|
+
/** Lazy reporter resolver evaluated inside the best-effort capture deadline. */
|
|
145
|
+
export type ErrorReporterResolver = () => MaybePromise<
|
|
146
|
+
ErrorReporterPort | undefined
|
|
147
|
+
>;
|
|
148
|
+
|
|
105
149
|
/**
|
|
106
150
|
* Captured exception stored by `createMemoryErrorReporter(...)`.
|
|
107
151
|
*/
|
|
@@ -252,6 +296,73 @@ export function reportMessage(
|
|
|
252
296
|
return reporter.captureMessage(message, options);
|
|
253
297
|
}
|
|
254
298
|
|
|
299
|
+
/**
|
|
300
|
+
* Best-effort exception capture for runtime boundaries.
|
|
301
|
+
*
|
|
302
|
+
* Missing reporters, reporter failures, and reporter-failure observer errors
|
|
303
|
+
* resolve to `undefined` so diagnostics cannot replace application behavior.
|
|
304
|
+
*/
|
|
305
|
+
export async function tryReportException(
|
|
306
|
+
options: TryReportExceptionOptions,
|
|
307
|
+
): Promise<ErrorReportResult | undefined> {
|
|
308
|
+
const reporterSource = options.reporter;
|
|
309
|
+
if (!reporterSource) return undefined;
|
|
310
|
+
|
|
311
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_ERROR_REPORTING_TIMEOUT_MS;
|
|
312
|
+
|
|
313
|
+
try {
|
|
314
|
+
return await runErrorReportingOperation(async () => {
|
|
315
|
+
const reporter =
|
|
316
|
+
typeof reporterSource === "function"
|
|
317
|
+
? await reporterSource()
|
|
318
|
+
: reporterSource;
|
|
319
|
+
if (!reporter) return undefined;
|
|
320
|
+
return reporter.captureException(options.error, options.reportOptions);
|
|
321
|
+
}, timeoutMs);
|
|
322
|
+
} catch (reportingError) {
|
|
323
|
+
try {
|
|
324
|
+
if (options.onReporterError) {
|
|
325
|
+
await runErrorReportingOperation(
|
|
326
|
+
() =>
|
|
327
|
+
options.onReporterError?.({
|
|
328
|
+
error: options.error,
|
|
329
|
+
reportingError,
|
|
330
|
+
}),
|
|
331
|
+
errorReportingObserverTimeout(timeoutMs),
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
} catch {
|
|
335
|
+
// Reporter failure observers must not replace application behavior.
|
|
336
|
+
}
|
|
337
|
+
return undefined;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Redact structured error-report metadata with Beignet's shared sensitive-key
|
|
343
|
+
* rules. The original exception is intentionally not part of this operation.
|
|
344
|
+
*/
|
|
345
|
+
export function redactErrorReportOptions(
|
|
346
|
+
options: ErrorReportOptions,
|
|
347
|
+
redactionOptions: RedactionOptions = {},
|
|
348
|
+
): ErrorReportOptions {
|
|
349
|
+
return {
|
|
350
|
+
...options,
|
|
351
|
+
user: options.user
|
|
352
|
+
? redactValue(options.user, redactionOptions)
|
|
353
|
+
: options.user,
|
|
354
|
+
tags: options.tags
|
|
355
|
+
? redactValue(options.tags, redactionOptions)
|
|
356
|
+
: options.tags,
|
|
357
|
+
contexts: options.contexts
|
|
358
|
+
? redactValue(options.contexts, redactionOptions)
|
|
359
|
+
: options.contexts,
|
|
360
|
+
extra: options.extra
|
|
361
|
+
? redactValue(options.extra, redactionOptions)
|
|
362
|
+
: options.extra,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
255
366
|
function withAmbientState(
|
|
256
367
|
port: MemoryErrorReporterPort,
|
|
257
368
|
options: ErrorReportOptions | undefined,
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
type MaybePromise<T> = T | Promise<T>;
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_ERROR_REPORTING_TIMEOUT_MS = 1000;
|
|
4
|
+
|
|
5
|
+
export class ErrorReportingTimeoutError extends Error {
|
|
6
|
+
readonly code = "ERROR_REPORTING_TIMEOUT" as const;
|
|
7
|
+
readonly timeoutMs: number;
|
|
8
|
+
|
|
9
|
+
constructor(timeoutMs: number) {
|
|
10
|
+
super(`Error reporting timed out after ${timeoutMs}ms.`);
|
|
11
|
+
this.name = "ErrorReportingTimeoutError";
|
|
12
|
+
this.timeoutMs = timeoutMs;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function errorReportingObserverTimeout(
|
|
17
|
+
timeoutMs: number | false,
|
|
18
|
+
): number | false {
|
|
19
|
+
return timeoutMs === false || (Number.isFinite(timeoutMs) && timeoutMs > 0)
|
|
20
|
+
? timeoutMs
|
|
21
|
+
: DEFAULT_ERROR_REPORTING_TIMEOUT_MS;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function runErrorReportingOperation<T>(
|
|
25
|
+
operation: () => MaybePromise<T>,
|
|
26
|
+
timeoutMs: number | false = DEFAULT_ERROR_REPORTING_TIMEOUT_MS,
|
|
27
|
+
): Promise<T> {
|
|
28
|
+
if (timeoutMs === false) return operation();
|
|
29
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
30
|
+
throw new TypeError("Error reporting timeoutMs must be a positive number.");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
34
|
+
try {
|
|
35
|
+
return await Promise.race([
|
|
36
|
+
Promise.resolve().then(operation),
|
|
37
|
+
new Promise<never>((_, reject) => {
|
|
38
|
+
timeout = setTimeout(
|
|
39
|
+
() => reject(new ErrorReportingTimeoutError(timeoutMs)),
|
|
40
|
+
timeoutMs,
|
|
41
|
+
);
|
|
42
|
+
}),
|
|
43
|
+
]);
|
|
44
|
+
} finally {
|
|
45
|
+
if (timeout !== undefined) clearTimeout(timeout);
|
|
46
|
+
}
|
|
47
|
+
}
|
package/src/events/index.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type { StandardSchemaV1 } from "@standard-schema/spec";
|
|
2
2
|
import { runWithResolvedTracingContext } from "../tracing/execution.js";
|
|
3
|
-
import
|
|
3
|
+
import {
|
|
4
|
+
parseTraceCarrier,
|
|
5
|
+
type TraceCarrier,
|
|
6
|
+
type TracingPort,
|
|
7
|
+
} from "../tracing/index.js";
|
|
4
8
|
|
|
5
9
|
/**
|
|
6
10
|
* Any Standard Schema compatible validator.
|
|
@@ -58,6 +62,12 @@ export interface EventDef<
|
|
|
58
62
|
export type InferEventPayload<E extends EventPayloadDef> =
|
|
59
63
|
E["payload"] extends StandardSchemaV1<unknown, infer Output> ? Output : never;
|
|
60
64
|
|
|
65
|
+
/** Metadata propagated with an event delivery. */
|
|
66
|
+
export interface EventPublishOptions {
|
|
67
|
+
/** Versioned trace context captured by the event producer. */
|
|
68
|
+
trace?: TraceCarrier;
|
|
69
|
+
}
|
|
70
|
+
|
|
61
71
|
/**
|
|
62
72
|
* Options for `defineEvent(...)`.
|
|
63
73
|
*/
|
|
@@ -140,13 +150,17 @@ export interface EventBusLike {
|
|
|
140
150
|
publish<E extends EventPayloadDef>(
|
|
141
151
|
event: E,
|
|
142
152
|
payload: InferEventPayload<E>,
|
|
153
|
+
options?: EventPublishOptions,
|
|
143
154
|
): MaybePromise<void>;
|
|
144
155
|
/**
|
|
145
156
|
* Subscribe to an event and return an unsubscribe function.
|
|
146
157
|
*/
|
|
147
158
|
subscribe<E extends EventPayloadDef>(
|
|
148
159
|
event: E,
|
|
149
|
-
handler: (
|
|
160
|
+
handler: (
|
|
161
|
+
payload: InferEventPayload<E>,
|
|
162
|
+
options?: EventPublishOptions,
|
|
163
|
+
) => MaybePromise<void>,
|
|
150
164
|
): () => void;
|
|
151
165
|
}
|
|
152
166
|
|
|
@@ -299,9 +313,10 @@ export async function publishEvent<E extends EventPayloadDef>(
|
|
|
299
313
|
eventBus: EventBusLike,
|
|
300
314
|
event: E,
|
|
301
315
|
payload: InferEventPayload<E>,
|
|
316
|
+
options?: EventPublishOptions,
|
|
302
317
|
): Promise<void> {
|
|
303
318
|
const parsed = await parseEventPayload(event, payload);
|
|
304
|
-
await eventBus.publish(event, parsed);
|
|
319
|
+
await eventBus.publish(event, parsed, options);
|
|
305
320
|
}
|
|
306
321
|
|
|
307
322
|
/**
|
|
@@ -316,7 +331,7 @@ export function registerListeners<Ctx>(
|
|
|
316
331
|
options: RegisterListenersOptions<Ctx> = {},
|
|
317
332
|
): () => void {
|
|
318
333
|
const unsubscribes = listeners.map((listener) =>
|
|
319
|
-
eventBus.subscribe(listener.event, async (rawPayload) => {
|
|
334
|
+
eventBus.subscribe(listener.event, async (rawPayload, publishOptions) => {
|
|
320
335
|
try {
|
|
321
336
|
const payload = await parseEventPayload(listener.event, rawPayload);
|
|
322
337
|
const traceAttributes = {
|
|
@@ -330,6 +345,7 @@ export function registerListeners<Ctx>(
|
|
|
330
345
|
name: `beignet.listener ${listener.name}`,
|
|
331
346
|
type: "listener",
|
|
332
347
|
kind: "consumer",
|
|
348
|
+
parent: parseTraceCarrier(publishOptions?.trace),
|
|
333
349
|
attributes: traceAttributes,
|
|
334
350
|
metricAttributes: traceAttributes,
|
|
335
351
|
},
|
package/src/idempotency/index.ts
CHANGED
|
@@ -36,6 +36,9 @@ export type IdempotencyScopeMode =
|
|
|
36
36
|
| "tenant"
|
|
37
37
|
| "actor-tenant";
|
|
38
38
|
|
|
39
|
+
/** Default lifetime for unfinished idempotency reservations. */
|
|
40
|
+
export const DEFAULT_IDEMPOTENCY_RESERVATION_TTL_SEC = 300;
|
|
41
|
+
|
|
39
42
|
/**
|
|
40
43
|
* Contract metadata for idempotency-aware routes.
|
|
41
44
|
*
|
|
@@ -65,9 +68,16 @@ export interface IdempotencyMeta {
|
|
|
65
68
|
scope?: IdempotencyScopeMode;
|
|
66
69
|
|
|
67
70
|
/**
|
|
68
|
-
* Time-to-live for
|
|
71
|
+
* Time-to-live for completed replay results.
|
|
69
72
|
*/
|
|
70
73
|
ttlSec?: number;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Time-to-live for unfinished reservations.
|
|
77
|
+
*
|
|
78
|
+
* @default 300
|
|
79
|
+
*/
|
|
80
|
+
reservationTtlSec?: number;
|
|
71
81
|
}
|
|
72
82
|
|
|
73
83
|
/**
|
|
@@ -91,9 +101,15 @@ export interface IdempotencyReserveInput {
|
|
|
91
101
|
*/
|
|
92
102
|
fingerprint: string;
|
|
93
103
|
/**
|
|
94
|
-
* Optional
|
|
104
|
+
* Optional time-to-live for the completed replay result.
|
|
95
105
|
*/
|
|
96
106
|
ttlSec?: number;
|
|
107
|
+
/**
|
|
108
|
+
* Time-to-live for unfinished work before a successor may reserve the key.
|
|
109
|
+
*
|
|
110
|
+
* @default 300
|
|
111
|
+
*/
|
|
112
|
+
reservationTtlSec?: number;
|
|
97
113
|
}
|
|
98
114
|
|
|
99
115
|
/**
|
|
@@ -106,6 +122,7 @@ export type IdempotencyReservation =
|
|
|
106
122
|
key: string;
|
|
107
123
|
scopeKey: string;
|
|
108
124
|
fingerprint: string;
|
|
125
|
+
reservationToken: string;
|
|
109
126
|
reservedAt: Date;
|
|
110
127
|
expiresAt: Date | null;
|
|
111
128
|
}
|
|
@@ -161,6 +178,8 @@ export interface IdempotencyCompleteInput {
|
|
|
161
178
|
* Fingerprint that must match the reserved operation.
|
|
162
179
|
*/
|
|
163
180
|
fingerprint: string;
|
|
181
|
+
/** Opaque identity returned by the matching reserved result. */
|
|
182
|
+
reservationToken: string;
|
|
164
183
|
/**
|
|
165
184
|
* Result to replay for future matching requests.
|
|
166
185
|
*/
|
|
@@ -187,6 +206,8 @@ export interface IdempotencyFailInput {
|
|
|
187
206
|
* Fingerprint that must match the reserved operation.
|
|
188
207
|
*/
|
|
189
208
|
fingerprint: string;
|
|
209
|
+
/** Opaque identity returned by the matching reserved result. */
|
|
210
|
+
reservationToken: string;
|
|
190
211
|
/**
|
|
191
212
|
* Error that caused the protected operation to fail.
|
|
192
213
|
*/
|
|
@@ -281,6 +302,8 @@ export interface MemoryIdempotencyEntry {
|
|
|
281
302
|
* Fingerprint of the logical command payload.
|
|
282
303
|
*/
|
|
283
304
|
fingerprint: string;
|
|
305
|
+
/** Opaque identity of the current in-progress reservation. */
|
|
306
|
+
reservationToken: string;
|
|
284
307
|
/**
|
|
285
308
|
* Memory store status.
|
|
286
309
|
*/
|
|
@@ -388,7 +411,9 @@ type CanonicalValue =
|
|
|
388
411
|
| readonly CanonicalValue[]
|
|
389
412
|
| { readonly [key: string]: CanonicalValue };
|
|
390
413
|
|
|
391
|
-
type MemoryRecord = MemoryIdempotencyEntry
|
|
414
|
+
type MemoryRecord = MemoryIdempotencyEntry & {
|
|
415
|
+
replayTtlSec?: number;
|
|
416
|
+
};
|
|
392
417
|
|
|
393
418
|
function assertNonEmptyString(name: string, value: string): void {
|
|
394
419
|
if (typeof value !== "string" || value.trim().length === 0) {
|
|
@@ -516,6 +541,8 @@ export interface MemoryIdempotencyStoreOptions {
|
|
|
516
541
|
* system clock.
|
|
517
542
|
*/
|
|
518
543
|
now?: () => Date;
|
|
544
|
+
/** Token factory used for deterministic tests. */
|
|
545
|
+
createReservationToken?: () => string;
|
|
519
546
|
}
|
|
520
547
|
|
|
521
548
|
/**
|
|
@@ -528,6 +555,8 @@ export function createMemoryIdempotencyStore(
|
|
|
528
555
|
options: MemoryIdempotencyStoreOptions = {},
|
|
529
556
|
): MemoryIdempotencyStore {
|
|
530
557
|
const storeNow = options.now ?? (() => new Date());
|
|
558
|
+
const createReservationToken =
|
|
559
|
+
options.createReservationToken ?? createRandomReservationToken;
|
|
531
560
|
const records = new Map<string, MemoryRecord>();
|
|
532
561
|
|
|
533
562
|
return {
|
|
@@ -540,6 +569,7 @@ export function createMemoryIdempotencyStore(
|
|
|
540
569
|
assertNonEmptyString("key", input.key);
|
|
541
570
|
assertNonEmptyString("fingerprint", input.fingerprint);
|
|
542
571
|
assertTtl(input.ttlSec);
|
|
572
|
+
assertTtl(input.reservationTtlSec);
|
|
543
573
|
|
|
544
574
|
const now = storeNow();
|
|
545
575
|
const storageKey = createIdempotencyStorageKey(input);
|
|
@@ -558,9 +588,14 @@ export function createMemoryIdempotencyStore(
|
|
|
558
588
|
key: input.key,
|
|
559
589
|
scopeKey: normalizeIdempotencyScope(input.scope),
|
|
560
590
|
fingerprint: input.fingerprint,
|
|
591
|
+
reservationToken: createReservationToken(),
|
|
592
|
+
replayTtlSec: input.ttlSec,
|
|
561
593
|
status: "in-progress",
|
|
562
594
|
reservedAt: now,
|
|
563
|
-
expiresAt: resolveExpiresAt(
|
|
595
|
+
expiresAt: resolveExpiresAt(
|
|
596
|
+
input.reservationTtlSec ?? DEFAULT_IDEMPOTENCY_RESERVATION_TTL_SEC,
|
|
597
|
+
now,
|
|
598
|
+
),
|
|
564
599
|
};
|
|
565
600
|
|
|
566
601
|
records.set(storageKey, record);
|
|
@@ -571,6 +606,7 @@ export function createMemoryIdempotencyStore(
|
|
|
571
606
|
key: record.key,
|
|
572
607
|
scopeKey: record.scopeKey,
|
|
573
608
|
fingerprint: record.fingerprint,
|
|
609
|
+
reservationToken: record.reservationToken,
|
|
574
610
|
reservedAt: record.reservedAt,
|
|
575
611
|
expiresAt: record.expiresAt,
|
|
576
612
|
};
|
|
@@ -583,11 +619,23 @@ export function createMemoryIdempotencyStore(
|
|
|
583
619
|
|
|
584
620
|
const storageKey = createIdempotencyStorageKey(input);
|
|
585
621
|
const existing = records.get(storageKey);
|
|
586
|
-
|
|
622
|
+
assertNonEmptyString("reservationToken", input.reservationToken);
|
|
623
|
+
if (
|
|
624
|
+
!existing ||
|
|
625
|
+
existing.fingerprint !== input.fingerprint ||
|
|
626
|
+
existing.reservationToken !== input.reservationToken ||
|
|
627
|
+
existing.status !== "in-progress"
|
|
628
|
+
) {
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
587
631
|
|
|
588
632
|
existing.status = "completed";
|
|
589
633
|
existing.result = input.result;
|
|
590
634
|
existing.completedAt = storeNow();
|
|
635
|
+
existing.expiresAt = resolveExpiresAt(
|
|
636
|
+
existing.replayTtlSec,
|
|
637
|
+
existing.completedAt,
|
|
638
|
+
);
|
|
591
639
|
},
|
|
592
640
|
|
|
593
641
|
async fail(input) {
|
|
@@ -597,8 +645,15 @@ export function createMemoryIdempotencyStore(
|
|
|
597
645
|
|
|
598
646
|
const storageKey = createIdempotencyStorageKey(input);
|
|
599
647
|
const existing = records.get(storageKey);
|
|
600
|
-
|
|
601
|
-
if (
|
|
648
|
+
assertNonEmptyString("reservationToken", input.reservationToken);
|
|
649
|
+
if (
|
|
650
|
+
!existing ||
|
|
651
|
+
existing.fingerprint !== input.fingerprint ||
|
|
652
|
+
existing.reservationToken !== input.reservationToken ||
|
|
653
|
+
existing.status === "completed"
|
|
654
|
+
) {
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
602
657
|
|
|
603
658
|
records.delete(storageKey);
|
|
604
659
|
},
|
|
@@ -627,6 +682,7 @@ export async function runIdempotently<Result>(
|
|
|
627
682
|
scope: options.scope,
|
|
628
683
|
fingerprint: options.fingerprint,
|
|
629
684
|
ttlSec: options.ttlSec,
|
|
685
|
+
reservationTtlSec: options.reservationTtlSec,
|
|
630
686
|
};
|
|
631
687
|
const reservation = await idempotency.reserve(operation);
|
|
632
688
|
|
|
@@ -653,6 +709,7 @@ export async function runIdempotently<Result>(
|
|
|
653
709
|
key: operation.key,
|
|
654
710
|
scope: operation.scope,
|
|
655
711
|
fingerprint: operation.fingerprint,
|
|
712
|
+
reservationToken: reservation.reservationToken,
|
|
656
713
|
error,
|
|
657
714
|
});
|
|
658
715
|
throw error;
|
|
@@ -663,6 +720,7 @@ export async function runIdempotently<Result>(
|
|
|
663
720
|
key: operation.key,
|
|
664
721
|
scope: operation.scope,
|
|
665
722
|
fingerprint: operation.fingerprint,
|
|
723
|
+
reservationToken: reservation.reservationToken,
|
|
666
724
|
result,
|
|
667
725
|
});
|
|
668
726
|
return result;
|
|
@@ -676,6 +734,22 @@ export async function runIdempotently<Result>(
|
|
|
676
734
|
}
|
|
677
735
|
}
|
|
678
736
|
|
|
737
|
+
function createRandomReservationToken(): string {
|
|
738
|
+
if (typeof globalThis.crypto?.randomUUID === "function") {
|
|
739
|
+
return globalThis.crypto.randomUUID();
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
const bytes = new Uint8Array(16);
|
|
743
|
+
globalThis.crypto?.getRandomValues?.(bytes);
|
|
744
|
+
if (bytes.some((value) => value !== 0)) {
|
|
745
|
+
return Array.from(bytes, (value) =>
|
|
746
|
+
value.toString(16).padStart(2, "0"),
|
|
747
|
+
).join("");
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
751
|
+
}
|
|
752
|
+
|
|
679
753
|
function normalizeOmitPath(
|
|
680
754
|
path: string | readonly string[],
|
|
681
755
|
): readonly string[] {
|
package/src/jobs/index.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import type { StandardSchemaV1 } from "@standard-schema/spec";
|
|
2
2
|
import type { LeaseHandle, LeaseMetadata, LocksPort } from "../locks/index.js";
|
|
3
3
|
import { runWithResolvedTracingContext } from "../tracing/execution.js";
|
|
4
|
-
import
|
|
4
|
+
import {
|
|
5
|
+
parseTraceCarrier,
|
|
6
|
+
type TraceCarrier,
|
|
7
|
+
type TracingPort,
|
|
8
|
+
} from "../tracing/index.js";
|
|
5
9
|
|
|
6
10
|
/**
|
|
7
11
|
* Any Standard Schema compatible validator.
|
|
@@ -582,6 +586,73 @@ export interface InlineJobDispatcherOptions<Ctx> {
|
|
|
582
586
|
hooks?: readonly JobHook<JobDef<string, StandardSchema, Ctx>, Ctx>[];
|
|
583
587
|
}
|
|
584
588
|
|
|
589
|
+
/** Metadata propagated when a job is dispatched. */
|
|
590
|
+
export interface JobDispatchOptions {
|
|
591
|
+
/** Versioned trace context captured by the job producer. */
|
|
592
|
+
trace?: TraceCarrier;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
const JOB_TRANSPORT_ENVELOPE_TYPE = "beignet.job";
|
|
596
|
+
const JOB_TRANSPORT_ENVELOPE_VERSION = 1;
|
|
597
|
+
|
|
598
|
+
/** Parsed payload and propagation metadata from a job transport envelope. */
|
|
599
|
+
export interface ParsedJobTransportEnvelope {
|
|
600
|
+
payload: unknown;
|
|
601
|
+
trace?: TraceCarrier;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Wrap a job payload with transport metadata when a trace is present.
|
|
606
|
+
* Payloads without metadata retain their legacy wire shape.
|
|
607
|
+
*/
|
|
608
|
+
export function createJobTransportEnvelope(
|
|
609
|
+
payload: unknown,
|
|
610
|
+
options?: JobDispatchOptions,
|
|
611
|
+
): unknown {
|
|
612
|
+
const trace = parseTraceCarrier(options?.trace);
|
|
613
|
+
if (!trace) return payload;
|
|
614
|
+
|
|
615
|
+
return {
|
|
616
|
+
__beignet: {
|
|
617
|
+
type: JOB_TRANSPORT_ENVELOPE_TYPE,
|
|
618
|
+
version: JOB_TRANSPORT_ENVELOPE_VERSION,
|
|
619
|
+
trace,
|
|
620
|
+
},
|
|
621
|
+
payload,
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* Decode a Beignet job transport envelope while accepting legacy raw payloads.
|
|
627
|
+
* Unknown or malformed trace metadata is ignored without dropping the payload.
|
|
628
|
+
*/
|
|
629
|
+
export function parseJobTransportEnvelope(
|
|
630
|
+
value: unknown,
|
|
631
|
+
): ParsedJobTransportEnvelope {
|
|
632
|
+
if (typeof value !== "object" || value === null || !("payload" in value)) {
|
|
633
|
+
return { payload: value };
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
const metadata = "__beignet" in value ? value.__beignet : undefined;
|
|
637
|
+
if (
|
|
638
|
+
typeof metadata !== "object" ||
|
|
639
|
+
metadata === null ||
|
|
640
|
+
!("type" in metadata) ||
|
|
641
|
+
metadata.type !== JOB_TRANSPORT_ENVELOPE_TYPE ||
|
|
642
|
+
!("version" in metadata) ||
|
|
643
|
+
metadata.version !== JOB_TRANSPORT_ENVELOPE_VERSION
|
|
644
|
+
) {
|
|
645
|
+
return { payload: value };
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
const trace =
|
|
649
|
+
"trace" in metadata ? parseTraceCarrier(metadata.trace) : undefined;
|
|
650
|
+
return {
|
|
651
|
+
payload: value.payload,
|
|
652
|
+
...(trace ? { trace } : {}),
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
|
|
585
656
|
/**
|
|
586
657
|
* Well-known symbol under which the inline dispatcher exposes a
|
|
587
658
|
* single-attempt dispatch. Delivery systems that own execution retries
|
|
@@ -597,7 +668,7 @@ export const SINGLE_ATTEMPT_DISPATCH: unique symbol = Symbol.for(
|
|
|
597
668
|
* Metadata passed to a single-attempt dispatch when another delivery layer
|
|
598
669
|
* owns retry scheduling.
|
|
599
670
|
*/
|
|
600
|
-
export interface SingleAttemptJobDispatchOptions {
|
|
671
|
+
export interface SingleAttemptJobDispatchOptions extends JobDispatchOptions {
|
|
601
672
|
/**
|
|
602
673
|
* One-based delivery attempt.
|
|
603
674
|
*/
|
|
@@ -625,6 +696,7 @@ export interface JobDispatcher {
|
|
|
625
696
|
dispatch<J extends JobDef>(
|
|
626
697
|
job: J,
|
|
627
698
|
payload: InferJobPayload<J>,
|
|
699
|
+
options?: JobDispatchOptions,
|
|
628
700
|
): MaybePromise<void>;
|
|
629
701
|
}
|
|
630
702
|
|
|
@@ -693,6 +765,7 @@ export interface InlineJobDispatcher<Ctx = unknown> {
|
|
|
693
765
|
dispatch<J extends JobDef<string, StandardSchema, Ctx>>(
|
|
694
766
|
job: J,
|
|
695
767
|
payload: InferJobPayload<J>,
|
|
768
|
+
options?: JobDispatchOptions,
|
|
696
769
|
): Promise<void>;
|
|
697
770
|
}
|
|
698
771
|
|
|
@@ -1275,6 +1348,8 @@ export interface RunJobHandlerOptions<
|
|
|
1275
1348
|
ctx: Ctx | (() => MaybePromise<Ctx>);
|
|
1276
1349
|
/** Runtime tracing port used before a lazy context factory runs. */
|
|
1277
1350
|
tracing?: TracingPort;
|
|
1351
|
+
/** Trace context captured by the dispatching process. */
|
|
1352
|
+
trace?: TraceCarrier;
|
|
1278
1353
|
/**
|
|
1279
1354
|
* Runner-level hooks. These wrap job-local hooks.
|
|
1280
1355
|
*/
|
|
@@ -1327,6 +1402,7 @@ export async function runJobHandler<
|
|
|
1327
1402
|
name: `beignet.job ${args.job.name}`,
|
|
1328
1403
|
type: "job",
|
|
1329
1404
|
kind: "consumer",
|
|
1405
|
+
parent: parseTraceCarrier(args.trace),
|
|
1330
1406
|
attributes: traceAttributes,
|
|
1331
1407
|
metricAttributes: traceAttributes,
|
|
1332
1408
|
},
|
|
@@ -1485,6 +1561,7 @@ export function createInlineJobDispatcher<Ctx>(
|
|
|
1485
1561
|
payload: parsed,
|
|
1486
1562
|
ctx,
|
|
1487
1563
|
hooks: options.hooks,
|
|
1564
|
+
trace: dispatchOptions.trace,
|
|
1488
1565
|
attempt: dispatchOptions.attempt ?? attempt,
|
|
1489
1566
|
maxAttempts: dispatchOptions.maxAttempts ?? maxAttempts,
|
|
1490
1567
|
});
|
|
@@ -1515,8 +1592,9 @@ export function createInlineJobDispatcher<Ctx>(
|
|
|
1515
1592
|
async dispatch<J extends JobDef<string, StandardSchema, Ctx>>(
|
|
1516
1593
|
job: J,
|
|
1517
1594
|
payload: InferJobPayload<J>,
|
|
1595
|
+
dispatchOptions?: JobDispatchOptions,
|
|
1518
1596
|
) {
|
|
1519
|
-
await run(job, payload, options.retry !== false);
|
|
1597
|
+
await run(job, payload, options.retry !== false, dispatchOptions);
|
|
1520
1598
|
},
|
|
1521
1599
|
};
|
|
1522
1600
|
|
|
@@ -1550,9 +1628,10 @@ export function createUniqueJobDispatcher(
|
|
|
1550
1628
|
async dispatch<J extends JobDef>(
|
|
1551
1629
|
job: J,
|
|
1552
1630
|
payload: InferJobPayload<J>,
|
|
1631
|
+
dispatchOptions?: JobDispatchOptions,
|
|
1553
1632
|
): Promise<void> {
|
|
1554
1633
|
if (!job.unique) {
|
|
1555
|
-
await options.jobs.dispatch(job, payload);
|
|
1634
|
+
await options.jobs.dispatch(job, payload, dispatchOptions);
|
|
1556
1635
|
return;
|
|
1557
1636
|
}
|
|
1558
1637
|
|
|
@@ -1561,7 +1640,7 @@ export function createUniqueJobDispatcher(
|
|
|
1561
1640
|
keyPrefix: options.keyPrefix,
|
|
1562
1641
|
});
|
|
1563
1642
|
if (!unique) {
|
|
1564
|
-
await options.jobs.dispatch(job, parsed);
|
|
1643
|
+
await options.jobs.dispatch(job, parsed, dispatchOptions);
|
|
1565
1644
|
return;
|
|
1566
1645
|
}
|
|
1567
1646
|
|
|
@@ -1587,7 +1666,7 @@ export function createUniqueJobDispatcher(
|
|
|
1587
1666
|
}
|
|
1588
1667
|
|
|
1589
1668
|
try {
|
|
1590
|
-
await options.jobs.dispatch(job, parsed);
|
|
1669
|
+
await options.jobs.dispatch(job, parsed, dispatchOptions);
|
|
1591
1670
|
} catch (error) {
|
|
1592
1671
|
try {
|
|
1593
1672
|
await result.lease.release();
|