@beignet/core 0.0.48 → 0.0.50
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 +46 -0
- package/README.md +224 -20
- package/dist/client/client.d.ts +0 -2
- package/dist/client/client.d.ts.map +1 -1
- package/dist/client/client.js +28 -25
- package/dist/client/client.js.map +1 -1
- package/dist/contracts/contract-builder.d.ts +7 -2
- package/dist/contracts/contract-builder.d.ts.map +1 -1
- package/dist/contracts/contract-builder.js +20 -2
- package/dist/contracts/contract-builder.js.map +1 -1
- package/dist/contracts/contract-group.d.ts.map +1 -1
- package/dist/contracts/contract-group.js +1 -0
- package/dist/contracts/contract-group.js.map +1 -1
- package/dist/contracts/contract-like.d.ts +2 -0
- package/dist/contracts/contract-like.d.ts.map +1 -1
- package/dist/contracts/contract-like.js +27 -1
- package/dist/contracts/contract-like.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/query-transport.d.ts +126 -0
- package/dist/contracts/query-transport.d.ts.map +1 -0
- package/dist/contracts/query-transport.js +406 -0
- package/dist/contracts/query-transport.js.map +1 -0
- package/dist/contracts/schema-shape.d.ts +11 -0
- package/dist/contracts/schema-shape.d.ts.map +1 -1
- package/dist/contracts/schema-shape.js +13 -0
- package/dist/contracts/schema-shape.js.map +1 -1
- 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/events/index.d.ts +54 -5
- package/dist/events/index.d.ts.map +1 -1
- package/dist/events/index.js +183 -32
- package/dist/events/index.js.map +1 -1
- package/dist/idempotency/index.d.ts +7 -3
- package/dist/idempotency/index.d.ts.map +1 -1
- package/dist/idempotency/index.js +45 -12
- package/dist/idempotency/index.js.map +1 -1
- package/dist/mail/index.d.ts.map +1 -1
- package/dist/mail/index.js +6 -3
- package/dist/mail/index.js.map +1 -1
- package/dist/openapi/index.d.ts +8 -0
- package/dist/openapi/index.d.ts.map +1 -1
- package/dist/openapi/index.js +79 -5
- package/dist/openapi/index.js.map +1 -1
- package/dist/outbox/index.d.ts +8 -5
- package/dist/outbox/index.d.ts.map +1 -1
- package/dist/outbox/index.js +17 -3
- package/dist/outbox/index.js.map +1 -1
- package/dist/ports/best-effort-work.d.ts +21 -0
- package/dist/ports/best-effort-work.d.ts.map +1 -0
- package/dist/ports/best-effort-work.js +2 -0
- package/dist/ports/best-effort-work.js.map +1 -0
- package/dist/ports/cache.d.ts +9 -1
- package/dist/ports/cache.d.ts.map +1 -1
- package/dist/ports/cache.js +20 -5
- package/dist/ports/cache.js.map +1 -1
- package/dist/ports/events.d.ts +7 -5
- package/dist/ports/events.d.ts.map +1 -1
- package/dist/ports/index.d.ts +7 -2
- package/dist/ports/index.d.ts.map +1 -1
- package/dist/ports/index.js +2 -1
- package/dist/ports/index.js.map +1 -1
- package/dist/ports/testing.d.ts +15 -0
- package/dist/ports/testing.d.ts.map +1 -1
- package/dist/ports/testing.js +38 -0
- package/dist/ports/testing.js.map +1 -1
- package/dist/providers/provider.d.ts +8 -5
- package/dist/providers/provider.d.ts.map +1 -1
- package/dist/providers/provider.js.map +1 -1
- package/dist/server/hooks/cors.d.ts +2 -2
- package/dist/server/hooks/cors.d.ts.map +1 -1
- package/dist/server/hooks/cors.js +2 -1
- package/dist/server/hooks/cors.js.map +1 -1
- package/dist/server/hooks/logging.d.ts +2 -2
- package/dist/server/hooks/logging.d.ts.map +1 -1
- package/dist/server/hooks/logging.js.map +1 -1
- package/dist/server/hooks/rate-limit.d.ts +16 -8
- package/dist/server/hooks/rate-limit.d.ts.map +1 -1
- package/dist/server/hooks/rate-limit.js +31 -17
- package/dist/server/hooks/rate-limit.js.map +1 -1
- package/dist/server/hooks/security.d.ts +2 -2
- package/dist/server/hooks/security.d.ts.map +1 -1
- package/dist/server/hooks/security.js.map +1 -1
- package/dist/server/http.d.ts +21 -2
- package/dist/server/http.d.ts.map +1 -1
- package/dist/server/index.d.ts +4 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +4 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/instrumentation.d.ts.map +1 -1
- package/dist/server/instrumentation.js +5 -3
- package/dist/server/instrumentation.js.map +1 -1
- package/dist/server/request-executor.d.ts.map +1 -1
- package/dist/server/request-executor.js +18 -9
- 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 +31 -11
- package/dist/server/request-preparation.js.map +1 -1
- package/dist/server/response-finalization.d.ts +2 -2
- package/dist/server/response-finalization.d.ts.map +1 -1
- package/dist/server/response-finalization.js +25 -8
- package/dist/server/response-finalization.js.map +1 -1
- package/dist/server/route-matching.d.ts.map +1 -1
- package/dist/server/route-matching.js +12 -1
- package/dist/server/route-matching.js.map +1 -1
- package/dist/server/server-sent-events.d.ts +94 -0
- package/dist/server/server-sent-events.d.ts.map +1 -0
- package/dist/server/server-sent-events.js +275 -0
- package/dist/server/server-sent-events.js.map +1 -0
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +43 -22
- package/dist/server/server.js.map +1 -1
- package/dist/server/trusted-proxy-internal.d.ts +4 -0
- package/dist/server/trusted-proxy-internal.d.ts.map +1 -1
- package/dist/server/trusted-proxy-internal.js +20 -0
- package/dist/server/trusted-proxy-internal.js.map +1 -1
- package/dist/server/trusted-proxy.d.ts.map +1 -1
- package/dist/server/trusted-proxy.js +3 -8
- package/dist/server/trusted-proxy.js.map +1 -1
- package/dist/server/use-case-route.d.ts +8 -5
- package/dist/server/use-case-route.d.ts.map +1 -1
- package/dist/server/use-case-route.js +44 -17
- package/dist/server/use-case-route.js.map +1 -1
- package/dist/testing/index.d.ts +17 -0
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +6 -1
- package/dist/testing/index.js.map +1 -1
- package/package.json +3 -3
- package/skills/app-architecture/SKILL.md +50 -4
- package/src/client/client.ts +29 -28
- package/src/contracts/contract-builder.ts +32 -2
- package/src/contracts/contract-group.ts +1 -0
- package/src/contracts/contract-like.ts +40 -1
- package/src/contracts/index.ts +23 -0
- package/src/contracts/query-transport.ts +697 -0
- package/src/contracts/schema-shape.ts +24 -0
- package/src/contracts/types.ts +5 -0
- package/src/events/index.ts +263 -38
- package/src/idempotency/index.ts +65 -17
- package/src/mail/index.ts +7 -3
- package/src/openapi/index.ts +126 -2
- package/src/outbox/index.ts +26 -5
- package/src/ports/best-effort-work.ts +21 -0
- package/src/ports/cache.ts +29 -7
- package/src/ports/events.ts +9 -4
- package/src/ports/index.ts +10 -1
- package/src/ports/testing.ts +45 -0
- package/src/providers/provider.ts +8 -5
- package/src/server/hooks/cors.ts +11 -5
- package/src/server/hooks/logging.ts +6 -2
- package/src/server/hooks/rate-limit.ts +50 -24
- package/src/server/hooks/security.ts +8 -4
- package/src/server/http.ts +23 -2
- package/src/server/index.ts +4 -0
- package/src/server/instrumentation.ts +12 -4
- package/src/server/request-executor.ts +31 -9
- package/src/server/request-preparation.ts +45 -12
- package/src/server/response-finalization.ts +51 -15
- package/src/server/route-matching.ts +24 -1
- package/src/server/server-sent-events.ts +415 -0
- package/src/server/server.ts +48 -22
- package/src/server/trusted-proxy-internal.ts +20 -0
- package/src/server/trusted-proxy.ts +6 -7
- package/src/server/use-case-route.ts +62 -23
- package/src/testing/index.ts +30 -0
- package/dist/query-codec.d.ts +0 -3
- package/dist/query-codec.d.ts.map +0 -1
- package/dist/query-codec.js +0 -110
- package/dist/query-codec.js.map +0 -1
- package/src/query-codec.ts +0 -130
package/src/openapi/index.ts
CHANGED
|
@@ -11,6 +11,8 @@ import {
|
|
|
11
11
|
getContractHeaderSchemas,
|
|
12
12
|
methodSupportsRequestBody,
|
|
13
13
|
parsePathTemplate,
|
|
14
|
+
type QueryFieldTransport,
|
|
15
|
+
queryTransportSchema,
|
|
14
16
|
resolveContract,
|
|
15
17
|
STANDARD_ERROR_RESPONSE_SCHEMA,
|
|
16
18
|
} from "../contracts/index.js";
|
|
@@ -20,7 +22,9 @@ import {
|
|
|
20
22
|
} from "../contracts/lifecycle.js";
|
|
21
23
|
import {
|
|
22
24
|
comparePathParamsToTemplate,
|
|
25
|
+
compareQueryTransportFields,
|
|
23
26
|
formatPathParamsMismatch,
|
|
27
|
+
formatQueryTransportMismatch,
|
|
24
28
|
} from "../contracts/schema-shape.js";
|
|
25
29
|
import {
|
|
26
30
|
createZodIntrospector,
|
|
@@ -157,6 +161,21 @@ export interface ParameterObject {
|
|
|
157
161
|
* Parameter schema.
|
|
158
162
|
*/
|
|
159
163
|
schema?: SchemaObject | ReferenceObject;
|
|
164
|
+
/**
|
|
165
|
+
* OpenAPI parameter serialization style.
|
|
166
|
+
*/
|
|
167
|
+
style?:
|
|
168
|
+
| "form"
|
|
169
|
+
| "simple"
|
|
170
|
+
| "matrix"
|
|
171
|
+
| "label"
|
|
172
|
+
| "spaceDelimited"
|
|
173
|
+
| "pipeDelimited"
|
|
174
|
+
| "deepObject";
|
|
175
|
+
/**
|
|
176
|
+
* Whether arrays and objects are expanded into separate parameter values.
|
|
177
|
+
*/
|
|
178
|
+
explode?: boolean;
|
|
160
179
|
/**
|
|
161
180
|
* Parameter description.
|
|
162
181
|
*/
|
|
@@ -627,12 +646,36 @@ function addQueryParams(
|
|
|
627
646
|
state: GeneratorState,
|
|
628
647
|
): void {
|
|
629
648
|
if (!contract.query) return;
|
|
649
|
+
const transport = contract.queryTransport;
|
|
650
|
+
if (!transport) {
|
|
651
|
+
throw new Error(
|
|
652
|
+
`Contract "${contract.name}" declares a query schema without a query transport.`,
|
|
653
|
+
);
|
|
654
|
+
}
|
|
630
655
|
|
|
631
656
|
const shape = state.introspector.getShape(contract.query);
|
|
632
|
-
if (!shape)
|
|
657
|
+
if (!shape) {
|
|
658
|
+
throw new Error(
|
|
659
|
+
`Unable to inspect query schema for contract "${contract.name}". Pass a schemaIntrospector that can read its fields so OpenAPI requiredness and constraints remain aligned with the declared query transport.`,
|
|
660
|
+
);
|
|
661
|
+
}
|
|
633
662
|
|
|
634
|
-
|
|
663
|
+
const schemaKeys = Object.keys(shape);
|
|
664
|
+
const transportKeys = Object.keys(transport.fields);
|
|
665
|
+
if (!compareQueryTransportFields({ schemaKeys, transportKeys })) {
|
|
666
|
+
throw new Error(
|
|
667
|
+
formatQueryTransportMismatch({
|
|
668
|
+
contractName: contract.name,
|
|
669
|
+
schemaKeys,
|
|
670
|
+
transportKeys,
|
|
671
|
+
}),
|
|
672
|
+
);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
for (const key of Object.keys(transport.fields)) {
|
|
635
676
|
const originalField = shape[key];
|
|
677
|
+
const fieldTransport = transport.fields[key];
|
|
678
|
+
if (!originalField || !fieldTransport) continue;
|
|
636
679
|
const optional = state.introspector.isOptional(originalField);
|
|
637
680
|
|
|
638
681
|
const field = originalField;
|
|
@@ -644,6 +687,7 @@ function addQueryParams(
|
|
|
644
687
|
state,
|
|
645
688
|
"input",
|
|
646
689
|
);
|
|
690
|
+
applyQueryTransportSchema(paramSchemaRef, fieldTransport, state);
|
|
647
691
|
|
|
648
692
|
const param: ParameterObject = {
|
|
649
693
|
name: key,
|
|
@@ -651,12 +695,92 @@ function addQueryParams(
|
|
|
651
695
|
required: !optional,
|
|
652
696
|
schema: paramSchemaRef,
|
|
653
697
|
description,
|
|
698
|
+
style: fieldTransport.kind === "deep-object" ? "deepObject" : "form",
|
|
699
|
+
explode: true,
|
|
654
700
|
};
|
|
655
701
|
|
|
656
702
|
addParameter(operation, param);
|
|
657
703
|
}
|
|
658
704
|
}
|
|
659
705
|
|
|
706
|
+
function applyQueryTransportSchema(
|
|
707
|
+
schema: SchemaObject | ReferenceObject,
|
|
708
|
+
transport: QueryFieldTransport,
|
|
709
|
+
state: GeneratorState,
|
|
710
|
+
): void {
|
|
711
|
+
if (!("$ref" in schema) || typeof schema.$ref !== "string") return;
|
|
712
|
+
const prefix = "#/components/schemas/";
|
|
713
|
+
if (!schema.$ref.startsWith(prefix)) return;
|
|
714
|
+
const name = schema.$ref.slice(prefix.length);
|
|
715
|
+
const converted = state.components.schemas?.[name];
|
|
716
|
+
if (!converted || !state.components.schemas) return;
|
|
717
|
+
state.components.schemas[name] = mergeQueryTransportSchema(
|
|
718
|
+
converted,
|
|
719
|
+
transport,
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function mergeQueryTransportSchema(
|
|
724
|
+
converted: SchemaObject,
|
|
725
|
+
transport: QueryFieldTransport,
|
|
726
|
+
): SchemaObject {
|
|
727
|
+
const declared = queryTransportSchema(transport);
|
|
728
|
+
const merged: SchemaObject = {
|
|
729
|
+
...converted,
|
|
730
|
+
type: declared.type,
|
|
731
|
+
};
|
|
732
|
+
if (declared.format) merged.format = declared.format;
|
|
733
|
+
if (transport.kind === "integer") {
|
|
734
|
+
merged.minimum = Math.max(
|
|
735
|
+
typeof converted.minimum === "number"
|
|
736
|
+
? converted.minimum
|
|
737
|
+
: Number.MIN_SAFE_INTEGER,
|
|
738
|
+
Number.MIN_SAFE_INTEGER,
|
|
739
|
+
);
|
|
740
|
+
merged.maximum = Math.min(
|
|
741
|
+
typeof converted.maximum === "number"
|
|
742
|
+
? converted.maximum
|
|
743
|
+
: Number.MAX_SAFE_INTEGER,
|
|
744
|
+
Number.MAX_SAFE_INTEGER,
|
|
745
|
+
);
|
|
746
|
+
}
|
|
747
|
+
if (declared["x-beignet-empty-query"]) {
|
|
748
|
+
merged["x-beignet-empty-query"] = declared["x-beignet-empty-query"];
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
if (transport.kind === "array") {
|
|
752
|
+
const convertedItems =
|
|
753
|
+
typeof converted.items === "object" && converted.items !== null
|
|
754
|
+
? (converted.items as SchemaObject)
|
|
755
|
+
: {};
|
|
756
|
+
merged.items = mergeQueryTransportSchema(convertedItems, transport.item);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
if (transport.kind === "deep-object") {
|
|
760
|
+
const convertedProperties =
|
|
761
|
+
typeof converted.properties === "object" && converted.properties !== null
|
|
762
|
+
? (converted.properties as Record<string, unknown>)
|
|
763
|
+
: {};
|
|
764
|
+
merged.properties = Object.fromEntries(
|
|
765
|
+
Object.entries(transport.fields).map(([key, field]) => {
|
|
766
|
+
const convertedProperty = convertedProperties[key];
|
|
767
|
+
return [
|
|
768
|
+
key,
|
|
769
|
+
mergeQueryTransportSchema(
|
|
770
|
+
typeof convertedProperty === "object" && convertedProperty !== null
|
|
771
|
+
? (convertedProperty as SchemaObject)
|
|
772
|
+
: {},
|
|
773
|
+
field,
|
|
774
|
+
),
|
|
775
|
+
];
|
|
776
|
+
}),
|
|
777
|
+
);
|
|
778
|
+
merged.additionalProperties = false;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
return merged;
|
|
782
|
+
}
|
|
783
|
+
|
|
660
784
|
/**
|
|
661
785
|
* Add header parameters from contract to operation.
|
|
662
786
|
*/
|
package/src/outbox/index.ts
CHANGED
|
@@ -555,7 +555,8 @@ export interface DrainOutboxOptions {
|
|
|
555
555
|
*/
|
|
556
556
|
registry: OutboxRegistry;
|
|
557
557
|
/**
|
|
558
|
-
* Event bus used for event messages.
|
|
558
|
+
* Event bus used for event messages. Required when the registry contains
|
|
559
|
+
* events.
|
|
559
560
|
*/
|
|
560
561
|
eventBus?: {
|
|
561
562
|
publish<E extends EventPayloadDef>(
|
|
@@ -565,7 +566,8 @@ export interface DrainOutboxOptions {
|
|
|
565
566
|
): MaybePromise<void>;
|
|
566
567
|
};
|
|
567
568
|
/**
|
|
568
|
-
* Job dispatcher used for job messages.
|
|
569
|
+
* Job dispatcher used for job messages. Required when the registry contains
|
|
570
|
+
* jobs.
|
|
569
571
|
*/
|
|
570
572
|
jobs?: JobDispatcherPort;
|
|
571
573
|
/**
|
|
@@ -1476,15 +1478,17 @@ async function deliverOutboxMessage(
|
|
|
1476
1478
|
* Claim and deliver one batch of outbox messages.
|
|
1477
1479
|
*
|
|
1478
1480
|
* This does not loop forever; production workers should call it on their own
|
|
1479
|
-
* polling cadence. Event and job messages require matching registry entries
|
|
1480
|
-
*
|
|
1481
|
-
*
|
|
1481
|
+
* polling cadence. Event and job messages require matching registry entries
|
|
1482
|
+
* and delivery transports. Required transports are validated before a batch
|
|
1483
|
+
* is claimed. Failed messages are retried with backoff until `maxAttempts`,
|
|
1484
|
+
* then dead-lettered.
|
|
1482
1485
|
*/
|
|
1483
1486
|
export async function drainOutbox(
|
|
1484
1487
|
options: DrainOutboxOptions,
|
|
1485
1488
|
): Promise<DrainOutboxResult> {
|
|
1486
1489
|
const batchSize = options.batchSize ?? 100;
|
|
1487
1490
|
assertPositiveInteger("batchSize", batchSize);
|
|
1491
|
+
assertOutboxDeliveryCapabilities(options);
|
|
1488
1492
|
const instrumentation = createProviderInstrumentation(
|
|
1489
1493
|
options.instrumentation,
|
|
1490
1494
|
{
|
|
@@ -1659,6 +1663,23 @@ export async function drainOutbox(
|
|
|
1659
1663
|
return result;
|
|
1660
1664
|
}
|
|
1661
1665
|
|
|
1666
|
+
function assertOutboxDeliveryCapabilities(options: DrainOutboxOptions): void {
|
|
1667
|
+
const missing: string[] = [];
|
|
1668
|
+
|
|
1669
|
+
if (options.registry.events.size > 0 && !options.eventBus) {
|
|
1670
|
+
missing.push("events require an event bus");
|
|
1671
|
+
}
|
|
1672
|
+
if (options.registry.jobs.size > 0 && !options.jobs) {
|
|
1673
|
+
missing.push("jobs require a job dispatcher");
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
if (missing.length > 0) {
|
|
1677
|
+
throw new OutboxRegistryError(
|
|
1678
|
+
`Cannot drain this outbox registry: ${missing.join("; ")}.`,
|
|
1679
|
+
);
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1662
1683
|
/**
|
|
1663
1684
|
* Domain event recorder port re-exported for outbox integrations.
|
|
1664
1685
|
*/
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Non-durable work scheduled outside the originating application operation.
|
|
3
|
+
*/
|
|
4
|
+
export type BestEffortWork = () => Promise<void> | void;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Non-durable, best-effort follow-up work.
|
|
8
|
+
*
|
|
9
|
+
* Use this for follow-up work that may be lost without changing the outcome
|
|
10
|
+
* of the originating operation. Required or retryable work belongs in a job
|
|
11
|
+
* or durable outbox instead.
|
|
12
|
+
*/
|
|
13
|
+
export interface BestEffortWorkPort {
|
|
14
|
+
/**
|
|
15
|
+
* Schedule work without waiting for it in the originating operation.
|
|
16
|
+
*
|
|
17
|
+
* Adapters should isolate scheduling and callback failures from the caller
|
|
18
|
+
* and report them through their configured error observer.
|
|
19
|
+
*/
|
|
20
|
+
defer(work: BestEffortWork): void;
|
|
21
|
+
}
|
package/src/ports/cache.ts
CHANGED
|
@@ -3,11 +3,35 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export interface CacheSetOptions {
|
|
5
5
|
/**
|
|
6
|
-
* Time-to-live in seconds.
|
|
6
|
+
* Time-to-live in seconds. Values must be positive safe integers. Omit this
|
|
7
|
+
* for a value that does not expire.
|
|
7
8
|
*/
|
|
8
9
|
ttlSeconds?: number;
|
|
9
10
|
}
|
|
10
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Resolve and validate the optional TTL shared by cache adapters.
|
|
14
|
+
*
|
|
15
|
+
* @param options - Cache write options.
|
|
16
|
+
* @returns The positive TTL, or `undefined` for a persistent value.
|
|
17
|
+
*/
|
|
18
|
+
export function resolveCacheTtlSeconds(
|
|
19
|
+
options?: CacheSetOptions,
|
|
20
|
+
): number | undefined {
|
|
21
|
+
const ttlSeconds = options?.ttlSeconds;
|
|
22
|
+
if (ttlSeconds === undefined) {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!Number.isSafeInteger(ttlSeconds) || ttlSeconds <= 0) {
|
|
27
|
+
throw new RangeError(
|
|
28
|
+
"Cache ttlSeconds must be a positive safe integer when provided.",
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return ttlSeconds;
|
|
33
|
+
}
|
|
34
|
+
|
|
11
35
|
/**
|
|
12
36
|
* App-facing string cache port.
|
|
13
37
|
*
|
|
@@ -54,15 +78,12 @@ type MemoryCacheEntry = {
|
|
|
54
78
|
};
|
|
55
79
|
|
|
56
80
|
function resolveExpiresAt(options: CacheSetOptions | undefined): number | null {
|
|
57
|
-
|
|
81
|
+
const ttlSeconds = resolveCacheTtlSeconds(options);
|
|
82
|
+
if (ttlSeconds === undefined) {
|
|
58
83
|
return null;
|
|
59
84
|
}
|
|
60
85
|
|
|
61
|
-
|
|
62
|
-
return Date.now();
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return Date.now() + options.ttlSeconds * 1000;
|
|
86
|
+
return Math.min(Number.MAX_SAFE_INTEGER, Date.now() + ttlSeconds * 1000);
|
|
66
87
|
}
|
|
67
88
|
|
|
68
89
|
function isExpired(entry: MemoryCacheEntry): boolean {
|
|
@@ -120,6 +141,7 @@ export function createMemoryCache(
|
|
|
120
141
|
return (await getFreshEntry(key)) != null;
|
|
121
142
|
},
|
|
122
143
|
async remember(key, factory, options) {
|
|
144
|
+
resolveCacheTtlSeconds(options);
|
|
123
145
|
const cached = await cache.get(key);
|
|
124
146
|
if (cached != null) {
|
|
125
147
|
return cached;
|
package/src/ports/events.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
EventPayloadDef,
|
|
3
3
|
EventPublishOptions,
|
|
4
|
+
EventSubscription,
|
|
4
5
|
InferEventPayload as InferContractEventPayload,
|
|
5
6
|
StandardSchema,
|
|
6
7
|
} from "../events/index.js";
|
|
8
|
+
|
|
9
|
+
export type { EventSubscription } from "../events/index.js";
|
|
10
|
+
|
|
7
11
|
import type {
|
|
8
12
|
JobDef as ContractJobDef,
|
|
9
13
|
InferJobPayload as InferContractJobPayload,
|
|
@@ -58,15 +62,16 @@ export type InferJobPayload<J extends JobDef> = InferContractJobPayload<J>;
|
|
|
58
62
|
* const eventBus = createMemoryEventBus();
|
|
59
63
|
*
|
|
60
64
|
* // Subscribe to an event
|
|
61
|
-
* const
|
|
65
|
+
* const subscription = eventBus.subscribe(UserRegistered, (payload) => {
|
|
62
66
|
* console.log(`User registered: ${payload.email}`);
|
|
63
67
|
* });
|
|
68
|
+
* await subscription.ready;
|
|
64
69
|
*
|
|
65
70
|
* // Publish an event
|
|
66
71
|
* await eventBus.publish(UserRegistered, { userId: "123", email: "test@example.com" });
|
|
67
72
|
*
|
|
68
73
|
* // Unsubscribe when done
|
|
69
|
-
* unsubscribe();
|
|
74
|
+
* await subscription.unsubscribe();
|
|
70
75
|
* ```
|
|
71
76
|
*/
|
|
72
77
|
export interface EventBusPort {
|
|
@@ -80,7 +85,7 @@ export interface EventBusPort {
|
|
|
80
85
|
): Promise<void> | void;
|
|
81
86
|
|
|
82
87
|
/**
|
|
83
|
-
* Subscribe to a domain event. Returns
|
|
88
|
+
* Subscribe to a domain event. Returns initial readiness and cleanup.
|
|
84
89
|
*/
|
|
85
90
|
subscribe<E extends DomainEventDef>(
|
|
86
91
|
event: E,
|
|
@@ -88,7 +93,7 @@ export interface EventBusPort {
|
|
|
88
93
|
payload: InferEventPayload<E>,
|
|
89
94
|
options?: EventPublishOptions,
|
|
90
95
|
) => Promise<void> | void,
|
|
91
|
-
):
|
|
96
|
+
): EventSubscription;
|
|
92
97
|
}
|
|
93
98
|
|
|
94
99
|
/**
|
package/src/ports/index.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* - `definePorts` – helper to define a typed ports object
|
|
9
9
|
* - `PortsContext` – a generic type that describes ctx objects that carry ports
|
|
10
10
|
* - `EventBusPort` – interface for event bus implementations
|
|
11
|
+
* - `BestEffortWorkPort` – interface for non-durable follow-up work
|
|
11
12
|
* - `JobDispatcherPort` – interface for job dispatch implementations
|
|
12
13
|
* - `UnitOfWorkPort` – interface for app-owned transaction boundaries
|
|
13
14
|
* - `OutboxPort` – interface for durable event/job delivery storage
|
|
@@ -468,6 +469,13 @@ export {
|
|
|
468
469
|
requireUserId,
|
|
469
470
|
TenantRequiredError,
|
|
470
471
|
} from "./auth.js";
|
|
472
|
+
/**
|
|
473
|
+
* Best-effort work port exports.
|
|
474
|
+
*/
|
|
475
|
+
export type {
|
|
476
|
+
BestEffortWork,
|
|
477
|
+
BestEffortWorkPort,
|
|
478
|
+
} from "./best-effort-work.js";
|
|
471
479
|
/**
|
|
472
480
|
* Ports builder exports.
|
|
473
481
|
*/
|
|
@@ -483,7 +491,7 @@ export type { CachePort, CacheSetOptions } from "./cache.js";
|
|
|
483
491
|
/**
|
|
484
492
|
* Cache helper exports.
|
|
485
493
|
*/
|
|
486
|
-
export { createMemoryCache } from "./cache.js";
|
|
494
|
+
export { createMemoryCache, resolveCacheTtlSeconds } from "./cache.js";
|
|
487
495
|
/**
|
|
488
496
|
* Clock port exports.
|
|
489
497
|
*/
|
|
@@ -498,6 +506,7 @@ export { createFrozenClock, createSystemClock } from "./clock.js";
|
|
|
498
506
|
export type {
|
|
499
507
|
DomainEventDef,
|
|
500
508
|
EventBusPort,
|
|
509
|
+
EventSubscription,
|
|
501
510
|
InferEventPayload,
|
|
502
511
|
InferJobPayload,
|
|
503
512
|
JobDef,
|
package/src/ports/testing.ts
CHANGED
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
createTenant,
|
|
39
39
|
createUserActor,
|
|
40
40
|
} from "./audit.js";
|
|
41
|
+
import type { BestEffortWork, BestEffortWorkPort } from "./best-effort-work.js";
|
|
41
42
|
import type { EventBusPort, JobDef, JobDispatcherPort } from "./events.js";
|
|
42
43
|
import {
|
|
43
44
|
type CreateGateOptions,
|
|
@@ -114,6 +115,50 @@ export function createRecordingEventBus(): {
|
|
|
114
115
|
return { bus, events };
|
|
115
116
|
}
|
|
116
117
|
|
|
118
|
+
/**
|
|
119
|
+
* Recording best-effort-work adapter for deterministic tests.
|
|
120
|
+
*
|
|
121
|
+
* Calls to `defer(...)` append work without running it. `flush()` runs the
|
|
122
|
+
* currently pending batch in FIFO order, then rejects with the callback error
|
|
123
|
+
* or an `AggregateError` after every callback in that batch has been attempted.
|
|
124
|
+
* Work deferred by a running callback remains pending for the next flush so
|
|
125
|
+
* one flush stays bounded.
|
|
126
|
+
*/
|
|
127
|
+
export function createRecordingBestEffortWork(): {
|
|
128
|
+
bestEffortWork: BestEffortWorkPort;
|
|
129
|
+
pending: BestEffortWork[];
|
|
130
|
+
flush(): Promise<void>;
|
|
131
|
+
} {
|
|
132
|
+
const pending: BestEffortWork[] = [];
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
bestEffortWork: {
|
|
136
|
+
defer(work) {
|
|
137
|
+
pending.push(work);
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
pending,
|
|
141
|
+
async flush() {
|
|
142
|
+
const batch = pending.splice(0);
|
|
143
|
+
const errors: unknown[] = [];
|
|
144
|
+
for (const work of batch) {
|
|
145
|
+
try {
|
|
146
|
+
await work();
|
|
147
|
+
} catch (error) {
|
|
148
|
+
errors.push(error);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (errors.length === 1) {
|
|
153
|
+
throw errors[0];
|
|
154
|
+
}
|
|
155
|
+
if (errors.length > 1) {
|
|
156
|
+
throw new AggregateError(errors, "Best-effort work batch failed.");
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
117
162
|
/**
|
|
118
163
|
* A job dispatch captured by `createRecordingJobDispatcher(...)`.
|
|
119
164
|
*/
|
|
@@ -55,9 +55,11 @@ export type MaybePromise<T> = T | Promise<T>;
|
|
|
55
55
|
/**
|
|
56
56
|
* Late-bound service context factory exposed to providers.
|
|
57
57
|
*
|
|
58
|
-
* Calling it
|
|
59
|
-
*
|
|
60
|
-
*
|
|
58
|
+
* Calling it during provider setup or after provider shutdown throws. Start
|
|
59
|
+
* and stop hooks may invoke it after every provider has contributed ports and
|
|
60
|
+
* the server has verified that no deferred port remains unbound. Runtime
|
|
61
|
+
* entrypoints such as job dispatch, listeners, or scheduled work can close
|
|
62
|
+
* over it for later use.
|
|
61
63
|
*
|
|
62
64
|
* App-local providers can type the factory by declaring `Context` and
|
|
63
65
|
* `ServiceInput` through the curried `createProvider<Requires, Context,
|
|
@@ -222,8 +224,9 @@ export interface ServiceProvider<
|
|
|
222
224
|
* @param ctx.ports - Ports contributed by previous providers
|
|
223
225
|
* @param ctx.config - Validated config (if config was defined), or undefined
|
|
224
226
|
* @param ctx.createServiceContext - Late-bound service context factory.
|
|
225
|
-
* Throws
|
|
226
|
-
* entrypoints such as job
|
|
227
|
+
* Throws during setup. Start and stop hooks may invoke it after all provider
|
|
228
|
+
* ports have been contributed and validated; runtime entrypoints such as job
|
|
229
|
+
* dispatchers and event listeners may call it lazily afterward.
|
|
227
230
|
*/
|
|
228
231
|
setup(ctx: {
|
|
229
232
|
ports: Readonly<Ports>;
|
package/src/server/hooks/cors.ts
CHANGED
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
* CORS hook utilities for @beignet/core/server
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import type {
|
|
5
|
+
import type {
|
|
6
|
+
HttpRequestLike,
|
|
7
|
+
HttpResponseHeaders,
|
|
8
|
+
ServerHook,
|
|
9
|
+
} from "../types.js";
|
|
6
10
|
|
|
7
11
|
/**
|
|
8
12
|
* CORS configuration for `createCorsHooks(...)`.
|
|
@@ -55,10 +59,12 @@ function resolveCorsConfig(config: CorsConfig): Required<CorsConfig> {
|
|
|
55
59
|
return resolved;
|
|
56
60
|
}
|
|
57
61
|
|
|
58
|
-
function appendVaryOrigin(headers:
|
|
62
|
+
function appendVaryOrigin(headers: HttpResponseHeaders): void {
|
|
59
63
|
const varyKey =
|
|
60
64
|
Object.keys(headers).find((key) => key.toLowerCase() === "vary") ?? "Vary";
|
|
61
|
-
const
|
|
65
|
+
const currentValue = headers[varyKey];
|
|
66
|
+
const current =
|
|
67
|
+
typeof currentValue === "string" ? currentValue : currentValue?.join(", ");
|
|
62
68
|
const values = current?.split(",").map((value) => value.trim().toLowerCase());
|
|
63
69
|
if (values?.includes("origin")) return;
|
|
64
70
|
|
|
@@ -72,7 +78,7 @@ function appendVaryOrigin(headers: Record<string, string>): void {
|
|
|
72
78
|
* when cookies or authorization headers are allowed cross-origin.
|
|
73
79
|
*/
|
|
74
80
|
export function applyCorsHeaders(
|
|
75
|
-
headers:
|
|
81
|
+
headers: HttpResponseHeaders,
|
|
76
82
|
req: HttpRequestLike,
|
|
77
83
|
corsConfig: CorsConfig,
|
|
78
84
|
): void {
|
|
@@ -121,7 +127,7 @@ export function createCorsHooks<Ctx>(config: CorsConfig): ServerHook<Ctx> {
|
|
|
121
127
|
) {
|
|
122
128
|
return undefined;
|
|
123
129
|
}
|
|
124
|
-
const headers:
|
|
130
|
+
const headers: HttpResponseHeaders = {};
|
|
125
131
|
applyCorsHeaders(headers, req, corsConfig);
|
|
126
132
|
return {
|
|
127
133
|
status: 204,
|
|
@@ -7,7 +7,11 @@ import {
|
|
|
7
7
|
resolveTrustedRequest,
|
|
8
8
|
type TrustedRequestInfo,
|
|
9
9
|
} from "../trusted-proxy.js";
|
|
10
|
-
import type {
|
|
10
|
+
import type {
|
|
11
|
+
HttpRequestLike,
|
|
12
|
+
HttpResponseHeaders,
|
|
13
|
+
ServerHook,
|
|
14
|
+
} from "../types.js";
|
|
11
15
|
import { getRequestIdFromContext } from "./utils.js";
|
|
12
16
|
|
|
13
17
|
/**
|
|
@@ -60,7 +64,7 @@ export interface LoggingConfig<Ctx> {
|
|
|
60
64
|
ctx?: Ctx;
|
|
61
65
|
req: HttpRequestLike;
|
|
62
66
|
requestInfo: TrustedRequestInfo;
|
|
63
|
-
res: { status: number; headers:
|
|
67
|
+
res: { status: number; headers: HttpResponseHeaders };
|
|
64
68
|
durationMs: number;
|
|
65
69
|
contract?: HttpContractConfig;
|
|
66
70
|
error?: unknown;
|