Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/effect-rc112-toolchain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"effect-http-recorder": minor
---

Upgrade to Effect `4.0.0-rc.112`, require TypeScript 7 and Bun `1.4.1` or newer, and update the development toolchain to TypeScript `7.0.2`, `@effect/tsgo` `0.39.1`, and Vitest `4.1.11`.

Adapt schema errors, JSON codecs, and WebSocket types to the updated dependencies while preserving the recorder API and cassette format. Remove the obsolete `skipLibCheck` workaround from consumer guidance and package verification.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:

- uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2
with:
bun-version: 1.3.14
bun-version: 1.4.1

- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:

- uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2
with:
bun-version: 1.3.14
bun-version: 1.4.1

- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@ Use `bun run format` and `bun run lint:fix` for local fixes. Tests live under `t

Add a Changeset with `bun run changeset` for user-facing changes. Review every cassette diff for credentials before committing it. Refresh a cassette by deleting only that cassette and rerunning its focused test.

Effect beta upgrades must update runtime, peer, development, documentation, and clean-consumer versions together. Use static imports only; dynamic `import()` and inline import-type expressions are not accepted.
Effect release candidate upgrades must update runtime, peer, development, documentation, and clean-consumer versions together. Use static imports only; dynamic `import()` and inline import-type expressions are not accepted.
20 changes: 5 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,16 @@ Record real Effect HTTP and WebSocket traffic once, then replay it from determin

Use it for provider integrations, retries, polling, multi-step flows, and any test where hand-written HTTP mocks hide too much of the real request shape.

> Public beta. The API depends on Effect 4 beta and may change with Effect's unstable transport modules.
> Public beta. The API depends on Effect 4 release candidates and may change with Effect's unstable transport modules.

## Install

```sh
bun add effect@4.0.0-beta.83
bun add -d effect-http-recorder@beta @effect/vitest@4.0.0-beta.83 vitest@^4
bun add effect@4.0.0-rc.112
bun add -d effect-http-recorder@beta @effect/vitest@4.0.0-rc.112 typescript@^7 vitest@^4.1
```

The package supports Node.js 22+ and Bun. It is not intended for browsers, workers, or Deno.

Effect `4.0.0-beta.83` currently contains unresolved symbols in its published declarations. Until those upstream declarations are fixed, TypeScript consumers need:

```json
{
"compilerOptions": {
"skipLibCheck": true
}
}
```
The package requires TypeScript 7 (`typescript@^7`), supports Node.js 22+ and Bun 1.4.1+, and is not intended for browsers, workers, or Deno.

## Quick Start

Expand Down Expand Up @@ -248,7 +238,7 @@ Cassettes are readable JSON files intended to be committed with your tests. HTTP
- Constructor-level WebSocket cassettes reproduce terminal close codes and reasons, but not selected subprotocols, handshake headers, transport timing, or transport failures. Lower-level `layerSocket` cassettes contain frames only.
- Failed and interrupted live WebSocket connections are not recorded.
- WebSocket transcripts are retained in memory until the connection finishes; avoid using this beta for unbounded sessions.
- The package currently requires the exact Effect beta listed above.
- The package currently requires the exact Effect release candidate listed above.
- Cassette format version `1` has no migration tooling yet.

## License
Expand Down
265 changes: 154 additions & 111 deletions bun.lock

Large diffs are not rendered by default.

34 changes: 19 additions & 15 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,15 @@
],
"engines": {
"node": ">=22",
"bun": ">=1.3.14"
"bun": ">=1.4.1"
},
"publishConfig": {
"access": "public",
"tag": "beta",
"provenance": true
},
"scripts": {
"prepare": "effect-tsgo patch --typescript",
"changeset": "changeset",
"format": "oxfmt .",
"format:check": "oxfmt --check .",
Expand All @@ -38,7 +39,7 @@
"test": "bun test --timeout 30000 --only-failures",
"test:examples": "vitest run --config examples/vitest.config.ts",
"record:examples": "rm -rf examples/recordings && bun run test:examples",
"typecheck": "tsgo --noEmit && tsgo -p test/tsconfig.json --noEmit",
"typecheck": "tsc --noEmit && tsc -p test/tsconfig.json --noEmit",
"build": "bun ./script/build.ts",
"verify:package": "bun ./script/verify-package.ts",
"check": "bun run format:check && bun run lint && bun run check:imports && bun run typecheck && bun run test && bun run test:examples && bun run verify:package",
Expand All @@ -61,26 +62,29 @@
],
"devDependencies": {
"@changesets/cli": "2.31.0",
"@effect/platform-node": "4.0.0-beta.83",
"@effect/vitest": "4.0.0-beta.83",
"@effect/platform-node": "4.0.0-rc.112",
"@effect/tsgo": "0.39.1",
"@effect/vitest": "4.0.0-rc.112",
"@tsconfig/node22": "22.0.2",
"@types/bun": "1.3.13",
"@types/bun": "latest",
"@types/node": "24.12.2",
"@typescript/native-preview": "7.0.0-dev.20251207.1",
"effect": "4.0.0-beta.83",
"oxfmt": "0.56.0",
"oxlint": "1.60.0",
"typescript": "5.8.2",
"vitest": "4.0.16"
"oxfmt": "0.66.0",
"oxlint": "1.81.0",
"oxlint-tsgolint": "7.0.2001",
"typescript": "7.0.2",
"vitest": "4.1.11"
},
"dependencies": {
"@effect/platform-node-shared": "4.0.0-beta.83"
"@effect/platform-bun": "4.0.0-rc.112",
"@effect/platform-node-shared": "4.0.0-rc.112",
"effect": "4.0.0-rc.112"
},
"peerDependencies": {
"effect": "4.0.0-beta.83"
"effect": "4.0.0-rc.112",
"typescript": "^7"
},
"packageManager": "bun@1.3.14",
"packageManager": "bun@1.4.1",
"overrides": {
"@effect/platform-node-shared": "4.0.0-beta.83"
"@effect/platform-node-shared": "4.0.0-rc.112"
}
}
2 changes: 0 additions & 2 deletions script/verify-package.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,6 @@ if (JSON.stringify(namespace) !== JSON.stringify(["hasCassetteSync", "layer", "l
moduleResolution: "NodeNext",
strict: true,
noEmit: true,
// Required by effect@4.0.0-beta.83: its declarations currently contain unresolved internal symbols.
skipLibCheck: true,
lib: ["ES2022", "DOM", "ESNext.Disposable"],
},
include: ["consumer.ts"],
Expand Down
21 changes: 10 additions & 11 deletions src/cassette/model.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
import { Schema } from "effect"
import type { CassetteMetadata, JsonValue } from "../api.js"
import type { JsonValue } from "../api.js"
import { HttpInteractionSchema } from "../http/model.js"
import { WebSocketInteractionSchema } from "../websocket/model.js"

export type { CassetteMetadata, JsonValue } from "../api.js"

const JsonValueSchema = Schema.suspend(
(): Schema.Codec<JsonValue> =>
Schema.Union([
Schema.Null,
Schema.Boolean,
Schema.Number,
Schema.String,
Schema.Array(JsonValueSchema),
Schema.Record(Schema.String, JsonValueSchema),
]),
const JsonValueSchema = Schema.suspend((): Schema.Codec<JsonValue> =>
Schema.Union([
Schema.Null,
Schema.Boolean,
Schema.Number,
Schema.String,
Schema.Array(JsonValueSchema),
Schema.Record(Schema.String, JsonValueSchema),
]),
)

export const CassetteMetadataSchema = Schema.Record(Schema.String, JsonValueSchema)
Expand Down
6 changes: 3 additions & 3 deletions src/cassette/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@ import { CassetteSchema, encodeCassette, type Cassette, type CassetteMetadata, t

const DEFAULT_RECORDINGS_DIR = path.resolve(process.cwd(), "test", "fixtures", "recordings")

export class CassetteNotFoundError extends Schema.TaggedErrorClass<CassetteNotFoundError>()("CassetteNotFoundError", {
export class CassetteNotFoundError extends Schema.TaggedError<CassetteNotFoundError>()("CassetteNotFoundError", {
cassetteName: Schema.String,
}) {
override get message() {
return `Cassette "${this.cassetteName}" not found`
}
}

export class InvalidCassetteError extends Schema.TaggedErrorClass<InvalidCassetteError>()("InvalidCassetteError", {
export class InvalidCassetteError extends Schema.TaggedError<InvalidCassetteError>()("InvalidCassetteError", {
cassetteName: Schema.String,
description: Schema.String,
}) {
Expand All @@ -23,7 +23,7 @@ export class InvalidCassetteError extends Schema.TaggedErrorClass<InvalidCassett
}
}

export class UnsafeCassetteError extends Schema.TaggedErrorClass<UnsafeCassetteError>()("UnsafeCassetteError", {
export class UnsafeCassetteError extends Schema.TaggedError<UnsafeCassetteError>()("UnsafeCassetteError", {
cassetteName: Schema.String,
findings: Schema.Array(SecretFindingSchema),
}) {
Expand Down
1 change: 0 additions & 1 deletion src/http/model.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Schema } from "effect"
import type { RequestSnapshot } from "../api.js"

export const RequestSnapshotSchema = Schema.Struct({
method: Schema.String,
Expand Down
2 changes: 1 addition & 1 deletion src/redaction/redactor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const DEFAULT_REDACT_QUERY = [
"x-amz-signature",
]

const decodeJson = Schema.decodeUnknownOption(Schema.UnknownFromJsonString)
const decodeJson = Schema.decodeUnknownOption(Schema.fromJsonString(Schema.Unknown))

const redactionSet = (values: ReadonlyArray<string> | undefined, defaults: ReadonlyArray<string>) =>
new Set([...defaults, ...(values ?? [])].map((value) => value.toLowerCase()))
Expand Down
2 changes: 1 addition & 1 deletion src/replay/comparison.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Option, Schema } from "effect"
import { REDACTED } from "../redaction/redactor.js"
import { secretFindings } from "../redaction/secrets.js"

export const decodeJson = Schema.decodeUnknownOption(Schema.UnknownFromJsonString)
export const decodeJson = Schema.decodeUnknownOption(Schema.fromJsonString(Schema.Unknown))

const isRecord = (value: unknown): value is Record<string, unknown> =>
value !== null && typeof value === "object" && !Array.isArray(value)
Expand Down
4 changes: 2 additions & 2 deletions src/websocket/recorder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ const webSocketFacade = (
readonly protocol: () => string
readonly extensions: () => string
readonly bufferedAmount: () => number
readonly send: (data: string | ArrayBufferLike | Blob | ArrayBufferView) => void
readonly send: (data: BufferSource | Blob | string) => void
readonly close: (code?: number, reason?: string) => void
},
): globalThis.WebSocket => {
Expand Down Expand Up @@ -496,7 +496,7 @@ const makeRecordingWebSocketConstructor = (
return new Proxy(native, {
get: (target, property) => {
if (property === "send")
return (data: string | ArrayBufferLike | Blob | ArrayBufferView) => {
return (data: BufferSource | Blob | string) => {
target.send(data)
appendEvent("client", data)
}
Expand Down
2 changes: 1 addition & 1 deletion test/http.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ describe("HTTP", () => {
const completed: string[] = []
using server = Bun.serve({
port: 0,
fetch: async (request) => {
fetch: async (request: Request) => {
const name = new URL(request.url).pathname.slice(1)
if (name === "first") {
await first.promise
Expand Down
1 change: 1 addition & 0 deletions test/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"extends": "../tsconfig.json",
"compilerOptions": {
"rootDir": "..",
"types": ["bun", "node"],
"noEmit": true,
"declaration": false,
"module": "preserve",
Expand Down
2 changes: 1 addition & 1 deletion test/websocket.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class EchoWebSocket extends EventTarget {
})
}

send(data: string | ArrayBufferLike | Blob | ArrayBufferView) {
send(data: BufferSource | Blob | string) {
queueMicrotask(() => this.dispatchEvent(new MessageEvent("message", { data })))
}

Expand Down