react-websocket 3.3.0 → 3.5.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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"timeline.js","names":[],"sources":["../src/timeline.ts"],"sourcesContent":["// Types only: the built entry has no imports, and its declarations point at `core`'s.\nimport type { WebSocketClient, WebSocketLatency } from './core';\n\n/** A server snapshot, as returned by the `snapshot` option. */\nexport interface TimelineSnapshot<S> {\n /** Server time the state is for, in ms. Must increase from one snapshot to the next. */\n readonly time: number;\n /** The authoritative state at `time`. */\n readonly state: S;\n /**\n * `seq` of the last input from this client that the server applied before taking the\n * snapshot. Inputs up to it are confirmed and no longer predicted. Omit it to keep the\n * previous one.\n */\n readonly ack?: number;\n}\n\n/** Client-side prediction for your own inputs. See {@link WebSocketTimelineOptions.input}. */\nexport interface TimelineInputOptions<S, I, Out> {\n /**\n * Build the message for an input, tagged with its `seq`. Annotate `input`: that's what\n * `timeline.input()` accepts. Sent with `volatile`, so inputs are never replayed late.\n */\n send: (input: I, seq: number) => Out;\n /**\n * Apply one input to a state, exactly as the server does. Must be pure: return a new state\n * instead of changing `state`, which is a snapshot from the server.\n */\n predict: (state: S, input: I) => S;\n /**\n * Drop an input instead of sending it while more than this many bytes are already waiting in\n * the browser's send buffer (see `SendOptions.maxBuffered`). For input sent every tick on a\n * slow link. A dropped input isn't predicted.\n */\n maxBuffered?: number;\n}\n\n/** Options for {@link createWebSocketTimeline}. */\nexport interface WebSocketTimelineOptions<S, I, In, Out> {\n /** Recognize a snapshot. Return `undefined` (or `null`) for any other message. */\n snapshot: (message: In) => TimelineSnapshot<S> | null | undefined;\n /**\n * Blend two states. `t` runs from 0 (`from`) to 1 (`to`), and above 1 only when\n * extrapolating. Without it, `at()` steps from one snapshot to the next.\n */\n interpolate?: (from: S, to: S, t: number) => S;\n /**\n * How far behind the newest snapshot to render, in ms: enough to cover the snapshot\n * interval plus jitter, so there are usually two snapshots to blend between. A function is\n * read on every call, for a delay that adapts.\n * @default 100\n */\n delay?: number | (() => number);\n /**\n * How far past the newest snapshot `at()` may extrapolate when the next one is late, in ms.\n * Needs `interpolate`. `0` holds the newest state instead.\n * @default 0\n */\n extrapolate?: number;\n /**\n * How many snapshots to keep (at least 2).\n * @default 30\n */\n buffer?: number;\n /**\n * Take the server clock from `createWebSocketLatency` on the same client. Its pongs must use\n * the same clock as snapshot `time`. Without it, or before the first pong with a time, the\n * server clock is estimated from when snapshots arrive.\n */\n latency?: Pick<WebSocketLatency, 'getSnapshot' | 'serverNow'>;\n /** Send inputs tagged with a sequence number, and predict their effect until the server confirms them. */\n input?: TimelineInputOptions<S, I, Out>;\n}\n\n/** Server snapshots on a timeline, with your own inputs predicted on top. Create with {@link createWebSocketTimeline}. */\nexport interface WebSocketTimeline<S, I> {\n /** The client whose snapshots this timeline keeps. */\n readonly client: WebSocketClient<any, any>;\n /**\n * The state at `time` (by default `renderTime()`), interpolated between the snapshots around\n * it. For what the server moves, such as other players. `undefined` before any snapshot.\n */\n at(time?: number): S | undefined;\n /**\n * The newest snapshot with your unconfirmed inputs replayed on top. For what your inputs\n * move, such as your own player. Cached, so it's cheap to call every frame.\n */\n predicted(): S | undefined;\n /**\n * Send an input and predict it. Returns its `seq`, or `undefined` if it couldn't be sent\n * (the socket isn't open, or `maxBuffered` dropped it), in which case it isn't predicted.\n */\n input(input: I): number | undefined;\n /** The newest snapshot, as `snapshot` returned it. */\n latest(): TimelineSnapshot<S> | undefined;\n /**\n * The server time `at()` renders by default: the newest snapshot's time, give or take, minus\n * `delay`. It never goes backward until the timeline starts over. `undefined` before any snapshot.\n */\n renderTime(): number | undefined;\n /** How many inputs were sent but not confirmed yet. */\n pending(): number;\n /**\n * Called after each new snapshot and on `reset()`. Compatible with `useSyncExternalStore` for\n * `latest` and `pending`, but not for `at`, `predicted` or `renderTime`, which change with time.\n */\n subscribe(listener: () => void): () => void;\n /** Forget all snapshots and pending inputs now. `seq` keeps counting. */\n reset(): void;\n /** Stop listening. Only needed for timelines created dynamically. */\n dispose(): void;\n}\n\n// Unconfirmed inputs kept at most. A server that stops confirming would otherwise make every\n// snapshot replay a growing list.\nconst MAX_PENDING = 1000;\n\n/**\n * Keep server snapshots on a timeline for multiplayer games: render what the server moves\n * slightly in the past, interpolated between snapshots, and what your inputs move right away,\n * predicted and corrected when the server confirms them.\n *\n * Read it once per frame (in `requestAnimationFrame` or React Three Fiber's `useFrame`): `at()`\n * for everyone else, `predicted()` for you. It listens from creation, starts over on reconnect,\n * and doesn't open the connection by itself.\n *\n * @example\n * ```ts\n * import { createWebSocketTimeline } from 'react-websocket/timeline';\n *\n * export const timeline = createWebSocketTimeline(game, {\n * snapshot: m => (m.type === 'snapshot' ? { time: m.time, ack: m.ack, state: m.world } : undefined),\n * interpolate: (from, to, t) => lerpWorld(from, to, t),\n * latency,\n * input: {\n * send: (input: Move, seq) => ({ type: 'input', seq, ...input }),\n * predict: (world, input) => move(world, me, input)\n * }\n * });\n *\n * // Every frame:\n * draw(timeline.at(), timeline.predicted());\n * // On a key press:\n * timeline.input({ dx: 1, dy: 0 });\n * ```\n */\n// `const` + `NoInfer`: take `In` and `Out` from the client only. `S` comes from `snapshot`, and\n// `I` from how `input.send` annotates its parameter (`never` without `input`).\nexport function createWebSocketTimeline<In, const Out, S, I = never>(\n client: WebSocketClient<In, Out>,\n options: WebSocketTimelineOptions<S, I, NoInfer<In>, NoInfer<Out>>\n): WebSocketTimeline<S, I> {\n const { interpolate, latency, input: inputs } = options;\n const delay = options.delay ?? 100;\n const extrapolate = options.extrapolate ?? 0;\n const keep = Math.max(2, options.buffer ?? 30);\n const listeners = new Set<() => void>();\n\n // Oldest first, `time` strictly increasing. `arrived` is `performance.now()` on receipt.\n let buffer: { snapshot: TimelineSnapshot<S>; arrived: number }[] = [];\n // Server time minus `performance.now()`, from the least delayed snapshot in the buffer.\n let arrival = -Infinity;\n // The last render time handed out, so that it never goes backward.\n let rendered = -Infinity;\n // A new connection opened: its first snapshot replaces the buffer.\n let stale = false;\n let seq = 0;\n let pending: { seq: number; input: I }[] = [];\n let prediction: S | undefined;\n // How many pending inputs `prediction` includes. `-1`: start again from the newest snapshot.\n let folded = -1;\n\n function notify() {\n for (const listener of [...listeners]) listener();\n }\n\n function clear() {\n buffer = [];\n arrival = rendered = -Infinity;\n stale = false;\n }\n\n function receive(snapshot: TimelineSnapshot<S>) {\n if (stale) clear();\n const newest = buffer.at(-1);\n // A WebSocket keeps order, so this is a duplicate or a server clock that stepped back.\n if (newest && snapshot.time <= newest.snapshot.time) return;\n buffer.push({ snapshot, arrived: performance.now() });\n if (buffer.length > keep) buffer.shift();\n arrival = -Infinity;\n for (const entry of buffer) arrival = Math.max(arrival, entry.snapshot.time - entry.arrived);\n const { ack } = snapshot;\n if (ack != null) pending = pending.filter(entry => entry.seq > ack);\n folded = -1;\n notify();\n }\n\n function renderTime() {\n if (!buffer.length) return;\n const measured = latency?.getSnapshot();\n // The server time of a snapshot arriving now: half a round trip behind the server's clock.\n const edge =\n measured?.offset !== undefined && measured.rtt !== undefined\n ? latency!.serverNow() - measured.rtt / 2\n : performance.now() + arrival;\n rendered = Math.max(rendered, edge - (typeof delay === 'function' ? delay() : delay));\n return rendered;\n }\n\n function at(time?: number): S | undefined {\n const n = buffer.length;\n if (!n) return;\n const t = time ?? renderTime()!;\n const last = buffer[n - 1]!.snapshot;\n if (t >= last.time) {\n const previous = buffer[n - 2]?.snapshot;\n if (!previous || !interpolate || !extrapolate || t === last.time) return last.state;\n const capped = Math.min(t, last.time + extrapolate);\n return interpolate(\n previous.state,\n last.state,\n (capped - previous.time) / (last.time - previous.time)\n );\n }\n // Reads are usually near the newest snapshot, so search from the end.\n let i = n - 2;\n while (i >= 0 && buffer[i]!.snapshot.time > t) i--;\n if (i < 0) return buffer[0]!.snapshot.state;\n const from = buffer[i]!.snapshot;\n const to = buffer[i + 1]!.snapshot;\n return !interpolate || t === from.time\n ? from.state\n : interpolate(from.state, to.state, (t - from.time) / (to.time - from.time));\n }\n\n function predicted(): S | undefined {\n const newest = buffer.at(-1);\n if (!newest) return;\n if (folded < 0) {\n prediction = newest.snapshot.state;\n folded = 0;\n }\n // Usually zero or one new input per frame; all of them once after each snapshot.\n while (folded < pending.length) {\n prediction = inputs!.predict(prediction!, pending[folded]!.input);\n folded++; // after `predict`, so one that throws is retried rather than skipped\n }\n return prediction;\n }\n\n const offs = [\n // Inputs on the old connection will never be confirmed. Keep showing the old snapshots\n // until the new connection sends its first one.\n client.on('open', () => {\n pending = [];\n folded = -1;\n stale = true;\n }),\n // A `snapshot` that throws leaves the timeline unchanged; the client re-throws it asynchronously.\n client.on('message', message => {\n const snapshot = options.snapshot(message);\n if (snapshot != null) receive(snapshot);\n })\n ];\n\n return {\n client,\n at,\n predicted,\n input(value) {\n const next = seq + 1;\n // Recorded only once it's sent, so a dropped input never shows as a move the server didn't make.\n const message = inputs!.send(value, next);\n if (!client.send(message, { volatile: true, maxBuffered: inputs!.maxBuffered })) return;\n seq = next;\n pending.push({ seq, input: value });\n if (pending.length > MAX_PENDING) {\n pending.shift();\n folded = -1;\n }\n return seq;\n },\n latest: () => buffer.at(-1)?.snapshot,\n renderTime,\n pending: () => pending.length,\n subscribe(listener) {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n reset() {\n clear();\n pending = [];\n folded = -1;\n notify();\n },\n dispose() {\n offs.forEach(off => off());\n }\n };\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-websocket",
3
- "version": "3.3.0",
3
+ "version": "3.5.0",
4
4
  "description": "Hooks-first WebSocket client for React: shared connections, auto-reconnect, typed messages, Suspense-ready",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -27,6 +27,26 @@
27
27
  "default": "./dist/core.cjs"
28
28
  }
29
29
  },
30
+ "./chaos": {
31
+ "import": {
32
+ "types": "./dist/chaos.d.ts",
33
+ "default": "./dist/chaos.js"
34
+ },
35
+ "require": {
36
+ "types": "./dist/chaos.d.cts",
37
+ "default": "./dist/chaos.cjs"
38
+ }
39
+ },
40
+ "./timeline": {
41
+ "import": {
42
+ "types": "./dist/timeline.d.ts",
43
+ "default": "./dist/timeline.js"
44
+ },
45
+ "require": {
46
+ "types": "./dist/timeline.d.cts",
47
+ "default": "./dist/timeline.cjs"
48
+ }
49
+ },
30
50
  "./package.json": "./package.json"
31
51
  },
32
52
  "files": [
@@ -49,12 +69,16 @@
49
69
  "format:check": "vp fmt --check",
50
70
  "check:package": "npm run build && publint --strict && attw --pack . --profile node16",
51
71
  "size": "npm run build && size-limit",
52
- "docs:dev": "vitepress dev docs",
53
- "docs:build": "vitepress build docs",
54
- "docs:preview": "vitepress preview docs",
72
+ "docs:dev": "rspress dev",
73
+ "docs:build": "rspress build",
74
+ "docs:preview": "rspress preview",
55
75
  "example:tornado": "vp dev examples/tornado",
56
76
  "example:tornado:server": "python3 examples/tornado/server.py",
57
77
  "example:nextjs": "npm --prefix examples/nextjs run update-library && npm --prefix examples/nextjs run dev",
78
+ "example:hono": "npm --prefix examples/hono run update-library && npm --prefix examples/hono run dev",
79
+ "example:astro": "npm --prefix examples/astro run update-library && npm --prefix examples/astro run dev",
80
+ "example:remix": "npm --prefix examples/remix run update-library && npm --prefix examples/remix run dev",
81
+ "example:game": "npm --prefix examples/game run update-library && npm --prefix examples/game run dev",
58
82
  "prepublishOnly": "npm run check && npm test && npm run check:package"
59
83
  },
60
84
  "repository": {
@@ -77,15 +101,17 @@
77
101
  "bugs": {
78
102
  "url": "https://github.com/mehmetkose/react-websocket/issues"
79
103
  },
80
- "homepage": "https://github.com/mehmetkose/react-websocket",
104
+ "homepage": "https://mehmetkose.github.io/react-websocket/",
81
105
  "peerDependencies": {
82
106
  "react": "^18.0.0 || ^19.0.0"
83
107
  },
84
108
  "devDependencies": {
85
109
  "@arethetypeswrong/cli": "^0.18.5",
86
110
  "@fontsource-variable/geist-mono": "^5.3.0",
111
+ "@rspress/core": "^2.0.23",
87
112
  "@size-limit/preset-small-lib": "^14.2.0",
88
113
  "@testing-library/react": "^16.3.3",
114
+ "@types/node": "^24.19.2",
89
115
  "@types/react": "^19.3.0",
90
116
  "@types/react-dom": "^19.3.0",
91
117
  "@vitejs/plugin-react": "^6.1.2",
@@ -98,7 +124,6 @@
98
124
  "typescript": "^7.0.2",
99
125
  "vite": "npm:@voidzero-dev/vite-plus-core@1.1.0",
100
126
  "vite-plus": "1.1.0",
101
- "vitepress": "^1.6.4",
102
127
  "vitest": "5.0.3"
103
128
  },
104
129
  "size-limit": [
@@ -151,18 +176,34 @@
151
176
  ],
152
177
  "limit": "1.2 kB",
153
178
  "gzip": true
179
+ },
180
+ {
181
+ "name": "react-websocket (adaptive send rate)",
182
+ "path": "dist/index.js",
183
+ "import": "{ createWebSocketSendRate }",
184
+ "ignore": [
185
+ "react"
186
+ ],
187
+ "limit": "0.7 kB",
188
+ "gzip": true
189
+ },
190
+ {
191
+ "name": "react-websocket/chaos (createChaosWebSocket)",
192
+ "path": "dist/chaos.js",
193
+ "import": "{ createChaosWebSocket }",
194
+ "limit": "2.1 kB",
195
+ "gzip": true
196
+ },
197
+ {
198
+ "name": "react-websocket/timeline (createWebSocketTimeline)",
199
+ "path": "dist/timeline.js",
200
+ "import": "{ createWebSocketTimeline }",
201
+ "limit": "1 kB",
202
+ "gzip": true
154
203
  }
155
204
  ],
156
205
  "overrides": {
157
- "@docsearch/react": {
158
- "react": "$react",
159
- "react-dom": "$react-dom",
160
- "@types/react": "$@types/react"
161
- },
162
206
  "vite": "npm:@voidzero-dev/vite-plus-core@1.1.0",
163
- "vitest": "5.0.3",
164
- "vitepress": {
165
- "vite": "^5.4.21"
166
- }
207
+ "vitest": "5.0.3"
167
208
  }
168
209
  }