@beignet/core 0.0.36 → 0.0.38
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/README.md +135 -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/contracts/contract-builder.d.ts +7 -0
- package/dist/contracts/contract-builder.d.ts.map +1 -1
- package/dist/contracts/contract-builder.js +22 -0
- package/dist/contracts/contract-builder.js.map +1 -1
- package/dist/contracts/contract-group.d.ts +7 -0
- package/dist/contracts/contract-group.d.ts.map +1 -1
- package/dist/contracts/contract-group.js +14 -0
- package/dist/contracts/contract-group.js.map +1 -1
- package/dist/contracts/index.d.ts +4 -0
- package/dist/contracts/index.d.ts.map +1 -1
- package/dist/contracts/index.js +4 -0
- package/dist/contracts/index.js.map +1 -1
- package/dist/contracts/lifecycle.d.ts +40 -0
- package/dist/contracts/lifecycle.d.ts.map +1 -0
- package/dist/contracts/lifecycle.js +174 -0
- package/dist/contracts/lifecycle.js.map +1 -0
- package/dist/contracts/types.d.ts +5 -0
- package/dist/contracts/types.d.ts.map +1 -1
- package/dist/contracts/types.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/openapi/index.d.ts +3 -1
- package/dist/openapi/index.d.ts.map +1 -1
- package/dist/openapi/index.js +14 -2
- package/dist/openapi/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 +7 -12
- 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 +5 -1
- package/dist/server/response-finalization.d.ts.map +1 -1
- package/dist/server/response-finalization.js +66 -2
- package/dist/server/response-finalization.js.map +1 -1
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +10 -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/contracts/contract-builder.ts +38 -0
- package/src/contracts/contract-group.ts +25 -0
- package/src/contracts/index.ts +8 -0
- package/src/contracts/lifecycle.ts +236 -0
- package/src/contracts/types.ts +5 -0
- 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/openapi/index.ts +22 -2
- 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 +10 -13
- package/src/server/request-preparation.ts +4 -2
- package/src/server/response-finalization.ts +94 -5
- package/src/server/server.ts +15 -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
- package/dist/domain/events.d.ts +0 -44
- package/dist/domain/events.d.ts.map +0 -1
- package/dist/domain/events.js +0 -24
- package/dist/domain/events.js.map +0 -1
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();
|
package/src/locks/index.ts
CHANGED
|
@@ -64,6 +64,25 @@ export type LeaseRenewOptions = {
|
|
|
64
64
|
ttlMs?: number;
|
|
65
65
|
};
|
|
66
66
|
|
|
67
|
+
/**
|
|
68
|
+
* State required to restore a lease handle in another runtime invocation.
|
|
69
|
+
*/
|
|
70
|
+
export type LeaseRestoreOptions = {
|
|
71
|
+
/**
|
|
72
|
+
* TTL to use when `renew()` is called without an explicit override.
|
|
73
|
+
*/
|
|
74
|
+
ttlMs: number;
|
|
75
|
+
/**
|
|
76
|
+
* Persisted expiry when the caller has it. Omitted values stay unknown.
|
|
77
|
+
*/
|
|
78
|
+
expiresAt?: Date;
|
|
79
|
+
/**
|
|
80
|
+
* Persisted fencing token when the caller has it. Omitted values stay
|
|
81
|
+
* unknown rather than being fabricated.
|
|
82
|
+
*/
|
|
83
|
+
fencingToken?: string | number;
|
|
84
|
+
};
|
|
85
|
+
|
|
67
86
|
/**
|
|
68
87
|
* Active lease handle returned by a lock provider.
|
|
69
88
|
*/
|
|
@@ -100,7 +119,11 @@ export type LocksPort = {
|
|
|
100
119
|
options: LeaseAcquireOptions,
|
|
101
120
|
fn: (ctx: { lease: LeaseHandle }) => MaybePromise<T>,
|
|
102
121
|
): Promise<T | undefined>;
|
|
103
|
-
restore(
|
|
122
|
+
restore(
|
|
123
|
+
key: string,
|
|
124
|
+
ownerToken: string,
|
|
125
|
+
options: LeaseRestoreOptions,
|
|
126
|
+
): LeaseHandle;
|
|
104
127
|
forceRelease(key: string): Promise<boolean>;
|
|
105
128
|
};
|
|
106
129
|
|
|
@@ -220,20 +243,21 @@ export function createMemoryLocks(
|
|
|
220
243
|
await result.lease.release();
|
|
221
244
|
}
|
|
222
245
|
},
|
|
223
|
-
restore(key, ownerToken) {
|
|
246
|
+
restore(key, ownerToken, restoreOptions) {
|
|
224
247
|
if (!key) throw new LeaseOptionsError("Lease key is required.");
|
|
225
248
|
if (!ownerToken) {
|
|
226
249
|
throw new LeaseOptionsError("Lease owner token is required.");
|
|
227
250
|
}
|
|
251
|
+
validateRestoreOptions(restoreOptions);
|
|
228
252
|
|
|
229
253
|
return createMemoryLease(
|
|
230
254
|
port,
|
|
231
255
|
{
|
|
232
256
|
key,
|
|
233
257
|
ownerToken,
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
fencingToken:
|
|
258
|
+
ttlMs: restoreOptions.ttlMs,
|
|
259
|
+
expiresAt: restoreOptions.expiresAt,
|
|
260
|
+
fencingToken: restoreOptions.fencingToken,
|
|
237
261
|
},
|
|
238
262
|
now,
|
|
239
263
|
);
|
|
@@ -306,32 +330,40 @@ export function withLease<T>(
|
|
|
306
330
|
|
|
307
331
|
function createMemoryLease(
|
|
308
332
|
port: MemoryLocksPort,
|
|
309
|
-
record:
|
|
333
|
+
record: {
|
|
334
|
+
key: string;
|
|
335
|
+
ownerToken: string;
|
|
336
|
+
ttlMs: number;
|
|
337
|
+
expiresAt?: Date;
|
|
338
|
+
fencingToken?: string | number;
|
|
339
|
+
},
|
|
310
340
|
now: () => Date,
|
|
311
341
|
): LeaseHandle {
|
|
312
342
|
const lease: LeaseHandle = {
|
|
313
343
|
key: record.key,
|
|
314
344
|
ownerToken: record.ownerToken,
|
|
315
|
-
expiresAt: record.expiresAt,
|
|
316
|
-
|
|
345
|
+
...(record.expiresAt ? { expiresAt: record.expiresAt } : {}),
|
|
346
|
+
...(record.fencingToken !== undefined
|
|
347
|
+
? { fencingToken: record.fencingToken }
|
|
348
|
+
: {}),
|
|
317
349
|
async renew(options) {
|
|
318
350
|
const active = port.leases.get(record.key);
|
|
319
|
-
if (
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
) {
|
|
351
|
+
if (!active || active.ownerToken !== record.ownerToken) {
|
|
352
|
+
return false;
|
|
353
|
+
}
|
|
354
|
+
if (active.expiresAt.getTime() <= now().getTime()) {
|
|
324
355
|
port.leases.delete(record.key);
|
|
325
356
|
return false;
|
|
326
357
|
}
|
|
327
358
|
|
|
328
|
-
const ttlMs = options?.ttlMs ??
|
|
359
|
+
const ttlMs = options?.ttlMs ?? record.ttlMs;
|
|
329
360
|
if (!Number.isFinite(ttlMs) || ttlMs <= 0) {
|
|
330
361
|
throw new LeaseOptionsError("Lease ttlMs must be a positive number.");
|
|
331
362
|
}
|
|
332
363
|
|
|
333
364
|
active.ttlMs = ttlMs;
|
|
334
365
|
active.expiresAt = new Date(now().getTime() + ttlMs);
|
|
366
|
+
record.ttlMs = ttlMs;
|
|
335
367
|
lease.expiresAt = active.expiresAt;
|
|
336
368
|
return true;
|
|
337
369
|
},
|
|
@@ -352,6 +384,23 @@ function createMemoryLease(
|
|
|
352
384
|
return lease;
|
|
353
385
|
}
|
|
354
386
|
|
|
387
|
+
function validateRestoreOptions(options: LeaseRestoreOptions): void {
|
|
388
|
+
if (!options || !Number.isFinite(options.ttlMs) || options.ttlMs <= 0) {
|
|
389
|
+
throw new LeaseOptionsError(
|
|
390
|
+
"Lease restore ttlMs must be a positive number.",
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
if (
|
|
394
|
+
options.expiresAt !== undefined &&
|
|
395
|
+
(!(options.expiresAt instanceof Date) ||
|
|
396
|
+
!Number.isFinite(options.expiresAt.getTime()))
|
|
397
|
+
) {
|
|
398
|
+
throw new LeaseOptionsError(
|
|
399
|
+
"Lease restore expiresAt must be a valid Date.",
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
355
404
|
function instrumentLocks(
|
|
356
405
|
locks: LocksPort,
|
|
357
406
|
instrumentation: ReturnType<typeof createProviderInstrumentation>,
|
|
@@ -387,8 +436,8 @@ function instrumentLocks(
|
|
|
387
436
|
await result.lease.release();
|
|
388
437
|
}
|
|
389
438
|
},
|
|
390
|
-
restore(key, ownerToken) {
|
|
391
|
-
return locks.restore(key, ownerToken);
|
|
439
|
+
restore(key, ownerToken, options) {
|
|
440
|
+
return locks.restore(key, ownerToken, options);
|
|
392
441
|
},
|
|
393
442
|
async forceRelease(key) {
|
|
394
443
|
const released = await locks.forceRelease(key);
|
package/src/openapi/index.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import {
|
|
8
8
|
type AnyContract,
|
|
9
|
+
type ContractDeprecationMeta,
|
|
9
10
|
type ContractLike,
|
|
10
11
|
getContractHeaderSchemas,
|
|
11
12
|
methodSupportsRequestBody,
|
|
@@ -13,6 +14,10 @@ import {
|
|
|
13
14
|
resolveContract,
|
|
14
15
|
STANDARD_ERROR_RESPONSE_SCHEMA,
|
|
15
16
|
} from "../contracts/index.js";
|
|
17
|
+
import {
|
|
18
|
+
assertValidContractLifecycle,
|
|
19
|
+
getContractOperationId,
|
|
20
|
+
} from "../contracts/lifecycle.js";
|
|
16
21
|
import {
|
|
17
22
|
comparePathParamsToTemplate,
|
|
18
23
|
formatPathParamsMismatch,
|
|
@@ -235,6 +240,8 @@ export interface OperationObject {
|
|
|
235
240
|
* Whether the operation is deprecated.
|
|
236
241
|
*/
|
|
237
242
|
deprecated?: boolean;
|
|
243
|
+
/** Beignet lifecycle details for a deprecated operation. */
|
|
244
|
+
"x-beignet-deprecation"?: ContractDeprecationMeta;
|
|
238
245
|
/**
|
|
239
246
|
* External documentation.
|
|
240
247
|
*/
|
|
@@ -409,9 +416,20 @@ export function contractsToOpenAPI(
|
|
|
409
416
|
createZodSchemaConverter(),
|
|
410
417
|
],
|
|
411
418
|
};
|
|
419
|
+
const operationIds = new Map<string, string>();
|
|
412
420
|
|
|
413
421
|
for (const contract of contracts) {
|
|
414
422
|
const config = resolveContract(contract);
|
|
423
|
+
assertValidContractLifecycle(config);
|
|
424
|
+
const operationId = getContractOperationId(config);
|
|
425
|
+
const route = `${config.method} ${config.path}`;
|
|
426
|
+
const conflictingRoute = operationIds.get(operationId);
|
|
427
|
+
if (conflictingRoute) {
|
|
428
|
+
throw new Error(
|
|
429
|
+
`Duplicate OpenAPI operationId: "${operationId}" is used by both ${conflictingRoute} and ${route}. Operation IDs must be unique within an OpenAPI document.`,
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
operationIds.set(operationId, route);
|
|
415
433
|
addContractToPaths(config, paths, state);
|
|
416
434
|
}
|
|
417
435
|
|
|
@@ -454,13 +472,15 @@ function addContractToPaths(
|
|
|
454
472
|
const pathItem = paths[pathKey];
|
|
455
473
|
|
|
456
474
|
const meta = contract.metadata?.openapi;
|
|
475
|
+
const deprecation = contract.metadata?.deprecation;
|
|
457
476
|
|
|
458
477
|
const operation: OperationObject = {
|
|
459
|
-
operationId:
|
|
478
|
+
operationId: getContractOperationId(contract),
|
|
460
479
|
summary: meta?.summary,
|
|
461
480
|
description: meta?.description,
|
|
462
481
|
tags: meta?.tags,
|
|
463
|
-
deprecated: meta?.deprecated,
|
|
482
|
+
deprecated: deprecation ? true : meta?.deprecated,
|
|
483
|
+
"x-beignet-deprecation": deprecation,
|
|
464
484
|
externalDocs: meta?.externalDocs,
|
|
465
485
|
security: meta?.security,
|
|
466
486
|
parameters: [],
|