@beignet/core 0.0.31 → 0.0.33
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 +115 -0
- package/README.md +346 -21
- package/dist/application/index.d.ts.map +1 -1
- package/dist/application/index.js +63 -47
- package/dist/application/index.js.map +1 -1
- package/dist/domain/entity.d.ts +37 -6
- package/dist/domain/entity.d.ts.map +1 -1
- package/dist/domain/entity.js +9 -7
- package/dist/domain/entity.js.map +1 -1
- package/dist/domain/index.d.ts +4 -4
- package/dist/domain/index.d.ts.map +1 -1
- package/dist/domain/index.js +4 -4
- package/dist/domain/index.js.map +1 -1
- package/dist/events/index.d.ts +6 -0
- package/dist/events/index.d.ts.map +1 -1
- package/dist/events/index.js +20 -10
- package/dist/events/index.js.map +1 -1
- package/dist/flags/index.d.ts +5 -4
- package/dist/flags/index.d.ts.map +1 -1
- package/dist/flags/index.js +10 -9
- package/dist/flags/index.js.map +1 -1
- package/dist/idempotency/index.d.ts +11 -1
- package/dist/idempotency/index.d.ts.map +1 -1
- package/dist/idempotency/index.js +4 -3
- package/dist/idempotency/index.js.map +1 -1
- package/dist/jobs/index.d.ts +449 -9
- package/dist/jobs/index.d.ts.map +1 -1
- package/dist/jobs/index.js +472 -12
- package/dist/jobs/index.js.map +1 -1
- package/dist/outbox/index.d.ts +170 -3
- package/dist/outbox/index.d.ts.map +1 -1
- package/dist/outbox/index.js +192 -8
- package/dist/outbox/index.js.map +1 -1
- package/dist/ports/events.d.ts +2 -2
- package/dist/ports/index.d.ts +6 -1
- package/dist/ports/index.d.ts.map +1 -1
- package/dist/ports/index.js +1 -0
- package/dist/ports/index.js.map +1 -1
- package/dist/providers/instrumentation.d.ts +4 -0
- package/dist/providers/instrumentation.d.ts.map +1 -1
- package/dist/providers/instrumentation.js.map +1 -1
- package/dist/providers/provider.d.ts +10 -2
- package/dist/providers/provider.d.ts.map +1 -1
- package/dist/providers/provider.js.map +1 -1
- package/dist/schedules/index.d.ts +8 -7
- package/dist/schedules/index.d.ts.map +1 -1
- package/dist/schedules/index.js +47 -16
- package/dist/schedules/index.js.map +1 -1
- package/dist/search/index.d.ts +2 -1
- package/dist/search/index.d.ts.map +1 -1
- package/dist/search/index.js +1 -0
- package/dist/search/index.js.map +1 -1
- package/dist/server/hooks/index.d.ts +1 -0
- package/dist/server/hooks/index.d.ts.map +1 -1
- package/dist/server/hooks/index.js +1 -0
- package/dist/server/hooks/index.js.map +1 -1
- package/dist/server/hooks/rate-limit.d.ts +26 -13
- package/dist/server/hooks/rate-limit.d.ts.map +1 -1
- package/dist/server/hooks/rate-limit.js +28 -34
- package/dist/server/hooks/rate-limit.js.map +1 -1
- package/dist/server/hooks/security.d.ts +179 -0
- package/dist/server/hooks/security.d.ts.map +1 -0
- package/dist/server/hooks/security.js +225 -0
- package/dist/server/hooks/security.js.map +1 -0
- package/dist/server/index.d.ts +8 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +8 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/instrumentation.d.ts.map +1 -1
- package/dist/server/instrumentation.js +22 -6
- package/dist/server/instrumentation.js.map +1 -1
- package/dist/server/providers/loadProviderConfig.d.ts.map +1 -1
- package/dist/server/providers/loadProviderConfig.js +15 -2
- package/dist/server/providers/loadProviderConfig.js.map +1 -1
- package/dist/server/request-context.d.ts +4 -0
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/request-context.js +3 -0
- package/dist/server/request-context.js.map +1 -1
- package/dist/server/runtime-integrity.d.ts +84 -0
- package/dist/server/runtime-integrity.d.ts.map +1 -0
- package/dist/server/runtime-integrity.js +160 -0
- package/dist/server/runtime-integrity.js.map +1 -0
- package/dist/server/server.d.ts +10 -0
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +55 -1
- package/dist/server/server.js.map +1 -1
- package/dist/server/trusted-proxy.d.ts +77 -0
- package/dist/server/trusted-proxy.d.ts.map +1 -0
- package/dist/server/trusted-proxy.js +129 -0
- package/dist/server/trusted-proxy.js.map +1 -0
- package/dist/tasks/index.d.ts +6 -7
- package/dist/tasks/index.d.ts.map +1 -1
- package/dist/tasks/index.js +22 -5
- package/dist/tasks/index.js.map +1 -1
- package/dist/tenancy/index.d.ts +41 -0
- package/dist/tenancy/index.d.ts.map +1 -0
- package/dist/tenancy/index.js +34 -0
- package/dist/tenancy/index.js.map +1 -0
- package/dist/testing/index.d.ts +6 -0
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +12 -3
- package/dist/testing/index.js.map +1 -1
- package/dist/tracing/execution.d.ts +14 -0
- package/dist/tracing/execution.d.ts.map +1 -0
- package/dist/tracing/execution.js +17 -0
- package/dist/tracing/execution.js.map +1 -0
- package/dist/tracing/index.d.ts +49 -0
- package/dist/tracing/index.d.ts.map +1 -1
- package/dist/tracing/index.js +59 -0
- package/dist/tracing/index.js.map +1 -1
- package/dist/uploads/client.d.ts.map +1 -1
- package/dist/uploads/client.js +44 -6
- package/dist/uploads/client.js.map +1 -1
- package/dist/uploads/index.d.ts +182 -2
- package/dist/uploads/index.d.ts.map +1 -1
- package/dist/uploads/index.js +468 -76
- package/dist/uploads/index.js.map +1 -1
- package/dist/webhooks/index.d.ts +56 -4
- package/dist/webhooks/index.d.ts.map +1 -1
- package/dist/webhooks/index.js +107 -2
- package/dist/webhooks/index.js.map +1 -1
- package/package.json +5 -1
- package/skills/app-architecture/SKILL.md +23 -2
- package/src/application/index.ts +88 -56
- package/src/domain/entity.ts +61 -13
- package/src/domain/index.ts +8 -7
- package/src/events/index.ts +27 -14
- package/src/flags/index.ts +23 -19
- package/src/idempotency/index.ts +17 -3
- package/src/jobs/index.ts +1022 -19
- package/src/outbox/index.ts +409 -9
- package/src/ports/events.ts +2 -2
- package/src/ports/index.ts +17 -0
- package/src/providers/instrumentation.ts +4 -0
- package/src/providers/provider.ts +11 -2
- package/src/schedules/index.ts +60 -26
- package/src/search/index.ts +3 -1
- package/src/server/hooks/index.ts +10 -0
- package/src/server/hooks/rate-limit.ts +52 -46
- package/src/server/hooks/security.ts +503 -0
- package/src/server/index.ts +8 -0
- package/src/server/instrumentation.ts +25 -5
- package/src/server/providers/loadProviderConfig.ts +19 -2
- package/src/server/request-context.ts +7 -0
- package/src/server/runtime-integrity.ts +322 -0
- package/src/server/server.ts +86 -1
- package/src/server/trusted-proxy.ts +249 -0
- package/src/tasks/index.ts +29 -12
- package/src/tenancy/index.ts +55 -0
- package/src/testing/index.ts +19 -3
- package/src/tracing/execution.ts +37 -0
- package/src/tracing/index.ts +137 -0
- package/src/uploads/client.ts +65 -6
- package/src/uploads/index.ts +713 -76
- package/src/webhooks/index.ts +240 -9
- package/src/domain/events.ts +0 -59
package/src/outbox/index.ts
CHANGED
|
@@ -16,6 +16,8 @@ import {
|
|
|
16
16
|
type InferJobPayload,
|
|
17
17
|
type JobDef,
|
|
18
18
|
parseJobPayload,
|
|
19
|
+
SINGLE_ATTEMPT_DISPATCH,
|
|
20
|
+
type SingleAttemptJobDispatch,
|
|
19
21
|
shouldRetryJob,
|
|
20
22
|
} from "../jobs/index.js";
|
|
21
23
|
import type { JobDispatcherPort } from "../ports/events.js";
|
|
@@ -267,6 +269,116 @@ export interface OutboxMarkFailedInput {
|
|
|
267
269
|
now?: Date;
|
|
268
270
|
}
|
|
269
271
|
|
|
272
|
+
/**
|
|
273
|
+
* Default maximum messages returned from outbox admin list calls.
|
|
274
|
+
*/
|
|
275
|
+
export const DEFAULT_OUTBOX_ADMIN_LIST_LIMIT = 50;
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Shared filters for outbox admin read operations.
|
|
279
|
+
*/
|
|
280
|
+
export interface OutboxMessageQuery {
|
|
281
|
+
/**
|
|
282
|
+
* Status or statuses to include.
|
|
283
|
+
*/
|
|
284
|
+
status?: OutboxMessageStatus | readonly OutboxMessageStatus[];
|
|
285
|
+
/**
|
|
286
|
+
* Message kind to include.
|
|
287
|
+
*/
|
|
288
|
+
kind?: OutboxMessageKind;
|
|
289
|
+
/**
|
|
290
|
+
* Event or job name to include.
|
|
291
|
+
*/
|
|
292
|
+
name?: string;
|
|
293
|
+
/**
|
|
294
|
+
* Include messages last updated before this timestamp.
|
|
295
|
+
*/
|
|
296
|
+
updatedBefore?: Date;
|
|
297
|
+
/**
|
|
298
|
+
* Include delivered messages delivered before this timestamp.
|
|
299
|
+
*/
|
|
300
|
+
deliveredBefore?: Date;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Options for listing outbox messages through the admin port.
|
|
305
|
+
*/
|
|
306
|
+
export interface OutboxListMessagesOptions extends OutboxMessageQuery {
|
|
307
|
+
/**
|
|
308
|
+
* Maximum messages to return. Defaults to
|
|
309
|
+
* `DEFAULT_OUTBOX_ADMIN_LIST_LIMIT`.
|
|
310
|
+
*/
|
|
311
|
+
limit?: number;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Options for counting outbox messages through the admin port.
|
|
316
|
+
*/
|
|
317
|
+
export interface OutboxCountMessagesOptions extends OutboxMessageQuery {}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Input for returning a dead-lettered message to the pending queue.
|
|
321
|
+
*/
|
|
322
|
+
export interface OutboxRequeueMessageInput {
|
|
323
|
+
/**
|
|
324
|
+
* Dead-lettered message ID.
|
|
325
|
+
*/
|
|
326
|
+
id: string;
|
|
327
|
+
/**
|
|
328
|
+
* Earliest time the message may be claimed again. Defaults to `now`.
|
|
329
|
+
*/
|
|
330
|
+
availableAt?: Date;
|
|
331
|
+
/**
|
|
332
|
+
* Reset attempts to zero before requeueing. Defaults to preserving the
|
|
333
|
+
* attempt count so operators can decide whether to grant a fresh retry
|
|
334
|
+
* budget.
|
|
335
|
+
*/
|
|
336
|
+
resetAttempts?: boolean;
|
|
337
|
+
/**
|
|
338
|
+
* Requeue timestamp.
|
|
339
|
+
*/
|
|
340
|
+
now?: Date;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Input for purging dead-lettered messages.
|
|
345
|
+
*/
|
|
346
|
+
export interface OutboxPurgeDeadLetteredInput {
|
|
347
|
+
/**
|
|
348
|
+
* Only purge messages last updated before this timestamp. Omit only when the
|
|
349
|
+
* caller intentionally wants to purge all dead-lettered messages.
|
|
350
|
+
*/
|
|
351
|
+
before?: Date;
|
|
352
|
+
/**
|
|
353
|
+
* Maximum messages to purge.
|
|
354
|
+
*/
|
|
355
|
+
limit?: number;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Input for pruning delivered messages.
|
|
360
|
+
*/
|
|
361
|
+
export interface OutboxPruneDeliveredInput {
|
|
362
|
+
/**
|
|
363
|
+
* Prune delivered messages delivered before this timestamp.
|
|
364
|
+
*/
|
|
365
|
+
before: Date;
|
|
366
|
+
/**
|
|
367
|
+
* Maximum messages to prune.
|
|
368
|
+
*/
|
|
369
|
+
limit?: number;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Result for outbox admin delete operations.
|
|
374
|
+
*/
|
|
375
|
+
export interface OutboxDeleteResult {
|
|
376
|
+
/**
|
|
377
|
+
* Number of rows deleted.
|
|
378
|
+
*/
|
|
379
|
+
deleted: number;
|
|
380
|
+
}
|
|
381
|
+
|
|
270
382
|
/**
|
|
271
383
|
* App-facing outbox storage port.
|
|
272
384
|
*
|
|
@@ -292,10 +404,48 @@ export interface OutboxPort {
|
|
|
292
404
|
markFailed(input: OutboxMarkFailedInput): Promise<void>;
|
|
293
405
|
}
|
|
294
406
|
|
|
407
|
+
/**
|
|
408
|
+
* Operational outbox admin port.
|
|
409
|
+
*
|
|
410
|
+
* Keep this separate from `OutboxPort` so request and drain contexts can expose
|
|
411
|
+
* only the hot-path delivery operations. Wire this port into maintenance
|
|
412
|
+
* contexts for CLI commands, runbooks, and devtools.
|
|
413
|
+
*/
|
|
414
|
+
export interface OutboxAdminPort {
|
|
415
|
+
/**
|
|
416
|
+
* List messages ordered by newest update first.
|
|
417
|
+
*/
|
|
418
|
+
listMessages(
|
|
419
|
+
options?: OutboxListMessagesOptions,
|
|
420
|
+
): Promise<readonly OutboxMessage[]>;
|
|
421
|
+
/**
|
|
422
|
+
* Count messages matching admin filters.
|
|
423
|
+
*/
|
|
424
|
+
countMessages(options?: OutboxCountMessagesOptions): Promise<number>;
|
|
425
|
+
/**
|
|
426
|
+
* Fetch one message by ID.
|
|
427
|
+
*/
|
|
428
|
+
getMessage(id: string): Promise<OutboxMessage | null>;
|
|
429
|
+
/**
|
|
430
|
+
* Return a dead-lettered message to pending state.
|
|
431
|
+
*/
|
|
432
|
+
requeueMessage(input: OutboxRequeueMessageInput): Promise<OutboxMessage>;
|
|
433
|
+
/**
|
|
434
|
+
* Delete dead-lettered messages.
|
|
435
|
+
*/
|
|
436
|
+
purgeDeadLettered(
|
|
437
|
+
input?: OutboxPurgeDeadLetteredInput,
|
|
438
|
+
): Promise<OutboxDeleteResult>;
|
|
439
|
+
/**
|
|
440
|
+
* Delete delivered messages older than a retention cutoff.
|
|
441
|
+
*/
|
|
442
|
+
pruneDelivered(input: OutboxPruneDeliveredInput): Promise<OutboxDeleteResult>;
|
|
443
|
+
}
|
|
444
|
+
|
|
295
445
|
/**
|
|
296
446
|
* In-memory outbox for tests and local examples.
|
|
297
447
|
*/
|
|
298
|
-
export interface MemoryOutboxPort extends OutboxPort {
|
|
448
|
+
export interface MemoryOutboxPort extends OutboxPort, OutboxAdminPort {
|
|
299
449
|
/**
|
|
300
450
|
* Current message snapshots.
|
|
301
451
|
*/
|
|
@@ -311,9 +461,13 @@ export interface MemoryOutboxPort extends OutboxPort {
|
|
|
311
461
|
*/
|
|
312
462
|
export interface CreateOutboxMessageOptions {
|
|
313
463
|
/**
|
|
314
|
-
* Generated message ID override.
|
|
464
|
+
* Generated message ID override. Wins over `input.id`.
|
|
315
465
|
*/
|
|
316
466
|
id?: string;
|
|
467
|
+
/**
|
|
468
|
+
* Fallback ID factory used when neither `id` nor `input.id` is provided.
|
|
469
|
+
*/
|
|
470
|
+
createId?: () => string;
|
|
317
471
|
/**
|
|
318
472
|
* Timestamp used for created/updated/available dates.
|
|
319
473
|
*/
|
|
@@ -498,6 +652,22 @@ export class OutboxClaimError extends Error {
|
|
|
498
652
|
}
|
|
499
653
|
}
|
|
500
654
|
|
|
655
|
+
/**
|
|
656
|
+
* Error thrown when an outbox admin operation cannot be completed safely.
|
|
657
|
+
*/
|
|
658
|
+
export class OutboxAdminError extends Error {
|
|
659
|
+
/**
|
|
660
|
+
* Message ID involved in the admin error, when applicable.
|
|
661
|
+
*/
|
|
662
|
+
readonly id?: string;
|
|
663
|
+
|
|
664
|
+
constructor(args: { message: string; id?: string }) {
|
|
665
|
+
super(args.message);
|
|
666
|
+
this.name = "OutboxAdminError";
|
|
667
|
+
this.id = args.id;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
501
671
|
function assertNonEmptyString(name: string, value: string): void {
|
|
502
672
|
if (typeof value !== "string" || value.trim().length === 0) {
|
|
503
673
|
throw new Error(`${name} must be a non-empty string`);
|
|
@@ -510,6 +680,12 @@ function assertPositiveInteger(name: string, value: number): void {
|
|
|
510
680
|
}
|
|
511
681
|
}
|
|
512
682
|
|
|
683
|
+
function assertValidDate(name: string, value: Date): void {
|
|
684
|
+
if (!(value instanceof Date) || Number.isNaN(value.getTime())) {
|
|
685
|
+
throw new Error(`${name} must be a valid Date`);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
513
689
|
function cloneDate(value: Date): Date {
|
|
514
690
|
return new Date(value.getTime());
|
|
515
691
|
}
|
|
@@ -639,6 +815,75 @@ function copyMessage(message: OutboxMessage): OutboxMessage {
|
|
|
639
815
|
};
|
|
640
816
|
}
|
|
641
817
|
|
|
818
|
+
function normalizeStatusFilter(
|
|
819
|
+
status: OutboxMessageQuery["status"],
|
|
820
|
+
): ReadonlySet<OutboxMessageStatus> | undefined {
|
|
821
|
+
if (status === undefined) return undefined;
|
|
822
|
+
return new Set(Array.isArray(status) ? status : [status]);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
function messageMatchesQuery(
|
|
826
|
+
message: OutboxMessage,
|
|
827
|
+
query: OutboxMessageQuery = {},
|
|
828
|
+
): boolean {
|
|
829
|
+
const statuses = normalizeStatusFilter(query.status);
|
|
830
|
+
if (statuses && !statuses.has(message.status)) return false;
|
|
831
|
+
if (query.kind !== undefined && message.kind !== query.kind) return false;
|
|
832
|
+
if (query.name !== undefined && message.name !== query.name) return false;
|
|
833
|
+
if (
|
|
834
|
+
query.updatedBefore !== undefined &&
|
|
835
|
+
message.updatedAt.getTime() >= query.updatedBefore.getTime()
|
|
836
|
+
) {
|
|
837
|
+
return false;
|
|
838
|
+
}
|
|
839
|
+
if (
|
|
840
|
+
query.deliveredBefore !== undefined &&
|
|
841
|
+
(message.deliveredAt === null ||
|
|
842
|
+
message.deliveredAt.getTime() >= query.deliveredBefore.getTime())
|
|
843
|
+
) {
|
|
844
|
+
return false;
|
|
845
|
+
}
|
|
846
|
+
return true;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
function compareMessagesForAdminList(
|
|
850
|
+
left: OutboxMessage,
|
|
851
|
+
right: OutboxMessage,
|
|
852
|
+
): number {
|
|
853
|
+
const updated = right.updatedAt.getTime() - left.updatedAt.getTime();
|
|
854
|
+
if (updated !== 0) return updated;
|
|
855
|
+
const created = right.createdAt.getTime() - left.createdAt.getTime();
|
|
856
|
+
if (created !== 0) return created;
|
|
857
|
+
return right.id.localeCompare(left.id);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
function compareMessagesForAdminCleanup(
|
|
861
|
+
left: OutboxMessage,
|
|
862
|
+
right: OutboxMessage,
|
|
863
|
+
): number {
|
|
864
|
+
const updated = left.updatedAt.getTime() - right.updatedAt.getTime();
|
|
865
|
+
if (updated !== 0) return updated;
|
|
866
|
+
const created = left.createdAt.getTime() - right.createdAt.getTime();
|
|
867
|
+
if (created !== 0) return created;
|
|
868
|
+
return left.id.localeCompare(right.id);
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
function compareDeliveredMessagesForPrune(
|
|
872
|
+
left: OutboxMessage,
|
|
873
|
+
right: OutboxMessage,
|
|
874
|
+
): number {
|
|
875
|
+
const delivered =
|
|
876
|
+
(left.deliveredAt?.getTime() ?? 0) - (right.deliveredAt?.getTime() ?? 0);
|
|
877
|
+
if (delivered !== 0) return delivered;
|
|
878
|
+
return compareMessagesForAdminCleanup(left, right);
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
function resolveAdminListLimit(limit: number | undefined): number {
|
|
882
|
+
const resolved = limit ?? DEFAULT_OUTBOX_ADMIN_LIST_LIMIT;
|
|
883
|
+
assertPositiveInteger("limit", resolved);
|
|
884
|
+
return resolved;
|
|
885
|
+
}
|
|
886
|
+
|
|
642
887
|
function toClaimedMessage(message: OutboxMessage): ClaimedOutboxMessage {
|
|
643
888
|
if (
|
|
644
889
|
message.status !== "claimed" ||
|
|
@@ -677,7 +922,7 @@ export function createOutboxMessage(
|
|
|
677
922
|
|
|
678
923
|
const now = options.now ?? new Date();
|
|
679
924
|
return {
|
|
680
|
-
id: options.id ?? input.id ?? createId(),
|
|
925
|
+
id: options.id ?? input.id ?? options.createId?.() ?? createId(),
|
|
681
926
|
kind: input.kind,
|
|
682
927
|
name: input.name,
|
|
683
928
|
payload: toOutboxJsonValue(input.payload),
|
|
@@ -709,12 +954,31 @@ function isEligible(message: OutboxMessage, now: Date): boolean {
|
|
|
709
954
|
);
|
|
710
955
|
}
|
|
711
956
|
|
|
957
|
+
/**
|
|
958
|
+
* Options for `createMemoryOutbox(...)`.
|
|
959
|
+
*/
|
|
960
|
+
export interface MemoryOutboxOptions {
|
|
961
|
+
/**
|
|
962
|
+
* Message and claim-token ID factory. Defaults to `crypto.randomUUID()`.
|
|
963
|
+
*/
|
|
964
|
+
id?: () => string;
|
|
965
|
+
/**
|
|
966
|
+
* Clock used for enqueue, claim, and completion timestamps when a call does
|
|
967
|
+
* not supply its own `now`. Defaults to the system clock.
|
|
968
|
+
*/
|
|
969
|
+
now?: () => Date;
|
|
970
|
+
}
|
|
971
|
+
|
|
712
972
|
/**
|
|
713
973
|
* Create an in-memory outbox for tests and local examples.
|
|
714
974
|
*
|
|
715
975
|
* The memory outbox is process-local and not durable.
|
|
716
976
|
*/
|
|
717
|
-
export function createMemoryOutbox(
|
|
977
|
+
export function createMemoryOutbox(
|
|
978
|
+
storeOptions: MemoryOutboxOptions = {},
|
|
979
|
+
): MemoryOutboxPort {
|
|
980
|
+
const createStoreId = storeOptions.id ?? createId;
|
|
981
|
+
const storeNow = storeOptions.now ?? (() => new Date());
|
|
718
982
|
const messages = new Map<string, OutboxMessage>();
|
|
719
983
|
|
|
720
984
|
function getClaimedOrThrow(id: string, claimToken: string): OutboxMessage {
|
|
@@ -739,8 +1003,125 @@ export function createMemoryOutbox(): MemoryOutboxPort {
|
|
|
739
1003
|
return [...messages.values()].map(copyMessage);
|
|
740
1004
|
},
|
|
741
1005
|
|
|
1006
|
+
async listMessages(options = {}) {
|
|
1007
|
+
const limit = resolveAdminListLimit(options.limit);
|
|
1008
|
+
if (options.updatedBefore) {
|
|
1009
|
+
assertValidDate("updatedBefore", options.updatedBefore);
|
|
1010
|
+
}
|
|
1011
|
+
if (options.deliveredBefore) {
|
|
1012
|
+
assertValidDate("deliveredBefore", options.deliveredBefore);
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
return [...messages.values()]
|
|
1016
|
+
.filter((message) => messageMatchesQuery(message, options))
|
|
1017
|
+
.sort(compareMessagesForAdminList)
|
|
1018
|
+
.slice(0, limit)
|
|
1019
|
+
.map(copyMessage);
|
|
1020
|
+
},
|
|
1021
|
+
|
|
1022
|
+
async countMessages(options = {}) {
|
|
1023
|
+
if (options.updatedBefore) {
|
|
1024
|
+
assertValidDate("updatedBefore", options.updatedBefore);
|
|
1025
|
+
}
|
|
1026
|
+
if (options.deliveredBefore) {
|
|
1027
|
+
assertValidDate("deliveredBefore", options.deliveredBefore);
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
return [...messages.values()].filter((message) =>
|
|
1031
|
+
messageMatchesQuery(message, options),
|
|
1032
|
+
).length;
|
|
1033
|
+
},
|
|
1034
|
+
|
|
1035
|
+
async getMessage(id) {
|
|
1036
|
+
assertNonEmptyString("id", id);
|
|
1037
|
+
const message = messages.get(id);
|
|
1038
|
+
return message ? copyMessage(message) : null;
|
|
1039
|
+
},
|
|
1040
|
+
|
|
1041
|
+
async requeueMessage(input) {
|
|
1042
|
+
assertNonEmptyString("id", input.id);
|
|
1043
|
+
const message = messages.get(input.id);
|
|
1044
|
+
if (!message) {
|
|
1045
|
+
throw new OutboxAdminError({
|
|
1046
|
+
id: input.id,
|
|
1047
|
+
message: `Outbox message "${input.id}" does not exist.`,
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
1050
|
+
if (message.status !== "deadLettered") {
|
|
1051
|
+
throw new OutboxAdminError({
|
|
1052
|
+
id: input.id,
|
|
1053
|
+
message: `Outbox message "${input.id}" is not dead-lettered.`,
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
const now = input.now ?? storeNow();
|
|
1058
|
+
const availableAt = input.availableAt ?? now;
|
|
1059
|
+
assertValidDate("now", now);
|
|
1060
|
+
assertValidDate("availableAt", availableAt);
|
|
1061
|
+
|
|
1062
|
+
message.status = "pending";
|
|
1063
|
+
message.availableAt = cloneDate(availableAt);
|
|
1064
|
+
message.claimToken = null;
|
|
1065
|
+
message.claimedAt = null;
|
|
1066
|
+
message.lockedUntil = null;
|
|
1067
|
+
if (input.resetAttempts) message.attempts = 0;
|
|
1068
|
+
message.updatedAt = cloneDate(now);
|
|
1069
|
+
|
|
1070
|
+
return copyMessage(message);
|
|
1071
|
+
},
|
|
1072
|
+
|
|
1073
|
+
async purgeDeadLettered(input = {}) {
|
|
1074
|
+
const limit =
|
|
1075
|
+
input.limit === undefined
|
|
1076
|
+
? undefined
|
|
1077
|
+
: resolveAdminListLimit(input.limit);
|
|
1078
|
+
if (input.before) assertValidDate("before", input.before);
|
|
1079
|
+
|
|
1080
|
+
const candidates = [...messages.values()]
|
|
1081
|
+
.filter(
|
|
1082
|
+
(message) =>
|
|
1083
|
+
message.status === "deadLettered" &&
|
|
1084
|
+
(input.before === undefined ||
|
|
1085
|
+
message.updatedAt.getTime() < input.before.getTime()),
|
|
1086
|
+
)
|
|
1087
|
+
.sort(compareMessagesForAdminCleanup)
|
|
1088
|
+
.slice(0, limit);
|
|
1089
|
+
|
|
1090
|
+
for (const message of candidates) {
|
|
1091
|
+
messages.delete(message.id);
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
return { deleted: candidates.length };
|
|
1095
|
+
},
|
|
1096
|
+
|
|
1097
|
+
async pruneDelivered(input) {
|
|
1098
|
+
assertValidDate("before", input.before);
|
|
1099
|
+
const limit =
|
|
1100
|
+
input.limit === undefined
|
|
1101
|
+
? undefined
|
|
1102
|
+
: resolveAdminListLimit(input.limit);
|
|
1103
|
+
const candidates = [...messages.values()]
|
|
1104
|
+
.filter(
|
|
1105
|
+
(message) =>
|
|
1106
|
+
message.status === "delivered" &&
|
|
1107
|
+
message.deliveredAt !== null &&
|
|
1108
|
+
message.deliveredAt.getTime() < input.before.getTime(),
|
|
1109
|
+
)
|
|
1110
|
+
.sort(compareDeliveredMessagesForPrune)
|
|
1111
|
+
.slice(0, limit);
|
|
1112
|
+
|
|
1113
|
+
for (const message of candidates) {
|
|
1114
|
+
messages.delete(message.id);
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
return { deleted: candidates.length };
|
|
1118
|
+
},
|
|
1119
|
+
|
|
742
1120
|
async enqueue(input) {
|
|
743
|
-
const message = createOutboxMessage(input
|
|
1121
|
+
const message = createOutboxMessage(input, {
|
|
1122
|
+
createId: createStoreId,
|
|
1123
|
+
now: storeNow(),
|
|
1124
|
+
});
|
|
744
1125
|
if (messages.has(message.id)) {
|
|
745
1126
|
throw new Error(`Outbox message "${message.id}" already exists.`);
|
|
746
1127
|
}
|
|
@@ -750,7 +1131,7 @@ export function createMemoryOutbox(): MemoryOutboxPort {
|
|
|
750
1131
|
|
|
751
1132
|
async claimBatch(options) {
|
|
752
1133
|
assertPositiveInteger("limit", options.limit);
|
|
753
|
-
const now = options.now ??
|
|
1134
|
+
const now = options.now ?? storeNow();
|
|
754
1135
|
const leaseMs = options.leaseMs ?? DEFAULT_OUTBOX_LEASE_MS;
|
|
755
1136
|
assertPositiveInteger("leaseMs", leaseMs);
|
|
756
1137
|
const lockedUntil = new Date(now.getTime() + leaseMs);
|
|
@@ -768,7 +1149,7 @@ export function createMemoryOutbox(): MemoryOutboxPort {
|
|
|
768
1149
|
for (const message of eligible) {
|
|
769
1150
|
message.status = "claimed";
|
|
770
1151
|
message.attempts += 1;
|
|
771
|
-
message.claimToken =
|
|
1152
|
+
message.claimToken = createStoreId();
|
|
772
1153
|
message.claimedAt = cloneDate(now);
|
|
773
1154
|
message.lockedUntil = cloneDate(lockedUntil);
|
|
774
1155
|
message.updatedAt = cloneDate(now);
|
|
@@ -782,7 +1163,7 @@ export function createMemoryOutbox(): MemoryOutboxPort {
|
|
|
782
1163
|
assertNonEmptyString("id", input.id);
|
|
783
1164
|
assertNonEmptyString("claimToken", input.claimToken);
|
|
784
1165
|
const message = getClaimedOrThrow(input.id, input.claimToken);
|
|
785
|
-
const now = input.now ??
|
|
1166
|
+
const now = input.now ?? storeNow();
|
|
786
1167
|
|
|
787
1168
|
message.status = "delivered";
|
|
788
1169
|
message.deliveredAt = cloneDate(now);
|
|
@@ -796,7 +1177,7 @@ export function createMemoryOutbox(): MemoryOutboxPort {
|
|
|
796
1177
|
assertNonEmptyString("id", input.id);
|
|
797
1178
|
assertNonEmptyString("claimToken", input.claimToken);
|
|
798
1179
|
const message = getClaimedOrThrow(input.id, input.claimToken);
|
|
799
|
-
const now = input.now ??
|
|
1180
|
+
const now = input.now ?? storeNow();
|
|
800
1181
|
|
|
801
1182
|
message.status = input.deadLetter ? "deadLettered" : "pending";
|
|
802
1183
|
message.lastError = serializeOutboxError(input.error);
|
|
@@ -1018,6 +1399,25 @@ async function deliverOutboxMessage(
|
|
|
1018
1399
|
}
|
|
1019
1400
|
|
|
1020
1401
|
const payload = await parseJobPayload(job, message.payload);
|
|
1402
|
+
|
|
1403
|
+
// The drain owns execution retries: failed deliveries are rescheduled with
|
|
1404
|
+
// the job's own policy via markFailed/retryAt. When the dispatcher exposes
|
|
1405
|
+
// a single-attempt dispatch (the inline dispatcher does), use it so the
|
|
1406
|
+
// retry policy runs in exactly one layer. Durable providers do not expose
|
|
1407
|
+
// it — for them dispatch is an enqueue and the queue owns execution.
|
|
1408
|
+
const singleAttempt = (
|
|
1409
|
+
options.jobs as {
|
|
1410
|
+
[SINGLE_ATTEMPT_DISPATCH]?: SingleAttemptJobDispatch;
|
|
1411
|
+
}
|
|
1412
|
+
)[SINGLE_ATTEMPT_DISPATCH];
|
|
1413
|
+
if (singleAttempt) {
|
|
1414
|
+
await singleAttempt(job, payload, {
|
|
1415
|
+
attempt: message.attempts,
|
|
1416
|
+
maxAttempts: message.maxAttempts,
|
|
1417
|
+
});
|
|
1418
|
+
return;
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1021
1421
|
await options.jobs.dispatch(job, payload);
|
|
1022
1422
|
}
|
|
1023
1423
|
|
package/src/ports/events.ts
CHANGED
|
@@ -10,8 +10,8 @@ import type {
|
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Represents a defined Domain Event with name and payload schema.
|
|
13
|
-
* This is a minimal interface that ports need -
|
|
14
|
-
*
|
|
13
|
+
* This is a minimal structural interface that ports need - event
|
|
14
|
+
* declarations from `defineEvent` in @beignet/core/events satisfy it.
|
|
15
15
|
*/
|
|
16
16
|
export interface DomainEventDef<
|
|
17
17
|
Name extends string = string,
|
package/src/ports/index.ts
CHANGED
|
@@ -281,14 +281,22 @@ export type {
|
|
|
281
281
|
*/
|
|
282
282
|
export type {
|
|
283
283
|
ClaimedOutboxMessage,
|
|
284
|
+
OutboxAdminPort,
|
|
284
285
|
OutboxClaimBatchOptions,
|
|
286
|
+
OutboxCountMessagesOptions,
|
|
287
|
+
OutboxDeleteResult,
|
|
285
288
|
OutboxEnqueueInput,
|
|
289
|
+
OutboxListMessagesOptions,
|
|
286
290
|
OutboxMarkDeliveredInput,
|
|
287
291
|
OutboxMarkFailedInput,
|
|
288
292
|
OutboxMessage,
|
|
289
293
|
OutboxMessageKind,
|
|
294
|
+
OutboxMessageQuery,
|
|
290
295
|
OutboxMessageStatus,
|
|
291
296
|
OutboxPort,
|
|
297
|
+
OutboxPruneDeliveredInput,
|
|
298
|
+
OutboxPurgeDeadLetteredInput,
|
|
299
|
+
OutboxRequeueMessageInput,
|
|
292
300
|
} from "../outbox/index.js";
|
|
293
301
|
/**
|
|
294
302
|
* Payments port exports.
|
|
@@ -359,6 +367,15 @@ export {
|
|
|
359
367
|
SearchOptionsError,
|
|
360
368
|
searchDocuments,
|
|
361
369
|
} from "../search/index.js";
|
|
370
|
+
/**
|
|
371
|
+
* Tenant scope exports.
|
|
372
|
+
*/
|
|
373
|
+
export type { TenantScope } from "../tenancy/index.js";
|
|
374
|
+
export {
|
|
375
|
+
createTenantScope,
|
|
376
|
+
requireTenantScope,
|
|
377
|
+
tenantScopeId,
|
|
378
|
+
} from "../tenancy/index.js";
|
|
362
379
|
/**
|
|
363
380
|
* Webhook exports.
|
|
364
381
|
*
|
|
@@ -39,6 +39,10 @@ export interface BaseProviderInstrumentationEvent {
|
|
|
39
39
|
* W3C traceparent header value, when available.
|
|
40
40
|
*/
|
|
41
41
|
traceparent?: string;
|
|
42
|
+
/**
|
|
43
|
+
* W3C tracestate value associated with the current operation.
|
|
44
|
+
*/
|
|
45
|
+
tracestate?: string;
|
|
42
46
|
/**
|
|
43
47
|
* Watcher category used by devtools and instrumentation sinks.
|
|
44
48
|
*/
|
|
@@ -36,6 +36,15 @@ export interface ProviderConfigDef<CfgSchema extends StandardSchemaV1> {
|
|
|
36
36
|
* and pass them to the schema for validation.
|
|
37
37
|
*/
|
|
38
38
|
envPrefix?: string;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Field-level config overrides, keyed by schema field name. Defined values
|
|
42
|
+
* are merged over the env-derived (or server-supplied) input before
|
|
43
|
+
* validation, so factory options win over environment variables and still
|
|
44
|
+
* satisfy required fields when the env var is absent. `undefined` values
|
|
45
|
+
* are ignored.
|
|
46
|
+
*/
|
|
47
|
+
overrides?: Record<string, unknown>;
|
|
39
48
|
}
|
|
40
49
|
|
|
41
50
|
/**
|
|
@@ -161,8 +170,8 @@ export interface ServiceProviderMetadata {
|
|
|
161
170
|
*
|
|
162
171
|
* @example
|
|
163
172
|
* ```ts
|
|
164
|
-
* const
|
|
165
|
-
* name: "redis",
|
|
173
|
+
* const redisCacheProvider = createProvider({
|
|
174
|
+
* name: "cache-redis",
|
|
166
175
|
* config: {
|
|
167
176
|
* schema: z.object({ URL: z.string().url() }),
|
|
168
177
|
* envPrefix: "REDIS_",
|