@useairfoil/effect-vcr 0.4.0 → 0.4.2
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/README.md +202 -44
- package/dist/cassette-store-BunkvLoV.d.ts +59 -0
- package/dist/cassette-store-CnXX5OLB.js +60 -0
- package/dist/cassette-store-CnXX5OLB.js.map +1 -0
- package/dist/cassette-store.d.ts +3 -0
- package/dist/cassette-store.js +3 -0
- package/dist/file-system-cassette-store-LXpwwrw3.d.ts +19 -0
- package/dist/file-system-cassette-store-zWkrhhlO.js +42 -0
- package/dist/file-system-cassette-store-zWkrhhlO.js.map +1 -0
- package/dist/file-system-cassette-store.d.ts +4 -0
- package/dist/file-system-cassette-store.js +5 -0
- package/dist/index.d.ts +5 -150
- package/dist/index.js +6 -450
- package/dist/types-BsuPrIpu.js +1 -0
- package/dist/types-DWwkMzxG.d.ts +85 -0
- package/dist/types.d.ts +2 -0
- package/dist/types.js +3 -0
- package/dist/vcr-http-client-BaMxQun5.js +351 -0
- package/dist/vcr-http-client-BaMxQun5.js.map +1 -0
- package/dist/vcr-http-client-aedmnq0C.d.ts +26 -0
- package/dist/vcr-http-client.d.ts +4 -0
- package/dist/vcr-http-client.js +5 -0
- package/dist/vitest-state-BOynp4Va.js +14 -0
- package/dist/vitest-state-BOynp4Va.js.map +1 -0
- package/package.json +24 -4
- package/src/cassette-store.ts +25 -29
- package/src/file-system-cassette-store.ts +15 -4
- package/src/index.ts +1 -1
- package/src/sanitize.ts +14 -10
- package/src/types.ts +7 -1
- package/src/vcr-http-client.ts +148 -147
- package/dist/index.js.map +0 -1
package/src/sanitize.ts
CHANGED
|
@@ -125,16 +125,20 @@ export const buildRequestKey = (
|
|
|
125
125
|
ignoreHeaders: options.ignoreHeaders,
|
|
126
126
|
ignoreBodyKeys: options.ignoreBodyKeys,
|
|
127
127
|
});
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
128
|
+
return (
|
|
129
|
+
stableStringify({
|
|
130
|
+
method: sanitized.method.toUpperCase(),
|
|
131
|
+
url: sanitized.url,
|
|
132
|
+
headers: sanitized.headers ?? {},
|
|
133
|
+
body: sanitized.body ?? "",
|
|
134
|
+
}) ??
|
|
135
|
+
JSON.stringify({
|
|
136
|
+
method: sanitized.method.toUpperCase(),
|
|
137
|
+
url: sanitized.url,
|
|
138
|
+
headers: sanitized.headers ?? {},
|
|
139
|
+
body: sanitized.body ?? "",
|
|
140
|
+
})
|
|
141
|
+
);
|
|
138
142
|
});
|
|
139
143
|
|
|
140
144
|
/**
|
package/src/types.ts
CHANGED
|
@@ -54,8 +54,14 @@ export type CassetteFile = {
|
|
|
54
54
|
/**
|
|
55
55
|
* VCR configuration.
|
|
56
56
|
*/
|
|
57
|
-
export type
|
|
57
|
+
export type VcrConfig = {
|
|
58
58
|
readonly vcrName?: string;
|
|
59
|
+
/**
|
|
60
|
+
* Cassette file name or basename.
|
|
61
|
+
*
|
|
62
|
+
* `users` resolves to `users.cassette`.
|
|
63
|
+
* `users.cassette` is preserved as-is.
|
|
64
|
+
*/
|
|
59
65
|
readonly cassetteName?: string;
|
|
60
66
|
readonly mode?: VcrMode;
|
|
61
67
|
/**
|
package/src/vcr-http-client.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Config, Effect, Option, Path } from "effect";
|
|
1
|
+
import { Config, Data, Effect, Option, Path } from "effect";
|
|
2
2
|
import {
|
|
3
3
|
HttpClient,
|
|
4
4
|
HttpClientError,
|
|
@@ -6,16 +6,9 @@ import {
|
|
|
6
6
|
HttpClientResponse,
|
|
7
7
|
} from "effect/unstable/http";
|
|
8
8
|
|
|
9
|
-
import type {
|
|
10
|
-
Cassette,
|
|
11
|
-
CassetteFile,
|
|
12
|
-
Configuration,
|
|
13
|
-
VcrEntry,
|
|
14
|
-
VcrRequest,
|
|
15
|
-
VcrResponse,
|
|
16
|
-
} from "./types";
|
|
9
|
+
import type { Cassette, CassetteFile, VcrConfig, VcrEntry, VcrRequest, VcrResponse } from "./types";
|
|
17
10
|
|
|
18
|
-
import { CassetteStore, createEmptyCassette } from "./cassette-store";
|
|
11
|
+
import { CassetteStore, type CassetteStoreService, createEmptyCassette } from "./cassette-store";
|
|
19
12
|
import { buildRequestKey, redactRequest, redactResponse } from "./sanitize";
|
|
20
13
|
import { getVitestState } from "./vitest-state";
|
|
21
14
|
|
|
@@ -24,6 +17,14 @@ import { getVitestState } from "./vitest-state";
|
|
|
24
17
|
*/
|
|
25
18
|
const decoder = new TextDecoder();
|
|
26
19
|
|
|
20
|
+
export class VcrHttpClientError extends Data.TaggedError("VcrHttpClientError")<{
|
|
21
|
+
readonly message: string;
|
|
22
|
+
readonly cause?: unknown;
|
|
23
|
+
}> {}
|
|
24
|
+
|
|
25
|
+
const toCassetteFileName = (name: string): string =>
|
|
26
|
+
name.endsWith(".cassette") ? name : `${name}.cassette`;
|
|
27
|
+
|
|
27
28
|
/**
|
|
28
29
|
* Resolve the cassette file name and export key.
|
|
29
30
|
*
|
|
@@ -32,34 +33,38 @@ const decoder = new TextDecoder();
|
|
|
32
33
|
* - <file-name>.cassette file name
|
|
33
34
|
* - export key = current test name (describe > test)
|
|
34
35
|
*/
|
|
35
|
-
const resolveCassetteLocation = (config:
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
36
|
+
const resolveCassetteLocation = Effect.fnUntraced(function* (config: VcrConfig) {
|
|
37
|
+
const path = yield* Path.Path;
|
|
38
|
+
if (config.cassetteName) {
|
|
39
|
+
return {
|
|
40
|
+
name: toCassetteFileName(config.cassetteName),
|
|
41
|
+
exportKey: "default",
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
const { testPath, currentTestName } = getVitestState();
|
|
46
|
+
if (!testPath || !currentTestName) {
|
|
47
|
+
return yield* Effect.fail(
|
|
48
|
+
new VcrHttpClientError({
|
|
49
|
+
message:
|
|
50
|
+
"VCR cassette path could not be inferred. Provide cassetteName when not running in Vitest.",
|
|
51
|
+
}),
|
|
52
|
+
);
|
|
53
|
+
}
|
|
49
54
|
|
|
50
|
-
|
|
51
|
-
|
|
55
|
+
const fileName = path.basename(testPath, ".ts");
|
|
56
|
+
const cassetteName = `${fileName}.cassette`;
|
|
52
57
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
+
return {
|
|
59
|
+
name: cassetteName,
|
|
60
|
+
exportKey: currentTestName,
|
|
61
|
+
};
|
|
62
|
+
});
|
|
58
63
|
|
|
59
64
|
/**
|
|
60
65
|
* Apply defaults for common VCR behavior while preserving explicit overrides.
|
|
61
66
|
*/
|
|
62
|
-
const normalizeConfig = (config:
|
|
67
|
+
const normalizeConfig = (config: VcrConfig): VcrConfig => ({
|
|
63
68
|
vcrName: config.vcrName,
|
|
64
69
|
cassetteName: config.cassetteName,
|
|
65
70
|
mode: config.mode ?? "auto",
|
|
@@ -181,7 +186,7 @@ const toVcrResponse = (
|
|
|
181
186
|
* Load a cassette and map store errors into HttpClient errors.
|
|
182
187
|
*/
|
|
183
188
|
const readCassetteFile = (
|
|
184
|
-
store:
|
|
189
|
+
store: CassetteStoreService,
|
|
185
190
|
name: string,
|
|
186
191
|
request: HttpClientRequest.HttpClientRequest,
|
|
187
192
|
) => store.load(name).pipe(Effect.mapError((error) => toRequestError(request, error)));
|
|
@@ -190,7 +195,7 @@ const readCassetteFile = (
|
|
|
190
195
|
* Load a cassette or initialize a new one if missing.
|
|
191
196
|
*/
|
|
192
197
|
const loadOrInitCassetteFile = (
|
|
193
|
-
store:
|
|
198
|
+
store: CassetteStoreService,
|
|
194
199
|
name: string,
|
|
195
200
|
request: HttpClientRequest.HttpClientRequest,
|
|
196
201
|
) => store.loadOrInit(name).pipe(Effect.mapError((error) => toRequestError(request, error)));
|
|
@@ -199,7 +204,7 @@ const loadOrInitCassetteFile = (
|
|
|
199
204
|
* Persist the updated cassette.
|
|
200
205
|
*/
|
|
201
206
|
const saveCassetteFile = (
|
|
202
|
-
store:
|
|
207
|
+
store: CassetteStoreService,
|
|
203
208
|
name: string,
|
|
204
209
|
cassette: CassetteFile,
|
|
205
210
|
request: HttpClientRequest.HttpClientRequest,
|
|
@@ -209,7 +214,7 @@ const saveCassetteFile = (
|
|
|
209
214
|
* Load a named cassette export from the file, or return a fresh empty cassette.
|
|
210
215
|
*/
|
|
211
216
|
const readCassetteExport = (
|
|
212
|
-
store:
|
|
217
|
+
store: CassetteStoreService,
|
|
213
218
|
name: string,
|
|
214
219
|
exportKey: string,
|
|
215
220
|
request: HttpClientRequest.HttpClientRequest,
|
|
@@ -228,7 +233,7 @@ const readCassetteExport = (
|
|
|
228
233
|
const findEntry = (
|
|
229
234
|
request: VcrRequest,
|
|
230
235
|
cassette: Cassette,
|
|
231
|
-
config:
|
|
236
|
+
config: VcrConfig,
|
|
232
237
|
): Effect.Effect<VcrEntry | undefined> => {
|
|
233
238
|
if (config.match) {
|
|
234
239
|
return Effect.succeed(
|
|
@@ -241,14 +246,25 @@ const findEntry = (
|
|
|
241
246
|
}).pipe(Effect.map((key) => cassette.entries[key]));
|
|
242
247
|
};
|
|
243
248
|
|
|
249
|
+
const replayResponse = (
|
|
250
|
+
request: HttpClientRequest.HttpClientRequest,
|
|
251
|
+
entry: VcrEntry,
|
|
252
|
+
): HttpClientResponse.HttpClientResponse => {
|
|
253
|
+
const web = new Response(entry.response.body, {
|
|
254
|
+
status: entry.response.status,
|
|
255
|
+
headers: entry.response.headers,
|
|
256
|
+
});
|
|
257
|
+
return HttpClientResponse.fromWeb(request, web);
|
|
258
|
+
};
|
|
259
|
+
|
|
244
260
|
/**
|
|
245
261
|
* Replay a response from cassette data using HttpClientResponse.fromWeb.
|
|
246
262
|
*/
|
|
247
263
|
const replay = (
|
|
248
|
-
store:
|
|
264
|
+
store: CassetteStoreService,
|
|
249
265
|
request: HttpClientRequest.HttpClientRequest,
|
|
250
266
|
vcrRequest: VcrRequest,
|
|
251
|
-
config:
|
|
267
|
+
config: VcrConfig,
|
|
252
268
|
name: string,
|
|
253
269
|
exportKey: string,
|
|
254
270
|
) =>
|
|
@@ -267,11 +283,7 @@ const replay = (
|
|
|
267
283
|
);
|
|
268
284
|
}
|
|
269
285
|
|
|
270
|
-
|
|
271
|
-
status: entry.response.status,
|
|
272
|
-
headers: entry.response.headers,
|
|
273
|
-
});
|
|
274
|
-
return Effect.succeed(HttpClientResponse.fromWeb(request, web));
|
|
286
|
+
return Effect.succeed(replayResponse(request, entry));
|
|
275
287
|
}),
|
|
276
288
|
),
|
|
277
289
|
),
|
|
@@ -280,134 +292,123 @@ const replay = (
|
|
|
280
292
|
/**
|
|
281
293
|
* Record a live response into the cassette and return the original response.
|
|
282
294
|
*/
|
|
283
|
-
const record = (
|
|
284
|
-
store:
|
|
295
|
+
const record = Effect.fnUntraced(function* (
|
|
296
|
+
store: CassetteStoreService,
|
|
285
297
|
request: HttpClientRequest.HttpClientRequest,
|
|
286
298
|
vcrRequest: VcrRequest,
|
|
287
299
|
effect: Effect.Effect<HttpClientResponse.HttpClientResponse, HttpClientError.HttpClientError>,
|
|
288
|
-
config:
|
|
300
|
+
config: VcrConfig,
|
|
289
301
|
name: string,
|
|
290
302
|
exportKey: string,
|
|
291
|
-
)
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
const key = yield* buildRequestKey(vcrRequest, {
|
|
315
|
-
ignoreHeaders: config.matchIgnore?.requestHeaders,
|
|
316
|
-
ignoreBodyKeys: config.matchIgnore?.requestBodyKeys,
|
|
317
|
-
});
|
|
318
|
-
const next: Cassette = {
|
|
319
|
-
...cassette,
|
|
320
|
-
entries: {
|
|
321
|
-
...cassette.entries,
|
|
322
|
-
[key]: {
|
|
323
|
-
request: sanitizedRequest,
|
|
324
|
-
response: sanitizedResponse,
|
|
325
|
-
},
|
|
326
|
-
},
|
|
327
|
-
};
|
|
328
|
-
const nextFile: CassetteFile = {
|
|
329
|
-
...file,
|
|
330
|
-
exports: {
|
|
331
|
-
...file.exports,
|
|
332
|
-
[exportKey]: next,
|
|
333
|
-
},
|
|
334
|
-
};
|
|
335
|
-
// Persist updated cassette to disk.
|
|
336
|
-
yield* saveCassetteFile(store, name, nextFile, request);
|
|
337
|
-
return response;
|
|
303
|
+
): Effect.fn.Return<HttpClientResponse.HttpClientResponse, HttpClientError.HttpClientError> {
|
|
304
|
+
const response = yield* effect;
|
|
305
|
+
const body = yield* response.text;
|
|
306
|
+
const vcrResponse = toVcrResponse(response, body);
|
|
307
|
+
|
|
308
|
+
const sanitizedRequest = config.redact
|
|
309
|
+
? redactRequest(vcrRequest, {
|
|
310
|
+
redactHeaders: config.redact.requestHeaders,
|
|
311
|
+
redactBodyKeys: config.redact.requestBodyKeys,
|
|
312
|
+
})
|
|
313
|
+
: vcrRequest;
|
|
314
|
+
const sanitizedResponse = config.redact
|
|
315
|
+
? redactResponse(vcrResponse, {
|
|
316
|
+
redactHeaders: config.redact.responseHeaders,
|
|
317
|
+
redactBodyKeys: config.redact.responseBodyKeys,
|
|
318
|
+
})
|
|
319
|
+
: vcrResponse;
|
|
320
|
+
|
|
321
|
+
const file = yield* loadOrInitCassetteFile(store, name, request);
|
|
322
|
+
const cassette = file.exports[exportKey] ?? (yield* createEmptyCassette());
|
|
323
|
+
const key = yield* buildRequestKey(vcrRequest, {
|
|
324
|
+
ignoreHeaders: config.matchIgnore?.requestHeaders,
|
|
325
|
+
ignoreBodyKeys: config.matchIgnore?.requestBodyKeys,
|
|
338
326
|
});
|
|
327
|
+
const next: Cassette = {
|
|
328
|
+
...cassette,
|
|
329
|
+
entries: {
|
|
330
|
+
...cassette.entries,
|
|
331
|
+
[key]: {
|
|
332
|
+
request: sanitizedRequest,
|
|
333
|
+
response: sanitizedResponse,
|
|
334
|
+
},
|
|
335
|
+
},
|
|
336
|
+
};
|
|
337
|
+
const nextFile: CassetteFile = {
|
|
338
|
+
...file,
|
|
339
|
+
exports: {
|
|
340
|
+
...file.exports,
|
|
341
|
+
[exportKey]: next,
|
|
342
|
+
},
|
|
343
|
+
};
|
|
344
|
+
yield* saveCassetteFile(store, name, nextFile, request);
|
|
345
|
+
return response;
|
|
346
|
+
});
|
|
339
347
|
|
|
340
348
|
/**
|
|
341
349
|
* Build a VCR-aware HttpClient that replays or records per config.
|
|
342
350
|
*/
|
|
343
|
-
const makeVcrHttpClient = (config:
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
const normalized = normalizeConfig(config);
|
|
351
|
+
const makeVcrHttpClient = Effect.fnUntraced(function* (config: VcrConfig = {}) {
|
|
352
|
+
const live = yield* HttpClient.HttpClient;
|
|
353
|
+
const normalized = normalizeConfig(config);
|
|
347
354
|
|
|
348
|
-
|
|
355
|
+
const isCi = yield* Config.boolean("CI").pipe(Config.withDefault(false));
|
|
349
356
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
357
|
+
const disabledVcrs = yield* AckDisableVcrConfig;
|
|
358
|
+
if (shouldDisableVcr(normalized.vcrName, disabledVcrs)) {
|
|
359
|
+
return live;
|
|
360
|
+
}
|
|
354
361
|
|
|
355
|
-
|
|
356
|
-
const store = yield* CassetteStore;
|
|
362
|
+
const store = yield* CassetteStore;
|
|
357
363
|
|
|
358
|
-
|
|
364
|
+
const { name, exportKey } = yield* resolveCassetteLocation(normalized);
|
|
359
365
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
366
|
+
return live.pipe(
|
|
367
|
+
HttpClient.transform(
|
|
368
|
+
Effect.fnUntraced(function* (effect, request) {
|
|
369
|
+
const vcrRequest = toVcrRequest(request);
|
|
370
|
+
if (normalized.mode === "replay") {
|
|
371
|
+
return yield* replay(store, request, vcrRequest, normalized, name, exportKey);
|
|
372
|
+
}
|
|
367
373
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
374
|
+
if (normalized.mode === "record") {
|
|
375
|
+
return yield* record(store, request, vcrRequest, effect, normalized, name, exportKey);
|
|
376
|
+
}
|
|
371
377
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
.pipe(Effect.mapError((error) => toRequestError(request, error)));
|
|
376
|
-
|
|
377
|
-
if (!available) {
|
|
378
|
-
if (isCi) {
|
|
379
|
-
return yield* Effect.fail(
|
|
380
|
-
new HttpClientError.HttpClientError({
|
|
381
|
-
reason: new HttpClientError.TransportError({
|
|
382
|
-
request,
|
|
383
|
-
description: "VCR cassette missing in CI for auto mode",
|
|
384
|
-
}),
|
|
385
|
-
}),
|
|
386
|
-
);
|
|
387
|
-
}
|
|
378
|
+
const available = yield* store
|
|
379
|
+
.exists(name)
|
|
380
|
+
.pipe(Effect.mapError((error) => toRequestError(request, error)));
|
|
388
381
|
|
|
389
|
-
|
|
382
|
+
if (!available) {
|
|
383
|
+
if (isCi) {
|
|
384
|
+
return yield* Effect.fail(
|
|
385
|
+
new HttpClientError.HttpClientError({
|
|
386
|
+
reason: new HttpClientError.TransportError({
|
|
387
|
+
request,
|
|
388
|
+
description: "VCR cassette missing in CI for auto mode",
|
|
389
|
+
}),
|
|
390
|
+
}),
|
|
391
|
+
);
|
|
390
392
|
}
|
|
391
393
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
+
return yield* record(store, request, vcrRequest, effect, normalized, name, exportKey);
|
|
395
|
+
}
|
|
394
396
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
status: entry.response.status,
|
|
398
|
-
headers: entry.response.headers,
|
|
399
|
-
});
|
|
400
|
-
return HttpClientResponse.fromWeb(request, web);
|
|
401
|
-
}
|
|
397
|
+
const cassette = yield* readCassetteExport(store, name, exportKey, request);
|
|
398
|
+
const entry = yield* findEntry(vcrRequest, cassette, normalized);
|
|
402
399
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
400
|
+
if (entry) {
|
|
401
|
+
return replayResponse(request, entry);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return yield* record(store, request, vcrRequest, effect, normalized, name, exportKey);
|
|
405
|
+
}),
|
|
406
|
+
),
|
|
407
|
+
);
|
|
408
|
+
});
|
|
408
409
|
|
|
409
410
|
/**
|
|
410
411
|
* Layer that provides a VCR-wrapped HttpClient.
|
|
411
412
|
*/
|
|
412
|
-
export const layer = (config:
|
|
413
|
+
export const layer = (config: VcrConfig = {}) =>
|
|
413
414
|
HttpClient.layerMergedContext(makeVcrHttpClient(config));
|
package/dist/index.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["CassetteStore: Context.Service<CassetteStore, CassetteStore>","layer","CassetteStore.CassetteStore","rootDir: string","CassetteStore.toStoreError","CassetteStore.parseCassette","CassetteStore.createEmptyCassetteFile","CassetteStore.make","record: Record<string, string>","record","filtered: Record<string, string>","next: Record<string, unknown>","record: Record<string, string>","record","next: Cassette"],"sources":["../src/cassette-store.ts","../src/vitest-state.ts","../src/file-system-cassette-store.ts","../src/sanitize.ts","../src/vcr-http-client.ts"],"sourcesContent":["import { Context, DateTime, Effect, Schema } from \"effect\";\n\nimport type { Cassette, CassetteFile } from \"./types\";\n\n/**\n * Storage contract for VCR cassettes.\n *\n */\nexport interface CassetteStore {\n /** Returns true if a cassette with the given name exists. */\n readonly exists: (name: string) => Effect.Effect<boolean, CassetteStoreError>;\n\n /** Loads a cassette by name, returning a `CassetteFile` if found. */\n readonly load: (name: string) => Effect.Effect<CassetteFile, CassetteStoreError>;\n\n /** Saves a cassette by name, overwriting any existing cassette with the same name. */\n readonly save: (name: string, content: CassetteFile) => Effect.Effect<void, CassetteStoreError>;\n\n /** Loads a cassette by name, initializing a new empty cassette if not found. */\n readonly loadOrInit: (name: string) => Effect.Effect<CassetteFile, CassetteStoreError>;\n}\n\n/**\n * Effect service tag for the cassette store.\n */\nexport const CassetteStore: Context.Service<CassetteStore, CassetteStore> = Context.Service(\n \"@useairfoil/effect-vcr/CassetteStore\",\n);\n\n/**\n * Creates a new empty cassette with a timestamp and format version.\n */\nexport const createEmptyCassette = (): Effect.Effect<Cassette> =>\n Effect.gen(function* () {\n const now = yield* DateTime.now;\n return {\n meta: {\n createdAt: DateTime.formatIso(now),\n version: \"1\",\n },\n entries: {},\n };\n });\n\nexport const createEmptyCassetteFile = (): Effect.Effect<CassetteFile> =>\n Effect.map(createEmptyCassette(), (cassette) => ({\n exports: { default: cassette },\n }));\n\n/**\n * Structured error for cassette persistence operations.\n *\n * `operation` indicates which store function failed.\n * `path` is the file or directory path that was targeted.\n */\nexport class CassetteStoreError extends Schema.TaggedErrorClass<CassetteStoreError>()(\n \"CassetteStoreError\",\n {\n operation: Schema.String,\n path: Schema.String,\n message: Schema.optional(Schema.String),\n cause: Schema.optional(Schema.Unknown),\n },\n) {}\n\nexport const toStoreError = (\n operation: \"exists\" | \"load\" | \"save\" | \"loadOrInit\",\n path: string,\n cause?: unknown,\n message?: string,\n) =>\n new CassetteStoreError({\n operation,\n path,\n cause,\n message,\n });\n\n/**\n * Parses cassette JSON and maps invalid JSON to a store error.\n */\nexport const parseCassette = (content: string, path: string) =>\n Effect.try({\n try: () => JSON.parse(content) as CassetteFile,\n catch: (error) => toStoreError(\"load\", path, error, \"Invalid JSON\"),\n }).pipe(\n Effect.flatMap((parsed) => {\n if (!parsed || typeof parsed !== \"object\" || !(\"exports\" in parsed)) {\n return Effect.fail(toStoreError(\"load\", path, undefined, \"Invalid cassette file format\"));\n }\n return Effect.succeed(parsed);\n }),\n );\n\n/** Creates a `CassetteStore` implementation that uses the given `impl` object. */\nexport const make = (impl: CassetteStore): CassetteStore =>\n CassetteStore.of({\n ...impl,\n });\n","export type VitestState = {\n readonly testPath?: string;\n readonly currentTestName?: string;\n};\n\nexport const getVitestState = (): VitestState => {\n const expectGlobal =\n // biome-ignore lint/suspicious/noExplicitAny: --\n (globalThis as { expect?: { getState?: () => any } }).expect;\n const symbolExpect =\n // biome-ignore lint/suspicious/noExplicitAny: --\n (globalThis as { [key: symbol]: { getState?: () => any } })[Symbol.for(\"expect-global\")];\n const state = expectGlobal?.getState?.() ?? symbolExpect?.getState?.();\n return {\n testPath: state?.testPath as string | undefined,\n currentTestName: state?.currentTestName as string | undefined,\n };\n};\n","import { Effect, Layer, FileSystem, Path } from \"effect\";\n\nimport type { CassetteFile } from \"./types\";\n\nimport * as CassetteStore from \"./cassette-store\";\nimport { getVitestState } from \"./vitest-state\";\n\n/**\n * FileSystem-backed cassette store.\n *\n * Requires a platform FileSystem layer (Node/Bun) to be provided by the user.\n */\nexport const layer = ({ cassetteDir }: { cassetteDir?: string } = {}) =>\n Layer.effect(CassetteStore.CassetteStore)(\n Effect.gen(function* () {\n const fs = yield* FileSystem.FileSystem;\n const path = yield* Path.Path;\n\n let rootDir: string;\n if (cassetteDir) {\n rootDir = cassetteDir;\n } else {\n const { testPath } = getVitestState();\n if (!testPath) {\n return yield* Effect.fail(new Error(\"vcr must be used within a vitest test\"));\n }\n const testFolder = path.dirname(testPath);\n rootDir = path.join(testFolder, \"__cassettes__\");\n }\n\n const fullPath = (name: string) => path.join(rootDir, name);\n\n const load = (name: string) =>\n fs.readFileString(fullPath(name)).pipe(\n Effect.mapError((error) => CassetteStore.toStoreError(\"load\", name, error)),\n Effect.flatMap((content) => CassetteStore.parseCassette(content, name)),\n );\n\n const save = (name: string, cassette: CassetteFile) => {\n const filePath = fullPath(name);\n const dir = path.dirname(filePath);\n const ensureDir =\n dir === \"\"\n ? Effect.void\n : fs\n .makeDirectory(dir, { recursive: true })\n .pipe(Effect.mapError((error) => CassetteStore.toStoreError(\"save\", dir, error)));\n return ensureDir.pipe(\n Effect.andThen(\n fs\n .writeFileString(filePath, JSON.stringify(cassette, null, 2))\n .pipe(Effect.mapError((error) => CassetteStore.toStoreError(\"save\", name, error))),\n ),\n );\n };\n\n const loadOrInit = (name: string) =>\n fs.exists(fullPath(name)).pipe(\n Effect.mapError((error) => CassetteStore.toStoreError(\"loadOrInit\", name, error)),\n Effect.flatMap((exists) =>\n exists\n ? load(name)\n : CassetteStore.createEmptyCassetteFile().pipe(\n Effect.tap((cassette) => save(name, cassette)),\n ),\n ),\n );\n\n return CassetteStore.make({\n exists: (name: string) =>\n fs\n .exists(fullPath(name))\n .pipe(Effect.mapError((error) => CassetteStore.toStoreError(\"exists\", name, error))),\n load,\n save,\n loadOrInit,\n });\n }),\n );\n","import { Effect, Option } from \"effect\";\nimport stableStringify from \"json-stable-stringify\";\n\nimport type { VcrRequest, VcrResponse } from \"./types\";\n\n/**\n * Normalize header names to match HttpClient's lowercase behavior.\n */\nconst normalizeHeaderKey = (key: string) => key.toLowerCase();\n\n/**\n * Canonicalize headers: lowercase keys and stable ordering.\n */\nconst toHeaderRecord = (headers?: Record<string, string>) => {\n if (!headers) return undefined;\n const entries = Object.entries(headers).map(\n ([key, value]) => [normalizeHeaderKey(key), value] as const,\n );\n entries.sort(([a], [b]) => a.localeCompare(b));\n const record: Record<string, string> = {};\n for (const [key, value] of entries) {\n record[key] = value;\n }\n return record;\n};\n\n/**\n * Remove specified header names (case-insensitive).\n */\nconst omitHeaderKeys = (\n headers: Record<string, string> | undefined,\n ignore: ReadonlyArray<string> | undefined,\n): Option.Option<Record<string, string>> => {\n if (!headers) return Option.none();\n if (!ignore || ignore.length === 0) return Option.some(headers);\n const ignoreSet = new Set(ignore.map(normalizeHeaderKey));\n const filtered: Record<string, string> = {};\n for (const [key, value] of Object.entries(headers)) {\n if (!ignoreSet.has(normalizeHeaderKey(key))) {\n filtered[key] = value;\n }\n }\n return Option.some(filtered);\n};\n\n/**\n * Best-effort JSON parse; returns undefined for non-JSON bodies.\n */\nconst tryParseJson = Option.liftThrowable((input: string) => JSON.parse(input));\n\n/**\n * Recursively drop keys from JSON objects to build stable request keys.\n */\nconst stripKeys = (value: unknown, ignore: Set<string>): unknown => {\n if (Array.isArray(value)) {\n return value.map((item) => stripKeys(item, ignore));\n }\n if (value && typeof value === \"object\") {\n const record = value as Record<string, unknown>;\n const next: Record<string, unknown> = {};\n for (const [key, entry] of Object.entries(record)) {\n if (!ignore.has(key)) {\n next[key] = stripKeys(entry, ignore);\n }\n }\n return next;\n }\n return value;\n};\n\n/**\n * Remove JSON keys from a string body when possible.\n */\nconst omitBodyKeys = (\n body: string | undefined,\n ignore: ReadonlyArray<string> | undefined,\n): Option.Option<string> => {\n if (!body) return Option.none();\n return Option.some(\n tryParseJson(body).pipe(\n Option.match({\n onNone: () => body,\n onSome: (parsed) => {\n if (!ignore || ignore.length === 0) {\n return stableStringify(parsed) ?? body;\n }\n const ignoreSet = new Set(ignore);\n return stableStringify(stripKeys(parsed, ignoreSet)) ?? body;\n },\n }),\n ),\n );\n};\n\n/**\n * Normalize request for matching: header canonicalization + JSON body filtering.\n */\nexport const sanitizeRequest = (\n request: VcrRequest,\n options: {\n readonly ignoreHeaders?: ReadonlyArray<string>;\n readonly ignoreBodyKeys?: ReadonlyArray<string>;\n },\n): VcrRequest => {\n const filteredHeaders = omitHeaderKeys(request.headers, options.ignoreHeaders);\n return {\n ...request,\n headers: toHeaderRecord(Option.getOrUndefined(filteredHeaders)),\n body: Option.getOrUndefined(omitBodyKeys(request.body, options.ignoreBodyKeys)),\n };\n};\n\n/**\n * Compute the stable request key used for cassette lookups.\n */\nexport const buildRequestKey = (\n request: VcrRequest,\n options: {\n readonly ignoreHeaders?: ReadonlyArray<string>;\n readonly ignoreBodyKeys?: ReadonlyArray<string>;\n },\n): Effect.Effect<string> =>\n Effect.sync(() => {\n const sanitized = sanitizeRequest(request, {\n ignoreHeaders: options.ignoreHeaders,\n ignoreBodyKeys: options.ignoreBodyKeys,\n });\n const key = stableStringify({\n method: sanitized.method.toUpperCase(),\n url: sanitized.url,\n headers: sanitized.headers ?? {},\n body: sanitized.body ?? \"\",\n });\n if (!key) {\n throw new Error(\"Failed to build VCR request key\");\n }\n return key;\n });\n\n/**\n * Remove sensitive request data before persisting to a cassette.\n */\nexport const redactRequest = (\n request: VcrRequest,\n options: {\n readonly redactHeaders?: ReadonlyArray<string>;\n readonly redactBodyKeys?: ReadonlyArray<string>;\n },\n): VcrRequest => ({\n ...request,\n headers: Option.getOrUndefined(omitHeaderKeys(request.headers, options.redactHeaders)),\n body: Option.getOrUndefined(omitBodyKeys(request.body, options.redactBodyKeys)),\n});\n\n/**\n * Remove sensitive response data before persisting to a cassette.\n */\nexport const redactResponse = (\n response: VcrResponse,\n options: {\n readonly redactHeaders?: ReadonlyArray<string>;\n readonly redactBodyKeys?: ReadonlyArray<string>;\n },\n): VcrResponse => ({\n ...response,\n headers: Option.getOrUndefined(omitHeaderKeys(response.headers, options.redactHeaders)),\n body: Option.getOrElse(omitBodyKeys(response.body, options.redactBodyKeys), () => response.body),\n});\n","import { Config, Effect, Option, Path } from \"effect\";\nimport {\n HttpClient,\n HttpClientError,\n type HttpClientRequest,\n HttpClientResponse,\n} from \"effect/unstable/http\";\n\nimport type {\n Cassette,\n CassetteFile,\n Configuration,\n VcrEntry,\n VcrRequest,\n VcrResponse,\n} from \"./types\";\n\nimport { CassetteStore, createEmptyCassette } from \"./cassette-store\";\nimport { buildRequestKey, redactRequest, redactResponse } from \"./sanitize\";\nimport { getVitestState } from \"./vitest-state\";\n\n/**\n * Decoder for Uint8Array request bodies when building cassette keys.\n */\nconst decoder = new TextDecoder();\n\n/**\n * Resolve the cassette file name and export key.\n *\n * If explicit cassetteName are provided, use them and default to\n * the \"default\" export. Otherwise infer from Vitest state:\n * - <file-name>.cassette file name\n * - export key = current test name (describe > test)\n */\nconst resolveCassetteLocation = (config: Configuration) =>\n Effect.gen(function* () {\n const path = yield* Path.Path;\n if (config.cassetteName) {\n return {\n name: `${config.cassetteName}.cassette`,\n exportKey: \"default\",\n };\n }\n\n const { testPath, currentTestName } = getVitestState();\n if (!testPath || !currentTestName) {\n throw new Error(\"VCR cassette path could not be inferred. Provide cassetteName.\");\n }\n\n const fileName = path.basename(testPath, \".ts\");\n const cassetteName = `${fileName}.cassette`;\n\n return {\n name: cassetteName,\n exportKey: currentTestName,\n };\n });\n\n/**\n * Apply defaults for common VCR behavior while preserving explicit overrides.\n */\nconst normalizeConfig = (config: Configuration) => ({\n vcrName: config.vcrName,\n cassetteName: config.cassetteName,\n mode: config.mode ?? \"auto\",\n matchIgnore: {\n requestHeaders: [\"authorization\"],\n ...config.matchIgnore,\n },\n redact: {\n requestHeaders: [\"authorization\"],\n ...config.redact,\n },\n match: config.match,\n});\n\nconst AckDisableVcrConfig = Config.option(Config.string(\"ACK_DISABLE_VCR\")).pipe(\n Config.map((value) =>\n Option.match(value, {\n onNone: () => new Set<string>(),\n onSome: (raw) =>\n raw === \"*\"\n ? \"*\"\n : new Set(\n raw\n .split(\",\")\n .map((segment) => segment.trim().toLowerCase())\n .filter((segment) => segment.length > 0),\n ),\n }),\n ),\n);\n\nconst shouldDisableVcr = (name: string | undefined, disabled: \"*\" | Set<string>): boolean => {\n if (disabled === \"*\") return true;\n if (!name) return false;\n const normalized = name.trim().toLowerCase();\n return disabled.has(normalized);\n};\n\n/**\n * Convert store or replay failures into HttpClient transport errors.\n */\nconst toRequestError = (\n request: HttpClientRequest.HttpClientRequest,\n cause: unknown,\n description?: string,\n) =>\n new HttpClientError.HttpClientError({\n reason: new HttpClientError.TransportError({\n request,\n cause,\n description,\n }),\n });\n\n/**\n * Convert header map to a plain record for JSON persistence.\n */\nconst recordFromHeaders = (headers: Record<string, string>) => {\n const record: Record<string, string> = {};\n for (const [key, value] of Object.entries(headers)) {\n record[key] = value;\n }\n return record;\n};\n\n/**\n * Serialize an Effect HttpClientRequest into a cassette request shape.\n *\n * Note: some body variants (Stream, FormData) are not safely serializable\n * without consuming the body, so we store placeholders to avoid breaking\n * live requests.\n */\nconst toVcrRequest = (request: HttpClientRequest.HttpClientRequest): VcrRequest => {\n const headers = recordFromHeaders(request.headers as Record<string, string>);\n const body = (() => {\n switch (request.body._tag) {\n case \"Empty\":\n return \"\";\n case \"Uint8Array\":\n return decoder.decode(request.body.body);\n case \"Raw\": {\n const raw = request.body.body as unknown;\n if (typeof raw === \"string\") return raw;\n if (raw instanceof Uint8Array) return decoder.decode(raw);\n try {\n return JSON.stringify(raw);\n } catch {\n return \"[raw]\";\n }\n }\n case \"FormData\":\n return \"[form-data]\";\n case \"Stream\":\n return \"[stream]\";\n }\n })();\n\n return {\n method: request.method,\n url: request.url,\n headers,\n body,\n };\n};\n\n/**\n * Serialize an Effect HttpClientResponse into a cassette response shape.\n */\nconst toVcrResponse = (\n response: HttpClientResponse.HttpClientResponse,\n body: string,\n): VcrResponse => ({\n status: response.status,\n body,\n headers: recordFromHeaders(response.headers as Record<string, string>),\n});\n\n/**\n * Load a cassette and map store errors into HttpClient errors.\n */\nconst readCassetteFile = (\n store: CassetteStore,\n name: string,\n request: HttpClientRequest.HttpClientRequest,\n) => store.load(name).pipe(Effect.mapError((error) => toRequestError(request, error)));\n\n/**\n * Load a cassette or initialize a new one if missing.\n */\nconst loadOrInitCassetteFile = (\n store: CassetteStore,\n name: string,\n request: HttpClientRequest.HttpClientRequest,\n) => store.loadOrInit(name).pipe(Effect.mapError((error) => toRequestError(request, error)));\n\n/**\n * Persist the updated cassette.\n */\nconst saveCassetteFile = (\n store: CassetteStore,\n name: string,\n cassette: CassetteFile,\n request: HttpClientRequest.HttpClientRequest,\n) => store.save(name, cassette).pipe(Effect.mapError((error) => toRequestError(request, error)));\n\n/**\n * Load a named cassette export from the file, or return a fresh empty cassette.\n */\nconst readCassetteExport = (\n store: CassetteStore,\n name: string,\n exportKey: string,\n request: HttpClientRequest.HttpClientRequest,\n) =>\n readCassetteFile(store, name, request).pipe(\n Effect.flatMap((file) => {\n const found = file.exports[exportKey];\n if (found) return Effect.succeed(found);\n return createEmptyCassette();\n }),\n );\n\n/**\n * Locate a cassette entry by custom matcher or stable request key.\n */\nconst findEntry = (\n request: VcrRequest,\n cassette: Cassette,\n config: Configuration,\n): Effect.Effect<VcrEntry | undefined> => {\n if (config.match) {\n return Effect.succeed(\n Object.values(cassette.entries).find((entry) => config.match?.(request, entry)),\n );\n }\n return buildRequestKey(request, {\n ignoreHeaders: config.matchIgnore?.requestHeaders,\n ignoreBodyKeys: config.matchIgnore?.requestBodyKeys,\n }).pipe(Effect.map((key) => cassette.entries[key]));\n};\n\n/**\n * Replay a response from cassette data using HttpClientResponse.fromWeb.\n */\nconst replay = (\n store: CassetteStore,\n request: HttpClientRequest.HttpClientRequest,\n vcrRequest: VcrRequest,\n config: Configuration,\n name: string,\n exportKey: string,\n) =>\n readCassetteExport(store, name, exportKey, request).pipe(\n Effect.flatMap((cassette) =>\n findEntry(vcrRequest, cassette, config).pipe(\n Effect.flatMap((entry) => {\n if (!entry) {\n return Effect.fail(\n new HttpClientError.HttpClientError({\n reason: new HttpClientError.TransportError({\n request,\n description: `VCR replay missing entry for ${request.method} ${request.url}`,\n }),\n }),\n );\n }\n\n const web = new Response(entry.response.body, {\n status: entry.response.status,\n headers: entry.response.headers,\n });\n return Effect.succeed(HttpClientResponse.fromWeb(request, web));\n }),\n ),\n ),\n );\n\n/**\n * Record a live response into the cassette and return the original response.\n */\nconst record = (\n store: CassetteStore,\n request: HttpClientRequest.HttpClientRequest,\n vcrRequest: VcrRequest,\n effect: Effect.Effect<HttpClientResponse.HttpClientResponse, HttpClientError.HttpClientError>,\n config: Configuration,\n name: string,\n exportKey: string,\n) =>\n Effect.gen(function* () {\n // Execute the live request and capture the response body (Effect caches reads).\n const response = yield* effect;\n const body = yield* response.text;\n const vcrResponse = toVcrResponse(response, body);\n\n const sanitizedRequest = config.redact\n ? redactRequest(vcrRequest, {\n redactHeaders: config.redact.requestHeaders,\n redactBodyKeys: config.redact.requestBodyKeys,\n })\n : vcrRequest;\n const sanitizedResponse = config.redact\n ? redactResponse(vcrResponse, {\n redactHeaders: config.redact.responseHeaders,\n redactBodyKeys: config.redact.responseBodyKeys,\n })\n : vcrResponse;\n\n // Load or create the cassette before inserting the new entry.\n const file = yield* loadOrInitCassetteFile(store, name, request);\n const cassette = file.exports[exportKey] ?? (yield* createEmptyCassette());\n const key = yield* buildRequestKey(vcrRequest, {\n ignoreHeaders: config.matchIgnore?.requestHeaders,\n ignoreBodyKeys: config.matchIgnore?.requestBodyKeys,\n });\n const next: Cassette = {\n ...cassette,\n entries: {\n ...cassette.entries,\n [key]: {\n request: sanitizedRequest,\n response: sanitizedResponse,\n },\n },\n };\n const nextFile: CassetteFile = {\n ...file,\n exports: {\n ...file.exports,\n [exportKey]: next,\n },\n };\n // Persist updated cassette to disk.\n yield* saveCassetteFile(store, name, nextFile, request);\n return response;\n });\n\n/**\n * Build a VCR-aware HttpClient that replays or records per config.\n */\nconst makeVcrHttpClient = (config: Configuration) =>\n Effect.gen(function* () {\n const live = yield* HttpClient.HttpClient;\n const normalized = normalizeConfig(config);\n\n const isCi = yield* Config.boolean(\"CI\").pipe(Config.withDefault(false));\n\n const disabledVcrs = yield* AckDisableVcrConfig;\n if (shouldDisableVcr(normalized.vcrName, disabledVcrs)) {\n return live;\n }\n\n // CassetteStore is provided via Layer for platform-specific persistence.\n const store = yield* CassetteStore;\n\n const { name, exportKey } = yield* resolveCassetteLocation(normalized);\n\n return live.pipe(\n HttpClient.transform((effect, request) =>\n Effect.gen(function* () {\n const vcrRequest = toVcrRequest(request);\n if (normalized.mode === \"replay\") {\n return yield* replay(store, request, vcrRequest, normalized, name, exportKey);\n }\n\n if (normalized.mode === \"record\") {\n return yield* record(store, request, vcrRequest, effect, normalized, name, exportKey);\n }\n\n // Auto mode: replay if cassette exists, otherwise record (or fail in CI).\n const available = yield* store\n .exists(name)\n .pipe(Effect.mapError((error) => toRequestError(request, error)));\n\n if (!available) {\n if (isCi) {\n return yield* Effect.fail(\n new HttpClientError.HttpClientError({\n reason: new HttpClientError.TransportError({\n request,\n description: \"VCR cassette missing in CI for auto mode\",\n }),\n }),\n );\n }\n\n return yield* record(store, request, vcrRequest, effect, normalized, name, exportKey);\n }\n\n const cassette = yield* readCassetteExport(store, name, exportKey, request);\n const entry = yield* findEntry(vcrRequest, cassette, normalized);\n\n if (entry) {\n const web = new Response(entry.response.body, {\n status: entry.response.status,\n headers: entry.response.headers,\n });\n return HttpClientResponse.fromWeb(request, web);\n }\n\n return yield* record(store, request, vcrRequest, effect, normalized, name, exportKey);\n }),\n ),\n );\n });\n\n/**\n * Layer that provides a VCR-wrapped HttpClient.\n */\nexport const layer = (config: Configuration = {}) =>\n HttpClient.layerMergedContext(makeVcrHttpClient(config));\n"],"mappings":"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"}
|