[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"project-93010":3},{"id":4,"name":5,"fullName":6,"owner":7,"repo":5,"description":8,"homepage":9,"htmlUrl":9,"language":10,"languages":9,"totalLinesOfCode":9,"stars":11,"forks":12,"watchers":13,"openIssues":12,"contributorsCount":14,"subscribersCount":14,"size":14,"stars1d":14,"stars7d":14,"stars30d":12,"stars90d":14,"forks30d":14,"starsTrendScore":14,"compositeScore":15,"rankGlobal":9,"rankLanguage":9,"license":16,"archived":17,"fork":17,"defaultBranch":18,"hasWiki":17,"hasPages":17,"topics":19,"createdAt":9,"pushedAt":9,"updatedAt":20,"readmeContent":21,"aiSummary":22,"trendingCount":14,"starSnapshotCount":14,"syncStatus":23,"lastSyncTime":24,"discoverSource":25},93010,"effect-http-recorder","anomalyco\u002Feffect-http-recorder","anomalyco","Record and replay Effect HTTP and WebSocket traffic with deterministic cassettes",null,"TypeScript",52,1,51,0,41,"MIT License",false,"dev",[],"2026-07-22 04:02:07","# effect-http-recorder\n\nRecord real Effect HTTP and WebSocket traffic once, then replay it from deterministic JSON cassettes.\n\nUse 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.\n\n> Public beta. The API depends on Effect 4 beta and may change with Effect's unstable transport modules.\n\n## Install\n\n```sh\nbun add effect@4.0.0-beta.83\nbun add -d effect-http-recorder@beta @effect\u002Fvitest@4.0.0-beta.83 vitest@^4\n```\n\nThe package supports Node.js 22+ and Bun. It is not intended for browsers, workers, or Deno.\n\nEffect `4.0.0-beta.83` currently contains unresolved symbols in its published declarations. Until those upstream declarations are fixed, TypeScript consumers need:\n\n```json\n{\n  \"compilerOptions\": {\n    \"skipLibCheck\": true\n  }\n}\n```\n\n## Quick Start\n\n```ts\nimport { assert, describe, it } from \"@effect\u002Fvitest\"\nimport { Effect, Schema } from \"effect\"\nimport { HttpClient, HttpClientRequest } from \"effect\u002Funstable\u002Fhttp\"\nimport { HttpRecorder } from \"effect-http-recorder\"\n\nconst User = Schema.Struct({\n  id: Schema.Number,\n  name: Schema.String,\n})\n\nconst getUser = Effect.gen(function* () {\n  const http = yield* HttpClient.HttpClient\n  const response = yield* http.execute(HttpClientRequest.get(\"https:\u002F\u002Fjsonplaceholder.typicode.com\u002Fusers\u002F1\"))\n  return yield* Schema.decodeUnknownEffect(User)(yield* response.json)\n})\n\ndescribe(\"getUser\", () => {\n  it.effect(\"loads a user\", () =>\n    Effect.gen(function* () {\n      const user = yield* getUser\n\n      assert.strictEqual(user.id, 1)\n      assert.strictEqual(user.name, \"Leanne Graham\")\n    }).pipe(Effect.provide(HttpRecorder.layerFetch(\"users\u002Fget-one\"))),\n  )\n})\n```\n\nRun the test with Vitest. The first local run calls the real API and records:\n\n```sh\nbunx vitest run users.test.ts\n```\n\n```text\ntest\u002Ffixtures\u002Frecordings\u002Fusers\u002Fget-one.json\n```\n\nLater runs replay that cassette without contacting the upstream server. When `CI=true`, missing cassettes fail instead of recording.\n\n```mermaid\nflowchart TD\n  Run[Run test] --> Recorded{Cassette recorded?}\n  Recorded -->|Yes| Replay[Replay cassette]\n  Recorded -->|No, local| Record[Call service and record cassette]\n  Recorded -->|No, CI| Fail[Fail: cassette missing]\n```\n\nApplication code does not need to know whether a response is live or replayed.\n\n## API\n\n```ts\nHttpRecorder.layer(name, options?)\nHttpRecorder.layerFetch(name, options?)\nHttpRecorder.layerSocket(name, options?)\nHttpRecorder.layerWebSocketConstructor(name, options?)\nHttpRecorder.hasCassetteSync(name, options?)\nHttpRecorder.removeCassetteSync(name, options?)\n```\n\nThat is the complete runtime API. `layer` decorates an application-provided `HttpClient`; `layerFetch` is the convenience layer that supplies Effect's fetch client. `layerWebSocketConstructor` decorates Effect's `Socket.WebSocketConstructor`, recording every dynamically selected URL and protocol. `layerSocket` is the lower-level transport-neutral decorator for an application-provided `Socket.Socket`.\n\nUse `hasCassetteSync` when registering fixture-gated tests. `removeCassetteSync` explicitly removes one cassette before a focused refresh; removing a missing cassette is a no-op. Both helpers use the same cassette-name validation and default directory as the recorder layers.\n\nUse `layer` to record through another Effect HTTP transport:\n\n```ts\nimport { NodeHttpClient } from \"@effect\u002Fplatform-node\"\nimport { Layer } from \"effect\"\n\nconst recorder = HttpRecorder.layer(\"users\u002Fget-one\").pipe(Layer.provide(NodeHttpClient.layerUndici))\n```\n\nThe `HttpRecorder` namespace also exposes the configuration types `RecorderOptions`, `SocketRecorderOptions`, `RedactOptions`, `RequestMatcher`, `RequestSnapshot`, and `CassetteMetadata`.\n\n## WebSockets\n\nReal applications often select WebSocket URLs inside domain services. Effect represents that capability with `Socket.WebSocketConstructor`; production supplies the platform implementation, while tests can decorate it without changing application code.\n\n```ts\nimport { NodeSocket } from \"@effect\u002Fplatform-node\"\nimport { it } from \"@effect\u002Fvitest\"\nimport { Deferred, Effect, Layer } from \"effect\"\nimport { Socket } from \"effect\u002Funstable\u002Fsocket\"\nimport { HttpRecorder } from \"effect-http-recorder\"\n\nconst roundTrip = Effect.fn(\"Echo.roundTrip\")(function* (url: string, message: string) {\n  const socket = yield* Socket.makeWebSocket(url, { closeCodeIsError: () => false })\n  const write = yield* socket.writer\n  const echoed = yield* Deferred.make\u003Cstring>()\n\n  yield* socket.runString(\n    (response) => {\n      return Deferred.succeed(echoed, response).pipe(\n        Effect.andThen(write(new Socket.CloseEvent(1000, \"done\"))),\n        Effect.orDie,\n      )\n    },\n    { onOpen: write(message).pipe(Effect.orDie) },\n  )\n\n  return yield* Deferred.await(echoed)\n})\n\nit.effect(\"round trips a message\", () =>\n  roundTrip(\"wss:\u002F\u002Fws.postman-echo.com\u002Fraw\", \"hello\").pipe(\n    Effect.scoped,\n    Effect.provide(\n      HttpRecorder.layerWebSocketConstructor(\"echo\u002Fround-trip\").pipe(\n        Layer.provide(NodeSocket.layerWebSocketConstructor),\n      ),\n    ),\n  ),\n)\n```\n\nThe production application supplies only `NodeSocket.layerWebSocketConstructor`. The recorder appears in test wiring and observes each call to `Socket.makeWebSocket`, including URLs selected at runtime.\n\n`socket.runString` owns the receive loop and finishes when the connection closes or fails. Its optional `onOpen` effect is the safe place to send protocols whose client speaks first. The writer is scoped because sending is valid only while a connection run is active.\n\nWebSocket cassettes preserve one ordered transcript of client and server text or binary frames. Replay releases recorded server frames until it reaches a client frame, waits for the application to write the matching frame, then continues. This preserves causal ordering without reproducing network timing.\n\nClient text frames containing JSON compare canonically, so object-key order does not matter. Changed fields, extra fields, non-JSON text, and binary frames must match exactly after redaction. There is intentionally no custom WebSocket matcher in this beta.\n\nIncoming frame handlers start in recorded order and may run concurrently, matching Effect's socket abstraction. Replay waits for all handlers before the socket run completes, but handler completion order is not guaranteed. Use Effect synchronization such as `Queue`, `Ref`, or `Deferred` instead of unsynchronized mutable state.\n\nA constructor cassette records the URL, requested protocols, frames, and terminal close for each connection. Replay validates the URL and protocols before opening the simulated socket. Closing before every recorded frame is consumed fails the test.\n\nUse `layerSocket` when a protocol layer already consumes one application-provided `Socket.Socket`, including non-WebSocket transports. Because that lower-level abstraction has no URL or protocols, its cassettes use the cassette name and connection order as identity.\n\nText frames use the same JSON-field and body redaction as HTTP bodies. Binary frames are stored losslessly as base64. Client and server frame kinds must match during replay.\n\n## Refresh A Cassette\n\nDelete exactly the recordings you want to replace, then rerun their tests:\n\n```sh\nrm test\u002Ffixtures\u002Frecordings\u002Fusers\u002Fget-one.json\nbun run test users.test.ts\n```\n\nThere is intentionally no public overwrite mode. Deletion makes the set of recordings being refreshed visible and reviewable.\n\n## Redaction\n\nSecure defaults remove most headers and redact common credentials in headers, URLs, and JSON bodies. Extend those defaults at layer construction:\n\n```ts\nHttpRecorder.layerFetch(\"anthropic\u002Fmessages\", {\n  redact: {\n    headers: [\"x-project-token\"],\n    allowRequestHeaders: [\"anthropic-version\"],\n    queryParameters: [\"session-id\"],\n    jsonFields: [\"user_id\"],\n    url: (url) => url.replace(\u002F\\\u002Faccounts\\\u002F[^\u002F]+\u002F, \"\u002Faccounts\u002F{account}\"),\n    body: (body) => body.replaceAll(\u002Fusr_[a-z0-9]+\u002Fg, \"usr_redacted\"),\n  },\n})\n```\n\n| Option                 | Purpose                                                              |\n| ---------------------- | -------------------------------------------------------------------- |\n| `headers`              | Add sensitive header names. They are retained as `[REDACTED]`.       |\n| `allowRequestHeaders`  | Preserve additional non-sensitive request headers for matching.      |\n| `allowResponseHeaders` | Preserve additional non-sensitive response headers for replay.       |\n| `queryParameters`      | Add sensitive URL query parameter names.                             |\n| `jsonFields`           | Recursively redact matching JSON keys in requests and responses.     |\n| `url`                  | Stabilize a URL after built-in redaction.                            |\n| `body`                 | Stabilize request and response bodies after built-in JSON redaction. |\n\nBefore writing, the recorder scans the complete cassette for common credential formats and values from credential-like environment variables. Unsafe cassettes fail without replacing an existing recording.\n\nRedaction is defense in depth, not a substitute for review. Inspect cassette diffs before committing them.\n\n## Matching And Ordering\n\nA runtime request atomically claims the first unused recorded interaction that matches it. Distinct requests may replay in any order or concurrently.\n\nRepeated identical requests consume their matching responses in cassette order, which models retries, polling, and cache tests deterministically. A mismatch consumes nothing, and JSON object keys are canonicalized before matching.\n\nConcurrent requests are recorded in request-start order even when their responses complete out of order. Each recorded interaction can be claimed only once, and leaving interactions unused fails when the recorder layer closes.\n\nSupply a custom equivalence rule when a request contains intentionally volatile data:\n\n```ts\nHttpRecorder.layerFetch(\"events\u002Fcreate\", {\n  match: (incoming, recorded) =>\n    incoming.method === recorded.method && new URL(incoming.url).pathname === new URL(recorded.url).pathname,\n})\n```\n\n## Configuration\n\n```ts\ninterface RecorderOptions {\n  readonly directory?: string\n  readonly metadata?: Readonly\u003CRecord\u003Cstring, JsonValue>>\n  readonly redact?: RedactOptions\n  readonly match?: RequestMatcher\n}\n\ntype SocketRecorderOptions = Omit\u003CRecorderOptions, \"match\">\n```\n\n`directory` defaults to `\u003Ccwd>\u002Ftest\u002Ffixtures\u002Frecordings`.\n\nSee [`examples\u002F`](.\u002Fexamples) for complete HTTP and WebSocket examples.\n\n## Cassettes\n\nCassettes are readable JSON files intended to be committed with your tests. HTTP interactions are stored in request order. WebSocket cassettes preserve the observed order of client and server frames. Text stays readable; binary bodies and frames are stored losslessly as base64.\n\n## Current Limits\n\n- Responses are buffered while recording and replaying, so this beta is not suitable for tests that assert streaming timing, cancellation, or backpressure.\n- WebSocket replay preserves frame chronology and content, not real network timing or backpressure.\n- 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.\n- Failed and interrupted live WebSocket connections are not recorded.\n- WebSocket transcripts are retained in memory until the connection finishes; avoid using this beta for unbounded sessions.\n- The package currently requires the exact Effect beta listed above.\n- Cassette format version `1` has no migration tooling yet.\n\n## License\n\nMIT\n","这是一个为 Effect 框架设计的 HTTP 与 WebSocket 流量录制与回放工具，用于在测试中可靠地捕获真实网络请求并生成确定性 JSON 录制带（cassettes）。核心功能包括自动录制首次请求、后续无网络回放、CI 环境下缺失录制带即失败、支持 Effect 4 的 HttpClient 和 WebSocket 构造器集成，并提供分层式依赖注入 API。适用于集成测试、多步业务流程验证、重试逻辑和轮询行为测试等场景，尤其适合需保留真实请求\u002F响应结构（如嵌套字段、headers、状态码）而避免手工 mock 失真时使用。",2,"2026-07-11 02:30:38","CREATED_QUERY"]