svelte-adapter-uws 0.4.3 → 0.4.4
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 +1 -15
- package/package.json +1 -1
- package/plugins/cursor/client.d.ts +34 -43
- package/plugins/cursor/client.js +196 -255
package/README.md
CHANGED
|
@@ -2008,26 +2008,12 @@ 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 with a `maxAge` option (in milliseconds). When set, cursor entries that haven't received an update within that window are automatically removed. This makes clients self-healing when the server fails to broadcast `remove` events under load:
|
|
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
|
-
|
|
2031
2017
|
#### Server API
|
|
2032
2018
|
|
|
2033
2019
|
| Method | Description |
|
package/package.json
CHANGED
|
@@ -1,43 +1,34 @@
|
|
|
1
|
-
import type { Readable } from 'svelte/store';
|
|
2
|
-
|
|
3
|
-
export interface CursorPosition<UserInfo = unknown, Data = unknown> {
|
|
4
|
-
/** User-identifying data from the server's `select` function. */
|
|
5
|
-
user: UserInfo;
|
|
6
|
-
/** Latest cursor/position data. */
|
|
7
|
-
data: Data;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Get a reactive store of cursor positions on a topic.
|
|
12
|
-
*
|
|
13
|
-
* Returns a `Readable<Map<string, CursorPosition>>` that updates
|
|
14
|
-
* automatically when cursors move or disconnect.
|
|
15
|
-
*
|
|
16
|
-
* @example
|
|
17
|
-
* ```svelte
|
|
18
|
-
* <script>
|
|
19
|
-
* import { cursor } from 'svelte-adapter-uws/plugins/cursor/client';
|
|
20
|
-
*
|
|
21
|
-
* const cursors = cursor('canvas');
|
|
22
|
-
* </script>
|
|
23
|
-
*
|
|
24
|
-
* {#each [...$cursors] as [key, { user, data }] (key)}
|
|
25
|
-
* <div style="left: {data.x}px; top: {data.y}px">
|
|
26
|
-
* {user.name}
|
|
27
|
-
* </div>
|
|
28
|
-
* {/each}
|
|
29
|
-
* ```
|
|
30
|
-
*/
|
|
31
|
-
export function cursor<UserInfo = unknown, Data = unknown>(
|
|
32
|
-
topic: string,
|
|
33
|
-
options?: {
|
|
34
|
-
|
|
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
|
-
}
|
|
43
|
-
): Readable<Map<string, CursorPosition<UserInfo, Data>>>;
|
|
1
|
+
import type { Readable } from 'svelte/store';
|
|
2
|
+
|
|
3
|
+
export interface CursorPosition<UserInfo = unknown, Data = unknown> {
|
|
4
|
+
/** User-identifying data from the server's `select` function. */
|
|
5
|
+
user: UserInfo;
|
|
6
|
+
/** Latest cursor/position data. */
|
|
7
|
+
data: Data;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Get a reactive store of cursor positions on a topic.
|
|
12
|
+
*
|
|
13
|
+
* Returns a `Readable<Map<string, CursorPosition>>` that updates
|
|
14
|
+
* automatically when cursors move or disconnect.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```svelte
|
|
18
|
+
* <script>
|
|
19
|
+
* import { cursor } from 'svelte-adapter-uws/plugins/cursor/client';
|
|
20
|
+
*
|
|
21
|
+
* const cursors = cursor('canvas');
|
|
22
|
+
* </script>
|
|
23
|
+
*
|
|
24
|
+
* {#each [...$cursors] as [key, { user, data }] (key)}
|
|
25
|
+
* <div style="left: {data.x}px; top: {data.y}px">
|
|
26
|
+
* {user.name}
|
|
27
|
+
* </div>
|
|
28
|
+
* {/each}
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export function cursor<UserInfo = unknown, Data = unknown>(
|
|
32
|
+
topic: string,
|
|
33
|
+
options?: { maxAge?: number }
|
|
34
|
+
): Readable<Map<string, CursorPosition<UserInfo, Data>>>;
|
package/plugins/cursor/client.js
CHANGED
|
@@ -1,255 +1,196 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Client-side cursor helper for svelte-adapter-uws.
|
|
3
|
-
*
|
|
4
|
-
* Subscribes to the internal `__cursor:{topic}` channel and maintains
|
|
5
|
-
* a live Map of cursor positions. The server handles throttling and
|
|
6
|
-
* cleanup; this module keeps the client-side state in sync.
|
|
7
|
-
*
|
|
8
|
-
* When `maxAge` is set, cursor entries that haven't received an update
|
|
9
|
-
* within that window are automatically removed. This makes clients
|
|
10
|
-
* self-healing when the server fails to broadcast a `remove` event
|
|
11
|
-
* (e.g. mass disconnects overwhelming Redis cleanup).
|
|
12
|
-
*
|
|
13
|
-
* @module svelte-adapter-uws/plugins/cursor/client
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import { on, connect, status } from '../../client.js';
|
|
17
|
-
import { writable } from 'svelte/store';
|
|
18
|
-
|
|
19
|
-
/** @type {Map<string, ReturnType<typeof cursor>>} */
|
|
20
|
-
const cursorStores = new Map();
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Get a reactive store of cursor positions on a topic.
|
|
24
|
-
*
|
|
25
|
-
* Returns a readable Svelte store containing a Map of connection keys
|
|
26
|
-
* to `{ user, data }` objects. The Map updates automatically when
|
|
27
|
-
* cursors move or disconnect.
|
|
28
|
-
*
|
|
29
|
-
* @template UserInfo, Data
|
|
30
|
-
* @param {string} topic - Topic to track cursors on
|
|
31
|
-
* @param {{ maxAge?: number
|
|
32
|
-
* @returns {import('svelte/store').Readable<Map<string, { user: UserInfo, data: Data }>>}
|
|
33
|
-
*
|
|
34
|
-
* @example
|
|
35
|
-
* ```svelte
|
|
36
|
-
* <script>
|
|
37
|
-
* import { cursor } from 'svelte-adapter-uws/plugins/cursor/client';
|
|
38
|
-
*
|
|
39
|
-
* const cursors = cursor('canvas');
|
|
40
|
-
* </script>
|
|
41
|
-
*
|
|
42
|
-
* {#each [...$cursors] as [key, { user, data }] (key)}
|
|
43
|
-
* <div style="left: {data.x}px; top: {data.y}px" class="cursor">
|
|
44
|
-
* {user.name}
|
|
45
|
-
* </div>
|
|
46
|
-
* {/each}
|
|
47
|
-
* ```
|
|
48
|
-
*
|
|
49
|
-
* @example
|
|
50
|
-
* ```svelte
|
|
51
|
-
* <script>
|
|
52
|
-
* // Self-healing: cursors expire after 30s without movement
|
|
53
|
-
* const cursors = cursor('canvas', { maxAge: 30_000 });
|
|
54
|
-
* </script>
|
|
55
|
-
* ```
|
|
56
|
-
*/
|
|
57
|
-
export function cursor(topic, options) {
|
|
58
|
-
const maxAge = options?.maxAge;
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if (
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
let
|
|
76
|
-
let
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
if (event.event === '
|
|
131
|
-
const { key
|
|
132
|
-
timestamps.
|
|
133
|
-
if (
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
// Request a snapshot of existing cursor positions every time the socket
|
|
198
|
-
// opens (initial connect and reconnects). Without this, the store would
|
|
199
|
-
// miss cursors that appeared while the client was offline.
|
|
200
|
-
statusUnsub = status.subscribe((s) => {
|
|
201
|
-
if (s === 'open' && !cancelled) {
|
|
202
|
-
connect().send({ type: 'cursor-snapshot', topic });
|
|
203
|
-
}
|
|
204
|
-
});
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
function stopListening() {
|
|
208
|
-
cancelled = true;
|
|
209
|
-
if (sourceUnsub) {
|
|
210
|
-
sourceUnsub();
|
|
211
|
-
sourceUnsub = null;
|
|
212
|
-
}
|
|
213
|
-
if (statusUnsub) {
|
|
214
|
-
statusUnsub();
|
|
215
|
-
statusUnsub = null;
|
|
216
|
-
}
|
|
217
|
-
if (sweepTimer) {
|
|
218
|
-
clearInterval(sweepTimer);
|
|
219
|
-
sweepTimer = null;
|
|
220
|
-
}
|
|
221
|
-
if (rafId !== null) {
|
|
222
|
-
cancelAnimationFrame(rafId);
|
|
223
|
-
rafId = null;
|
|
224
|
-
}
|
|
225
|
-
targets.clear();
|
|
226
|
-
cursorMap = new Map();
|
|
227
|
-
timestamps.clear();
|
|
228
|
-
// Push the cleared state to the output store so a new subscriber does
|
|
229
|
-
// not see ghost cursors from the previous subscription cycle.
|
|
230
|
-
output.set(new Map());
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
const store = {
|
|
234
|
-
subscribe(fn) {
|
|
235
|
-
if (refCount++ === 0) startListening();
|
|
236
|
-
const unsub = output.subscribe(fn);
|
|
237
|
-
return () => {
|
|
238
|
-
unsub();
|
|
239
|
-
if (--refCount === 0) {
|
|
240
|
-
stopListening();
|
|
241
|
-
cursorStores.delete(cacheKey);
|
|
242
|
-
}
|
|
243
|
-
};
|
|
244
|
-
}
|
|
245
|
-
};
|
|
246
|
-
|
|
247
|
-
cursorStores.set(cacheKey, store);
|
|
248
|
-
|
|
249
|
-
// If nothing subscribes before the next microtask, remove the cache entry.
|
|
250
|
-
queueMicrotask(() => {
|
|
251
|
-
if (refCount === 0) cursorStores.delete(cacheKey);
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
return store;
|
|
255
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Client-side cursor helper for svelte-adapter-uws.
|
|
3
|
+
*
|
|
4
|
+
* Subscribes to the internal `__cursor:{topic}` channel and maintains
|
|
5
|
+
* a live Map of cursor positions. The server handles throttling and
|
|
6
|
+
* cleanup; this module keeps the client-side state in sync.
|
|
7
|
+
*
|
|
8
|
+
* When `maxAge` is set, cursor entries that haven't received an update
|
|
9
|
+
* within that window are automatically removed. This makes clients
|
|
10
|
+
* self-healing when the server fails to broadcast a `remove` event
|
|
11
|
+
* (e.g. mass disconnects overwhelming Redis cleanup).
|
|
12
|
+
*
|
|
13
|
+
* @module svelte-adapter-uws/plugins/cursor/client
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { on, connect, status } from '../../client.js';
|
|
17
|
+
import { writable } from 'svelte/store';
|
|
18
|
+
|
|
19
|
+
/** @type {Map<string, ReturnType<typeof cursor>>} */
|
|
20
|
+
const cursorStores = new Map();
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Get a reactive store of cursor positions on a topic.
|
|
24
|
+
*
|
|
25
|
+
* Returns a readable Svelte store containing a Map of connection keys
|
|
26
|
+
* to `{ user, data }` objects. The Map updates automatically when
|
|
27
|
+
* cursors move or disconnect.
|
|
28
|
+
*
|
|
29
|
+
* @template UserInfo, Data
|
|
30
|
+
* @param {string} topic - Topic to track cursors on
|
|
31
|
+
* @param {{ maxAge?: number }} [options] - Options
|
|
32
|
+
* @returns {import('svelte/store').Readable<Map<string, { user: UserInfo, data: Data }>>}
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```svelte
|
|
36
|
+
* <script>
|
|
37
|
+
* import { cursor } from 'svelte-adapter-uws/plugins/cursor/client';
|
|
38
|
+
*
|
|
39
|
+
* const cursors = cursor('canvas');
|
|
40
|
+
* </script>
|
|
41
|
+
*
|
|
42
|
+
* {#each [...$cursors] as [key, { user, data }] (key)}
|
|
43
|
+
* <div style="left: {data.x}px; top: {data.y}px" class="cursor">
|
|
44
|
+
* {user.name}
|
|
45
|
+
* </div>
|
|
46
|
+
* {/each}
|
|
47
|
+
* ```
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```svelte
|
|
51
|
+
* <script>
|
|
52
|
+
* // Self-healing: cursors expire after 30s without movement
|
|
53
|
+
* const cursors = cursor('canvas', { maxAge: 30_000 });
|
|
54
|
+
* </script>
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
export function cursor(topic, options) {
|
|
58
|
+
const maxAge = options?.maxAge;
|
|
59
|
+
const cacheKey = maxAge > 0 ? topic + '\0' + maxAge : topic;
|
|
60
|
+
|
|
61
|
+
const cached = cursorStores.get(cacheKey);
|
|
62
|
+
if (cached) return cached;
|
|
63
|
+
|
|
64
|
+
const cursorTopic = '__cursor:' + topic;
|
|
65
|
+
|
|
66
|
+
/** @type {Map<string, { user: any, data: any }>} */
|
|
67
|
+
let cursorMap = new Map();
|
|
68
|
+
/** @type {Map<string, number>} */
|
|
69
|
+
const timestamps = new Map();
|
|
70
|
+
const output = writable(/** @type {Map<string, any>} */ (new Map()));
|
|
71
|
+
|
|
72
|
+
let sourceUnsub = /** @type {(() => void) | null} */ (null);
|
|
73
|
+
let statusUnsub = /** @type {(() => void) | null} */ (null);
|
|
74
|
+
/** @type {ReturnType<typeof setInterval> | null} */
|
|
75
|
+
let sweepTimer = null;
|
|
76
|
+
let refCount = 0;
|
|
77
|
+
let cancelled = false;
|
|
78
|
+
|
|
79
|
+
function sweep() {
|
|
80
|
+
if (!maxAge || maxAge <= 0) return;
|
|
81
|
+
const cutoff = Date.now() - maxAge;
|
|
82
|
+
let changed = false;
|
|
83
|
+
for (const [key, ts] of timestamps) {
|
|
84
|
+
if (ts < cutoff) {
|
|
85
|
+
timestamps.delete(key);
|
|
86
|
+
if (cursorMap.delete(key)) changed = true;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (changed) output.set(new Map(cursorMap));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function startListening() {
|
|
93
|
+
cancelled = false;
|
|
94
|
+
const source = on(cursorTopic);
|
|
95
|
+
sourceUnsub = source.subscribe((event) => {
|
|
96
|
+
if (event === null) return;
|
|
97
|
+
|
|
98
|
+
if (event.event === 'update' && event.data != null) {
|
|
99
|
+
const { key, user, data } = event.data;
|
|
100
|
+
cursorMap.set(key, { user, data });
|
|
101
|
+
timestamps.set(key, Date.now());
|
|
102
|
+
output.set(new Map(cursorMap));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (event.event === 'snapshot' && Array.isArray(event.data)) {
|
|
107
|
+
cursorMap = new Map();
|
|
108
|
+
timestamps.clear();
|
|
109
|
+
const now = Date.now();
|
|
110
|
+
for (const entry of event.data) {
|
|
111
|
+
const { key, user, data } = entry;
|
|
112
|
+
cursorMap.set(key, { user, data });
|
|
113
|
+
timestamps.set(key, now);
|
|
114
|
+
}
|
|
115
|
+
output.set(new Map(cursorMap));
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (event.event === 'bulk' && Array.isArray(event.data)) {
|
|
120
|
+
const now = Date.now();
|
|
121
|
+
for (const entry of event.data) {
|
|
122
|
+
const { key, user, data } = entry;
|
|
123
|
+
cursorMap.set(key, { user, data });
|
|
124
|
+
timestamps.set(key, now);
|
|
125
|
+
}
|
|
126
|
+
output.set(new Map(cursorMap));
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (event.event === 'remove' && event.data != null) {
|
|
131
|
+
const { key } = event.data;
|
|
132
|
+
timestamps.delete(key);
|
|
133
|
+
if (cursorMap.delete(key)) {
|
|
134
|
+
output.set(new Map(cursorMap));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
if (maxAge > 0) {
|
|
140
|
+
sweepTimer = setInterval(sweep, Math.max(maxAge / 2, 1000));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Request a snapshot of existing cursor positions every time the socket
|
|
144
|
+
// opens (initial connect and reconnects). Without this, the store would
|
|
145
|
+
// miss cursors that appeared while the client was offline.
|
|
146
|
+
statusUnsub = status.subscribe((s) => {
|
|
147
|
+
if (s === 'open' && !cancelled) {
|
|
148
|
+
connect().send({ type: 'cursor-snapshot', topic });
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function stopListening() {
|
|
154
|
+
cancelled = true;
|
|
155
|
+
if (sourceUnsub) {
|
|
156
|
+
sourceUnsub();
|
|
157
|
+
sourceUnsub = null;
|
|
158
|
+
}
|
|
159
|
+
if (statusUnsub) {
|
|
160
|
+
statusUnsub();
|
|
161
|
+
statusUnsub = null;
|
|
162
|
+
}
|
|
163
|
+
if (sweepTimer) {
|
|
164
|
+
clearInterval(sweepTimer);
|
|
165
|
+
sweepTimer = null;
|
|
166
|
+
}
|
|
167
|
+
cursorMap = new Map();
|
|
168
|
+
timestamps.clear();
|
|
169
|
+
// Push the cleared state to the output store so a new subscriber does
|
|
170
|
+
// not see ghost cursors from the previous subscription cycle.
|
|
171
|
+
output.set(new Map());
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const store = {
|
|
175
|
+
subscribe(fn) {
|
|
176
|
+
if (refCount++ === 0) startListening();
|
|
177
|
+
const unsub = output.subscribe(fn);
|
|
178
|
+
return () => {
|
|
179
|
+
unsub();
|
|
180
|
+
if (--refCount === 0) {
|
|
181
|
+
stopListening();
|
|
182
|
+
cursorStores.delete(cacheKey);
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
cursorStores.set(cacheKey, store);
|
|
189
|
+
|
|
190
|
+
// If nothing subscribes before the next microtask, remove the cache entry.
|
|
191
|
+
queueMicrotask(() => {
|
|
192
|
+
if (refCount === 0) cursorStores.delete(cacheKey);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
return store;
|
|
196
|
+
}
|