@schmock/core 1.3.0 → 1.7.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.
package/dist/builder.d.ts CHANGED
@@ -13,6 +13,7 @@ export declare class CallableMockInstance {
13
13
  private callableRef;
14
14
  private server;
15
15
  private serverInfo;
16
+ private listeners;
16
17
  constructor(globalConfig?: Schmock.GlobalConfig);
17
18
  defineRoute(route: Schmock.RouteKey, generator: Schmock.Generator, config: Schmock.RouteConfig): this;
18
19
  setCallableRef(ref: Schmock.CallableMockInstance): void;
@@ -21,6 +22,11 @@ export declare class CallableMockInstance {
21
22
  called(method?: Schmock.HttpMethod, path?: string): boolean;
22
23
  callCount(method?: Schmock.HttpMethod, path?: string): number;
23
24
  lastRequest(method?: Schmock.HttpMethod, path?: string): Schmock.RequestRecord | undefined;
25
+ getRoutes(): Schmock.RouteInfo[];
26
+ getState(): Record<string, unknown>;
27
+ on(event: string, listener: (data: unknown) => void): this;
28
+ off(event: string, listener: (data: unknown) => void): this;
29
+ private emit;
24
30
  reset(): void;
25
31
  resetHistory(): void;
26
32
  resetState(): void;
@@ -1 +1 @@
1
- {"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../src/builder.ts"],"names":[],"mappings":"AA4EA;;;;GAIG;AACH,qBAAa,oBAAoB;IAUnB,OAAO,CAAC,YAAY;IAThC,OAAO,CAAC,MAAM,CAA+B;IAC7C,OAAO,CAAC,YAAY,CAA4C;IAChE,OAAO,CAAC,OAAO,CAAwB;IACvC,OAAO,CAAC,MAAM,CAAc;IAC5B,OAAO,CAAC,cAAc,CAA+B;IACrD,OAAO,CAAC,WAAW,CAA2C;IAC9D,OAAO,CAAC,MAAM,CAAqB;IACnC,OAAO,CAAC,UAAU,CAAiC;gBAE/B,YAAY,GAAE,OAAO,CAAC,YAAiB;IAa3D,WAAW,CACT,KAAK,EAAE,OAAO,CAAC,QAAQ,EACvB,SAAS,EAAE,OAAO,CAAC,SAAS,EAC5B,MAAM,EAAE,OAAO,CAAC,WAAW,GAC1B,IAAI;IAqFP,cAAc,CAAC,GAAG,EAAE,OAAO,CAAC,oBAAoB,GAAG,IAAI;IAIvD,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,MAAM,GAAG,IAAI;IAoBlC,OAAO,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,UAAU,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,EAAE;IAS5E,MAAM,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,UAAU,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO;IAS3D,SAAS,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,UAAU,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM;IAS7D,WAAW,CACT,MAAM,CAAC,EAAE,OAAO,CAAC,UAAU,EAC3B,IAAI,CAAC,EAAE,MAAM,GACZ,OAAO,CAAC,aAAa,GAAG,SAAS;IAYpC,KAAK,IAAI,IAAI;IAcb,YAAY,IAAI,IAAI;IAKpB,UAAU,IAAI,IAAI;IAWlB,MAAM,CAAC,IAAI,SAAI,EAAE,QAAQ,SAAc,GAAG,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC;IAmFrE,KAAK,IAAI,IAAI;IAUP,MAAM,CACV,MAAM,EAAE,OAAO,CAAC,UAAU,EAC1B,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,OAAO,CAAC,cAAc,GAC/B,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC;IAoL5B;;;;OAIG;YACW,UAAU;IAcxB;;;;;;;OAOG;IACH,OAAO,CAAC,aAAa;IAwDrB;;;;;;;;;;OAUG;YACW,iBAAiB;IAqG/B;;;;;;;;OAQG;IACH,OAAO,CAAC,SAAS;IA0BjB;;;;;;;OAOG;IACH,OAAO,CAAC,aAAa;CActB"}
1
+ {"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../src/builder.ts"],"names":[],"mappings":"AA4EA;;;;GAIG;AACH,qBAAa,oBAAoB;IAWnB,OAAO,CAAC,YAAY;IAVhC,OAAO,CAAC,MAAM,CAA+B;IAC7C,OAAO,CAAC,YAAY,CAA4C;IAChE,OAAO,CAAC,OAAO,CAAwB;IACvC,OAAO,CAAC,MAAM,CAAc;IAC5B,OAAO,CAAC,cAAc,CAA+B;IACrD,OAAO,CAAC,WAAW,CAA2C;IAC9D,OAAO,CAAC,MAAM,CAAqB;IACnC,OAAO,CAAC,UAAU,CAAiC;IACnD,OAAO,CAAC,SAAS,CAAmD;gBAEhD,YAAY,GAAE,OAAO,CAAC,YAAiB;IAa3D,WAAW,CACT,KAAK,EAAE,OAAO,CAAC,QAAQ,EACvB,SAAS,EAAE,OAAO,CAAC,SAAS,EAC5B,MAAM,EAAE,OAAO,CAAC,WAAW,GAC1B,IAAI;IAqFP,cAAc,CAAC,GAAG,EAAE,OAAO,CAAC,oBAAoB,GAAG,IAAI;IAIvD,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,MAAM,GAAG,IAAI;IAoBlC,OAAO,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,UAAU,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,EAAE;IAS5E,MAAM,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,UAAU,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO;IAS3D,SAAS,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,UAAU,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM;IAS7D,WAAW,CACT,MAAM,CAAC,EAAE,OAAO,CAAC,UAAU,EAC3B,IAAI,CAAC,EAAE,MAAM,GACZ,OAAO,CAAC,aAAa,GAAG,SAAS;IAYpC,SAAS,IAAI,OAAO,CAAC,SAAS,EAAE;IAQhC,QAAQ,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAMnC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,GAAG,IAAI;IAU1D,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,GAAG,IAAI;IAK3D,OAAO,CAAC,IAAI;IAWZ,KAAK,IAAI,IAAI;IAeb,YAAY,IAAI,IAAI;IAKpB,UAAU,IAAI,IAAI;IAWlB,MAAM,CAAC,IAAI,SAAI,EAAE,QAAQ,SAAc,GAAG,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC;IAmFrE,KAAK,IAAI,IAAI;IAUP,MAAM,CACV,MAAM,EAAE,OAAO,CAAC,UAAU,EAC1B,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,OAAO,CAAC,cAAc,GAC/B,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC;IAgN5B;;;;OAIG;YACW,UAAU;IAgBxB;;;;;;;OAOG;IACH,OAAO,CAAC,aAAa;IAwDrB;;;;;;;;;;OAUG;YACW,iBAAiB;IAqG/B;;;;;;;;OAQG;IACH,OAAO,CAAC,SAAS;IA0BjB;;;;;;;OAOG;IACH,OAAO,CAAC,aAAa;CActB"}
package/dist/builder.js CHANGED
@@ -60,6 +60,7 @@ export class CallableMockInstance {
60
60
  callableRef;
61
61
  server;
62
62
  serverInfo;
63
+ listeners = new Map();
63
64
  constructor(globalConfig = {}) {
64
65
  this.globalConfig = globalConfig;
65
66
  this.logger = new DebugLogger(globalConfig.debug || false);
@@ -182,6 +183,39 @@ export class CallableMockInstance {
182
183
  }
183
184
  return this.requestHistory[this.requestHistory.length - 1];
184
185
  }
186
+ // ===== Introspection =====
187
+ getRoutes() {
188
+ return this.routes.map((r) => ({
189
+ method: r.method,
190
+ path: r.path,
191
+ hasParams: r.params.length > 0,
192
+ }));
193
+ }
194
+ getState() {
195
+ return this.globalConfig.state || {};
196
+ }
197
+ // ===== Lifecycle Events =====
198
+ on(event, listener) {
199
+ let set = this.listeners.get(event);
200
+ if (!set) {
201
+ set = new Set();
202
+ this.listeners.set(event, set);
203
+ }
204
+ set.add(listener);
205
+ return this;
206
+ }
207
+ off(event, listener) {
208
+ this.listeners.get(event)?.delete(listener);
209
+ return this;
210
+ }
211
+ emit(event, data) {
212
+ const set = this.listeners.get(event);
213
+ if (set) {
214
+ for (const listener of set) {
215
+ listener(data);
216
+ }
217
+ }
218
+ }
185
219
  // ===== Reset / Lifecycle =====
186
220
  reset() {
187
221
  this.close();
@@ -189,6 +223,7 @@ export class CallableMockInstance {
189
223
  this.staticRoutes.clear();
190
224
  this.plugins = [];
191
225
  this.requestHistory = [];
226
+ this.listeners.clear();
192
227
  if (this.globalConfig.state) {
193
228
  for (const key of Object.keys(this.globalConfig.state)) {
194
229
  delete this.globalConfig.state[key];
@@ -286,12 +321,18 @@ export class CallableMockInstance {
286
321
  }
287
322
  async handle(method, path, options) {
288
323
  const requestId = crypto.randomUUID();
324
+ const handleStart = performance.now();
289
325
  this.logger.log("request", `[${requestId}] ${method} ${path}`, {
290
326
  headers: options?.headers,
291
327
  query: options?.query,
292
328
  bodyType: options?.body ? typeof options.body : "none",
293
329
  });
294
330
  this.logger.time(`request-${requestId}`);
331
+ this.emit("request:start", {
332
+ method,
333
+ path,
334
+ headers: options?.headers || {},
335
+ });
295
336
  try {
296
337
  // Apply namespace if configured
297
338
  let requestPath = path;
@@ -324,18 +365,31 @@ export class CallableMockInstance {
324
365
  const matchedRoute = this.findRoute(method, requestPath);
325
366
  if (!matchedRoute) {
326
367
  this.logger.log("route", `[${requestId}] No route found for ${method} ${requestPath}`);
368
+ this.emit("request:notfound", { method, path: requestPath });
327
369
  const error = new RouteNotFoundError(method, path);
328
370
  const response = {
329
371
  status: 404,
330
372
  body: { error: error.message, code: error.code },
331
373
  headers: {},
332
374
  };
375
+ this.emit("request:end", {
376
+ method,
377
+ path: requestPath,
378
+ status: 404,
379
+ duration: performance.now() - handleStart,
380
+ });
333
381
  this.logger.timeEnd(`request-${requestId}`);
334
382
  return response;
335
383
  }
336
384
  this.logger.log("route", `[${requestId}] Matched route: ${method} ${matchedRoute.path}`);
337
385
  // Extract parameters from the matched route
338
386
  const params = this.extractParams(matchedRoute, requestPath);
387
+ this.emit("request:match", {
388
+ method,
389
+ path: requestPath,
390
+ routePath: matchedRoute.path,
391
+ params,
392
+ });
339
393
  // Generate initial response from route handler
340
394
  const context = {
341
395
  method,
@@ -377,8 +431,8 @@ export class CallableMockInstance {
377
431
  }
378
432
  // Parse and prepare response
379
433
  const response = this.parseResponse(result, matchedRoute.config);
380
- // Apply global delay if configured
381
- await this.applyDelay();
434
+ // Apply delay (route-level overrides global)
435
+ await this.applyDelay(matchedRoute.config.delay);
382
436
  // Record request in history
383
437
  this.requestHistory.push({
384
438
  method,
@@ -390,6 +444,12 @@ export class CallableMockInstance {
390
444
  timestamp: Date.now(),
391
445
  response: { status: response.status, body: response.body },
392
446
  });
447
+ this.emit("request:end", {
448
+ method,
449
+ path: requestPath,
450
+ status: response.status,
451
+ duration: performance.now() - handleStart,
452
+ });
393
453
  // Log successful response
394
454
  this.logger.log("response", `[${requestId}] Sending response ${response.status}`, {
395
455
  status: response.status,
@@ -410,7 +470,7 @@ export class CallableMockInstance {
410
470
  },
411
471
  headers: {},
412
472
  };
413
- // Apply global delay if configured (even for error responses)
473
+ // Apply delay even for error responses
414
474
  await this.applyDelay();
415
475
  this.logger.log("error", `[${requestId}] Returning error response 500`);
416
476
  this.logger.timeEnd(`request-${requestId}`);
@@ -422,16 +482,16 @@ export class CallableMockInstance {
422
482
  * Supports both fixed delays and random delays within a range
423
483
  * @private
424
484
  */
425
- async applyDelay() {
426
- if (!this.globalConfig.delay) {
485
+ async applyDelay(routeDelay) {
486
+ const effectiveDelay = routeDelay ?? this.globalConfig.delay;
487
+ if (!effectiveDelay) {
427
488
  return;
428
489
  }
429
- const delay = Array.isArray(this.globalConfig.delay)
430
- ? Math.random() *
431
- (this.globalConfig.delay[1] - this.globalConfig.delay[0]) +
432
- this.globalConfig.delay[0]
433
- : this.globalConfig.delay;
434
- await new Promise((resolve) => setTimeout(resolve, delay));
490
+ const ms = Array.isArray(effectiveDelay)
491
+ ? Math.random() * (effectiveDelay[1] - effectiveDelay[0]) +
492
+ effectiveDelay[0]
493
+ : effectiveDelay;
494
+ await new Promise((resolve) => setTimeout(resolve, ms));
435
495
  }
436
496
  /**
437
497
  * Parse and normalize response result into Response object
package/dist/index.d.ts CHANGED
@@ -24,5 +24,5 @@
24
24
  export declare function schmock(config?: Schmock.GlobalConfig): Schmock.CallableMockInstance;
25
25
  export { HTTP_METHODS, isHttpMethod, isStatusTuple, ROUTE_NOT_FOUND_CODE, toHttpMethod, } from "./constants.js";
26
26
  export { PluginError, ResourceLimitError, ResponseGenerationError, RouteDefinitionError, RouteNotFoundError, RouteParseError, SchemaGenerationError, SchemaValidationError, SchmockError, } from "./errors.js";
27
- export type { CallableMockInstance, Generator, GeneratorFunction, GlobalConfig, HttpMethod, Plugin, PluginContext, PluginResult, RequestContext, RequestOptions, RequestRecord, Response, ResponseBody, ResponseResult, RouteConfig, RouteKey, ServerInfo, StaticData, } from "./types.js";
27
+ export type { CallableMockInstance, Generator, GeneratorFunction, GlobalConfig, HttpMethod, Plugin, PluginContext, PluginResult, RequestContext, RequestOptions, RequestRecord, Response, ResponseBody, ResponseResult, RouteConfig, RouteInfo, RouteKey, ServerInfo, StaticData, } from "./types.js";
28
28
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,OAAO,CACrB,MAAM,CAAC,EAAE,OAAO,CAAC,YAAY,GAC5B,OAAO,CAAC,oBAAoB,CAmC9B;AAGD,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,aAAa,EACb,oBAAoB,EACpB,YAAY,GACb,MAAM,gBAAgB,CAAC;AAExB,OAAO,EACL,WAAW,EACX,kBAAkB,EAClB,uBAAuB,EACvB,oBAAoB,EACpB,kBAAkB,EAClB,eAAe,EACf,qBAAqB,EACrB,qBAAqB,EACrB,YAAY,GACb,MAAM,aAAa,CAAC;AAErB,YAAY,EACV,oBAAoB,EACpB,SAAS,EACT,iBAAiB,EACjB,YAAY,EACZ,UAAU,EACV,MAAM,EACN,aAAa,EACb,YAAY,EACZ,cAAc,EACd,cAAc,EACd,aAAa,EACb,QAAQ,EACR,YAAY,EACZ,cAAc,EACd,WAAW,EACX,QAAQ,EACR,UAAU,EACV,UAAU,GACX,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,OAAO,CACrB,MAAM,CAAC,EAAE,OAAO,CAAC,YAAY,GAC5B,OAAO,CAAC,oBAAoB,CA6C9B;AAGD,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,aAAa,EACb,oBAAoB,EACpB,YAAY,GACb,MAAM,gBAAgB,CAAC;AAExB,OAAO,EACL,WAAW,EACX,kBAAkB,EAClB,uBAAuB,EACvB,oBAAoB,EACpB,kBAAkB,EAClB,eAAe,EACf,qBAAqB,EACrB,qBAAqB,EACrB,YAAY,GACb,MAAM,aAAa,CAAC;AAErB,YAAY,EACV,oBAAoB,EACpB,SAAS,EACT,iBAAiB,EACjB,YAAY,EACZ,UAAU,EACV,MAAM,EACN,aAAa,EACb,YAAY,EACZ,cAAc,EACd,cAAc,EACd,aAAa,EACb,QAAQ,EACR,YAAY,EACZ,cAAc,EACd,WAAW,EACX,SAAS,EACT,QAAQ,EACR,UAAU,EACV,UAAU,GACX,MAAM,YAAY,CAAC"}
package/dist/index.js CHANGED
@@ -42,6 +42,16 @@ export function schmock(config) {
42
42
  reset: instance.reset.bind(instance),
43
43
  resetHistory: instance.resetHistory.bind(instance),
44
44
  resetState: instance.resetState.bind(instance),
45
+ on: ((event, listener) => {
46
+ instance.on(event, listener);
47
+ return callableInstance;
48
+ }),
49
+ off: ((event, listener) => {
50
+ instance.off(event, listener);
51
+ return callableInstance;
52
+ }),
53
+ getRoutes: instance.getRoutes.bind(instance),
54
+ getState: instance.getState.bind(instance),
45
55
  listen: instance.listen.bind(instance),
46
56
  close: instance.close.bind(instance),
47
57
  });
package/dist/types.d.ts CHANGED
@@ -16,4 +16,5 @@ export type PluginResult = Schmock.PluginResult;
16
16
  export type StaticData = Schmock.StaticData;
17
17
  export type RequestRecord = Schmock.RequestRecord;
18
18
  export type ServerInfo = Schmock.ServerInfo;
19
+ export type RouteInfo = Schmock.RouteInfo;
19
20
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;AAC5C,MAAM,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;AACxC,MAAM,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC;AAChD,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;AACpD,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;AACpD,MAAM,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;AACxC,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;AACpD,MAAM,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC;AAChD,MAAM,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC;AAC9C,MAAM,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC;AAC1C,MAAM,MAAM,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC;AAC1D,MAAM,MAAM,oBAAoB,GAAG,OAAO,CAAC,oBAAoB,CAAC;AAChE,MAAM,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;AACpC,MAAM,MAAM,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC;AAClD,MAAM,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC;AAChD,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;AAC5C,MAAM,MAAM,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC;AAClD,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;AAC5C,MAAM,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;AACxC,MAAM,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC;AAChD,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;AACpD,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;AACpD,MAAM,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;AACxC,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;AACpD,MAAM,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC;AAChD,MAAM,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC;AAC9C,MAAM,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC;AAC1C,MAAM,MAAM,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC;AAC1D,MAAM,MAAM,oBAAoB,GAAG,OAAO,CAAC,oBAAoB,CAAC;AAChE,MAAM,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;AACpC,MAAM,MAAM,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC;AAClD,MAAM,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC;AAChD,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;AAC5C,MAAM,MAAM,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC;AAClD,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;AAC5C,MAAM,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC"}
package/dist/types.js CHANGED
@@ -1,2 +1,2 @@
1
- /// <reference path="../../../types/schmock.d.ts" />
1
+ /// <reference path="../schmock.d.ts" />
2
2
  export {};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@schmock/core",
3
3
  "description": "Core functionality for Schmock",
4
- "version": "1.3.0",
4
+ "version": "1.7.0",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
package/src/builder.ts CHANGED
@@ -88,6 +88,7 @@ export class CallableMockInstance {
88
88
  private callableRef: Schmock.CallableMockInstance | undefined;
89
89
  private server: Server | undefined;
90
90
  private serverInfo: Schmock.ServerInfo | undefined;
91
+ private listeners = new Map<string, Set<(data: unknown) => void>>();
91
92
 
92
93
  constructor(private globalConfig: Schmock.GlobalConfig = {}) {
93
94
  this.logger = new DebugLogger(globalConfig.debug || false);
@@ -255,6 +256,46 @@ export class CallableMockInstance {
255
256
  return this.requestHistory[this.requestHistory.length - 1];
256
257
  }
257
258
 
259
+ // ===== Introspection =====
260
+
261
+ getRoutes(): Schmock.RouteInfo[] {
262
+ return this.routes.map((r) => ({
263
+ method: r.method,
264
+ path: r.path,
265
+ hasParams: r.params.length > 0,
266
+ }));
267
+ }
268
+
269
+ getState(): Record<string, unknown> {
270
+ return this.globalConfig.state || {};
271
+ }
272
+
273
+ // ===== Lifecycle Events =====
274
+
275
+ on(event: string, listener: (data: unknown) => void): this {
276
+ let set = this.listeners.get(event);
277
+ if (!set) {
278
+ set = new Set();
279
+ this.listeners.set(event, set);
280
+ }
281
+ set.add(listener);
282
+ return this;
283
+ }
284
+
285
+ off(event: string, listener: (data: unknown) => void): this {
286
+ this.listeners.get(event)?.delete(listener);
287
+ return this;
288
+ }
289
+
290
+ private emit(event: string, data: unknown): void {
291
+ const set = this.listeners.get(event);
292
+ if (set) {
293
+ for (const listener of set) {
294
+ listener(data);
295
+ }
296
+ }
297
+ }
298
+
258
299
  // ===== Reset / Lifecycle =====
259
300
 
260
301
  reset(): void {
@@ -263,6 +304,7 @@ export class CallableMockInstance {
263
304
  this.staticRoutes.clear();
264
305
  this.plugins = [];
265
306
  this.requestHistory = [];
307
+ this.listeners.clear();
266
308
  if (this.globalConfig.state) {
267
309
  for (const key of Object.keys(this.globalConfig.state)) {
268
310
  delete this.globalConfig.state[key];
@@ -386,6 +428,7 @@ export class CallableMockInstance {
386
428
  options?: Schmock.RequestOptions,
387
429
  ): Promise<Schmock.Response> {
388
430
  const requestId = crypto.randomUUID();
431
+ const handleStart = performance.now();
389
432
  this.logger.log("request", `[${requestId}] ${method} ${path}`, {
390
433
  headers: options?.headers,
391
434
  query: options?.query,
@@ -393,6 +436,12 @@ export class CallableMockInstance {
393
436
  });
394
437
  this.logger.time(`request-${requestId}`);
395
438
 
439
+ this.emit("request:start", {
440
+ method,
441
+ path,
442
+ headers: options?.headers || {},
443
+ });
444
+
396
445
  try {
397
446
  // Apply namespace if configured
398
447
  let requestPath = path;
@@ -440,12 +489,19 @@ export class CallableMockInstance {
440
489
  "route",
441
490
  `[${requestId}] No route found for ${method} ${requestPath}`,
442
491
  );
492
+ this.emit("request:notfound", { method, path: requestPath });
443
493
  const error = new RouteNotFoundError(method, path);
444
494
  const response = {
445
495
  status: 404,
446
496
  body: { error: error.message, code: error.code },
447
497
  headers: {},
448
498
  };
499
+ this.emit("request:end", {
500
+ method,
501
+ path: requestPath,
502
+ status: 404,
503
+ duration: performance.now() - handleStart,
504
+ });
449
505
  this.logger.timeEnd(`request-${requestId}`);
450
506
  return response;
451
507
  }
@@ -458,6 +514,13 @@ export class CallableMockInstance {
458
514
  // Extract parameters from the matched route
459
515
  const params = this.extractParams(matchedRoute, requestPath);
460
516
 
517
+ this.emit("request:match", {
518
+ method,
519
+ path: requestPath,
520
+ routePath: matchedRoute.path,
521
+ params,
522
+ });
523
+
461
524
  // Generate initial response from route handler
462
525
  const context: Schmock.RequestContext = {
463
526
  method,
@@ -510,8 +573,8 @@ export class CallableMockInstance {
510
573
  // Parse and prepare response
511
574
  const response = this.parseResponse(result, matchedRoute.config);
512
575
 
513
- // Apply global delay if configured
514
- await this.applyDelay();
576
+ // Apply delay (route-level overrides global)
577
+ await this.applyDelay(matchedRoute.config.delay);
515
578
 
516
579
  // Record request in history
517
580
  this.requestHistory.push({
@@ -525,6 +588,13 @@ export class CallableMockInstance {
525
588
  response: { status: response.status, body: response.body },
526
589
  });
527
590
 
591
+ this.emit("request:end", {
592
+ method,
593
+ path: requestPath,
594
+ status: response.status,
595
+ duration: performance.now() - handleStart,
596
+ });
597
+
528
598
  // Log successful response
529
599
  this.logger.log(
530
600
  "response",
@@ -555,7 +625,7 @@ export class CallableMockInstance {
555
625
  headers: {},
556
626
  };
557
627
 
558
- // Apply global delay if configured (even for error responses)
628
+ // Apply delay even for error responses
559
629
  await this.applyDelay();
560
630
 
561
631
  this.logger.log("error", `[${requestId}] Returning error response 500`);
@@ -569,18 +639,20 @@ export class CallableMockInstance {
569
639
  * Supports both fixed delays and random delays within a range
570
640
  * @private
571
641
  */
572
- private async applyDelay(): Promise<void> {
573
- if (!this.globalConfig.delay) {
642
+ private async applyDelay(
643
+ routeDelay?: number | [number, number],
644
+ ): Promise<void> {
645
+ const effectiveDelay = routeDelay ?? this.globalConfig.delay;
646
+ if (!effectiveDelay) {
574
647
  return;
575
648
  }
576
649
 
577
- const delay = Array.isArray(this.globalConfig.delay)
578
- ? Math.random() *
579
- (this.globalConfig.delay[1] - this.globalConfig.delay[0]) +
580
- this.globalConfig.delay[0]
581
- : this.globalConfig.delay;
650
+ const ms = Array.isArray(effectiveDelay)
651
+ ? Math.random() * (effectiveDelay[1] - effectiveDelay[0]) +
652
+ effectiveDelay[0]
653
+ : effectiveDelay;
582
654
 
583
- await new Promise((resolve) => setTimeout(resolve, delay));
655
+ await new Promise((resolve) => setTimeout(resolve, ms));
584
656
  }
585
657
 
586
658
  /**
package/src/index.ts CHANGED
@@ -52,6 +52,16 @@ export function schmock(
52
52
  reset: instance.reset.bind(instance),
53
53
  resetHistory: instance.resetHistory.bind(instance),
54
54
  resetState: instance.resetState.bind(instance),
55
+ on: ((event: string, listener: (data: unknown) => void) => {
56
+ instance.on(event, listener);
57
+ return callableInstance;
58
+ }) as Schmock.CallableMockInstance["on"],
59
+ off: ((event: string, listener: (data: unknown) => void) => {
60
+ instance.off(event, listener);
61
+ return callableInstance;
62
+ }) as Schmock.CallableMockInstance["off"],
63
+ getRoutes: instance.getRoutes.bind(instance),
64
+ getState: instance.getState.bind(instance),
55
65
  listen: instance.listen.bind(instance),
56
66
  close: instance.close.bind(instance),
57
67
  },
@@ -99,6 +109,7 @@ export type {
99
109
  ResponseBody,
100
110
  ResponseResult,
101
111
  RouteConfig,
112
+ RouteInfo,
102
113
  RouteKey,
103
114
  ServerInfo,
104
115
  StaticData,
@@ -0,0 +1,142 @@
1
+ import { describeFeature, loadFeature } from "@amiceli/vitest-cucumber";
2
+ import { expect } from "vitest";
3
+ import { schmock } from "../index";
4
+
5
+ const feature = await loadFeature("../../features/lifecycle-events.feature");
6
+
7
+ describeFeature(feature, ({ Scenario }) => {
8
+ let mock: Schmock.CallableMockInstance;
9
+ let events: Array<{ type: string; data: unknown }>;
10
+ let removedFired: boolean;
11
+
12
+ function collectEvent(type: string) {
13
+ return (data: unknown) => {
14
+ events.push({ type, data });
15
+ };
16
+ }
17
+
18
+ Scenario("Events fire at correct times", ({ Given, And, When, Then }) => {
19
+ Given('a mock with a route "GET /items"', () => {
20
+ mock = schmock({ state: {} });
21
+ mock("GET /items", [{ id: 1 }], {});
22
+ });
23
+
24
+ And("I register listeners for all events", () => {
25
+ events = [];
26
+ mock.on("request:start", collectEvent("request:start"));
27
+ mock.on("request:match", collectEvent("request:match"));
28
+ mock.on("request:notfound", collectEvent("request:notfound"));
29
+ mock.on("request:end", collectEvent("request:end"));
30
+ });
31
+
32
+ When('I request "GET /items"', async () => {
33
+ await mock.handle("GET", "/items");
34
+ });
35
+
36
+ Then('the "request:start" event fired', () => {
37
+ expect(events.some((e) => e.type === "request:start")).toBe(true);
38
+ });
39
+
40
+ And('the "request:match" event fired with routePath "/items"', () => {
41
+ const match = events.find((e) => e.type === "request:match");
42
+ expect(match).toBeDefined();
43
+ const data = match?.data as Record<string, unknown>;
44
+ expect(data.routePath).toBe("/items");
45
+ });
46
+
47
+ And('the "request:end" event fired with status 200', () => {
48
+ const end = events.find((e) => e.type === "request:end");
49
+ expect(end).toBeDefined();
50
+ const data = end?.data as Record<string, unknown>;
51
+ expect(data.status).toBe(200);
52
+ });
53
+ });
54
+
55
+ Scenario("Not found event fires for unmatched routes", ({ Given, And, When, Then }) => {
56
+ Given('a mock with a route "GET /items"', () => {
57
+ mock = schmock({ state: {} });
58
+ mock("GET /items", [{ id: 1 }], {});
59
+ });
60
+
61
+ And("I register listeners for all events", () => {
62
+ events = [];
63
+ mock.on("request:start", collectEvent("request:start"));
64
+ mock.on("request:match", collectEvent("request:match"));
65
+ mock.on("request:notfound", collectEvent("request:notfound"));
66
+ mock.on("request:end", collectEvent("request:end"));
67
+ });
68
+
69
+ When('I request "GET /missing"', async () => {
70
+ await mock.handle("GET", "/missing");
71
+ });
72
+
73
+ Then('the "request:start" event fired', () => {
74
+ expect(events.some((e) => e.type === "request:start")).toBe(true);
75
+ });
76
+
77
+ And('the "request:notfound" event fired', () => {
78
+ expect(events.some((e) => e.type === "request:notfound")).toBe(true);
79
+ });
80
+
81
+ And('the "request:end" event fired with status 404', () => {
82
+ const end = events.find((e) => e.type === "request:end");
83
+ expect(end).toBeDefined();
84
+ const data = end?.data as Record<string, unknown>;
85
+ expect(data.status).toBe(404);
86
+ });
87
+ });
88
+
89
+ Scenario("Off removes listener", ({ Given, And, When, Then }) => {
90
+ Given('a mock with a route "GET /items"', () => {
91
+ mock = schmock({ state: {} });
92
+ mock("GET /items", [{ id: 1 }], {});
93
+ });
94
+
95
+ And("I register and remove a listener", () => {
96
+ removedFired = false;
97
+ const listener = () => {
98
+ removedFired = true;
99
+ };
100
+ mock.on("request:start", listener);
101
+ mock.off("request:start", listener);
102
+ });
103
+
104
+ When('I request "GET /items"', async () => {
105
+ await mock.handle("GET", "/items");
106
+ });
107
+
108
+ Then("the removed listener did not fire", () => {
109
+ expect(removedFired).toBe(false);
110
+ });
111
+ });
112
+
113
+ Scenario("Reset clears all listeners", ({ Given, And, When, Then }) => {
114
+ Given('a mock with a route "GET /items"', () => {
115
+ mock = schmock({ state: {} });
116
+ mock("GET /items", [{ id: 1 }], {});
117
+ });
118
+
119
+ And("I register listeners for all events", () => {
120
+ events = [];
121
+ mock.on("request:start", collectEvent("request:start"));
122
+ mock.on("request:end", collectEvent("request:end"));
123
+ });
124
+
125
+ When("I reset the mock", () => {
126
+ mock.reset();
127
+ events = [];
128
+ });
129
+
130
+ And('I add a route "GET /items" again', () => {
131
+ mock("GET /items", [{ id: 1 }], {});
132
+ });
133
+
134
+ And('I request "GET /items" after reset', async () => {
135
+ await mock.handle("GET", "/items");
136
+ });
137
+
138
+ Then("no events were collected after reset", () => {
139
+ expect(events).toHaveLength(0);
140
+ });
141
+ });
142
+ });
@@ -0,0 +1,85 @@
1
+ import { describeFeature, loadFeature } from "@amiceli/vitest-cucumber";
2
+ import { expect } from "vitest";
3
+ import { schmock } from "../index";
4
+
5
+ const feature = await loadFeature("../../features/response-delay.feature");
6
+
7
+ describeFeature(feature, ({ Scenario }) => {
8
+ let mock: Schmock.CallableMockInstance;
9
+ let elapsed: number;
10
+ let elapsed2: number;
11
+
12
+ Scenario("Route delay overrides global delay", ({ Given, And, When, Then }) => {
13
+ Given("a mock with global delay of 200ms", () => {
14
+ mock = schmock({ delay: 200, state: {} });
15
+ });
16
+
17
+ And('a route "GET /fast" with delay of 10ms', () => {
18
+ mock("GET /fast", { ok: true }, { delay: 10 });
19
+ });
20
+
21
+ And('a route "GET /default" with no delay override', () => {
22
+ mock("GET /default", { ok: true }, {});
23
+ });
24
+
25
+ When('I request "GET /fast"', async () => {
26
+ const start = performance.now();
27
+ await mock.handle("GET", "/fast");
28
+ elapsed = performance.now() - start;
29
+ });
30
+
31
+ Then("the response took less than 100ms", () => {
32
+ expect(elapsed).toBeLessThan(100);
33
+ });
34
+
35
+ When('I request "GET /default" with timing', async () => {
36
+ const start = performance.now();
37
+ await mock.handle("GET", "/default");
38
+ elapsed2 = performance.now() - start;
39
+ });
40
+
41
+ Then("that response took at least 150ms", () => {
42
+ expect(elapsed2).toBeGreaterThanOrEqual(150);
43
+ });
44
+ });
45
+
46
+ Scenario("Route delay supports random range", ({ Given, And, When, Then }) => {
47
+ Given("a mock with no global delay", () => {
48
+ mock = schmock({ state: {} });
49
+ });
50
+
51
+ And('a route "GET /random" with delay range 10 to 30', () => {
52
+ mock("GET /random", { ok: true }, { delay: [10, 30] });
53
+ });
54
+
55
+ When('I request "GET /random"', async () => {
56
+ const start = performance.now();
57
+ await mock.handle("GET", "/random");
58
+ elapsed = performance.now() - start;
59
+ });
60
+
61
+ Then("the response took at least 10ms", () => {
62
+ expect(elapsed).toBeGreaterThanOrEqual(8); // small tolerance
63
+ });
64
+ });
65
+
66
+ Scenario("No route delay inherits global delay", ({ Given, And, When, Then }) => {
67
+ Given("a mock with global delay of 50ms", () => {
68
+ mock = schmock({ delay: 50, state: {} });
69
+ });
70
+
71
+ And('a route "GET /items" with no delay override', () => {
72
+ mock("GET /items", { ok: true }, {});
73
+ });
74
+
75
+ When('I request "GET /items" with timing', async () => {
76
+ const start = performance.now();
77
+ await mock.handle("GET", "/items");
78
+ elapsed = performance.now() - start;
79
+ });
80
+
81
+ Then("that response took at least 40ms", () => {
82
+ expect(elapsed).toBeGreaterThanOrEqual(40);
83
+ });
84
+ });
85
+ });
package/src/types.ts CHANGED
@@ -1,6 +1,6 @@
1
- /// <reference path="../../../types/schmock.d.ts" />
1
+ /// <reference path="../schmock.d.ts" />
2
2
 
3
- // Re-export types for internal use
3
+ // Re-export ambient types for consumers
4
4
  export type HttpMethod = Schmock.HttpMethod;
5
5
  export type RouteKey = Schmock.RouteKey;
6
6
  export type ResponseBody = Schmock.ResponseBody;
@@ -19,3 +19,4 @@ export type PluginResult = Schmock.PluginResult;
19
19
  export type StaticData = Schmock.StaticData;
20
20
  export type RequestRecord = Schmock.RequestRecord;
21
21
  export type ServerInfo = Schmock.ServerInfo;
22
+ export type RouteInfo = Schmock.RouteInfo;
@@ -1 +0,0 @@
1
- {"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["builder.ts"],"names":[],"mappings":"AAAA,KAAK,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;AAW7B,OAAO,KAAK,EACV,OAAO,EACP,aAAa,EAEb,YAAY,EAGZ,MAAM,EACP,MAAM,SAAS,CAAC;AAajB;;;;GAIG;AACH,qBAAa,cAAc,CAAC,MAAM,GAAG,OAAO,CAAE,YAAW,OAAO,CAAC,MAAM,CAAC;IACtE,OAAO,CAAC,OAAO,CAEb;IAEF,MAAM,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC;IAK/C,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC;IAK/C,KAAK,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAShC,GAAG,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAKpC,KAAK,IAAI,YAAY,CAAC,MAAM,CAAC;IAQ7B;;;OAGG;IACH,OAAO,CAAC,aAAa;CA0BtB"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["errors.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,qBAAa,YAAa,SAAQ,KAAK;aAGnB,IAAI,EAAE,MAAM;aACZ,OAAO,CAAC,EAAE,OAAO;gBAFjC,OAAO,EAAE,MAAM,EACC,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,OAAO,YAAA;CAMpC;AAED;;GAEG;AACH,qBAAa,kBAAmB,SAAQ,YAAY;gBACtC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM;CAQzC;AAED;;GAEG;AACH,qBAAa,eAAgB,SAAQ,YAAY;gBACnC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;CAQ7C;AAED;;GAEG;AACH,qBAAa,uBAAwB,SAAQ,YAAY;gBAC3C,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK;CAQxC;AAED;;GAEG;AACH,qBAAa,WAAY,SAAQ,YAAY;gBAC/B,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK;CAQ7C;AAED;;GAEG;AACH,qBAAa,oBAAqB,SAAQ,YAAY;gBACxC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;CAQ7C;AAED;;GAEG;AACH,qBAAa,qBAAsB,SAAQ,YAAY;gBACzC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM;CAQnE;AAED;;GAEG;AACH,qBAAa,qBAAsB,SAAQ,YAAY;gBACzC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,CAAC,EAAE,OAAO;CAQ1D;AAED;;GAEG;AACH,qBAAa,kBAAmB,SAAQ,YAAY;gBACtC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM;CAQ7D"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAEvC;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,OAAO,IAAI,OAAO,CAEjC;AAGD,YAAY,EACV,OAAO,EACP,aAAa,EACb,UAAU,EACV,YAAY,EACZ,eAAe,EACf,gBAAgB,EAChB,cAAc,EACd,eAAe,EACf,QAAQ,EACR,MAAM,GACP,MAAM,SAAS,CAAC;AAGjB,OAAO,EACL,YAAY,EACZ,kBAAkB,EAClB,eAAe,EACf,uBAAuB,EACvB,WAAW,EACX,oBAAoB,EACpB,qBAAqB,EACrB,qBAAqB,EACrB,kBAAkB,GACnB,MAAM,UAAU,CAAC"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["parser.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAE1C,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,UAAU,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,WAAW,CAuC3D"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["types.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;AAC5C,MAAM,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;AACxC,MAAM,MAAM,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC;AAClD,MAAM,MAAM,eAAe,CAAC,CAAC,GAAG,OAAO,IAAI,OAAO,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;AACtE,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;AACpD,MAAM,MAAM,gBAAgB,CAAC,CAAC,GAAG,OAAO,IAAI,OAAO,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC;AACxE,MAAM,MAAM,eAAe,CAAC,CAAC,GAAG,OAAO,IAAI,OAAO,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;AACtE,MAAM,MAAM,MAAM,CAAC,CAAC,GAAG,OAAO,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;AACpD,MAAM,MAAM,OAAO,CAAC,CAAC,GAAG,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;AACtD,MAAM,MAAM,YAAY,CAAC,CAAC,GAAG,OAAO,IAAI,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC"}