{"id":18285925,"url":"https://github.com/exodusmovement/replay","last_synced_at":"2025-04-05T08:30:35.809Z","repository":{"id":253241254,"uuid":"838919219","full_name":"ExodusMovement/replay","owner":"ExodusMovement","description":"Record/inspect/replay fetch and WebSocket sessions","archived":false,"fork":false,"pushed_at":"2024-09-11T14:04:59.000Z","size":309,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-03-21T00:33:06.972Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://npmjs.com/@exodus/replay","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ExodusMovement.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-08-06T15:43:17.000Z","updated_at":"2024-09-11T14:05:57.000Z","dependencies_parsed_at":"2024-08-18T14:29:58.217Z","dependency_job_id":"76d7f538-5ff1-48f8-8232-edf32763f618","html_url":"https://github.com/ExodusMovement/replay","commit_stats":null,"previous_names":["exodusmovement/replay"],"tags_count":4,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ExodusMovement%2Freplay","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ExodusMovement%2Freplay/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ExodusMovement%2Freplay/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ExodusMovement%2Freplay/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ExodusMovement","download_url":"https://codeload.github.com/ExodusMovement/replay/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247311266,"owners_count":20918331,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-05T13:18:12.526Z","updated_at":"2025-04-05T08:30:35.491Z","avatar_url":"https://github.com/ExodusMovement.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Record/inspect/replay `fetch` and `WebSocket` sessions\n\nFor offline testing\n\nThese implementations are not specific to `@exodus/test` and can be used separately\n\nZero-dependencies, bundleable\n\nReplayers can run in V8 / JavaScriptCore / Hermes even with no network API implemented at all\n\n```js\nimport { fetchRecorder, fetchReplayer, WebSocketRecorder, WebSocketReplayer, prettyJSON } from '@exodus/replay'\n```\n\nGenerated logs [are human-readable](#samples)\n\n### `fetchRecorder(log[, { fetch }])`\n\nReturns a `fetch` implementation that appends all requests to the provided `log` array\n\n`log` is expected to be an array and is mutated by appending new requests\n\nIf a base `fetch` implementation is not passed, global `fetch` is used as the base\n\nThat can be overriden, e.g. `fetchRecorder(log, { fetch: require('node-fetch') })`\n\n### `fetchReplayer(log)`\n\nReturns a `fetch` implementation that replies with requests from the provided `log` array\n\nRequests are served by \"first matching\" order and are discarded from an internal log clone\n\n`log` is expected to be an array and is not mutated\n\n### `WebSocketRecorder(log[, { WebSocket }])`\n\nReturns a `WebSocket` implementation that appends all sessions to the provided `log` array\n\n`log` is expected to be an array and is mutated by appending new sessions\n\nIf a base `WebSocket` implementation is not passed, global `WebSocket` is used as the base\n\nThat can be overriden, e.g. `WebSocketRecorder(log, { WebSocket: require('ws') })`\n\n### `WebSocketReplayer(log[, { interval = 0 }])`\n\nReturns a `WebSocket` implementation that replies with sessions from the provided `log` array\n\nSessions are served by \"first matching\" order and are discarded from an internal log clone\n\n`log` is expected to be an array and is not mutated\n\nOptionally, `interval` can be used to control replay speed:\n\n- `interval = 0` for immediate event firing without delays (default)\n\n- `interval = Infinity` for event timing matching the original recording\n\n- `interval = number` for delay between events of `Math.min(number, recorededDelay)`\n\n### `prettyJSON(data, { width = 120 })`\n\nUse it to pretty-print logs: `prettyJSON(log)`\n\nLike `JSON.stringify()`, but with pretty-printing for readability and ease if inspection\n\nThe output is parse-able with `JSON.parse()`\n\nFor simplicity, fitting into `width` is not guaranteed, but the output is stable between runs\n\n## Samples\n\n### `fetch` recording\n\n```json\n{\n  \"request\": {\n    \"resource\": \"https://jsonplaceholder.typicode.com/users/2\",\n    \"options\": { \"headers\": [[\"accept\", \"application/json\"]] }\n  },\n  \"status\": 200,\n  \"statusText\": \"OK\",\n  \"ok\": true,\n  \"headers\": [\n    ...\n    [\"connection\", \"keep-alive\"],\n    [\"content-encoding\", \"br\"],\n    [\"content-type\", \"application/json; charset=utf-8\"],\n    [\"date\", \"Sat, 03 Aug 2024 16:57:51 GMT\"],\n    ...\n  ],\n  \"url\": \"https://jsonplaceholder.typicode.com/users/2\",\n  \"redirected\": false,\n  \"type\": \"basic\",\n  \"bodyType\": \"json\",\n  \"body\": {\n    \"id\": 2,\n    \"name\": \"Ervin Howell\",\n    \"username\": \"Antonette\",\n    \"email\": \"Shanna@melissa.tv\",\n    \"address\": {\n      \"street\": \"Victor Plains\",\n      \"suite\": \"Suite 879\",\n      \"city\": \"Wisokyburgh\",\n      \"zipcode\": \"90566-7771\",\n      \"geo\": { \"lat\": \"-43.9509\", \"lng\": \"-34.4618\" }\n    },\n    \"phone\": \"010-692-6593 x09125\",\n    \"website\": \"anastasia.net\",\n    \"company\": {\n      \"name\": \"Deckow-Crist\",\n      \"catchPhrase\": \"Proactive didactic contingency\",\n      \"bs\": \"synergize scalable supply-chains\"\n    }\n  }\n}\n```\n\n### `WebSocket` recording\n\n```json\n{\n  \"url\": \"wss://javascript.info/article/websocket/demo/hello\",\n  \"log\": [\n    { \"type\": \"get readyState\", \"at\": 24, \"value\": 0 },\n    { \"type\": \"open\", \"at\": 426 },\n    { \"type\": \"get readyState\", \"at\": 426, \"value\": 1 },\n    { \"type\": \"get bufferedAmount\", \"at\": 427, \"value\": 0 },\n    { \"type\": \"send()\", \"at\": 427, \"data\": \"Hi there\" },\n    { \"type\": \"get bufferedAmount\", \"at\": 427, \"value\": 8 },\n    { \"type\": \"message\", \"at\": 543, \"data\": \"Hello from server, there!\" },\n    { \"type\": \"close()\", \"at\": 543 },\n    { \"type\": \"get readyState\", \"at\": 543, \"value\": 2 },\n    { \"type\": \"close\", \"at\": 661, \"code\": 1005, \"reason\": \"\", \"wasClean\": true },\n    { \"type\": \"get readyState\", \"at\": 661, \"value\": 3 }\n  ]\n}\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fexodusmovement%2Freplay","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fexodusmovement%2Freplay","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fexodusmovement%2Freplay/lists"}