@geekmidas/constructs 7.0.1 → 9.1.0

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 (186) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/dist/{AWSScheduledFunction-CTIa90EH.d.cts → AWSScheduledFunction-BHHlWlfV.d.cts} +2 -2
  3. package/dist/{AWSScheduledFunction-ysaCsdWx.d.mts.map → AWSScheduledFunction-BHHlWlfV.d.cts.map} +1 -1
  4. package/dist/{AWSScheduledFunction-ysaCsdWx.d.mts → AWSScheduledFunction-BzGCUbkk.d.mts} +2 -2
  5. package/dist/{AWSScheduledFunction-CTIa90EH.d.cts.map → AWSScheduledFunction-BzGCUbkk.d.mts.map} +1 -1
  6. package/dist/{AWSScheduledFunction-DbkDr8CA.cjs → AWSScheduledFunction-C42adp8A.cjs} +16 -5
  7. package/dist/AWSScheduledFunction-C42adp8A.cjs.map +1 -0
  8. package/dist/{AWSScheduledFunction-CufMMhGB.mjs → AWSScheduledFunction-KP6lqbsW.mjs} +15 -4
  9. package/dist/AWSScheduledFunction-KP6lqbsW.mjs.map +1 -0
  10. package/dist/{Construct-r0U2zBwW.mjs → Construct-BWF5HxaM.mjs} +5 -2
  11. package/dist/Construct-BWF5HxaM.mjs.map +1 -0
  12. package/dist/{Construct-_1wX7p_Y.d.cts → Construct-JlyQkBkv.d.cts} +39 -4
  13. package/dist/Construct-JlyQkBkv.d.cts.map +1 -0
  14. package/dist/{Construct-vXMZJgRp.cjs → Construct-TGIM_E4o.cjs} +5 -2
  15. package/dist/Construct-TGIM_E4o.cjs.map +1 -0
  16. package/dist/{Construct-DCMuYOox.d.mts → Construct-pxwC6fDD.d.mts} +39 -4
  17. package/dist/Construct-pxwC6fDD.d.mts.map +1 -0
  18. package/dist/{Endpoint-CLPSIl6S.d.mts → Endpoint-B0P_pEMr.d.mts} +3 -3
  19. package/dist/{Endpoint-CLPSIl6S.d.mts.map → Endpoint-B0P_pEMr.d.mts.map} +1 -1
  20. package/dist/{Endpoint-CAzNbDEt.d.cts → Endpoint-Cwjhc1Hl.d.cts} +2 -2
  21. package/dist/Endpoint-Cwjhc1Hl.d.cts.map +1 -0
  22. package/dist/{HonoEndpointAdaptor-DJDT3PMx.mjs → HonoEndpointAdaptor-BMVwx2cz.mjs} +18 -11
  23. package/dist/HonoEndpointAdaptor-BMVwx2cz.mjs.map +1 -0
  24. package/dist/{HonoEndpointAdaptor-CkJENANP.cjs → HonoEndpointAdaptor-DPDsDi-D.cjs} +19 -12
  25. package/dist/HonoEndpointAdaptor-DPDsDi-D.cjs.map +1 -0
  26. package/dist/{Queue-HBDf0fqz.d.mts → Queue-BL68oDJ-.d.mts} +2 -2
  27. package/dist/{Queue-DaxjwmOQ.d.cts.map → Queue-BL68oDJ-.d.mts.map} +1 -1
  28. package/dist/{Queue-DaxjwmOQ.d.cts → Queue-DxJ6btRE.d.cts} +2 -2
  29. package/dist/{Queue-HBDf0fqz.d.mts.map → Queue-DxJ6btRE.d.cts.map} +1 -1
  30. package/dist/{Subscriber-B69ngPwl.d.cts → Subscriber-BpVhcGhX.d.cts} +2 -2
  31. package/dist/{Subscriber-B69ngPwl.d.cts.map → Subscriber-BpVhcGhX.d.cts.map} +1 -1
  32. package/dist/{Subscriber-D8Xm-86W.d.mts → Subscriber-DF6xsBAJ.d.mts} +2 -2
  33. package/dist/{Subscriber-D8Xm-86W.d.mts.map → Subscriber-DF6xsBAJ.d.mts.map} +1 -1
  34. package/dist/{Topic-DT10AqHk.d.mts → Topic-DJXoDeMP.d.cts} +3 -3
  35. package/dist/{Topic-C5ZMfveW.d.cts.map → Topic-DJXoDeMP.d.cts.map} +1 -1
  36. package/dist/{Topic-C5ZMfveW.d.cts → Topic-DxWQBgMN.d.mts} +3 -3
  37. package/dist/{Topic-DT10AqHk.d.mts.map → Topic-DxWQBgMN.d.mts.map} +1 -1
  38. package/dist/adaptors/aws.cjs +14 -7
  39. package/dist/adaptors/aws.cjs.map +1 -1
  40. package/dist/adaptors/aws.d.cts +6 -6
  41. package/dist/adaptors/aws.d.cts.map +1 -1
  42. package/dist/adaptors/aws.d.mts +8 -8
  43. package/dist/adaptors/aws.d.mts.map +1 -1
  44. package/dist/adaptors/aws.mjs +14 -7
  45. package/dist/adaptors/aws.mjs.map +1 -1
  46. package/dist/adaptors/hono.cjs +7 -6
  47. package/dist/adaptors/hono.d.cts +3 -3
  48. package/dist/adaptors/hono.d.cts.map +1 -1
  49. package/dist/adaptors/hono.d.mts +4 -4
  50. package/dist/adaptors/hono.d.mts.map +1 -1
  51. package/dist/adaptors/hono.mjs +7 -6
  52. package/dist/adaptors/testing.cjs +161 -126
  53. package/dist/adaptors/testing.cjs.map +1 -1
  54. package/dist/adaptors/testing.d.cts +7 -5
  55. package/dist/adaptors/testing.d.cts.map +1 -1
  56. package/dist/adaptors/testing.d.mts +8 -6
  57. package/dist/adaptors/testing.d.mts.map +1 -1
  58. package/dist/adaptors/testing.mjs +162 -127
  59. package/dist/adaptors/testing.mjs.map +1 -1
  60. package/dist/clone-I_WIpiFf.cjs +45 -0
  61. package/dist/clone-I_WIpiFf.cjs.map +1 -0
  62. package/dist/clone-pdXxc0t3.mjs +39 -0
  63. package/dist/clone-pdXxc0t3.mjs.map +1 -0
  64. package/dist/crons/index.cjs +14 -32
  65. package/dist/crons/index.cjs.map +1 -1
  66. package/dist/crons/index.d.cts +7 -7
  67. package/dist/crons/index.d.cts.map +1 -1
  68. package/dist/crons/index.d.mts +3 -3
  69. package/dist/crons/index.d.mts.map +1 -1
  70. package/dist/crons/index.mjs +14 -32
  71. package/dist/crons/index.mjs.map +1 -1
  72. package/dist/endpoints/index.cjs +74 -61
  73. package/dist/endpoints/index.cjs.map +1 -1
  74. package/dist/endpoints/index.d.cts +14 -6
  75. package/dist/endpoints/index.d.cts.map +1 -1
  76. package/dist/endpoints/index.d.mts +13 -5
  77. package/dist/endpoints/index.d.mts.map +1 -1
  78. package/dist/endpoints/index.mjs +74 -61
  79. package/dist/endpoints/index.mjs.map +1 -1
  80. package/dist/functions/index.cjs +3 -2
  81. package/dist/functions/index.d.cts +2 -2
  82. package/dist/functions/index.d.mts +2 -2
  83. package/dist/functions/index.mjs +3 -2
  84. package/dist/{functions-B-qt91O7.mjs → functions-EqTloHjo.mjs} +107 -46
  85. package/dist/functions-EqTloHjo.mjs.map +1 -0
  86. package/dist/{functions-C8i1kCMM.cjs → functions-SqtCDc9c.cjs} +118 -45
  87. package/dist/functions-SqtCDc9c.cjs.map +1 -0
  88. package/dist/{index-DP6fqv0I.d.mts → index-C7iynzLI.d.mts} +8 -2
  89. package/dist/index-C7iynzLI.d.mts.map +1 -0
  90. package/dist/{index-BGeTScmi.d.cts → index-DdgJRm4q.d.cts} +12 -6
  91. package/dist/index-DdgJRm4q.d.cts.map +1 -0
  92. package/dist/index.cjs +1 -1
  93. package/dist/index.d.cts +2 -2
  94. package/dist/index.d.mts +4 -4
  95. package/dist/index.mjs +1 -1
  96. package/dist/{lazyAccessors-DfqZR6C4.mjs → lazyAccessors-CCTK19xj.mjs} +3 -3
  97. package/dist/{lazyAccessors-DfqZR6C4.mjs.map → lazyAccessors-CCTK19xj.mjs.map} +1 -1
  98. package/dist/{lazyAccessors-DmTG8sQH.cjs → lazyAccessors-D04oftGw.cjs} +3 -3
  99. package/dist/{lazyAccessors-DmTG8sQH.cjs.map → lazyAccessors-D04oftGw.cjs.map} +1 -1
  100. package/dist/monitors/index.cjs +49 -0
  101. package/dist/monitors/index.cjs.map +1 -0
  102. package/dist/monitors/index.d.cts +45 -0
  103. package/dist/monitors/index.d.cts.map +1 -0
  104. package/dist/monitors/index.d.mts +45 -0
  105. package/dist/monitors/index.d.mts.map +1 -0
  106. package/dist/monitors/index.mjs +48 -0
  107. package/dist/monitors/index.mjs.map +1 -0
  108. package/dist/{processAudits-CKjjivyz.cjs → processAudits-BJZT5gmy.cjs} +2 -2
  109. package/dist/{processAudits-CKjjivyz.cjs.map → processAudits-BJZT5gmy.cjs.map} +1 -1
  110. package/dist/{processAudits-ySnizdMG.mjs → processAudits-CwyaaSQc.mjs} +1 -1
  111. package/dist/{processAudits-ySnizdMG.mjs.map → processAudits-CwyaaSQc.mjs.map} +1 -1
  112. package/dist/{publisher-lFQleddL.cjs → publisher-BZgcrVPy.cjs} +1 -1
  113. package/dist/{publisher-lFQleddL.cjs.map → publisher-BZgcrVPy.cjs.map} +1 -1
  114. package/dist/{publisher-BrFOF847.mjs → publisher-CnpeMv-r.mjs} +1 -1
  115. package/dist/{publisher-BrFOF847.mjs.map → publisher-CnpeMv-r.mjs.map} +1 -1
  116. package/dist/queue/index.cjs +9 -22
  117. package/dist/queue/index.cjs.map +1 -1
  118. package/dist/queue/index.d.cts +4 -4
  119. package/dist/queue/index.d.cts.map +1 -1
  120. package/dist/queue/index.d.mts +2 -2
  121. package/dist/queue/index.d.mts.map +1 -1
  122. package/dist/queue/index.mjs +9 -22
  123. package/dist/queue/index.mjs.map +1 -1
  124. package/dist/subscribers/index.cjs +9 -22
  125. package/dist/subscribers/index.cjs.map +1 -1
  126. package/dist/subscribers/index.d.cts +5 -5
  127. package/dist/subscribers/index.d.cts.map +1 -1
  128. package/dist/subscribers/index.d.mts +3 -3
  129. package/dist/subscribers/index.d.mts.map +1 -1
  130. package/dist/subscribers/index.mjs +9 -22
  131. package/dist/subscribers/index.mjs.map +1 -1
  132. package/dist/{telemetry-3vdxGn4q.d.mts → telemetry-C--EgYVr.d.mts} +1 -1
  133. package/dist/{telemetry-3vdxGn4q.d.mts.map → telemetry-C--EgYVr.d.mts.map} +1 -1
  134. package/dist/topic/index.cjs +4 -7
  135. package/dist/topic/index.cjs.map +1 -1
  136. package/dist/topic/index.d.cts +2 -2
  137. package/dist/topic/index.d.cts.map +1 -1
  138. package/dist/topic/index.d.mts +2 -2
  139. package/dist/topic/index.d.mts.map +1 -1
  140. package/dist/topic/index.mjs +4 -7
  141. package/dist/topic/index.mjs.map +1 -1
  142. package/dist/{types-BDNGr7pv.d.mts → types-DNngH0fX.d.mts} +1 -1
  143. package/dist/{types-BDNGr7pv.d.mts.map → types-DNngH0fX.d.mts.map} +1 -1
  144. package/dist/types.d.mts +1 -1
  145. package/package.json +32 -22
  146. package/src/Construct.ts +5 -0
  147. package/src/__tests__/builder-reuse.spec.ts +587 -0
  148. package/src/__tests__/monitor.spec.ts +636 -0
  149. package/src/clone.ts +38 -0
  150. package/src/crons/CronBuilder.ts +35 -45
  151. package/src/crons/__tests__/Cron.spec.ts +10 -2
  152. package/src/crons/__tests__/CronBuilder.state-isolation.spec.ts +64 -18
  153. package/src/endpoints/AmazonApiGatewayEndpointAdaptor.ts +23 -2
  154. package/src/endpoints/EndpointBuilder.ts +61 -70
  155. package/src/endpoints/EndpointFactory.ts +31 -0
  156. package/src/endpoints/HonoEndpointAdaptor.ts +16 -1
  157. package/src/endpoints/TestEndpointAdaptor.ts +194 -150
  158. package/src/endpoints/__tests__/EndpointBuilder.audit.spec.ts +6 -3
  159. package/src/endpoints/__tests__/EndpointBuilder.responseType.spec.ts +4 -1
  160. package/src/endpoints/__tests__/EndpointBuilder.spec.ts +16 -8
  161. package/src/functions/AWSLambdaFunction.ts +28 -2
  162. package/src/functions/BaseFunctionBuilder.ts +33 -22
  163. package/src/functions/FunctionBuilder.ts +41 -51
  164. package/src/functions/TestFunctionAdaptor.ts +66 -31
  165. package/src/functions/__tests__/FunctionBuilder.state-isolation.spec.ts +55 -12
  166. package/src/index.ts +6 -0
  167. package/src/monitor.ts +145 -0
  168. package/src/monitors/heartbeat.ts +86 -0
  169. package/src/monitors/index.ts +6 -0
  170. package/src/queue/QueueBuilder.ts +22 -22
  171. package/src/subscribers/SubscriberBuilder.ts +19 -22
  172. package/src/topic/TopicBuilder.ts +9 -7
  173. package/tsdown.config.ts +1 -0
  174. package/dist/AWSScheduledFunction-CufMMhGB.mjs.map +0 -1
  175. package/dist/AWSScheduledFunction-DbkDr8CA.cjs.map +0 -1
  176. package/dist/Construct-DCMuYOox.d.mts.map +0 -1
  177. package/dist/Construct-_1wX7p_Y.d.cts.map +0 -1
  178. package/dist/Construct-r0U2zBwW.mjs.map +0 -1
  179. package/dist/Construct-vXMZJgRp.cjs.map +0 -1
  180. package/dist/Endpoint-CAzNbDEt.d.cts.map +0 -1
  181. package/dist/HonoEndpointAdaptor-CkJENANP.cjs.map +0 -1
  182. package/dist/HonoEndpointAdaptor-DJDT3PMx.mjs.map +0 -1
  183. package/dist/functions-B-qt91O7.mjs.map +0 -1
  184. package/dist/functions-C8i1kCMM.cjs.map +0 -1
  185. package/dist/index-BGeTScmi.d.cts.map +0 -1
  186. package/dist/index-DP6fqv0I.d.mts.map +0 -1
@@ -1,16 +1,14 @@
1
1
  import type { EventPublisher } from '@geekmidas/events';
2
2
  import type { Logger } from '@geekmidas/logger';
3
- import { ConsoleLogger } from '@geekmidas/logger/console';
4
3
  import type { ComposableStandardSchema } from '@geekmidas/schema';
5
4
  import type { Service } from '@geekmidas/services';
6
5
  import type { StandardSchemaV1 } from '@standard-schema/spec';
7
6
  import uniqBy from 'lodash.uniqby';
8
7
  import { ConstructType } from '../Construct';
8
+ import { cloneWith } from '../clone';
9
9
  import { FunctionBuilder, type FunctionHandler } from '../functions';
10
10
  import { Cron, type ScheduleExpression } from './Cron';
11
11
 
12
- const DEFAULT_LOGGER = new ConsoleLogger() as any;
13
-
14
12
  export class CronBuilder<
15
13
  TInput extends ComposableStandardSchema,
16
14
  TServices extends Service[],
@@ -39,8 +37,7 @@ export class CronBuilder<
39
37
  }
40
38
 
41
39
  override memorySize(memorySize: number): this {
42
- this._memorySize = memorySize;
43
- return this;
40
+ return cloneWith(this, { _memorySize: memorySize });
44
41
  }
45
42
 
46
43
  schedule(
@@ -55,8 +52,7 @@ export class CronBuilder<
55
52
  TDatabase,
56
53
  TDatabaseServiceName
57
54
  > {
58
- this._schedule = _expression;
59
- return this;
55
+ return cloneWith(this, { _schedule: _expression });
60
56
  }
61
57
 
62
58
  override input<T extends ComposableStandardSchema>(
@@ -71,9 +67,9 @@ export class CronBuilder<
71
67
  TDatabase,
72
68
  TDatabaseServiceName
73
69
  > {
74
- this.inputSchema = schema as unknown as TInput;
75
-
76
- return this as unknown as CronBuilder<
70
+ return cloneWith(this, {
71
+ inputSchema: schema as unknown as TInput,
72
+ }) as unknown as CronBuilder<
77
73
  T,
78
74
  TServices,
79
75
  TLogger,
@@ -97,9 +93,9 @@ export class CronBuilder<
97
93
  TDatabase,
98
94
  TDatabaseServiceName
99
95
  > {
100
- this.outputSchema = schema as unknown as OutSchema;
101
-
102
- return this as unknown as CronBuilder<
96
+ return cloneWith(this, {
97
+ outputSchema: schema as unknown as OutSchema,
98
+ }) as unknown as CronBuilder<
103
99
  TInput,
104
100
  TServices,
105
101
  TLogger,
@@ -123,12 +119,12 @@ export class CronBuilder<
123
119
  TDatabase,
124
120
  TDatabaseServiceName
125
121
  > {
126
- this._services = uniqBy(
127
- [...this._services, ...services],
128
- (s) => s.serviceName,
129
- ) as TServices;
130
-
131
- return this as unknown as CronBuilder<
122
+ return cloneWith(this, {
123
+ _services: uniqBy(
124
+ [...this._services, ...services],
125
+ (s) => s.serviceName,
126
+ ) as TServices,
127
+ }) as unknown as CronBuilder<
132
128
  TInput,
133
129
  [...TServices, ...T],
134
130
  TLogger,
@@ -152,9 +148,9 @@ export class CronBuilder<
152
148
  TDatabase,
153
149
  TDatabaseServiceName
154
150
  > {
155
- this._logger = logger as unknown as TLogger;
156
-
157
- return this as unknown as CronBuilder<
151
+ return cloneWith(this, {
152
+ _logger: logger as unknown as TLogger,
153
+ }) as unknown as CronBuilder<
158
154
  TInput,
159
155
  TServices,
160
156
  T,
@@ -178,12 +174,12 @@ export class CronBuilder<
178
174
  TDatabase,
179
175
  TDatabaseServiceName
180
176
  > {
181
- this._publisher = publisher as unknown as Service<
182
- TEventPublisherServiceName,
183
- TEventPublisher
184
- >;
185
-
186
- return this as unknown as CronBuilder<
177
+ return cloneWith(this, {
178
+ _publisher: publisher as unknown as Service<
179
+ TEventPublisherServiceName,
180
+ TEventPublisher
181
+ >,
182
+ }) as unknown as CronBuilder<
187
183
  TInput,
188
184
  TServices,
189
185
  TLogger,
@@ -211,12 +207,12 @@ export class CronBuilder<
211
207
  T,
212
208
  TName
213
209
  > {
214
- this._databaseService = service as unknown as Service<
215
- TDatabaseServiceName,
216
- TDatabase
217
- >;
218
-
219
- return this as unknown as CronBuilder<
210
+ return cloneWith(this, {
211
+ _databaseService: service as unknown as Service<
212
+ TDatabaseServiceName,
213
+ TDatabase
214
+ >,
215
+ }) as unknown as CronBuilder<
220
216
  TInput,
221
217
  TServices,
222
218
  TLogger,
@@ -254,17 +250,11 @@ export class CronBuilder<
254
250
  this._databaseService,
255
251
  );
256
252
 
257
- // Reset builder state after creating the cron to prevent pollution
258
- this._services = [] as Service[] as TServices;
259
- this._logger = DEFAULT_LOGGER;
260
- this._events = [];
261
- this._publisher = undefined;
262
- this._databaseService = undefined;
263
- this._schedule = undefined;
264
- this.inputSchema = undefined;
265
- this.outputSchema = undefined;
266
- this._timeout = undefined;
267
- this._memorySize = undefined;
253
+ // No reset. `.handle()` reads this builder and leaves it alone, so a
254
+ // configured base keeps its configuration for every construct built
255
+ // from it. The reset that used to be here wiped the base on first use.
256
+
257
+ cron.monitors = this._monitors;
268
258
 
269
259
  return cron;
270
260
  }
@@ -210,7 +210,12 @@ describe('Cron', () => {
210
210
  const schedule: CronExpression = 'cron(0 0 * * SUN)';
211
211
 
212
212
  const result = builder.schedule(schedule);
213
- expect(result).toBe(builder); // Should return self for chaining
213
+
214
+ // A new builder carrying the schedule, so the base it came from stays
215
+ // clean for the next cron built off it.
216
+ expect(result).toBeInstanceOf(CronBuilder);
217
+ expect(result).not.toBe(builder);
218
+ expect((result as any)._schedule).toBe(schedule);
214
219
  });
215
220
 
216
221
  it('should build Cron with handle() method', () => {
@@ -261,7 +266,10 @@ describe('Cron', () => {
261
266
  .schedule(schedule)
262
267
  .input(z.object({ data: z.string() }));
263
268
 
264
- expect(chained).toBe(builder);
269
+ expect(chained).toBeInstanceOf(CronBuilder);
270
+ expect(chained).not.toBe(builder);
271
+ expect((chained as any)._schedule).toBe(schedule);
272
+ expect((chained as any)._timeout).toBe(30000);
265
273
  });
266
274
  });
267
275
 
@@ -17,6 +17,8 @@ const ServiceB = {
17
17
  },
18
18
  } satisfies Service<'b', any>;
19
19
 
20
+ const customLogger = new ConsoleLogger({ app: 'reusable-base' });
21
+
20
22
  describe('CronBuilder - State Isolation', () => {
21
23
  describe('singleton instance state reset', () => {
22
24
  it('should reset services after handle() is called', () => {
@@ -135,28 +137,26 @@ describe('CronBuilder - State Isolation', () => {
135
137
  it('should not share references between different builder chains', () => {
136
138
  const c = new CronBuilder();
137
139
 
138
- // Start two separate chains
139
- const builder1 = c.schedule('rate(5 minutes)').services([ServiceA]);
140
- const builder2 = c.schedule('rate(10 minutes)').services([ServiceB]);
140
+ // Two chains off one base. They used to be the *same object* — every
141
+ // method mutated the base and returned it — so chain 1's function came
142
+ // out holding chain 2's services as well, and the base only survived
143
+ // because `.handle()` wiped it afterwards. That wipe is also what made
144
+ // a configured base unusable twice.
145
+ const builder1 = c.services([ServiceA]);
146
+ const builder2 = c.services([ServiceB]);
141
147
 
142
- // They should be the same instance (singleton)
143
- expect(builder1).toBe(builder2);
144
- expect(builder1).toBe(c);
148
+ expect(builder1).not.toBe(builder2);
149
+ expect(builder1).not.toBe(c);
145
150
 
146
- // But after handle, state is reset
147
- const cron1 = builder1.handle(async () => ({}));
151
+ const fn1 = builder1.handle(async () => ({}));
152
+ const fn2 = builder2.handle(async () => ({}));
148
153
 
149
- // Now builder2 should have reset state
150
- expect((builder2 as any)._services).toEqual([]);
154
+ // Each function gets exactly what its own chain declared.
155
+ expect(fn1.services.map((s) => s.serviceName)).toEqual(['a']);
156
+ expect(fn2.services.map((s) => s.serviceName)).toEqual(['b']);
151
157
 
152
- // Add services again
153
- const cron2 = builder2
154
- .schedule('rate(15 minutes)')
155
- .services([ServiceB])
156
- .handle(async () => ({}));
157
-
158
- expect(cron1.services.map((s) => s.serviceName)).toEqual(['a', 'b']);
159
- expect(cron2.services.map((s) => s.serviceName)).toEqual(['b']);
158
+ // And the base is untouched, so it stays reusable.
159
+ expect((c as any)._services).toEqual([]);
160
160
  });
161
161
  });
162
162
 
@@ -224,4 +224,50 @@ describe('CronBuilder - State Isolation', () => {
224
224
  expect(cron3.schedule).toBe('rate(1 hour)');
225
225
  });
226
226
  });
227
+
228
+ describe('a configured base, reused', () => {
229
+ it('carries its configuration into every cron built from it', () => {
230
+ const base = new CronBuilder()
231
+ .logger(customLogger)
232
+ .timeout(60_000)
233
+ .schedule('rate(1 day)');
234
+
235
+ const first = base.services([ServiceA]).handle(async () => ({}));
236
+ const second = base.services([ServiceB]).handle(async () => ({}));
237
+
238
+ // The point of the whole shape: `const base = c.logger(log)` is a base you
239
+ // can keep. It used to survive exactly one `.handle()` — the reset put
240
+ // the logger and the timeout back to their defaults — so the second
241
+ // cron built from it silently logged somewhere else and timed out
242
+ // on a different clock.
243
+ for (const built of [first, second]) {
244
+ expect(built.logger).toBe(customLogger);
245
+ expect(built.timeout).toBe(60_000);
246
+ }
247
+
248
+ // And each still gets only what its own chain declared.
249
+ expect(first.services.map((s) => s.serviceName)).toEqual(['a']);
250
+ expect(second.services.map((s) => s.serviceName)).toEqual(['b']);
251
+ });
252
+
253
+ it('persists configuration when handle() is called on the base itself', () => {
254
+ const base = new CronBuilder()
255
+ .logger(customLogger)
256
+ .timeout(60_000)
257
+ .schedule('rate(1 day)');
258
+
259
+ // No clone in between: `.handle()` is called straight on the base,
260
+ // twice. Cloning the config methods was not enough on its own —
261
+ // `.handle()` still ran a reset block that wiped whatever it was
262
+ // called on, so the second cron came back on the default logger and
263
+ // the default timeout.
264
+ const first = base.handle(async () => ({}));
265
+ const second = base.handle(async () => ({}));
266
+
267
+ for (const built of [first, second]) {
268
+ expect(built.logger).toBe(customLogger);
269
+ expect(built.timeout).toBe(60_000);
270
+ }
271
+ });
272
+ });
227
273
  });
@@ -38,6 +38,7 @@ import {
38
38
  ServiceDiscovery,
39
39
  type ServiceRecord,
40
40
  } from '@geekmidas/services';
41
+ import { startMonitors } from '../monitor';
41
42
  import { publishConstructEvents } from '../publisher';
42
43
  import type { MappedAudit } from './audit';
43
44
  import type { CookieFn, HeaderFn } from './Endpoint';
@@ -134,6 +135,8 @@ export abstract class AmazonApiGatewayEndpoint<
134
135
  'Error processing request',
135
136
  );
136
137
  const wrappedError = wrapError(req.error);
138
+ // Kept for the monitors, which only see the response otherwise.
139
+ (req.event as any).__error = req.error;
137
140
 
138
141
  // Set the response with the proper status code from the HttpError
139
142
  req.response = {
@@ -600,8 +603,26 @@ export abstract class AmazonApiGatewayEndpoint<
600
603
 
601
604
  // Cast event to any since Middy middlewares enrich the event with
602
605
  // services, logger, session, etc. during the middleware chain execution
603
- return runWithRequestContext({ logger, requestId, startTime }, () =>
604
- chain(event as unknown as Parameters<typeof chain>[0], context),
606
+ return runWithRequestContext(
607
+ { logger, requestId, startTime },
608
+ async () => {
609
+ const monitor = await startMonitors(
610
+ this.endpoint,
611
+ ServiceDiscovery.getInstance(this.envParser),
612
+ logger,
613
+ );
614
+ const response = await chain(
615
+ event as unknown as Parameters<typeof chain>[0],
616
+ context,
617
+ );
618
+ // Awaited: Lambda freezes the process once the response is
619
+ // returned, so an unawaited ping may never be sent.
620
+ await monitor.settle(
621
+ (response as any)?.statusCode ?? 200,
622
+ (event as any).__error,
623
+ );
624
+ return response;
625
+ },
605
626
  );
606
627
  };
607
628
 
@@ -10,6 +10,7 @@ import type { Service } from '@geekmidas/services';
10
10
  import type { StandardSchemaV1 } from '@standard-schema/spec';
11
11
  import uniqBy from 'lodash.uniqby';
12
12
  import { ConstructType } from '../Construct';
13
+ import { cloneWith } from '../clone';
13
14
  import { BaseFunctionBuilder } from '../functions';
14
15
  import type { HttpMethod } from '../types';
15
16
  import type { Authorizer, SecurityScheme } from './Authorizer';
@@ -102,30 +103,27 @@ export class EndpointBuilder<
102
103
  }
103
104
 
104
105
  description(description: string): this {
105
- this._description = description;
106
- return this;
106
+ return cloneWith(this, { _description: description });
107
107
  }
108
108
 
109
109
  status(status: SuccessStatus): this {
110
- this._status = status;
111
- return this;
110
+ return cloneWith(this, { _status: status });
112
111
  }
113
112
 
114
113
  override event<TEvent extends MappedEvent<TEventPublisher, OutSchema>>(
115
114
  event: TEvent,
116
115
  ): this {
117
- this._events.push(event);
118
- return this;
116
+ // Replaced rather than pushed: a clone shares whatever array the field
117
+ // points at, so an in-place push would be seen by the base it came from.
118
+ return cloneWith(this, { _events: [...this._events, event] });
119
119
  }
120
120
 
121
121
  tags(tags: string[]): this {
122
- this._tags = tags;
123
- return this;
122
+ return cloneWith(this, { _tags: tags });
124
123
  }
125
124
 
126
125
  memorySize(memorySize: number): this {
127
- this._memorySize = memorySize;
128
- return this;
126
+ return cloneWith(this, { _memorySize: memorySize });
129
127
  }
130
128
 
131
129
  /**
@@ -147,8 +145,7 @@ export class EndpointBuilder<
147
145
  * ```
148
146
  */
149
147
  responseType(type: string): this {
150
- this._responseType = type;
151
- return this;
148
+ return cloneWith(this, { _responseType: type });
152
149
  }
153
150
 
154
151
  override publisher<T extends EventPublisher<any>, TName extends string>(
@@ -170,12 +167,12 @@ export class EndpointBuilder<
170
167
  TDatabase,
171
168
  TDatabaseServiceName
172
169
  > {
173
- this._publisher = publisher as unknown as Service<
174
- TEventPublisherServiceName,
175
- TEventPublisher
176
- >;
177
-
178
- return this as unknown as EndpointBuilder<
170
+ return cloneWith(this, {
171
+ _publisher: publisher as unknown as Service<
172
+ TEventPublisherServiceName,
173
+ TEventPublisher
174
+ >,
175
+ }) as unknown as EndpointBuilder<
179
176
  TRoute,
180
177
  TMethod,
181
178
  TInput,
@@ -213,9 +210,10 @@ export class EndpointBuilder<
213
210
  TDatabase,
214
211
  TDatabaseServiceName
215
212
  > {
216
- this.schemas.body = schema as unknown as T;
213
+ // A new object rather than a field set on the shared one: a clone
214
+ // keeps its base's `schemas` reference.
217
215
  // @ts-expect-error
218
- return this;
216
+ return cloneWith(this, { schemas: { ...this.schemas, body: schema } });
219
217
  }
220
218
 
221
219
  search<T extends StandardSchemaV1>(
@@ -237,9 +235,10 @@ export class EndpointBuilder<
237
235
  TDatabase,
238
236
  TDatabaseServiceName
239
237
  > {
240
- this.schemas.query = schema as unknown as T;
238
+ // A new object rather than a field set on the shared one: a clone
239
+ // keeps its base's `schemas` reference.
241
240
  // @ts-expect-error
242
- return this;
241
+ return cloneWith(this, { schemas: { ...this.schemas, query: schema } });
243
242
  }
244
243
 
245
244
  query<T extends StandardSchemaV1>(
@@ -283,21 +282,20 @@ export class EndpointBuilder<
283
282
  TDatabase,
284
283
  TDatabaseServiceName
285
284
  > {
286
- this.schemas.params = schema as unknown as T;
285
+ // A new object rather than a field set on the shared one: a clone
286
+ // keeps its base's `schemas` reference.
287
287
  // @ts-expect-error
288
- return this;
288
+ return cloneWith(this, { schemas: { ...this.schemas, params: schema } });
289
289
  }
290
290
 
291
291
  authorize(
292
292
  fn: AuthorizeFn<TServices, TLogger, TSession, TInput, TDatabase>,
293
293
  ): this {
294
- this._authorize = fn;
295
- return this;
294
+ return cloneWith(this, { _authorize: fn });
296
295
  }
297
296
 
298
297
  rateLimit(config: RateLimitConfig): this {
299
- this._rateLimit = config;
300
- return this;
298
+ return cloneWith(this, { _rateLimit: config });
301
299
  }
302
300
 
303
301
  authorizer(
@@ -321,8 +319,7 @@ export class EndpointBuilder<
321
319
  > {
322
320
  // Special case: 'none' explicitly marks endpoint as having no authorizer
323
321
  if (name === 'none') {
324
- this._authorizerName = undefined;
325
- return this;
322
+ return cloneWith(this, { _authorizerName: undefined });
326
323
  }
327
324
 
328
325
  // Validate that the authorizer exists in available authorizers
@@ -337,8 +334,7 @@ export class EndpointBuilder<
337
334
  `Authorizer "${name as string}" not found in available authorizers: ${available}`,
338
335
  );
339
336
  }
340
- this._authorizerName = name;
341
- return this;
337
+ return cloneWith(this, { _authorizerName: name });
342
338
  }
343
339
 
344
340
  services<T extends Service[]>(
@@ -360,12 +356,12 @@ export class EndpointBuilder<
360
356
  TDatabase,
361
357
  TDatabaseServiceName
362
358
  > {
363
- this._services = uniqBy(
364
- [...this._services, ...services],
365
- (s) => s.serviceName,
366
- ) as TServices;
367
-
368
- return this as unknown as EndpointBuilder<
359
+ return cloneWith(this, {
360
+ _services: uniqBy(
361
+ [...this._services, ...services],
362
+ (s) => s.serviceName,
363
+ ) as TServices,
364
+ }) as unknown as EndpointBuilder<
369
365
  TRoute,
370
366
  TMethod,
371
367
  TInput,
@@ -403,9 +399,9 @@ export class EndpointBuilder<
403
399
  TDatabase,
404
400
  TDatabaseServiceName
405
401
  > {
406
- this._logger = logger as unknown as TLogger;
407
-
408
- return this as unknown as EndpointBuilder<
402
+ return cloneWith(this, {
403
+ _logger: logger as unknown as TLogger,
404
+ }) as unknown as EndpointBuilder<
409
405
  TRoute,
410
406
  TMethod,
411
407
  TInput,
@@ -443,9 +439,9 @@ export class EndpointBuilder<
443
439
  TDatabase,
444
440
  TDatabaseServiceName
445
441
  > {
446
- this.outputSchema = schema as unknown as OutSchema;
447
-
448
- return this as unknown as EndpointBuilder<
442
+ return cloneWith(this, {
443
+ outputSchema: schema as unknown as OutSchema,
444
+ }) as unknown as EndpointBuilder<
449
445
  TRoute,
450
446
  TMethod,
451
447
  TInput,
@@ -488,12 +484,12 @@ export class EndpointBuilder<
488
484
  TDatabase,
489
485
  TDatabaseServiceName
490
486
  > {
491
- this._auditorStorage = storage as unknown as Service<
492
- TAuditStorageServiceName,
493
- TAuditStorage
494
- >;
495
-
496
- return this as unknown as EndpointBuilder<
487
+ return cloneWith(this, {
488
+ _auditorStorage: storage as unknown as Service<
489
+ TAuditStorageServiceName,
490
+ TAuditStorage
491
+ >,
492
+ }) as unknown as EndpointBuilder<
497
493
  TRoute,
498
494
  TMethod,
499
495
  TInput,
@@ -535,8 +531,7 @@ export class EndpointBuilder<
535
531
  TDatabase,
536
532
  TDatabaseServiceName
537
533
  > {
538
- this._actorExtractor = extractor;
539
- return this;
534
+ return cloneWith(this, { _actorExtractor: extractor });
540
535
  }
541
536
 
542
537
  /**
@@ -557,8 +552,7 @@ export class EndpointBuilder<
557
552
  * ```
558
553
  */
559
554
  audit(audits: MappedAudit<TAuditAction, OutSchema>[]): this {
560
- this._audits = audits;
561
- return this;
555
+ return cloneWith(this, { _audits: audits });
562
556
  }
563
557
 
564
558
  /**
@@ -595,12 +589,12 @@ export class EndpointBuilder<
595
589
  T,
596
590
  TName
597
591
  > {
598
- this._databaseService = service as unknown as Service<
599
- TDatabaseServiceName,
600
- TDatabase
601
- >;
602
-
603
- return this as unknown as EndpointBuilder<
592
+ return cloneWith(this, {
593
+ _databaseService: service as unknown as Service<
594
+ TDatabaseServiceName,
595
+ TDatabase
596
+ >,
597
+ }) as unknown as EndpointBuilder<
604
598
  TRoute,
605
599
  TMethod,
606
600
  TInput,
@@ -642,13 +636,9 @@ export class EndpointBuilder<
642
636
  config: RlsConfig<TServices, TSession, TLogger> | false | RlsBypass,
643
637
  ): this {
644
638
  if (config === false || config === RLS_BYPASS) {
645
- this._rlsBypass = true;
646
- this._rlsConfig = undefined;
647
- } else {
648
- this._rlsConfig = config;
649
- this._rlsBypass = false;
639
+ return cloneWith(this, { _rlsBypass: true, _rlsConfig: undefined });
650
640
  }
651
- return this;
641
+ return cloneWith(this, { _rlsConfig: config, _rlsBypass: false });
652
642
  }
653
643
 
654
644
  /**
@@ -665,9 +655,7 @@ export class EndpointBuilder<
665
655
  * ```
666
656
  */
667
657
  rlsBypass(): this {
668
- this._rlsBypass = true;
669
- this._rlsConfig = undefined;
670
- return this;
658
+ return cloneWith(this, { _rlsBypass: true, _rlsConfig: undefined });
671
659
  }
672
660
 
673
661
  // EndpointBuilder doesn't have a generic input method - it uses body, query, params instead
@@ -727,7 +715,7 @@ export class EndpointBuilder<
727
715
  }
728
716
  }
729
717
 
730
- return new Endpoint({
718
+ const endpoint = new Endpoint({
731
719
  fn,
732
720
  method: this.method,
733
721
  route: this.route,
@@ -754,5 +742,8 @@ export class EndpointBuilder<
754
742
  rlsBypass: this._rlsBypass,
755
743
  responseType: this._responseType,
756
744
  });
745
+ endpoint.monitors = this._monitors;
746
+
747
+ return endpoint;
757
748
  }
758
749
  }