svelte-adapter-uws-extensions 0.1.2 → 0.1.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/package.json +1 -1
- package/redis/presence.js +55 -26
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "svelte-adapter-uws-extensions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
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
|
@@ -333,6 +333,10 @@ export function createPresence(client, options = {}) {
|
|
|
333
333
|
// Subscribe to cross-instance events for this topic
|
|
334
334
|
await subscribeToTopic(topic, platform);
|
|
335
335
|
|
|
336
|
+
// Guard: if ws was closed during the async gap, leave() already
|
|
337
|
+
// cleaned local state -- undo any Redis write and bail out.
|
|
338
|
+
if (!wsTopics.has(ws)) return;
|
|
339
|
+
|
|
336
340
|
if (prevCount === 0) {
|
|
337
341
|
// New user on this instance -- check if globally new via atomic Lua
|
|
338
342
|
const now = Date.now();
|
|
@@ -345,6 +349,13 @@ export function createPresence(client, options = {}) {
|
|
|
345
349
|
);
|
|
346
350
|
await redis.expire(hashKey(topic), presenceTtl);
|
|
347
351
|
|
|
352
|
+
// Guard: if ws closed while awaiting Redis, remove the field
|
|
353
|
+
// we just set and bail -- leave() already cleaned local state.
|
|
354
|
+
if (!wsTopics.has(ws)) {
|
|
355
|
+
await redis.hdel(hashKey(topic), field).catch(() => {});
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
|
|
348
359
|
if (isFirstGlobally === 1) {
|
|
349
360
|
// No other live instance has this user -- broadcast join
|
|
350
361
|
const payload = { key, data };
|
|
@@ -446,7 +457,18 @@ export function createPresence(client, options = {}) {
|
|
|
446
457
|
}
|
|
447
458
|
|
|
448
459
|
// --- Leave all topics ---
|
|
460
|
+
// Phase 1: Synchronous cleanup of ALL local state before any async
|
|
461
|
+
// work. This prevents the heartbeat from refreshing dead entries and
|
|
462
|
+
// lets concurrent join() calls detect the closed ws via wsTopics.
|
|
449
463
|
const connTopics = wsTopics.get(ws);
|
|
464
|
+
wsTopics.delete(ws);
|
|
465
|
+
|
|
466
|
+
const syncTopics = syncObservers.get(ws);
|
|
467
|
+
syncObservers.delete(ws);
|
|
468
|
+
|
|
469
|
+
/** @type {Array<{ topic: string, key: string, data: Record<string, any>, needsUnsub: boolean }>} */
|
|
470
|
+
const pendingLeaves = [];
|
|
471
|
+
|
|
450
472
|
if (connTopics) {
|
|
451
473
|
for (const [topic, { key, data }] of connTopics) {
|
|
452
474
|
const counts = localCounts.get(topic);
|
|
@@ -456,60 +478,67 @@ export function createPresence(client, options = {}) {
|
|
|
456
478
|
if (current <= 1) {
|
|
457
479
|
counts.delete(key);
|
|
458
480
|
|
|
459
|
-
// Clean up local data for this user on this topic
|
|
460
481
|
const topicData = localData.get(topic);
|
|
461
482
|
if (topicData) {
|
|
462
483
|
topicData.delete(key);
|
|
463
484
|
if (topicData.size === 0) localData.delete(topic);
|
|
464
485
|
}
|
|
465
486
|
|
|
487
|
+
let needsUnsub = false;
|
|
466
488
|
if (counts.size === 0) {
|
|
467
489
|
localCounts.delete(topic);
|
|
468
490
|
activeTopics.delete(topic);
|
|
469
|
-
// Unsubscribe from Redis channel if no sync observers remain
|
|
470
491
|
if (!syncCounts.has(topic)) {
|
|
471
|
-
|
|
492
|
+
needsUnsub = true;
|
|
472
493
|
}
|
|
473
494
|
}
|
|
474
495
|
|
|
475
|
-
|
|
476
|
-
// is still present on another instance (ignoring stale entries)
|
|
477
|
-
const field = compoundField(key);
|
|
478
|
-
const suffix = '|' + key;
|
|
479
|
-
const now = Date.now();
|
|
480
|
-
const userGone = await redis.eval(
|
|
481
|
-
LEAVE_SCRIPT, 1, hashKey(topic), field, suffix, now, presenceTtlMs
|
|
482
|
-
);
|
|
483
|
-
|
|
484
|
-
if (userGone === 1) {
|
|
485
|
-
// No other instance has this user -- broadcast leave
|
|
486
|
-
const payload = { key, data };
|
|
487
|
-
platform.publish('__presence:' + topic, 'leave', payload);
|
|
488
|
-
await publishEvent(topic, 'leave', payload);
|
|
489
|
-
}
|
|
496
|
+
pendingLeaves.push({ topic, key, data, needsUnsub });
|
|
490
497
|
} else {
|
|
491
498
|
counts.set(key, current - 1);
|
|
492
499
|
}
|
|
493
500
|
}
|
|
494
|
-
wsTopics.delete(ws);
|
|
495
501
|
}
|
|
496
502
|
|
|
497
|
-
// Handle sync-only observers
|
|
498
|
-
const syncTopics = syncObservers.get(ws);
|
|
499
503
|
if (syncTopics) {
|
|
500
504
|
for (const topic of syncTopics) {
|
|
501
505
|
const count = (syncCounts.get(topic) || 1) - 1;
|
|
502
506
|
if (count <= 0) {
|
|
503
507
|
syncCounts.delete(topic);
|
|
504
|
-
// Unsubscribe from Redis channel if no joined users remain
|
|
505
|
-
if (!localCounts.has(topic)) {
|
|
506
|
-
await unsubscribeFromTopic(topic);
|
|
507
|
-
}
|
|
508
508
|
} else {
|
|
509
509
|
syncCounts.set(topic, count);
|
|
510
510
|
}
|
|
511
511
|
}
|
|
512
|
-
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Phase 2: Async Redis cleanup. Local state is already clean so
|
|
515
|
+
// the heartbeat will not refresh any of these entries.
|
|
516
|
+
for (const { topic, key, data, needsUnsub } of pendingLeaves) {
|
|
517
|
+
if (needsUnsub) {
|
|
518
|
+
await unsubscribeFromTopic(topic);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const field = compoundField(key);
|
|
522
|
+
const suffix = '|' + key;
|
|
523
|
+
const now = Date.now();
|
|
524
|
+
const userGone = await redis.eval(
|
|
525
|
+
LEAVE_SCRIPT, 1, hashKey(topic), field, suffix, now, presenceTtlMs
|
|
526
|
+
);
|
|
527
|
+
|
|
528
|
+
if (userGone === 1) {
|
|
529
|
+
const payload = { key, data };
|
|
530
|
+
platform.publish('__presence:' + topic, 'leave', payload);
|
|
531
|
+
await publishEvent(topic, 'leave', payload);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Async unsubscribe for sync-only observer topics
|
|
536
|
+
if (syncTopics) {
|
|
537
|
+
for (const topic of syncTopics) {
|
|
538
|
+
if (!syncCounts.has(topic) && !localCounts.has(topic)) {
|
|
539
|
+
await unsubscribeFromTopic(topic);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
513
542
|
}
|
|
514
543
|
},
|
|
515
544
|
|