svelte-adapter-uws 0.4.1 → 0.4.3
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 +15 -1
- package/package.json +1 -1
- package/plugins/cursor/client.d.ts +10 -1
- package/plugins/cursor/client.js +63 -4
package/README.md
CHANGED
|
@@ -2008,12 +2008,26 @@ The client store is a `Readable<Map<string, { user, data }>>`. The Map updates w
|
|
|
2008
2008
|
|
|
2009
2009
|
**Initial sync and reconnect.** The `cursor(topic)` store sends a `{ type: 'cursor-snapshot', topic }` message every time the WebSocket connection opens -- both on first connect and on every reconnect. The server calls `cursors.snapshot(ws, topic, platform)` in its `message` handler, which sends a `snapshot` event back with the current cursor state (or an empty array if nobody is active). The client replaces its entire cursor map with the snapshot contents, clearing any stale entries from before the disconnect. Wire `cursors.snapshot()` in your message handler as shown in the server example above.
|
|
2010
2010
|
|
|
2011
|
-
The `cursor()` function accepts an optional second argument
|
|
2011
|
+
The `cursor()` function accepts an optional second argument:
|
|
2012
2012
|
|
|
2013
2013
|
```js
|
|
2014
2014
|
const positions = cursor('canvas', { maxAge: 30_000 });
|
|
2015
2015
|
```
|
|
2016
2016
|
|
|
2017
|
+
`maxAge` (milliseconds) -- cursor entries that haven't received an update within that window are automatically removed. Makes clients self-healing when the server fails to broadcast `remove` events under load.
|
|
2018
|
+
|
|
2019
|
+
`interpolate` -- enables smooth cursor rendering via `requestAnimationFrame`. Incoming `update` events set a lerp target and the displayed position moves toward it at 30% of the remaining distance per frame, eliminating visual jitter from network timing. The first update for each cursor snaps immediately (no lerp from 0,0). `snapshot`, `bulk`, and `remove` events always snap. Only applies to cursor data with numeric `x` and `y` fields -- non-numeric data falls back to direct assignment.
|
|
2020
|
+
|
|
2021
|
+
```js
|
|
2022
|
+
const positions = cursor('canvas', { interpolate: true });
|
|
2023
|
+
```
|
|
2024
|
+
|
|
2025
|
+
Options can be combined:
|
|
2026
|
+
|
|
2027
|
+
```js
|
|
2028
|
+
const positions = cursor('canvas', { maxAge: 30_000, interpolate: true });
|
|
2029
|
+
```
|
|
2030
|
+
|
|
2017
2031
|
#### Server API
|
|
2018
2032
|
|
|
2019
2033
|
| Method | Description |
|
package/package.json
CHANGED
|
@@ -30,5 +30,14 @@ export interface CursorPosition<UserInfo = unknown, Data = unknown> {
|
|
|
30
30
|
*/
|
|
31
31
|
export function cursor<UserInfo = unknown, Data = unknown>(
|
|
32
32
|
topic: string,
|
|
33
|
-
options?: {
|
|
33
|
+
options?: {
|
|
34
|
+
maxAge?: number;
|
|
35
|
+
/**
|
|
36
|
+
* Enable smooth lerp interpolation for cursor movement.
|
|
37
|
+
* When enabled, `update` events with numeric `x` and `y` fields
|
|
38
|
+
* are interpolated via requestAnimationFrame instead of snapping.
|
|
39
|
+
* `snapshot`, `bulk`, and `remove` events always snap immediately.
|
|
40
|
+
*/
|
|
41
|
+
interpolate?: boolean;
|
|
42
|
+
}
|
|
34
43
|
): Readable<Map<string, CursorPosition<UserInfo, Data>>>;
|
package/plugins/cursor/client.js
CHANGED
|
@@ -28,7 +28,7 @@ const cursorStores = new Map();
|
|
|
28
28
|
*
|
|
29
29
|
* @template UserInfo, Data
|
|
30
30
|
* @param {string} topic - Topic to track cursors on
|
|
31
|
-
* @param {{ maxAge?: number }} [options] - Options
|
|
31
|
+
* @param {{ maxAge?: number, interpolate?: boolean }} [options] - Options
|
|
32
32
|
* @returns {import('svelte/store').Readable<Map<string, { user: UserInfo, data: Data }>>}
|
|
33
33
|
*
|
|
34
34
|
* @example
|
|
@@ -56,7 +56,10 @@ const cursorStores = new Map();
|
|
|
56
56
|
*/
|
|
57
57
|
export function cursor(topic, options) {
|
|
58
58
|
const maxAge = options?.maxAge;
|
|
59
|
-
const
|
|
59
|
+
const interpolate = options?.interpolate === true;
|
|
60
|
+
let cacheKey = topic;
|
|
61
|
+
if (maxAge > 0) cacheKey += '\0' + maxAge;
|
|
62
|
+
if (interpolate) cacheKey += '\0lerp';
|
|
60
63
|
|
|
61
64
|
const cached = cursorStores.get(cacheKey);
|
|
62
65
|
if (cached) return cached;
|
|
@@ -73,9 +76,38 @@ export function cursor(topic, options) {
|
|
|
73
76
|
let statusUnsub = /** @type {(() => void) | null} */ (null);
|
|
74
77
|
/** @type {ReturnType<typeof setInterval> | null} */
|
|
75
78
|
let sweepTimer = null;
|
|
79
|
+
/** @type {Map<string, { x: number, y: number }>} */
|
|
80
|
+
const targets = new Map();
|
|
81
|
+
let rafId = /** @type {number | null} */ (null);
|
|
76
82
|
let refCount = 0;
|
|
77
83
|
let cancelled = false;
|
|
78
84
|
|
|
85
|
+
function tick() {
|
|
86
|
+
let changed = false;
|
|
87
|
+
for (const [key, target] of targets) {
|
|
88
|
+
const entry = cursorMap.get(key);
|
|
89
|
+
if (!entry) { targets.delete(key); continue; }
|
|
90
|
+
const dx = target.x - entry.data.x;
|
|
91
|
+
const dy = target.y - entry.data.y;
|
|
92
|
+
if (Math.abs(dx) < 0.5 && Math.abs(dy) < 0.5) {
|
|
93
|
+
if (entry.data.x !== target.x || entry.data.y !== target.y) {
|
|
94
|
+
entry.data = { ...entry.data, x: target.x, y: target.y };
|
|
95
|
+
changed = true;
|
|
96
|
+
}
|
|
97
|
+
targets.delete(key);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
entry.data = { ...entry.data, x: entry.data.x + dx * 0.5, y: entry.data.y + dy * 0.5 };
|
|
101
|
+
changed = true;
|
|
102
|
+
}
|
|
103
|
+
if (changed) output.set(new Map(cursorMap));
|
|
104
|
+
if (targets.size > 0) {
|
|
105
|
+
rafId = requestAnimationFrame(tick);
|
|
106
|
+
} else {
|
|
107
|
+
rafId = null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
79
111
|
function sweep() {
|
|
80
112
|
if (!maxAge || maxAge <= 0) return;
|
|
81
113
|
const cutoff = Date.now() - maxAge;
|
|
@@ -97,15 +129,35 @@ export function cursor(topic, options) {
|
|
|
97
129
|
|
|
98
130
|
if (event.event === 'update' && event.data != null) {
|
|
99
131
|
const { key, user, data } = event.data;
|
|
100
|
-
cursorMap.set(key, { user, data });
|
|
101
132
|
timestamps.set(key, Date.now());
|
|
102
|
-
|
|
133
|
+
if (interpolate && typeof data?.x === 'number' && typeof data?.y === 'number') {
|
|
134
|
+
const existing = cursorMap.get(key);
|
|
135
|
+
if (!existing) {
|
|
136
|
+
cursorMap.set(key, { user, data });
|
|
137
|
+
} else {
|
|
138
|
+
const dx = data.x - existing.data.x;
|
|
139
|
+
const dy = data.y - existing.data.y;
|
|
140
|
+
existing.user = user;
|
|
141
|
+
existing.data = {
|
|
142
|
+
...existing.data,
|
|
143
|
+
x: existing.data.x + dx * 0.5,
|
|
144
|
+
y: existing.data.y + dy * 0.5
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
targets.set(key, { x: data.x, y: data.y });
|
|
148
|
+
output.set(new Map(cursorMap));
|
|
149
|
+
if (rafId === null) rafId = requestAnimationFrame(tick);
|
|
150
|
+
} else {
|
|
151
|
+
cursorMap.set(key, { user, data });
|
|
152
|
+
output.set(new Map(cursorMap));
|
|
153
|
+
}
|
|
103
154
|
return;
|
|
104
155
|
}
|
|
105
156
|
|
|
106
157
|
if (event.event === 'snapshot' && Array.isArray(event.data)) {
|
|
107
158
|
cursorMap = new Map();
|
|
108
159
|
timestamps.clear();
|
|
160
|
+
targets.clear();
|
|
109
161
|
const now = Date.now();
|
|
110
162
|
for (const entry of event.data) {
|
|
111
163
|
const { key, user, data } = entry;
|
|
@@ -117,6 +169,7 @@ export function cursor(topic, options) {
|
|
|
117
169
|
}
|
|
118
170
|
|
|
119
171
|
if (event.event === 'bulk' && Array.isArray(event.data)) {
|
|
172
|
+
targets.clear();
|
|
120
173
|
const now = Date.now();
|
|
121
174
|
for (const entry of event.data) {
|
|
122
175
|
const { key, user, data } = entry;
|
|
@@ -130,6 +183,7 @@ export function cursor(topic, options) {
|
|
|
130
183
|
if (event.event === 'remove' && event.data != null) {
|
|
131
184
|
const { key } = event.data;
|
|
132
185
|
timestamps.delete(key);
|
|
186
|
+
targets.delete(key);
|
|
133
187
|
if (cursorMap.delete(key)) {
|
|
134
188
|
output.set(new Map(cursorMap));
|
|
135
189
|
}
|
|
@@ -164,6 +218,11 @@ export function cursor(topic, options) {
|
|
|
164
218
|
clearInterval(sweepTimer);
|
|
165
219
|
sweepTimer = null;
|
|
166
220
|
}
|
|
221
|
+
if (rafId !== null) {
|
|
222
|
+
cancelAnimationFrame(rafId);
|
|
223
|
+
rafId = null;
|
|
224
|
+
}
|
|
225
|
+
targets.clear();
|
|
167
226
|
cursorMap = new Map();
|
|
168
227
|
timestamps.clear();
|
|
169
228
|
// Push the cleared state to the output store so a new subscriber does
|