definitely-fine 0.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.
package/dist/index.js ADDED
@@ -0,0 +1,729 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ import { readFileSync, realpathSync, statSync } from "node:fs";
3
+ import { mkdir, rm, writeFile } from "node:fs/promises";
4
+ import { basename, dirname, isAbsolute, join, parse, relative } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { randomUUID } from "node:crypto";
7
+
8
+ //#region src/runtime-scenario-context.ts
9
+ const runtimeScenarioContextStorage = new AsyncLocalStorage();
10
+ /**
11
+ * Runs a callback inside a request-scoped runtime scenario context.
12
+ * @public
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * runWithRuntimeScenarioContext({ scenarioId: "checkout" }, () => {
17
+ * return getRuntimeScenarioId();
18
+ * });
19
+ * ```
20
+ */
21
+ function runWithRuntimeScenarioContext(context, callback) {
22
+ return runtimeScenarioContextStorage.run(context, callback);
23
+ }
24
+ /**
25
+ * Returns the active runtime scenario id for the current async scope.
26
+ * @public
27
+ *
28
+ * @example
29
+ * ```ts
30
+ * const scenarioId = getRuntimeScenarioId();
31
+ * ```
32
+ */
33
+ function getRuntimeScenarioId() {
34
+ return runtimeScenarioContextStorage.getStore()?.scenarioId;
35
+ }
36
+
37
+ //#endregion
38
+ //#region src/services/utils/get-scenario-file-path.ts
39
+ /**
40
+ * Builds the persisted JSON file path for a scenario id.
41
+ * @internal
42
+ */
43
+ function getScenarioFilePath(directory, scenarioId) {
44
+ return join(directory, `${scenarioId}.json`);
45
+ }
46
+
47
+ //#endregion
48
+ //#region src/services/impls/JsonScenarioStorageAdapter.ts
49
+ function createJsonScenarioStorageAdapterError(message, cause) {
50
+ return new Error(message, { cause });
51
+ }
52
+ function findNodeModulesDirectory(startPath) {
53
+ let currentPath = dirname(startPath);
54
+ const { root } = parse(currentPath);
55
+ while (currentPath !== root) {
56
+ if (basename(currentPath) === "node_modules") return currentPath;
57
+ currentPath = dirname(currentPath);
58
+ }
59
+ if (basename(currentPath) === "node_modules") return currentPath;
60
+ return void 0;
61
+ }
62
+ function isDirectory(path) {
63
+ try {
64
+ return statSync(path).isDirectory();
65
+ } catch (_error) {
66
+ return false;
67
+ }
68
+ }
69
+ function findInstalledPackageNodeModulesDirectory(packageRootPath) {
70
+ let currentPath = process.cwd();
71
+ const { root } = parse(currentPath);
72
+ while (currentPath !== root) {
73
+ const nodeModulesDirectory$1 = join(currentPath, "node_modules");
74
+ const installedPackageDirectory$1 = join(nodeModulesDirectory$1, "definitely-fine");
75
+ if (isDirectory(installedPackageDirectory$1) && realpathSync(installedPackageDirectory$1) === packageRootPath) return nodeModulesDirectory$1;
76
+ currentPath = dirname(currentPath);
77
+ }
78
+ const nodeModulesDirectory = join(currentPath, "node_modules");
79
+ const installedPackageDirectory = join(nodeModulesDirectory, "definitely-fine");
80
+ if (isDirectory(installedPackageDirectory) && realpathSync(installedPackageDirectory) === packageRootPath) return nodeModulesDirectory;
81
+ return void 0;
82
+ }
83
+ function isPathInsideDirectory(path, directory) {
84
+ const relativePath = relative(directory, path);
85
+ return relativePath === "" || !relativePath.startsWith("..") && !isAbsolute(relativePath);
86
+ }
87
+ /**
88
+ * Resolves the default directory used by the built-in JSON scenario adapter.
89
+ * @public
90
+ *
91
+ * @example
92
+ * ```ts
93
+ * import { resolveJsonScenarioStorageAdapterDirectory } from "definitely-fine";
94
+ *
95
+ * const directory = resolveJsonScenarioStorageAdapterDirectory({
96
+ * directory: ".definitely-fine",
97
+ * });
98
+ * ```
99
+ */
100
+ function resolveJsonScenarioStorageAdapterDirectory(options) {
101
+ if (options.directory !== void 0) return options.directory;
102
+ const unresolvedModuleFilePath = fileURLToPath(options.moduleUrl ?? import.meta.url);
103
+ const symlinkedNodeModulesDirectory = findNodeModulesDirectory(unresolvedModuleFilePath);
104
+ if (symlinkedNodeModulesDirectory !== void 0) return join(symlinkedNodeModulesDirectory, ".cache", "definitely-fine", "scenarios");
105
+ const moduleFilePath = realpathSync(unresolvedModuleFilePath);
106
+ const nodeModulesDirectory = findNodeModulesDirectory(moduleFilePath);
107
+ if (nodeModulesDirectory !== void 0) return join(nodeModulesDirectory, ".cache", "definitely-fine", "scenarios");
108
+ if (options.moduleUrl === void 0 && !isPathInsideDirectory(moduleFilePath, process.cwd())) {
109
+ const workingDirectoryNodeModulesDirectory = findInstalledPackageNodeModulesDirectory(dirname(dirname(moduleFilePath)));
110
+ if (workingDirectoryNodeModulesDirectory !== void 0) return join(workingDirectoryNodeModulesDirectory, ".cache", "definitely-fine", "scenarios");
111
+ }
112
+ throw new Error(`Could not infer the default file scenario adapter directory from ${moduleFilePath}. Provide an explicit directory instead.`);
113
+ }
114
+ function isRecord(value) {
115
+ return typeof value === "object" && value !== null;
116
+ }
117
+ function isValidIsoTimestamp(value) {
118
+ if (typeof value !== "string") return false;
119
+ const parsed = new Date(value);
120
+ return !Number.isNaN(parsed.valueOf()) && parsed.toISOString() === value;
121
+ }
122
+ function isSerializedTarget(value) {
123
+ if (!isRecord(value) || typeof value.kind !== "string") return false;
124
+ if (value.kind === "function") return typeof value.function === "string";
125
+ if (value.kind === "service-method") return typeof value.service === "string" && typeof value.method === "string";
126
+ return false;
127
+ }
128
+ function isSerializedAction(value) {
129
+ if (!isRecord(value) || typeof value.kind !== "string") return false;
130
+ if (value.kind === "return") return "value" in value;
131
+ if (value.kind === "throw-message") return typeof value.message === "string";
132
+ if (value.kind === "throw-factory") return typeof value.factory === "string";
133
+ return false;
134
+ }
135
+ function isSerializedRule(value) {
136
+ return isRecord(value) && typeof value.callNumber === "number" && Number.isInteger(value.callNumber) && value.callNumber > 0 && isSerializedTarget(value.target) && isSerializedAction(value.action);
137
+ }
138
+ function isSerializedScenario(value) {
139
+ return isRecord(value) && typeof value.id === "string" && value.version === 1 && isValidIsoTimestamp(value.createdAt) && Array.isArray(value.rules) && value.rules.every((rule) => isSerializedRule(rule));
140
+ }
141
+ function loadSerializedScenario(scenarioId, scenarioFilePath) {
142
+ let serializedScenarioText;
143
+ try {
144
+ serializedScenarioText = readFileSync(scenarioFilePath, "utf8");
145
+ } catch (error) {
146
+ if (isRecord(error) && "code" in error && error.code === "ENOENT") return void 0;
147
+ throw error;
148
+ }
149
+ try {
150
+ const parsedValue = JSON.parse(serializedScenarioText);
151
+ if (!isSerializedScenario(parsedValue) || parsedValue.id !== scenarioId) throw new Error(`Invalid persisted scenario data in ${scenarioFilePath}.`);
152
+ return parsedValue;
153
+ } catch (error) {
154
+ if (error instanceof Error && error.message === `Invalid persisted scenario data in ${scenarioFilePath}.`) throw error;
155
+ throw new Error(`Invalid persisted scenario data in ${scenarioFilePath}.`, { cause: error });
156
+ }
157
+ }
158
+ /**
159
+ * Persists scenarios as JSON files in a local directory.
160
+ *
161
+ * @example
162
+ * ```ts
163
+ * import { JsonScenarioStorageAdapter } from "definitely-fine";
164
+ *
165
+ * const adapter = new JsonScenarioStorageAdapter({
166
+ * directory: ".definitely-fine",
167
+ * });
168
+ * ```
169
+ * @public
170
+ */
171
+ var JsonScenarioStorageAdapter = class {
172
+ directory;
173
+ constructor(options = {}) {
174
+ this.directory = resolveJsonScenarioStorageAdapterDirectory(options);
175
+ }
176
+ async deleteScenario(scenarioId) {
177
+ await rm(getScenarioFilePath(this.directory, scenarioId), { force: true });
178
+ }
179
+ describeScenarioLocation(scenarioId) {
180
+ return getScenarioFilePath(this.directory, scenarioId);
181
+ }
182
+ loadScenario(scenarioId) {
183
+ return loadSerializedScenario(scenarioId, getScenarioFilePath(this.directory, scenarioId));
184
+ }
185
+ async saveScenario(scenario) {
186
+ try {
187
+ await mkdir(this.directory, { recursive: true });
188
+ } catch (error) {
189
+ throw createJsonScenarioStorageAdapterError(`Could not create the file scenario adapter directory at ${this.directory}.`, error);
190
+ }
191
+ const scenarioFilePath = getScenarioFilePath(this.directory, scenario.id);
192
+ try {
193
+ await writeFile(scenarioFilePath, JSON.stringify(scenario, null, 2));
194
+ } catch (error) {
195
+ throw createJsonScenarioStorageAdapterError(`Could not write the file scenario adapter payload to ${scenarioFilePath}.`, error);
196
+ }
197
+ }
198
+ };
199
+
200
+ //#endregion
201
+ //#region src/runtime-errors.ts
202
+ /**
203
+ * Signals that runtime setup is missing required configuration.
204
+ * @public
205
+ *
206
+ * @example
207
+ * ```ts
208
+ * import { RuntimeConfigurationError } from "definitely-fine";
209
+ *
210
+ * throw new RuntimeConfigurationError("A scenario adapter is required.");
211
+ * ```
212
+ */
213
+ var RuntimeConfigurationError = class extends Error {
214
+ constructor(message, options) {
215
+ super(message, options);
216
+ this.name = "RuntimeConfigurationError";
217
+ }
218
+ };
219
+
220
+ //#endregion
221
+ //#region src/adapter-resolution.ts
222
+ /**
223
+ * Normalizes storage options into an adapter, directory, or missing state.
224
+ * @internal
225
+ */
226
+ function resolveScenarioStorageSelection(options) {
227
+ if (options.adapter !== void 0 && options.directory !== void 0) throw new RuntimeConfigurationError("A storage adapter and directory cannot be used together.");
228
+ if (options.adapter) return {
229
+ kind: "adapter",
230
+ adapter: options.adapter
231
+ };
232
+ if (options.directory !== void 0) return {
233
+ kind: "directory",
234
+ directory: options.directory
235
+ };
236
+ return { kind: "default" };
237
+ }
238
+
239
+ //#endregion
240
+ //#region src/services/impls/RuntimeErrorFactory.ts
241
+ function hasRuntimeFactory(errorFactories, factoryName) {
242
+ return factoryName in errorFactories;
243
+ }
244
+ /**
245
+ * Default runtime factory-error resolver implementation.
246
+ * @internal
247
+ */
248
+ var RuntimeErrorFactory = class {
249
+ errorFactories;
250
+ constructor(errorFactories) {
251
+ this.errorFactories = errorFactories ?? {};
252
+ }
253
+ create(factoryName, ...input) {
254
+ if (!hasRuntimeFactory(this.errorFactories, factoryName)) throw new RuntimeConfigurationError(`No runtime error factory registered for "${factoryName}".`);
255
+ const factory = this.errorFactories[factoryName];
256
+ if (typeof factory !== "function") throw new RuntimeConfigurationError(`No runtime error factory registered for "${factoryName}".`);
257
+ return Reflect.apply(factory, void 0, input);
258
+ }
259
+ };
260
+
261
+ //#endregion
262
+ //#region src/services/impls/RuntimeScenarioCacheService.ts
263
+ const DEFAULT_RUNTIME_SCENARIO_CACHE_TTL_MS = 6e4;
264
+ function scheduleEntryEviction(cacheEntries, scenarioId, ttlMs) {
265
+ const evictionTimer = setTimeout(() => {
266
+ cacheEntries.delete(scenarioId);
267
+ }, ttlMs);
268
+ evictionTimer.unref?.();
269
+ return evictionTimer;
270
+ }
271
+ /**
272
+ * Default runtime scenario cache with inactivity-based eviction.
273
+ * @internal
274
+ */
275
+ var RuntimeScenarioCacheService = class {
276
+ ttlMs;
277
+ cacheEntries = new Map();
278
+ constructor(options = {}) {
279
+ this.ttlMs = options.ttlMs ?? DEFAULT_RUNTIME_SCENARIO_CACHE_TTL_MS;
280
+ }
281
+ has(scenarioId) {
282
+ return this.cacheEntries.has(scenarioId);
283
+ }
284
+ get(scenarioId) {
285
+ const cacheEntry = this.cacheEntries.get(scenarioId);
286
+ if (cacheEntry) {
287
+ clearTimeout(cacheEntry.evictionTimer);
288
+ cacheEntry.evictionTimer = scheduleEntryEviction(this.cacheEntries, scenarioId, this.ttlMs);
289
+ return cacheEntry.runtimeScenario;
290
+ }
291
+ return void 0;
292
+ }
293
+ set(scenarioId, runtimeScenario) {
294
+ const cacheEntry = this.cacheEntries.get(scenarioId);
295
+ if (cacheEntry) clearTimeout(cacheEntry.evictionTimer);
296
+ this.cacheEntries.set(scenarioId, {
297
+ runtimeScenario,
298
+ evictionTimer: scheduleEntryEviction(this.cacheEntries, scenarioId, this.ttlMs)
299
+ });
300
+ }
301
+ };
302
+
303
+ //#endregion
304
+ //#region src/runtime-matcher.ts
305
+ /**
306
+ * Reusable runtime scenario state bound to a single scenario id.
307
+ * @internal
308
+ */
309
+ var RuntimeScenario = class {
310
+ /**
311
+ * Stable identifier for the runtime scenario state.
312
+ */
313
+ id;
314
+ #scenario;
315
+ #targetCounters = new Map();
316
+ constructor(scenarioId, scenario) {
317
+ this.id = scenarioId;
318
+ this.#scenario = scenario;
319
+ }
320
+ /**
321
+ * Replaces the currently loaded persisted scenario document.
322
+ * @internal
323
+ */
324
+ setSerializedScenario(scenario) {
325
+ this.#scenario = scenario;
326
+ }
327
+ /**
328
+ * Resolves the next matching rule for a target call, if this scenario has any.
329
+ * @internal
330
+ */
331
+ resolveRuleMatch(target) {
332
+ if (!this.#scenario || !hasScenarioRuleForTarget(this.#scenario, target)) return void 0;
333
+ return resolveRuntimeRuleMatch({
334
+ scenario: this.#scenario,
335
+ target,
336
+ targetCounters: this.#targetCounters
337
+ });
338
+ }
339
+ };
340
+ function getTargetKey(target) {
341
+ if (target.kind === "function") return `function:${target.function}`;
342
+ return `service:${target.service}:${target.method}`;
343
+ }
344
+ function isSameTarget(left, right) {
345
+ if (left.kind !== right.kind) return false;
346
+ if (left.kind === "function" && right.kind === "function") return left.function === right.function;
347
+ if (left.kind === "service-method" && right.kind === "service-method") return left.service === right.service && left.method === right.method;
348
+ return false;
349
+ }
350
+ function incrementTargetCounter(targetCounters, target) {
351
+ const targetKey = getTargetKey(target);
352
+ const nextCallNumber = (targetCounters.get(targetKey) ?? 0) + 1;
353
+ targetCounters.set(targetKey, nextCallNumber);
354
+ return nextCallNumber;
355
+ }
356
+ /**
357
+ * Checks whether a scenario contains any rule for a target.
358
+ * @internal
359
+ */
360
+ function hasScenarioRuleForTarget(scenario, target) {
361
+ if (!scenario) return false;
362
+ return scenario.rules.some((rule) => isSameTarget(rule.target, target));
363
+ }
364
+ /**
365
+ * Resolves the next matching rule for a target call.
366
+ * @internal
367
+ */
368
+ function resolveRuntimeRuleMatch(options) {
369
+ const callNumber = incrementTargetCounter(options.targetCounters, options.target);
370
+ const rule = options.scenario.rules.find((candidate) => {
371
+ return candidate.callNumber === callNumber && isSameTarget(candidate.target, options.target);
372
+ });
373
+ return {
374
+ callNumber,
375
+ rule
376
+ };
377
+ }
378
+
379
+ //#endregion
380
+ //#region src/services/impls/RuntimeScenarioLoaderService.ts
381
+ /**
382
+ * Default runtime scenario loader implementation.
383
+ * @internal
384
+ */
385
+ var RuntimeScenarioLoaderService = class {
386
+ adapter;
387
+ cacheService;
388
+ resolveScenarioId;
389
+ constructor(options) {
390
+ this.adapter = options.adapter;
391
+ this.cacheService = options.cacheService;
392
+ this.resolveScenarioId = options.resolveScenarioId;
393
+ }
394
+ resolve() {
395
+ const scenarioId = this.resolveScenarioId();
396
+ if (!scenarioId) return void 0;
397
+ const cachedScenario = this.cacheService.get(scenarioId);
398
+ if (cachedScenario) return cachedScenario;
399
+ const runtimeScenario = new RuntimeScenario(scenarioId, this.adapter.loadScenario(scenarioId));
400
+ this.cacheService.set(scenarioId, runtimeScenario);
401
+ return runtimeScenario;
402
+ }
403
+ };
404
+
405
+ //#endregion
406
+ //#region src/services/impls/ScenarioBuilderService.ts
407
+ function isSameSerializedTarget(left, right) {
408
+ if (left.kind !== right.kind) return false;
409
+ if (left.kind === "function" && right.kind === "function") return left.function === right.function;
410
+ if (left.kind === "service-method" && right.kind === "service-method") return left.service === right.service && left.method === right.method;
411
+ return false;
412
+ }
413
+ function formatSerializedTarget(target) {
414
+ if (target.kind === "function") return `function ${target.function}`;
415
+ return `service method ${target.service}.${target.method}`;
416
+ }
417
+ function createActionBuilder(context, target, callNumber) {
418
+ function pushRuleAndReturnBuilder(action) {
419
+ const duplicateRule = context.rules.find((rule) => {
420
+ return rule.callNumber === callNumber && isSameSerializedTarget(rule.target, target);
421
+ });
422
+ if (duplicateRule) throw new Error(`Duplicate scenario rule for ${formatSerializedTarget(target)} on call ${callNumber}.`);
423
+ context.rules.push({
424
+ action,
425
+ callNumber,
426
+ target
427
+ });
428
+ if (!context.scenario) throw new Error("scenario builder reference is not initialized");
429
+ return context.scenario;
430
+ }
431
+ return {
432
+ returns(value) {
433
+ return pushRuleAndReturnBuilder({
434
+ kind: "return",
435
+ value
436
+ });
437
+ },
438
+ throwsMessage(message) {
439
+ return pushRuleAndReturnBuilder({
440
+ kind: "throw-message",
441
+ message
442
+ });
443
+ },
444
+ throwsFactory(factory, ...args) {
445
+ const [input] = args;
446
+ return pushRuleAndReturnBuilder({
447
+ kind: "throw-factory",
448
+ factory,
449
+ input
450
+ });
451
+ }
452
+ };
453
+ }
454
+ function createScenarioServiceMethodRuleBuilder(context, service, method) {
455
+ return { onCall(callNumber) {
456
+ return createActionBuilder(context, {
457
+ kind: "service-method",
458
+ method,
459
+ service
460
+ }, callNumber);
461
+ } };
462
+ }
463
+ function createScenarioServiceRuleBuilder(context, service) {
464
+ return { method(method) {
465
+ return createScenarioServiceMethodRuleBuilder(context, service, method);
466
+ } };
467
+ }
468
+ function createScenarioFunctionRuleBuilder(context, fn) {
469
+ return { onCall(callNumber) {
470
+ return createActionBuilder(context, {
471
+ kind: "function",
472
+ function: fn
473
+ }, callNumber);
474
+ } };
475
+ }
476
+ /**
477
+ * Default scenario builder implementation.
478
+ * @internal
479
+ */
480
+ var ScenarioBuilderService = class {
481
+ id;
482
+ adapter;
483
+ createdAt;
484
+ context;
485
+ constructor(adapter) {
486
+ this.adapter = adapter;
487
+ this.id = randomUUID();
488
+ this.createdAt = new Date().toISOString();
489
+ this.context = {
490
+ rules: [],
491
+ scenario: this
492
+ };
493
+ }
494
+ service(service) {
495
+ return createScenarioServiceRuleBuilder(this.context, service);
496
+ }
497
+ fn(fn) {
498
+ return createScenarioFunctionRuleBuilder(this.context, fn);
499
+ }
500
+ async save() {
501
+ await this.adapter.saveScenario({
502
+ id: this.id,
503
+ version: 1,
504
+ createdAt: this.createdAt,
505
+ rules: this.context.rules
506
+ });
507
+ }
508
+ async dispose() {
509
+ await this.adapter.deleteScenario(this.id);
510
+ }
511
+ };
512
+
513
+ //#endregion
514
+ //#region src/runtime-wrappers.ts
515
+ /**
516
+ * Wraps a synchronous function with scenario interception behavior.
517
+ * @internal
518
+ */
519
+ function createWrappedSyncFunction(implementation, options) {
520
+ return new Proxy(implementation, { apply(target, thisArg, args) {
521
+ const runtimeScenario = options.runtimeScenarioLoaderService.resolve();
522
+ if (!runtimeScenario) return Reflect.apply(target, thisArg, args);
523
+ const match = runtimeScenario.resolveRuleMatch(options.target);
524
+ if (match?.rule?.action.kind === "return") return match.rule.action.value;
525
+ if (match?.rule?.action.kind === "throw-message") throw new Error(match.rule.action.message);
526
+ if (match?.rule?.action.kind === "throw-factory") throw options.runtimeErrorFactory.create(match.rule.action.factory, match.rule.action.input);
527
+ return Reflect.apply(target, thisArg, args);
528
+ } });
529
+ }
530
+ /**
531
+ * Wraps an async function with scenario interception behavior.
532
+ * @internal
533
+ */
534
+ function createWrappedAsyncFunction(implementation, options) {
535
+ return new Proxy(implementation, { apply(target, thisArg, args) {
536
+ const runtimeScenario = options.runtimeScenarioLoaderService.resolve();
537
+ if (!runtimeScenario) return Reflect.apply(target, thisArg, args);
538
+ const match = runtimeScenario.resolveRuleMatch(options.target);
539
+ if (match?.rule?.action.kind === "return") return Promise.resolve(match.rule.action.value);
540
+ if (match?.rule?.action.kind === "throw-message") return Promise.reject(new Error(match.rule.action.message));
541
+ if (match?.rule?.action.kind === "throw-factory") try {
542
+ return Promise.reject(options.runtimeErrorFactory.create(match.rule.action.factory, match.rule.action.input));
543
+ } catch (error) {
544
+ return Promise.reject(error);
545
+ }
546
+ return Reflect.apply(target, thisArg, args);
547
+ } });
548
+ }
549
+ /**
550
+ * Wraps each callable method on a service with scenario interception behavior.
551
+ * @internal
552
+ */
553
+ function createWrappedService(options) {
554
+ const wrappedMethods = new Map();
555
+ return new Proxy(options.implementation, { get(target, property, _receiver) {
556
+ const value = Reflect.get(target, property, target);
557
+ if (typeof property !== "string" || typeof value !== "function") return value;
558
+ const targetRule = {
559
+ kind: "service-method",
560
+ method: property,
561
+ service: options.service
562
+ };
563
+ const wrappedMethod = wrappedMethods.get(property);
564
+ if (wrappedMethod) return wrappedMethod;
565
+ const nextWrappedMethod = options.createWrapper(value.bind(options.implementation), {
566
+ runtimeScenarioLoaderService: options.runtimeScenarioLoaderService,
567
+ target: targetRule,
568
+ runtimeErrorFactory: options.runtimeErrorFactory
569
+ });
570
+ wrappedMethods.set(property, nextWrappedMethod);
571
+ return nextWrappedMethod;
572
+ } });
573
+ }
574
+
575
+ //#endregion
576
+ //#region src/index.ts
577
+ function resolveScenarioStorageAdapter(options = {}) {
578
+ const storageSelection = resolveScenarioStorageSelection({
579
+ adapter: "adapter" in options ? options.adapter : void 0,
580
+ directory: "directory" in options ? options.directory : void 0
581
+ });
582
+ return storageSelection.kind === "adapter" ? storageSelection.adapter : new JsonScenarioStorageAdapter(storageSelection.kind === "directory" ? { directory: storageSelection.directory } : {});
583
+ }
584
+ /**
585
+ * Creates a scenario builder that records interception rules and persists them as JSON.
586
+ *
587
+ * @public
588
+ *
589
+ * @example
590
+ * ```ts
591
+ * import { JsonScenarioStorageAdapter, createScenario } from "definitely-fine";
592
+ *
593
+ * const scenario = createScenario({
594
+ * adapter: new JsonScenarioStorageAdapter({}),
595
+ * });
596
+ *
597
+ * scenario.fn("fetchUser").onCall(1).returns({ id: "user-1" });
598
+ * await scenario.save();
599
+ * ```
600
+ */
601
+ function createScenario(options = {}) {
602
+ const adapter = resolveScenarioStorageAdapter(options);
603
+ return new ScenarioBuilderService(adapter);
604
+ }
605
+ /**
606
+ * Creates a runtime that loads a persisted scenario and wraps implementations.
607
+ *
608
+ * @public
609
+ *
610
+ * @example
611
+ * ```ts
612
+ * import { createRuntime, type IScenarioStorageAdapter } from "definitely-fine";
613
+ *
614
+ * const customAdapter: IScenarioStorageAdapter = {
615
+ * async deleteScenario(_scenarioId) {},
616
+ * loadScenario(_scenarioId) {
617
+ * return undefined;
618
+ * },
619
+ * async saveScenario(_scenario) {},
620
+ * };
621
+ *
622
+ * const runtime = createRuntime({
623
+ * adapter: customAdapter,
624
+ * errorFactories: {},
625
+ * });
626
+ *
627
+ * const wrappedFetchUser = runtime.wrapAsyncFunction("fetchUser", fetchUser);
628
+ * ```
629
+ */
630
+ function createRuntime(options = {}) {
631
+ if (options.enabled === false) return {
632
+ wrapSyncFunction(_name, implementation) {
633
+ return implementation;
634
+ },
635
+ wrapAsyncFunction(_name, implementation) {
636
+ return implementation;
637
+ },
638
+ wrapSyncServiceMethod(_service, _method, implementation) {
639
+ return implementation;
640
+ },
641
+ wrapAsyncServiceMethod(_service, _method, implementation) {
642
+ return implementation;
643
+ },
644
+ wrapSyncService(_service, implementation) {
645
+ return implementation;
646
+ },
647
+ wrapAsyncService(_service, implementation) {
648
+ return implementation;
649
+ }
650
+ };
651
+ const storageSelection = resolveScenarioStorageSelection({
652
+ adapter: "adapter" in options ? options.adapter : void 0,
653
+ directory: "directory" in options ? options.directory : void 0
654
+ });
655
+ const adapter = storageSelection.kind === "adapter" ? storageSelection.adapter : new JsonScenarioStorageAdapter(storageSelection.kind === "directory" ? { directory: storageSelection.directory } : {});
656
+ const runtimeErrorFactory = new RuntimeErrorFactory(options.errorFactories);
657
+ const cacheService = new RuntimeScenarioCacheService(options.cache);
658
+ const runtimeScenarioLoaderService = new RuntimeScenarioLoaderService({
659
+ adapter,
660
+ cacheService,
661
+ resolveScenarioId: getRuntimeScenarioId
662
+ });
663
+ return {
664
+ wrapSyncFunction(name, implementation) {
665
+ return createWrappedSyncFunction(implementation, {
666
+ runtimeErrorFactory,
667
+ runtimeScenarioLoaderService,
668
+ target: {
669
+ kind: "function",
670
+ function: name
671
+ }
672
+ });
673
+ },
674
+ wrapAsyncFunction(name, implementation) {
675
+ return createWrappedAsyncFunction(implementation, {
676
+ runtimeErrorFactory,
677
+ runtimeScenarioLoaderService,
678
+ target: {
679
+ kind: "function",
680
+ function: name
681
+ }
682
+ });
683
+ },
684
+ wrapSyncServiceMethod(service, method, implementation) {
685
+ return createWrappedSyncFunction(implementation, {
686
+ runtimeErrorFactory,
687
+ runtimeScenarioLoaderService,
688
+ target: {
689
+ kind: "service-method",
690
+ method,
691
+ service
692
+ }
693
+ });
694
+ },
695
+ wrapAsyncServiceMethod(service, method, implementation) {
696
+ return createWrappedAsyncFunction(implementation, {
697
+ runtimeErrorFactory,
698
+ runtimeScenarioLoaderService,
699
+ target: {
700
+ kind: "service-method",
701
+ method,
702
+ service
703
+ }
704
+ });
705
+ },
706
+ wrapSyncService(service, implementation) {
707
+ return createWrappedService({
708
+ implementation,
709
+ createWrapper: createWrappedSyncFunction,
710
+ runtimeErrorFactory,
711
+ service,
712
+ runtimeScenarioLoaderService
713
+ });
714
+ },
715
+ wrapAsyncService(service, implementation) {
716
+ return createWrappedService({
717
+ implementation,
718
+ createWrapper: createWrappedAsyncFunction,
719
+ runtimeErrorFactory,
720
+ service,
721
+ runtimeScenarioLoaderService
722
+ });
723
+ }
724
+ };
725
+ }
726
+
727
+ //#endregion
728
+ export { JsonScenarioStorageAdapter, RuntimeConfigurationError, createRuntime, createScenario, getRuntimeScenarioId, resolveJsonScenarioStorageAdapterDirectory, runWithRuntimeScenarioContext };
729
+ //# sourceMappingURL=index.js.map