@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.
Files changed (156) hide show
  1. package/CHANGELOG.md +115 -0
  2. package/README.md +346 -21
  3. package/dist/application/index.d.ts.map +1 -1
  4. package/dist/application/index.js +63 -47
  5. package/dist/application/index.js.map +1 -1
  6. package/dist/domain/entity.d.ts +37 -6
  7. package/dist/domain/entity.d.ts.map +1 -1
  8. package/dist/domain/entity.js +9 -7
  9. package/dist/domain/entity.js.map +1 -1
  10. package/dist/domain/index.d.ts +4 -4
  11. package/dist/domain/index.d.ts.map +1 -1
  12. package/dist/domain/index.js +4 -4
  13. package/dist/domain/index.js.map +1 -1
  14. package/dist/events/index.d.ts +6 -0
  15. package/dist/events/index.d.ts.map +1 -1
  16. package/dist/events/index.js +20 -10
  17. package/dist/events/index.js.map +1 -1
  18. package/dist/flags/index.d.ts +5 -4
  19. package/dist/flags/index.d.ts.map +1 -1
  20. package/dist/flags/index.js +10 -9
  21. package/dist/flags/index.js.map +1 -1
  22. package/dist/idempotency/index.d.ts +11 -1
  23. package/dist/idempotency/index.d.ts.map +1 -1
  24. package/dist/idempotency/index.js +4 -3
  25. package/dist/idempotency/index.js.map +1 -1
  26. package/dist/jobs/index.d.ts +449 -9
  27. package/dist/jobs/index.d.ts.map +1 -1
  28. package/dist/jobs/index.js +472 -12
  29. package/dist/jobs/index.js.map +1 -1
  30. package/dist/outbox/index.d.ts +170 -3
  31. package/dist/outbox/index.d.ts.map +1 -1
  32. package/dist/outbox/index.js +192 -8
  33. package/dist/outbox/index.js.map +1 -1
  34. package/dist/ports/events.d.ts +2 -2
  35. package/dist/ports/index.d.ts +6 -1
  36. package/dist/ports/index.d.ts.map +1 -1
  37. package/dist/ports/index.js +1 -0
  38. package/dist/ports/index.js.map +1 -1
  39. package/dist/providers/instrumentation.d.ts +4 -0
  40. package/dist/providers/instrumentation.d.ts.map +1 -1
  41. package/dist/providers/instrumentation.js.map +1 -1
  42. package/dist/providers/provider.d.ts +10 -2
  43. package/dist/providers/provider.d.ts.map +1 -1
  44. package/dist/providers/provider.js.map +1 -1
  45. package/dist/schedules/index.d.ts +8 -7
  46. package/dist/schedules/index.d.ts.map +1 -1
  47. package/dist/schedules/index.js +47 -16
  48. package/dist/schedules/index.js.map +1 -1
  49. package/dist/search/index.d.ts +2 -1
  50. package/dist/search/index.d.ts.map +1 -1
  51. package/dist/search/index.js +1 -0
  52. package/dist/search/index.js.map +1 -1
  53. package/dist/server/hooks/index.d.ts +1 -0
  54. package/dist/server/hooks/index.d.ts.map +1 -1
  55. package/dist/server/hooks/index.js +1 -0
  56. package/dist/server/hooks/index.js.map +1 -1
  57. package/dist/server/hooks/rate-limit.d.ts +26 -13
  58. package/dist/server/hooks/rate-limit.d.ts.map +1 -1
  59. package/dist/server/hooks/rate-limit.js +28 -34
  60. package/dist/server/hooks/rate-limit.js.map +1 -1
  61. package/dist/server/hooks/security.d.ts +179 -0
  62. package/dist/server/hooks/security.d.ts.map +1 -0
  63. package/dist/server/hooks/security.js +225 -0
  64. package/dist/server/hooks/security.js.map +1 -0
  65. package/dist/server/index.d.ts +8 -0
  66. package/dist/server/index.d.ts.map +1 -1
  67. package/dist/server/index.js +8 -0
  68. package/dist/server/index.js.map +1 -1
  69. package/dist/server/instrumentation.d.ts.map +1 -1
  70. package/dist/server/instrumentation.js +22 -6
  71. package/dist/server/instrumentation.js.map +1 -1
  72. package/dist/server/providers/loadProviderConfig.d.ts.map +1 -1
  73. package/dist/server/providers/loadProviderConfig.js +15 -2
  74. package/dist/server/providers/loadProviderConfig.js.map +1 -1
  75. package/dist/server/request-context.d.ts +4 -0
  76. package/dist/server/request-context.d.ts.map +1 -1
  77. package/dist/server/request-context.js +3 -0
  78. package/dist/server/request-context.js.map +1 -1
  79. package/dist/server/runtime-integrity.d.ts +84 -0
  80. package/dist/server/runtime-integrity.d.ts.map +1 -0
  81. package/dist/server/runtime-integrity.js +160 -0
  82. package/dist/server/runtime-integrity.js.map +1 -0
  83. package/dist/server/server.d.ts +10 -0
  84. package/dist/server/server.d.ts.map +1 -1
  85. package/dist/server/server.js +55 -1
  86. package/dist/server/server.js.map +1 -1
  87. package/dist/server/trusted-proxy.d.ts +77 -0
  88. package/dist/server/trusted-proxy.d.ts.map +1 -0
  89. package/dist/server/trusted-proxy.js +129 -0
  90. package/dist/server/trusted-proxy.js.map +1 -0
  91. package/dist/tasks/index.d.ts +6 -7
  92. package/dist/tasks/index.d.ts.map +1 -1
  93. package/dist/tasks/index.js +22 -5
  94. package/dist/tasks/index.js.map +1 -1
  95. package/dist/tenancy/index.d.ts +41 -0
  96. package/dist/tenancy/index.d.ts.map +1 -0
  97. package/dist/tenancy/index.js +34 -0
  98. package/dist/tenancy/index.js.map +1 -0
  99. package/dist/testing/index.d.ts +6 -0
  100. package/dist/testing/index.d.ts.map +1 -1
  101. package/dist/testing/index.js +12 -3
  102. package/dist/testing/index.js.map +1 -1
  103. package/dist/tracing/execution.d.ts +14 -0
  104. package/dist/tracing/execution.d.ts.map +1 -0
  105. package/dist/tracing/execution.js +17 -0
  106. package/dist/tracing/execution.js.map +1 -0
  107. package/dist/tracing/index.d.ts +49 -0
  108. package/dist/tracing/index.d.ts.map +1 -1
  109. package/dist/tracing/index.js +59 -0
  110. package/dist/tracing/index.js.map +1 -1
  111. package/dist/uploads/client.d.ts.map +1 -1
  112. package/dist/uploads/client.js +44 -6
  113. package/dist/uploads/client.js.map +1 -1
  114. package/dist/uploads/index.d.ts +182 -2
  115. package/dist/uploads/index.d.ts.map +1 -1
  116. package/dist/uploads/index.js +468 -76
  117. package/dist/uploads/index.js.map +1 -1
  118. package/dist/webhooks/index.d.ts +56 -4
  119. package/dist/webhooks/index.d.ts.map +1 -1
  120. package/dist/webhooks/index.js +107 -2
  121. package/dist/webhooks/index.js.map +1 -1
  122. package/package.json +5 -1
  123. package/skills/app-architecture/SKILL.md +23 -2
  124. package/src/application/index.ts +88 -56
  125. package/src/domain/entity.ts +61 -13
  126. package/src/domain/index.ts +8 -7
  127. package/src/events/index.ts +27 -14
  128. package/src/flags/index.ts +23 -19
  129. package/src/idempotency/index.ts +17 -3
  130. package/src/jobs/index.ts +1022 -19
  131. package/src/outbox/index.ts +409 -9
  132. package/src/ports/events.ts +2 -2
  133. package/src/ports/index.ts +17 -0
  134. package/src/providers/instrumentation.ts +4 -0
  135. package/src/providers/provider.ts +11 -2
  136. package/src/schedules/index.ts +60 -26
  137. package/src/search/index.ts +3 -1
  138. package/src/server/hooks/index.ts +10 -0
  139. package/src/server/hooks/rate-limit.ts +52 -46
  140. package/src/server/hooks/security.ts +503 -0
  141. package/src/server/index.ts +8 -0
  142. package/src/server/instrumentation.ts +25 -5
  143. package/src/server/providers/loadProviderConfig.ts +19 -2
  144. package/src/server/request-context.ts +7 -0
  145. package/src/server/runtime-integrity.ts +322 -0
  146. package/src/server/server.ts +86 -1
  147. package/src/server/trusted-proxy.ts +249 -0
  148. package/src/tasks/index.ts +29 -12
  149. package/src/tenancy/index.ts +55 -0
  150. package/src/testing/index.ts +19 -3
  151. package/src/tracing/execution.ts +37 -0
  152. package/src/tracing/index.ts +137 -0
  153. package/src/uploads/client.ts +65 -6
  154. package/src/uploads/index.ts +713 -76
  155. package/src/webhooks/index.ts +240 -9
  156. package/src/domain/events.ts +0 -59
@@ -5,6 +5,8 @@ import {
5
5
  } from "../providers/instrumentation.js";
6
6
  import {
7
7
  createChildTraceContext,
8
+ resolveTracingPort,
9
+ runWithTracing,
8
10
  type TraceContextInput,
9
11
  } from "../tracing/index.js";
10
12
 
@@ -482,6 +484,7 @@ function getInstrumentationTrace(ctx: unknown): TraceContextInput | undefined {
482
484
  spanId: context.spanId,
483
485
  parentSpanId: context.parentSpanId,
484
486
  traceparent: context.traceparent,
487
+ tracestate: context.tracestate,
485
488
  };
486
489
  }
487
490
 
@@ -517,9 +520,9 @@ function startUseCaseRunInstrumentation(args: {
517
520
  if (!useCasesEnabled && !errorsEnabled) return undefined;
518
521
 
519
522
  const requestId = getInstrumentationRequestId(args.ctx);
520
- const trace = createChildTraceContext(
521
- getInstrumentationTrace(args.ctx) ?? {},
522
- );
523
+ const trace =
524
+ resolveTracingPort(ports)?.current() ??
525
+ createChildTraceContext(getInstrumentationTrace(args.ctx) ?? {});
523
526
 
524
527
  const record = (event: ProviderInstrumentationEventInput) => {
525
528
  try {
@@ -542,6 +545,7 @@ function startUseCaseRunInstrumentation(args: {
542
545
  spanId: trace.spanId,
543
546
  parentSpanId: trace.parentSpanId,
544
547
  traceparent: trace.traceparent,
548
+ tracestate: trace.tracestate,
545
549
  name: args.name,
546
550
  kind: args.kind,
547
551
  phase,
@@ -558,6 +562,7 @@ function startUseCaseRunInstrumentation(args: {
558
562
  spanId: trace.spanId,
559
563
  parentSpanId: trace.parentSpanId,
560
564
  traceparent: trace.traceparent,
565
+ tracestate: trace.tracestate,
561
566
  message: getRunErrorMessage(error),
562
567
  stack: error instanceof Error ? error.stack : undefined,
563
568
  useCaseName: args.name,
@@ -725,61 +730,88 @@ class UseCaseBuilder<
725
730
  : never,
726
731
  parseInput: boolean,
727
732
  ) => {
728
- const startedAt = Date.now();
729
- const instrumentation = instrumented
730
- ? startUseCaseRunInstrumentation({
731
- ctx: args.ctx,
733
+ const traceAttributes = {
734
+ "beignet.use_case.name": useCaseName,
735
+ "beignet.use_case.kind": useCaseKind,
736
+ } as const;
737
+
738
+ return await runWithTracing(
739
+ args.ctx,
740
+ {
741
+ name: `beignet.use_case ${useCaseName}`,
742
+ type: "useCase",
743
+ kind: "internal",
744
+ attributes: traceAttributes,
745
+ metricAttributes: traceAttributes,
746
+ },
747
+ async () => {
748
+ const startedAt = Date.now();
749
+ const instrumentation = instrumented
750
+ ? startUseCaseRunInstrumentation({
751
+ ctx: args.ctx,
752
+ name: useCaseName,
753
+ kind: useCaseKind,
754
+ })
755
+ : undefined;
756
+ onRun?.({
732
757
  name: useCaseName,
733
758
  kind: useCaseKind,
734
- })
735
- : undefined;
736
- onRun?.({
737
- name: useCaseName,
738
- kind: useCaseKind,
739
- phase: "start",
740
- ctx: args.ctx,
741
- });
742
-
743
- try {
744
- const parsedInput =
745
- parseInput && validation.input
746
- ? await parseSchema(inputSchema, args.input, useCaseName, "input")
747
- : (args.input as SchemaOutput<InputSchema>);
748
-
749
- const rawResult = await fn({
750
- ctx: args.ctx,
751
- input: parsedInput,
752
- events: eventHelpers,
753
- });
754
-
755
- const result = validation.output
756
- ? await parseSchema(outputSchema, rawResult, useCaseName, "output")
757
- : (rawResult as SchemaOutput<OutputSchema>);
758
-
759
- const durationMs = Date.now() - startedAt;
760
- instrumentation?.end(durationMs);
761
- onRun?.({
762
- name: useCaseName,
763
- kind: useCaseKind,
764
- phase: "end",
765
- durationMs,
766
- ctx: args.ctx,
767
- });
768
-
769
- return result;
770
- } catch (err) {
771
- const durationMs = Date.now() - startedAt;
772
- instrumentation?.error(durationMs, err);
773
- onRun?.({
774
- name: useCaseName,
775
- kind: useCaseKind,
776
- phase: "error",
777
- durationMs,
778
- error: err,
779
- ctx: args.ctx,
780
- });
781
- throw err;
782
- }
759
+ phase: "start",
760
+ ctx: args.ctx,
761
+ });
762
+
763
+ try {
764
+ const parsedInput =
765
+ parseInput && validation.input
766
+ ? await parseSchema(
767
+ inputSchema,
768
+ args.input,
769
+ useCaseName,
770
+ "input",
771
+ )
772
+ : (args.input as SchemaOutput<InputSchema>);
773
+
774
+ const rawResult = await fn({
775
+ ctx: args.ctx,
776
+ input: parsedInput,
777
+ events: eventHelpers,
778
+ });
779
+
780
+ const result = validation.output
781
+ ? await parseSchema(
782
+ outputSchema,
783
+ rawResult,
784
+ useCaseName,
785
+ "output",
786
+ )
787
+ : (rawResult as SchemaOutput<OutputSchema>);
788
+
789
+ const durationMs = Date.now() - startedAt;
790
+ instrumentation?.end(durationMs);
791
+ onRun?.({
792
+ name: useCaseName,
793
+ kind: useCaseKind,
794
+ phase: "end",
795
+ durationMs,
796
+ ctx: args.ctx,
797
+ });
798
+
799
+ return result;
800
+ } catch (err) {
801
+ const durationMs = Date.now() - startedAt;
802
+ instrumentation?.error(durationMs, err);
803
+ onRun?.({
804
+ name: useCaseName,
805
+ kind: useCaseKind,
806
+ phase: "error",
807
+ durationMs,
808
+ error: err,
809
+ ctx: args.ctx,
810
+ });
811
+ throw err;
812
+ }
813
+ },
814
+ );
783
815
  };
784
816
 
785
817
  type RunArgs = Parameters<typeof execute>[0];
@@ -66,9 +66,47 @@ function validateSchemaSync<T>(
66
66
  // biome-ignore lint/suspicious/noExplicitAny: Base methods type for flexibility
67
67
  type AnyMethods = Record<string, any>;
68
68
 
69
+ /**
70
+ * Base entity instance passed to the `.methods(...)` builder: validated props
71
+ * plus the `with` and `toJSON` helpers, before domain methods attach.
72
+ *
73
+ * `with(...)` is typed as returning the base instance here because the final
74
+ * methods object is still being defined while the builder runs. At runtime
75
+ * the returned instance carries the entity's methods, and `EntityInstance`
76
+ * maps base-instance returns back to the full instance type so consumers keep
77
+ * typed method chaining.
78
+ */
79
+ export type EntityBaseInstance<Schema extends StandardSchema> =
80
+ InferOutput<Schema> & {
81
+ /**
82
+ * Create a new validated entity instance with patched properties.
83
+ */
84
+ with(
85
+ patch: Partial<InferOutput<Schema>>,
86
+ ): EntityBaseInstance<Schema> | Promise<EntityBaseInstance<Schema>>;
87
+ /**
88
+ * Convert the entity to a plain JSON object for persistence.
89
+ */
90
+ toJSON(): InferOutput<Schema>;
91
+ };
92
+
93
+ /**
94
+ * Replace base-instance returns with the full entity instance type, unwrapping
95
+ * promises, so methods that return `self` or `self.with(...)` chain with the
96
+ * entity's methods attached.
97
+ */
98
+ type WithEntityInstance<R, Instance, Schema extends StandardSchema> =
99
+ R extends Promise<infer P>
100
+ ? Promise<WithEntityInstance<P, Instance, Schema>>
101
+ : R extends EntityBaseInstance<Schema>
102
+ ? Instance
103
+ : R;
104
+
69
105
  /**
70
106
  * Entity instance with validated props, attached methods, and immutable update
71
- * helpers.
107
+ * helpers. Method returns that are typed as the base instance (`self` or
108
+ * `self.with(...)`) are mapped to the full instance type so chaining keeps
109
+ * the entity's methods.
72
110
  */
73
111
  export type EntityInstance<
74
112
  Name extends string,
@@ -87,7 +125,13 @@ export type EntityInstance<
87
125
  * Convert the entity to a plain JSON object for persistence.
88
126
  */
89
127
  toJSON(): InferOutput<Schema>;
90
- } & Methods;
128
+ } & {
129
+ [K in keyof Methods]: Methods[K] extends (...args: infer A) => infer R
130
+ ? (
131
+ ...args: A
132
+ ) => WithEntityInstance<R, EntityInstance<Name, Schema, Methods>, Schema>
133
+ : Methods[K];
134
+ };
91
135
 
92
136
  /**
93
137
  * Entity definition returned by `defineEntity(...).build()`.
@@ -135,8 +179,7 @@ class EntityBuilder<
135
179
  private readonly cfg: {
136
180
  name: Name;
137
181
  schema?: Schema;
138
- // biome-ignore lint/suspicious/noExplicitAny: Methods builder receives base instance
139
- methods?: (self: any) => Methods;
182
+ methods?: (self: EntityBaseInstance<Schema>) => Methods;
140
183
  },
141
184
  ) {}
142
185
 
@@ -147,17 +190,22 @@ class EntityBuilder<
147
190
  return new EntityBuilder<Name, S, Methods>({
148
191
  name: this.cfg.name,
149
192
  schema,
150
- methods: this.cfg.methods,
193
+ // Methods declared before props were typed against the previous schema;
194
+ // canonical builder order is props-then-methods, so this widening only
195
+ // affects the discouraged reverse order.
196
+ methods: this.cfg.methods as unknown as
197
+ | ((self: EntityBaseInstance<S>) => Methods)
198
+ | undefined,
151
199
  });
152
200
  }
153
201
 
154
202
  /**
155
203
  * Define methods to attach to entity instances.
156
- * The method builder receives the base instance (with props, with, and toJSON).
204
+ * The method builder receives the typed base instance: validated props plus
205
+ * `with` and `toJSON`.
157
206
  */
158
207
  methods<M extends AnyMethods>(
159
- // biome-ignore lint/suspicious/noExplicitAny: Methods builder receives base instance with props
160
- build: (self: any) => M,
208
+ build: (self: EntityBaseInstance<Schema>) => M,
161
209
  ): EntityBuilder<Name, Schema, M> {
162
210
  return new EntityBuilder<Name, Schema, M>({
163
211
  name: this.cfg.name,
@@ -190,8 +238,7 @@ class EntityBuilder<
190
238
  // Cast parsed to a plain object for spreading
191
239
  const parsedObj = parsed as Record<string, unknown>;
192
240
 
193
- // biome-ignore lint/suspicious/noExplicitAny: Dynamic object construction
194
- const base: any = {
241
+ const base = {
195
242
  ...parsedObj,
196
243
  // Immutable update - creates a new validated instance
197
244
  // Note: Re-validates the merged props for data integrity
@@ -205,7 +252,7 @@ class EntityBuilder<
205
252
  toJSON() {
206
253
  return parsed;
207
254
  },
208
- };
255
+ } as EntityBaseInstance<Schema>;
209
256
 
210
257
  const methods = methodsBuilder?.(base) ?? {};
211
258
  const instance = Object.freeze({
@@ -262,6 +309,7 @@ class EntityBuilder<
262
309
  * ```
263
310
  */
264
311
  export function defineEntity<Name extends string>(name: Name) {
265
- // biome-ignore lint/suspicious/noExplicitAny: Initial schema type will be set via .props()
266
- return new EntityBuilder<Name, any, Record<string, never>>({ name } as any);
312
+ return new EntityBuilder<Name, StandardSchema, Record<string, never>>({
313
+ name,
314
+ });
267
315
  }
@@ -1,18 +1,19 @@
1
1
  /**
2
2
  * @beignet/core/domain
3
3
  *
4
- * Domain modeling helpers for Beignet - value objects, entities, and domain events.
4
+ * Domain modeling helpers for Beignet - value objects and entities.
5
5
  *
6
6
  * This package provides small, framework-agnostic helpers for domain modeling:
7
7
  * - A Value Object builder (defineValueObject)
8
8
  * - An Entity/Aggregate builder (defineEntity)
9
- * - A Domain Event helper (defineDomainEvent)
9
+ *
10
+ * Domain events are declared with `defineEvent` from `@beignet/core/events`.
10
11
  */
11
12
 
12
- export { defineEntity, type EntityDef, type EntityInstance } from "./entity.js";
13
13
  export {
14
- type DomainEventDef,
15
- defineDomainEvent,
16
- type InferEventPayload,
17
- } from "./events.js";
14
+ defineEntity,
15
+ type EntityBaseInstance,
16
+ type EntityDef,
17
+ type EntityInstance,
18
+ } from "./entity.js";
18
19
  export { defineValueObject, type ValueObjectDef } from "./value-object.js";
@@ -1,4 +1,6 @@
1
1
  import type { StandardSchemaV1 } from "@standard-schema/spec";
2
+ import { runWithResolvedTracingContext } from "../tracing/execution.js";
3
+ import type { TracingPort } from "../tracing/index.js";
2
4
 
3
5
  /**
4
6
  * Any Standard Schema compatible validator.
@@ -160,6 +162,11 @@ export interface RegisterListenersOptions<Ctx> {
160
162
  * Static listener context or factory evaluated for each delivered event.
161
163
  */
162
164
  ctx?: Ctx | (() => MaybePromise<Ctx>);
165
+ /**
166
+ * Runtime tracing port used to start the listener span before a lazy context
167
+ * factory runs.
168
+ */
169
+ tracing?: TracingPort;
163
170
  /**
164
171
  * Called when a listener fails. When omitted, listener errors are rethrown to
165
172
  * the event bus subscription callback.
@@ -243,16 +250,6 @@ async function parsePayload<Schema extends StandardSchemaV1>(
243
250
  throw new Error("Invalid Standard Schema result: missing value");
244
251
  }
245
252
 
246
- async function resolveCtx<Ctx>(
247
- ctx: Ctx | (() => MaybePromise<Ctx>) | undefined,
248
- ): Promise<Ctx> {
249
- if (typeof ctx === "function") {
250
- return (ctx as () => MaybePromise<Ctx>)();
251
- }
252
-
253
- return ctx as Ctx;
254
- }
255
-
256
253
  /**
257
254
  * Define a typed event.
258
255
  *
@@ -326,10 +323,26 @@ export function registerListeners<Ctx>(
326
323
  eventBus.subscribe(listener.event, async (rawPayload) => {
327
324
  try {
328
325
  const payload = await parseEventPayload(listener.event, rawPayload);
329
- await listener.handle({
330
- event: listener.event,
331
- payload,
332
- ctx: await resolveCtx(options.ctx),
326
+ const traceAttributes = {
327
+ "beignet.listener.name": listener.name,
328
+ "beignet.event.name": listener.event.name,
329
+ } as const;
330
+ await runWithResolvedTracingContext({
331
+ tracing: options.tracing,
332
+ ctx: options.ctx as Ctx | (() => MaybePromise<Ctx>),
333
+ operation: {
334
+ name: `beignet.listener ${listener.name}`,
335
+ type: "listener",
336
+ kind: "consumer",
337
+ attributes: traceAttributes,
338
+ metricAttributes: traceAttributes,
339
+ },
340
+ run: (ctx) =>
341
+ listener.handle({
342
+ event: listener.event,
343
+ payload,
344
+ ctx,
345
+ }),
333
346
  });
334
347
  } catch (error) {
335
348
  options.onError?.(error, listener);
@@ -38,7 +38,7 @@ export type FlagKey = string;
38
38
  /**
39
39
  * Feature flag value kind.
40
40
  */
41
- export type FlagKind = "boolean" | "string" | "number" | "object";
41
+ export type FlagValueKind = "boolean" | "string" | "number" | "object";
42
42
 
43
43
  /**
44
44
  * Subject used for provider-neutral flag targeting.
@@ -104,8 +104,9 @@ export type FlagEvaluationContext = {
104
104
  * Flag definition registered through `defineFlags(...)`.
105
105
  */
106
106
  export type FlagDef<TValue extends FlagValue = FlagValue> = {
107
+ kind: "flag";
107
108
  key: FlagKey;
108
- kind: FlagKindForValue<TValue>;
109
+ valueKind: FlagValueKindForValue<TValue>;
109
110
  defaultValue: TValue;
110
111
  description?: string;
111
112
  metadata?: Record<string, unknown>;
@@ -120,13 +121,14 @@ export type InferFlagValue<TFlag> =
120
121
  /**
121
122
  * Map a value type to its runtime flag kind.
122
123
  */
123
- export type FlagKindForValue<TValue extends FlagValue> = TValue extends boolean
124
- ? "boolean"
125
- : TValue extends string
126
- ? "string"
127
- : TValue extends number
128
- ? "number"
129
- : "object";
124
+ export type FlagValueKindForValue<TValue extends FlagValue> =
125
+ TValue extends boolean
126
+ ? "boolean"
127
+ : TValue extends string
128
+ ? "string"
129
+ : TValue extends number
130
+ ? "number"
131
+ : "object";
130
132
 
131
133
  /**
132
134
  * Options accepted when declaring a flag.
@@ -184,7 +186,7 @@ export type FlagEvaluationDetails<TValue extends FlagValue = FlagValue> = {
184
186
  key: string;
185
187
  value: TValue;
186
188
  defaultValue: TValue;
187
- kind: FlagKindForValue<TValue>;
189
+ valueKind: FlagValueKindForValue<TValue>;
188
190
  reason: FlagEvaluationReason;
189
191
  defaulted: boolean;
190
192
  variant?: string;
@@ -320,20 +322,21 @@ export type MemoryFlagsPort = FlagsPort & {
320
322
  };
321
323
 
322
324
  function createFlag<TValue extends FlagValue>(
323
- kind: FlagKind,
325
+ valueKind: FlagValueKind,
324
326
  key: string,
325
327
  options: DefineFlagOptions<TValue>,
326
328
  ): FlagDef<TValue> {
327
329
  const defaultValue = options.default;
328
- if (!matchesKind(defaultValue, kind)) {
330
+ if (!matchesValueKind(defaultValue, valueKind)) {
329
331
  throw new Error(
330
- `Flag "${key}" default value does not match "${kind}" flag kind.`,
332
+ `Flag "${key}" default value does not match "${valueKind}" flag value kind.`,
331
333
  );
332
334
  }
333
335
 
334
336
  return {
337
+ kind: "flag",
335
338
  key,
336
- kind: kind as FlagKindForValue<TValue>,
339
+ valueKind: valueKind as FlagValueKindForValue<TValue>,
337
340
  defaultValue,
338
341
  description: options.description,
339
342
  metadata: options.metadata,
@@ -454,9 +457,9 @@ export function createMemoryFlags(
454
457
  ...port,
455
458
  values,
456
459
  set(flag, value) {
457
- if (!matchesKind(value, flag.kind)) {
460
+ if (!matchesValueKind(value, flag.valueKind)) {
458
461
  throw new Error(
459
- `Flag "${flag.key}" value does not match "${flag.kind}" flag kind.`,
462
+ `Flag "${flag.key}" value does not match "${flag.valueKind}" flag value kind.`,
460
463
  );
461
464
  }
462
465
  values.set(flag.key, value);
@@ -485,13 +488,14 @@ function createFlagsFromStore(
485
488
  const values = readValues();
486
489
  const stored = values[flag.key];
487
490
  const hasStored = Object.hasOwn(values, flag.key);
488
- const usedStoredValue = hasStored && matchesKind(stored, flag.kind);
491
+ const usedStoredValue =
492
+ hasStored && matchesValueKind(stored, flag.valueKind);
489
493
  const value = usedStoredValue
490
494
  ? (stored as typeof defaultValue)
491
495
  : defaultValue;
492
496
  const details: FlagEvaluationDetails<typeof defaultValue> = {
493
497
  key: flag.key,
494
- kind: flag.kind as FlagKindForValue<typeof defaultValue>,
498
+ valueKind: flag.valueKind as FlagValueKindForValue<typeof defaultValue>,
495
499
  value,
496
500
  defaultValue,
497
501
  reason: usedStoredValue ? "static" : "default",
@@ -551,7 +555,7 @@ function createFlagsFromStore(
551
555
  return flags;
552
556
  }
553
557
 
554
- function matchesKind(value: unknown, kind: FlagKind): boolean {
558
+ function matchesValueKind(value: unknown, kind: FlagValueKind): boolean {
555
559
  switch (kind) {
556
560
  case "boolean":
557
561
  return typeof value === "boolean";
@@ -507,13 +507,27 @@ function reservationFromRecord(
507
507
  };
508
508
  }
509
509
 
510
+ /**
511
+ * Options for `createMemoryIdempotencyStore(...)`.
512
+ */
513
+ export interface MemoryIdempotencyStoreOptions {
514
+ /**
515
+ * Clock used for reservation and completion timestamps. Defaults to the
516
+ * system clock.
517
+ */
518
+ now?: () => Date;
519
+ }
520
+
510
521
  /**
511
522
  * Create an in-memory idempotency store for tests and local examples.
512
523
  *
513
524
  * The memory store is process-local and not suitable for multi-process
514
525
  * production deployments.
515
526
  */
516
- export function createMemoryIdempotencyStore(): MemoryIdempotencyStore {
527
+ export function createMemoryIdempotencyStore(
528
+ options: MemoryIdempotencyStoreOptions = {},
529
+ ): MemoryIdempotencyStore {
530
+ const storeNow = options.now ?? (() => new Date());
517
531
  const records = new Map<string, MemoryRecord>();
518
532
 
519
533
  return {
@@ -527,7 +541,7 @@ export function createMemoryIdempotencyStore(): MemoryIdempotencyStore {
527
541
  assertNonEmptyString("fingerprint", input.fingerprint);
528
542
  assertTtl(input.ttlSec);
529
543
 
530
- const now = new Date();
544
+ const now = storeNow();
531
545
  const storageKey = createIdempotencyStorageKey(input);
532
546
  const existing = records.get(storageKey);
533
547
 
@@ -573,7 +587,7 @@ export function createMemoryIdempotencyStore(): MemoryIdempotencyStore {
573
587
 
574
588
  existing.status = "completed";
575
589
  existing.result = input.result;
576
- existing.completedAt = new Date();
590
+ existing.completedAt = storeNow();
577
591
  },
578
592
 
579
593
  async fail(input) {