svelte-adapter-uws-extensions 0.1.6 → 0.1.7

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/redis/cursor.js +49 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelte-adapter-uws-extensions",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "Redis and Postgres extensions for svelte-adapter-uws - distributed pub/sub, replay buffers, presence tracking, rate limiting, groups, and DB change notifications",
5
5
  "author": "Kevin Radziszewski",
6
6
  "license": "MIT",
package/redis/cursor.js CHANGED
@@ -221,6 +221,45 @@ export function createCursor(client, options = {}) {
221
221
  redis.publish(channel, msg).catch(() => {});
222
222
  }
223
223
 
224
+ /**
225
+ * Flush all coalesced entries for a topic as a single "bulk" event.
226
+ * The client receives one event with all cursor positions instead of
227
+ * N individual events landing in the same microtask. This turns N
228
+ * store updates per frame into one, and reduces Redis PUBLISH calls
229
+ * from N to 1 per flush window.
230
+ *
231
+ * Each entry is still persisted individually to the Redis hash so
232
+ * the per-key TTL and staleness detection work unchanged.
233
+ */
234
+ function flushBulk(topic, dirty) {
235
+ const entries = [];
236
+ const now = Date.now();
237
+ let flushPlatform = null;
238
+
239
+ for (const [k, v] of dirty) {
240
+ entries.push({ key: k, user: v.user, data: v.data });
241
+ flushPlatform = v.platform;
242
+ // Persist each entry individually to Redis hash
243
+ redis.hset(hashKey(topic), k, JSON.stringify({ user: v.user, data: v.data, ts: now })).catch(() => {});
244
+ }
245
+
246
+ redis.expire(hashKey(topic), cursorTtl).catch(() => {});
247
+
248
+ if (flushPlatform) {
249
+ // Single local broadcast with all positions
250
+ flushPlatform.publish('__cursor:' + topic, 'bulk', entries);
251
+
252
+ // Single relay to other instances
253
+ const msg = JSON.stringify({
254
+ instanceId,
255
+ topic,
256
+ event: 'bulk',
257
+ payload: entries
258
+ });
259
+ redis.publish(channel, msg).catch(() => {});
260
+ }
261
+ }
262
+
224
263
  function broadcast(topic, key, user, data, platform) {
225
264
  if (topicThrottleMs <= 0) {
226
265
  doBroadcast(topic, key, user, data, platform);
@@ -243,8 +282,13 @@ export function createCursor(client, options = {}) {
243
282
  if (now - state.lastFlush >= topicThrottleMs) {
244
283
  if (state.timer) { clearTimeout(state.timer); state.timer = null; }
245
284
  state.lastFlush = now;
246
- for (const [k, v] of state.dirty) {
285
+ if (state.dirty.size === 1) {
286
+ // Single entry: use normal event so the client does not
287
+ // need to handle bulk for the common non-contended case.
288
+ const [k, v] = state.dirty.entries().next().value;
247
289
  doBroadcast(topic, k, v.user, v.data, v.platform);
290
+ } else {
291
+ flushBulk(topic, state.dirty);
248
292
  }
249
293
  state.dirty.clear();
250
294
  return;
@@ -257,8 +301,11 @@ export function createCursor(client, options = {}) {
257
301
  if (!s) return;
258
302
  s.timer = null;
259
303
  s.lastFlush = Date.now();
260
- for (const [k, v] of s.dirty) {
304
+ if (s.dirty.size === 1) {
305
+ const [k, v] = s.dirty.entries().next().value;
261
306
  doBroadcast(topic, k, v.user, v.data, v.platform);
307
+ } else {
308
+ flushBulk(topic, s.dirty);
262
309
  }
263
310
  s.dirty.clear();
264
311
  }, topicThrottleMs - (now - state.lastFlush));