svelte-adapter-uws-extensions 0.1.6 → 0.1.8

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/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.8",
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));
package/redis/presence.js CHANGED
@@ -304,6 +304,15 @@ export function createPresence(client, options = {}) {
304
304
  redis.eval(CLEANUP_SCRIPT, 1, hashKey(topic), now, presenceTtlMs).catch((err) => {
305
305
  console.warn('presence heartbeat: stale cleanup failed for topic "' + topic + '":', err.message);
306
306
  });
307
+
308
+ // Publish heartbeat event so the adapter core client can
309
+ // refresh maxAge timestamps for active users. Without this,
310
+ // client-side maxAge evicts live users that have not had a
311
+ // fresh join/list event within the window.
312
+ if (activePlatform && data && data.size > 0) {
313
+ const keys = [...data.keys()];
314
+ activePlatform.publish('__presence:' + topic, 'heartbeat', keys);
315
+ }
307
316
  }
308
317
  }, heartbeatInterval);
309
318
  if (heartbeatTimer.unref) heartbeatTimer.unref();