@beignet/provider-db-drizzle 0.0.37 → 0.0.39
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 +49 -11
- package/dist/mysql/index.d.ts +2 -1
- package/dist/mysql/index.d.ts.map +1 -1
- package/dist/mysql/index.js +13 -2
- package/dist/mysql/index.js.map +1 -1
- package/dist/mysql/schema.d.ts +17 -0
- package/dist/mysql/schema.d.ts.map +1 -1
- package/dist/mysql/schema.js +1 -0
- package/dist/mysql/schema.js.map +1 -1
- package/dist/postgres/index.d.ts.map +1 -1
- package/dist/postgres/index.js +1 -0
- package/dist/postgres/index.js.map +1 -1
- package/dist/postgres/schema.d.ts +17 -0
- package/dist/postgres/schema.d.ts.map +1 -1
- package/dist/postgres/schema.js +1 -0
- package/dist/postgres/schema.js.map +1 -1
- package/dist/shared/idempotency-core.d.ts +2 -0
- package/dist/shared/idempotency-core.d.ts.map +1 -1
- package/dist/shared/idempotency-core.js +56 -8
- package/dist/shared/idempotency-core.js.map +1 -1
- package/dist/shared/outbox-core.d.ts.map +1 -1
- package/dist/shared/outbox-core.js +2 -0
- package/dist/shared/outbox-core.js.map +1 -1
- package/dist/shared/rows.d.ts +1 -0
- package/dist/shared/rows.d.ts.map +1 -1
- package/dist/shared/rows.js +13 -0
- package/dist/shared/rows.js.map +1 -1
- package/dist/sqlite/index.d.ts.map +1 -1
- package/dist/sqlite/index.js +1 -0
- package/dist/sqlite/index.js.map +1 -1
- package/dist/sqlite/schema.d.ts +19 -0
- package/dist/sqlite/schema.d.ts.map +1 -1
- package/dist/sqlite/schema.js +1 -0
- package/dist/sqlite/schema.js.map +1 -1
- package/package.json +1 -1
- package/src/mysql/index.ts +19 -2
- package/src/mysql/schema.ts +1 -0
- package/src/postgres/index.ts +1 -0
- package/src/postgres/schema.ts +1 -0
- package/src/shared/idempotency-core.ts +88 -7
- package/src/shared/outbox-core.ts +2 -0
- package/src/shared/rows.ts +13 -0
- package/src/sqlite/index.ts +1 -0
- package/src/sqlite/schema.ts +1 -0
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import {
|
|
15
15
|
createIdempotencyStorageKey,
|
|
16
|
+
DEFAULT_IDEMPOTENCY_RESERVATION_TTL_SEC,
|
|
16
17
|
type IdempotencyCompleteInput,
|
|
17
18
|
type IdempotencyFailInput,
|
|
18
19
|
type IdempotencyPort,
|
|
@@ -57,6 +58,8 @@ export interface IdempotencyPortCoreOptions {
|
|
|
57
58
|
* Clock used by tests and deterministic environments.
|
|
58
59
|
*/
|
|
59
60
|
now?: () => Date;
|
|
61
|
+
/** Token factory used for deterministic tests. */
|
|
62
|
+
createReservationToken?: () => string;
|
|
60
63
|
}
|
|
61
64
|
|
|
62
65
|
/**
|
|
@@ -84,7 +87,7 @@ export type IdempotencyMutationErrorConstructor = new (
|
|
|
84
87
|
export function formatIdempotencyMutationErrorMessage(
|
|
85
88
|
args: IdempotencyMutationErrorArgs,
|
|
86
89
|
): string {
|
|
87
|
-
return `Idempotency ${args.action} for key "${args.key}" in namespace "${args.namespace}" matched no in-progress reservation with this fingerprint. The reservation may be missing, expired, already completed or released, or
|
|
90
|
+
return `Idempotency ${args.action} for key "${args.key}" in namespace "${args.namespace}" matched no in-progress reservation with this fingerprint and reservation token. The reservation may be missing, expired, already completed or released, or owned by a different execution.`;
|
|
88
91
|
}
|
|
89
92
|
|
|
90
93
|
function nowFrom(options: IdempotencyPortCoreOptions): Date {
|
|
@@ -117,6 +120,7 @@ function validateReserveInput(input: IdempotencyReserveInput): void {
|
|
|
117
120
|
assertNonEmptyString("key", input.key);
|
|
118
121
|
assertNonEmptyString("fingerprint", input.fingerprint);
|
|
119
122
|
assertIdempotencyTtl(input.ttlSec);
|
|
123
|
+
assertIdempotencyTtl(input.reservationTtlSec);
|
|
120
124
|
}
|
|
121
125
|
|
|
122
126
|
function validateMutationInput(
|
|
@@ -125,6 +129,65 @@ function validateMutationInput(
|
|
|
125
129
|
assertNonEmptyString("namespace", input.namespace);
|
|
126
130
|
assertNonEmptyString("key", input.key);
|
|
127
131
|
assertNonEmptyString("fingerprint", input.fingerprint);
|
|
132
|
+
assertNonEmptyString("reservationToken", input.reservationToken);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
type StoredReservationState = {
|
|
136
|
+
reservationToken: string;
|
|
137
|
+
replayTtlSec: number | null;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
function encodeReservationState(
|
|
141
|
+
reservationToken: string,
|
|
142
|
+
replayTtlSec: number | undefined,
|
|
143
|
+
): string {
|
|
144
|
+
return JSON.stringify({
|
|
145
|
+
reservationToken,
|
|
146
|
+
replayTtlSec: replayTtlSec ?? null,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function decodeReservationState(
|
|
151
|
+
value: string | null,
|
|
152
|
+
): StoredReservationState | undefined {
|
|
153
|
+
if (value === null) return undefined;
|
|
154
|
+
try {
|
|
155
|
+
const parsed = JSON.parse(value) as {
|
|
156
|
+
reservationToken?: unknown;
|
|
157
|
+
replayTtlSec?: unknown;
|
|
158
|
+
};
|
|
159
|
+
if (
|
|
160
|
+
typeof parsed.reservationToken !== "string" ||
|
|
161
|
+
(parsed.replayTtlSec !== null &&
|
|
162
|
+
(!Number.isInteger(parsed.replayTtlSec) ||
|
|
163
|
+
Number(parsed.replayTtlSec) <= 0))
|
|
164
|
+
) {
|
|
165
|
+
return undefined;
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
reservationToken: parsed.reservationToken,
|
|
169
|
+
replayTtlSec:
|
|
170
|
+
parsed.replayTtlSec === null ? null : Number(parsed.replayTtlSec),
|
|
171
|
+
};
|
|
172
|
+
} catch {
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function createRandomReservationToken(): string {
|
|
178
|
+
if (typeof globalThis.crypto?.randomUUID === "function") {
|
|
179
|
+
return globalThis.crypto.randomUUID();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const bytes = new Uint8Array(16);
|
|
183
|
+
globalThis.crypto?.getRandomValues?.(bytes);
|
|
184
|
+
if (bytes.some((value) => value !== 0)) {
|
|
185
|
+
return Array.from(bytes, (value) =>
|
|
186
|
+
value.toString(16).padStart(2, "0"),
|
|
187
|
+
).join("");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
128
191
|
}
|
|
129
192
|
|
|
130
193
|
/**
|
|
@@ -172,7 +235,15 @@ export function createIdempotencyPortCore(
|
|
|
172
235
|
|
|
173
236
|
const now = nowFrom(options);
|
|
174
237
|
const nowIso = encodeTimestamp(now);
|
|
175
|
-
const expiresAt = resolveIdempotencyExpiresAt(
|
|
238
|
+
const expiresAt = resolveIdempotencyExpiresAt(
|
|
239
|
+
input.reservationTtlSec ?? DEFAULT_IDEMPOTENCY_RESERVATION_TTL_SEC,
|
|
240
|
+
now,
|
|
241
|
+
);
|
|
242
|
+
const reservationToken = encodeReservationState(
|
|
243
|
+
(options.createReservationToken ?? createRandomReservationToken)(),
|
|
244
|
+
input.ttlSec,
|
|
245
|
+
);
|
|
246
|
+
const reservationJson = reservationToken;
|
|
176
247
|
const storageKey = createIdempotencyStorageKey(input);
|
|
177
248
|
const scopeKey = normalizeIdempotencyScope(input.scope);
|
|
178
249
|
const reserved = (): IdempotencyReservation => ({
|
|
@@ -181,6 +252,7 @@ export function createIdempotencyPortCore(
|
|
|
181
252
|
key: input.key,
|
|
182
253
|
scopeKey,
|
|
183
254
|
fingerprint: input.fingerprint,
|
|
255
|
+
reservationToken,
|
|
184
256
|
reservedAt: now,
|
|
185
257
|
expiresAt,
|
|
186
258
|
});
|
|
@@ -204,7 +276,7 @@ export function createIdempotencyPortCore(
|
|
|
204
276
|
${scopeKey},
|
|
205
277
|
${input.fingerprint},
|
|
206
278
|
'in-progress',
|
|
207
|
-
|
|
279
|
+
${reservationJson},
|
|
208
280
|
${nowIso},
|
|
209
281
|
null,
|
|
210
282
|
${expiresAt ? encodeTimestamp(expiresAt) : null},
|
|
@@ -225,7 +297,7 @@ export function createIdempotencyPortCore(
|
|
|
225
297
|
scope_key = ${scopeKey},
|
|
226
298
|
fingerprint = ${input.fingerprint},
|
|
227
299
|
status = 'in-progress',
|
|
228
|
-
result_json =
|
|
300
|
+
result_json = ${reservationJson},
|
|
229
301
|
reserved_at = ${nowIso},
|
|
230
302
|
completed_at = null,
|
|
231
303
|
expires_at = ${expiresAt ? encodeTimestamp(expiresAt) : null},
|
|
@@ -265,8 +337,14 @@ export function createIdempotencyPortCore(
|
|
|
265
337
|
async complete(input) {
|
|
266
338
|
validateMutationInput(input);
|
|
267
339
|
|
|
268
|
-
const
|
|
340
|
+
const completedAt = nowFrom(options);
|
|
341
|
+
const completedAtIso = encodeTimestamp(completedAt);
|
|
269
342
|
const storageKey = createIdempotencyStorageKey(input);
|
|
343
|
+
const reservationState = decodeReservationState(input.reservationToken);
|
|
344
|
+
const expiresAt = resolveIdempotencyExpiresAt(
|
|
345
|
+
reservationState?.replayTtlSec ?? undefined,
|
|
346
|
+
completedAt,
|
|
347
|
+
);
|
|
270
348
|
const serializedResult = JSON.stringify(input.result);
|
|
271
349
|
const resultJson =
|
|
272
350
|
serializedResult === undefined ? null : serializedResult;
|
|
@@ -275,10 +353,12 @@ export function createIdempotencyPortCore(
|
|
|
275
353
|
set
|
|
276
354
|
status = 'completed',
|
|
277
355
|
result_json = ${resultJson},
|
|
278
|
-
completed_at = ${
|
|
279
|
-
|
|
356
|
+
completed_at = ${completedAtIso},
|
|
357
|
+
expires_at = ${expiresAt ? encodeTimestamp(expiresAt) : null},
|
|
358
|
+
updated_at = ${completedAtIso}
|
|
280
359
|
where storage_key = ${storageKey}
|
|
281
360
|
and fingerprint = ${input.fingerprint}
|
|
361
|
+
and result_json = ${input.reservationToken}
|
|
282
362
|
and status = 'in-progress'`);
|
|
283
363
|
|
|
284
364
|
assertIdempotencyMutationSucceeded("complete", input, rowsAffected);
|
|
@@ -291,6 +371,7 @@ export function createIdempotencyPortCore(
|
|
|
291
371
|
const rowsAffected = await executor.run(sql`delete from ${executor.table}
|
|
292
372
|
where storage_key = ${storageKey}
|
|
293
373
|
and fingerprint = ${input.fingerprint}
|
|
374
|
+
and result_json = ${input.reservationToken}
|
|
294
375
|
and status = 'in-progress'`);
|
|
295
376
|
|
|
296
377
|
assertIdempotencyMutationSucceeded("fail", input, rowsAffected);
|
|
@@ -362,6 +362,7 @@ export function createOutboxPortCore(
|
|
|
362
362
|
kind,
|
|
363
363
|
name,
|
|
364
364
|
payload_json,
|
|
365
|
+
trace_context_json,
|
|
365
366
|
status,
|
|
366
367
|
attempts,
|
|
367
368
|
max_attempts,
|
|
@@ -378,6 +379,7 @@ export function createOutboxPortCore(
|
|
|
378
379
|
${message.kind},
|
|
379
380
|
${message.name},
|
|
380
381
|
${JSON.stringify(message.payload)},
|
|
382
|
+
${message.trace ? JSON.stringify(message.trace) : null},
|
|
381
383
|
${message.status},
|
|
382
384
|
${message.attempts},
|
|
383
385
|
${message.maxAttempts},
|
package/src/shared/rows.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
type OutboxMessageKind,
|
|
20
20
|
type OutboxMessageStatus,
|
|
21
21
|
} from "@beignet/core/outbox";
|
|
22
|
+
import { parseTraceCarrier, type TraceCarrier } from "@beignet/core/tracing";
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
25
|
* Encode a timestamp for storage. All dialects store ISO-8601 UTC strings.
|
|
@@ -56,6 +57,7 @@ export interface OutboxRow {
|
|
|
56
57
|
kind: OutboxMessageKind;
|
|
57
58
|
name: string;
|
|
58
59
|
payload_json: string;
|
|
60
|
+
trace_context_json: string | null;
|
|
59
61
|
status: OutboxMessageStatus;
|
|
60
62
|
attempts: number;
|
|
61
63
|
max_attempts: number;
|
|
@@ -69,15 +71,26 @@ export interface OutboxRow {
|
|
|
69
71
|
updated_at: string;
|
|
70
72
|
}
|
|
71
73
|
|
|
74
|
+
function parseStoredTrace(value: string | null): TraceCarrier | undefined {
|
|
75
|
+
if (!value) return undefined;
|
|
76
|
+
try {
|
|
77
|
+
return parseTraceCarrier(JSON.parse(value));
|
|
78
|
+
} catch {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
72
83
|
/**
|
|
73
84
|
* Decode a raw outbox row into an `OutboxMessage`.
|
|
74
85
|
*/
|
|
75
86
|
export function rowToMessage(row: OutboxRow): OutboxMessage {
|
|
87
|
+
const trace = parseStoredTrace(row.trace_context_json);
|
|
76
88
|
return {
|
|
77
89
|
id: row.id,
|
|
78
90
|
kind: row.kind,
|
|
79
91
|
name: row.name,
|
|
80
92
|
payload: JSON.parse(row.payload_json) as OutboxJsonValue,
|
|
93
|
+
...(trace ? { trace } : {}),
|
|
81
94
|
status: row.status,
|
|
82
95
|
attempts: row.attempts,
|
|
83
96
|
maxAttempts: row.max_attempts,
|
package/src/sqlite/index.ts
CHANGED
|
@@ -408,6 +408,7 @@ export function createDrizzleSqliteOutboxSetupStatements(
|
|
|
408
408
|
kind text NOT NULL,
|
|
409
409
|
name text NOT NULL,
|
|
410
410
|
payload_json text NOT NULL,
|
|
411
|
+
trace_context_json text,
|
|
411
412
|
status text NOT NULL,
|
|
412
413
|
attempts integer DEFAULT 0 NOT NULL,
|
|
413
414
|
max_attempts integer DEFAULT 3 NOT NULL,
|
package/src/sqlite/schema.ts
CHANGED
|
@@ -43,6 +43,7 @@ export const outboxMessages = sqliteTable(
|
|
|
43
43
|
kind: text("kind").notNull(),
|
|
44
44
|
name: text("name").notNull(),
|
|
45
45
|
payloadJson: text("payload_json").notNull(),
|
|
46
|
+
traceContextJson: text("trace_context_json"),
|
|
46
47
|
status: text("status").notNull(),
|
|
47
48
|
attempts: integer("attempts").notNull().default(0),
|
|
48
49
|
maxAttempts: integer("max_attempts").notNull().default(3),
|