svelte-adapter-uws-extensions 0.1.7 → 0.1.9

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/presence.js +30 -68
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelte-adapter-uws-extensions",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
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/presence.js CHANGED
@@ -20,57 +20,32 @@
20
20
  import { randomBytes } from 'node:crypto';
21
21
 
22
22
  /**
23
- * Lua script for atomic join: set this instance's field, expire the key,
24
- * check if the user already exists on another instance (non-stale),
25
- * and return all hash entries so the caller can skip a separate HGETALL.
23
+ * Lua script for atomic join: set this instance's field and expire the key.
26
24
  *
27
25
  * KEYS[1] = hash key
28
26
  * ARGV[1] = field to set (instanceId|userKey)
29
27
  * ARGV[2] = field value (JSON with data and ts)
30
- * ARGV[3] = "|userKey" suffix to match
31
- * ARGV[4] = now (ms)
32
- * ARGV[5] = presenceTtlMs
33
- * ARGV[6] = presenceTtl (seconds, for EXPIRE)
28
+ * ARGV[3] = presenceTtl (seconds, for EXPIRE)
34
29
  *
35
- * Returns {isFirst, field1, val1, field2, val2, ...} so a single round
36
- * trip handles HSET + EXPIRE + dedup check + HGETALL.
30
+ * Cross-instance dedup is intentionally omitted. The local localCounts
31
+ * map already prevents duplicate joins from the same user on the same
32
+ * instance (the script only runs when prevCount === 0). If the same
33
+ * user joins on a second instance, both broadcast "join" -- the client
34
+ * handles this as an idempotent Map.set on the same key.
35
+ *
36
+ * Removing the HGETALL + O(N) scan that was here before drops per-join
37
+ * Redis work from O(N) to O(1), fixing the O(N^2) total cost that
38
+ * killed the server at 2000+ concurrent joins.
37
39
  */
38
40
  const JOIN_SCRIPT = `
39
41
  local key = KEYS[1]
40
42
  local field = ARGV[1]
41
43
  local value = ARGV[2]
42
- local suffix = ARGV[3]
43
- local now = tonumber(ARGV[4])
44
- local ttlMs = tonumber(ARGV[5])
45
- local ttlSec = tonumber(ARGV[6])
44
+ local ttlSec = tonumber(ARGV[3])
46
45
 
47
46
  redis.call('hset', key, field, value)
48
47
  redis.call('expire', key, ttlSec)
49
-
50
- local all = redis.call('hgetall', key)
51
- local isFirst = 1
52
- for i = 1, #all, 2 do
53
- local f = all[i]
54
- if f ~= field and #f >= #suffix and string.sub(f, -#suffix) == suffix then
55
- local ok, parsed = pcall(cjson.decode, all[i+1])
56
- if ok and parsed.ts and (now - parsed.ts) <= ttlMs then
57
- isFirst = 0
58
- break
59
- end
60
- end
61
- end
62
-
63
- -- For small hashes return entries inline (saves a round trip).
64
- -- For large hashes return only the flag; the caller fetches the
65
- -- list via a coalesced HGETALL shared across concurrent joiners.
66
- if #all <= 200 then
67
- local result = {isFirst}
68
- for i = 1, #all do
69
- result[#result + 1] = all[i]
70
- end
71
- return result
72
- end
73
- return {isFirst}
48
+ return 1
74
49
  `;
75
50
 
76
51
  /**
@@ -304,6 +279,15 @@ export function createPresence(client, options = {}) {
304
279
  redis.eval(CLEANUP_SCRIPT, 1, hashKey(topic), now, presenceTtlMs).catch((err) => {
305
280
  console.warn('presence heartbeat: stale cleanup failed for topic "' + topic + '":', err.message);
306
281
  });
282
+
283
+ // Publish heartbeat event so the adapter core client can
284
+ // refresh maxAge timestamps for active users. Without this,
285
+ // client-side maxAge evicts live users that have not had a
286
+ // fresh join/list event within the window.
287
+ if (activePlatform && data && data.size > 0) {
288
+ const keys = [...data.keys()];
289
+ activePlatform.publish('__presence:' + topic, 'heartbeat', keys);
290
+ }
307
291
  }
308
292
  }, heartbeatInterval);
309
293
  if (heartbeatTimer.unref) heartbeatTimer.unref();
@@ -407,21 +391,14 @@ export function createPresence(client, options = {}) {
407
391
  // cleaned local state -- undo any Redis write and bail out.
408
392
  if (!wsTopics.has(ws)) return;
409
393
 
410
- let all;
411
-
412
394
  if (prevCount === 0) {
413
- // New user on this instance -- single Lua call does
414
- // HSET + EXPIRE + dedup check. For small hashes the
415
- // script returns all entries inline (1 round trip).
416
- // For large hashes it returns only the dedup flag and
417
- // we fetch the list via coalesceHgetall below.
395
+ // New user on this instance -- HSET + EXPIRE in one Lua call.
418
396
  const now = Date.now();
419
397
  const field = compoundField(key);
420
398
  const value = JSON.stringify({ data, ts: now });
421
- const suffix = '|' + key;
422
- const result = await redis.eval(
399
+ await redis.eval(
423
400
  JOIN_SCRIPT, 1, hashKey(topic),
424
- field, value, suffix, now, presenceTtlMs, presenceTtl
401
+ field, value, presenceTtl
425
402
  );
426
403
 
427
404
  // Guard: if ws closed while awaiting Redis, remove the field
@@ -431,29 +408,14 @@ export function createPresence(client, options = {}) {
431
408
  return;
432
409
  }
433
410
 
434
- const isFirstGlobally = result[0];
435
-
436
- if (result.length > 1) {
437
- // Small hash: entries returned inline
438
- all = {};
439
- for (let i = 1; i < result.length; i += 2) {
440
- all[result[i]] = result[i + 1];
441
- }
442
- }
443
-
444
- if (isFirstGlobally === 1) {
445
- const payload = { key, data };
446
- platform.publish('__presence:' + topic, 'join', payload);
447
- await publishEvent(topic, 'join', payload);
448
- }
411
+ const payload = { key, data };
412
+ platform.publish('__presence:' + topic, 'join', payload);
413
+ await publishEvent(topic, 'join', payload);
449
414
  }
450
415
 
451
- // Fetch initial list via coalesced HGETALL when the script
452
- // did not return entries inline (large hash or dedup join).
416
+ // Fetch initial list via coalesced HGETALL.
453
417
  // Concurrent joiners share a single Redis round trip.
454
- if (!all) {
455
- all = await coalesceHgetall(topic);
456
- }
418
+ const all = await coalesceHgetall(topic);
457
419
 
458
420
  // Subscribe ws to presence channel (may have closed during async gap)
459
421
  try {