svelte-adapter-uws-extensions 0.5.6 → 0.5.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.
- package/package.json +2 -2
- package/redis/cursor.js +50 -71
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "svelte-adapter-uws-extensions",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.7",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"tag": "latest"
|
|
6
6
|
},
|
|
@@ -154,7 +154,7 @@
|
|
|
154
154
|
"node": ">=22.0.0"
|
|
155
155
|
},
|
|
156
156
|
"peerDependencies": {
|
|
157
|
-
"svelte-adapter-uws": "^0.5.
|
|
157
|
+
"svelte-adapter-uws": "^0.5.6"
|
|
158
158
|
},
|
|
159
159
|
"dependencies": {
|
|
160
160
|
"ioredis": "^5.0.0"
|
package/redis/cursor.js
CHANGED
|
@@ -403,16 +403,11 @@ export function createCursor(client, options = {}) {
|
|
|
403
403
|
* - `lastFlush`: target-anchored timestamp of the most recent flush.
|
|
404
404
|
* Advanced by `topicThrottleMs` per cycle (not to actual fire time) so
|
|
405
405
|
* a single late tick does not compound drift on subsequent cycles.
|
|
406
|
-
*
|
|
407
|
-
*
|
|
408
|
-
*
|
|
409
|
-
* `inboundDirty` and ship as one combined frame when the microtask
|
|
410
|
-
* runs. Without this, a single co-arriving cursor fired alone as a
|
|
411
|
-
* single-cursor UPDATE while every other cursor in the same pass
|
|
412
|
-
* queued for the trailing tick - 86 percent fragmentation observed at
|
|
413
|
-
* 1000-mover load on Hetzner CCX13 with `topicThrottle: 8`.
|
|
406
|
+
* Initialized to `Date.now() - topicThrottleMs` so the first broadcast
|
|
407
|
+
* on a new topic is "cycle ready" without polluting drift stats with
|
|
408
|
+
* the full `Date.now()` lateness an init of 0 would imply.
|
|
414
409
|
*
|
|
415
|
-
* @type {Map<string, { dirty: Map<string, { user: any, data: any, platform: any }>, inboundDirty: Map<string, { data: any, platform: any }>, lastFlush: number
|
|
410
|
+
* @type {Map<string, { dirty: Map<string, { user: any, data: any, platform: any }>, inboundDirty: Map<string, { data: any, platform: any }>, lastFlush: number }>}
|
|
416
411
|
*/
|
|
417
412
|
const topicFlush = new Map();
|
|
418
413
|
|
|
@@ -570,9 +565,10 @@ export function createCursor(client, options = {}) {
|
|
|
570
565
|
dirtyTopics.delete(topic);
|
|
571
566
|
|
|
572
567
|
// Target-anchored: advance lastFlush by the cadence amount.
|
|
573
|
-
// Multi-cycle backlog collapse to `now` so the next
|
|
574
|
-
//
|
|
575
|
-
//
|
|
568
|
+
// Multi-cycle backlog collapse to `now` so the next
|
|
569
|
+
// broadcast's `Date.now() - lastFlush >= topicThrottleMs`
|
|
570
|
+
// delay computation does not fire every queued cycle on
|
|
571
|
+
// this turn.
|
|
576
572
|
state.lastFlush = drift < topicThrottleMs ? deadline : now;
|
|
577
573
|
} else if (deadline < nextDeadline) {
|
|
578
574
|
nextDeadline = deadline;
|
|
@@ -591,21 +587,31 @@ export function createCursor(client, options = {}) {
|
|
|
591
587
|
}
|
|
592
588
|
|
|
593
589
|
/**
|
|
594
|
-
* Schedule a local cursor for the next coalesced flush.
|
|
595
|
-
*
|
|
596
|
-
*
|
|
597
|
-
* microtask.
|
|
598
|
-
* function OR `enqueueInbound` - they share `state.pendingMicroflush`)
|
|
599
|
-
* see `now - state.lastFlush < topicThrottleMs` and take the trailing
|
|
600
|
-
* path, accumulating into `state.dirty` / `state.inboundDirty`. The
|
|
601
|
-
* microtask then flushes everything as one combined frame.
|
|
590
|
+
* Schedule a local cursor for the next coalesced flush. Always-tick: every
|
|
591
|
+
* call appends to `state.dirty`, adds the topic to `dirtyTopics`, and arms
|
|
592
|
+
* the tracker-wide tick timer. NO leading-edge synchronous fire and NO
|
|
593
|
+
* microtask defer.
|
|
602
594
|
*
|
|
603
|
-
*
|
|
604
|
-
*
|
|
605
|
-
*
|
|
606
|
-
*
|
|
607
|
-
*
|
|
608
|
-
*
|
|
595
|
+
* Why: uWS dispatches each WS message as its own JS task, and N-API
|
|
596
|
+
* drains microtasks at the C++/JS boundary between dispatches. A
|
|
597
|
+
* `queueMicrotask`-deferred flush fires BEFORE the next socket's message
|
|
598
|
+
* handler runs, so cross-socket coalescing is impossible at the microtask
|
|
599
|
+
* level. `setTimeout(0)` is in libuv's timers phase and fires only after
|
|
600
|
+
* the poll phase processes every ready message on every socket - so all
|
|
601
|
+
* broadcasts dispatched in the same loop iteration end up in one flush
|
|
602
|
+
* regardless of how many task boundaries separate them.
|
|
603
|
+
*
|
|
604
|
+
* 0.5.5/0.5.6 shipped a `queueMicrotask` + `pendingMicroflush` variant
|
|
605
|
+
* built on the wrong dispatch-model assumption (that co-arriving
|
|
606
|
+
* broadcasts share a JS task). Demo measured ~99% single-cursor UPDATE /
|
|
607
|
+
* ~1% BULK at 1000-mover load on the deployed 0.5.6 - essentially the
|
|
608
|
+
* pre-fix shape. The bench validated the assumed input shape (all
|
|
609
|
+
* broadcasts in one synchronous task) instead of the input shape uWS
|
|
610
|
+
* actually produces (per-message tasks separated by microtask drains).
|
|
611
|
+
*
|
|
612
|
+
* First-cursor latency cost of always-tick: up to `topicThrottleMs` (16ms
|
|
613
|
+
* default, one frame budget) before fanout. Below the perceptual floor
|
|
614
|
+
* for cursors. Same trade-off the adapter's bundled cursor plugin makes.
|
|
609
615
|
*/
|
|
610
616
|
function broadcast(topic, key, user, data, platform) {
|
|
611
617
|
if (topicThrottleMs <= 0) {
|
|
@@ -615,34 +621,19 @@ export function createCursor(client, options = {}) {
|
|
|
615
621
|
|
|
616
622
|
let state = topicFlush.get(topic);
|
|
617
623
|
if (!state) {
|
|
618
|
-
|
|
624
|
+
// Anchor `lastFlush` one cycle in the past so the first broadcast
|
|
625
|
+
// is treated as "cycle ready" with zero drift on the very first
|
|
626
|
+
// tick. Without this, `Date.now() - 0` would be a huge "lateness"
|
|
627
|
+
// that pollutes the drift stats forever.
|
|
628
|
+
state = { dirty: new Map(), inboundDirty: new Map(), lastFlush: Date.now() - topicThrottleMs };
|
|
619
629
|
topicFlush.set(topic, state);
|
|
620
630
|
}
|
|
621
631
|
state.dirty.set(key, { user, data, platform });
|
|
622
|
-
|
|
623
|
-
const now = Date.now();
|
|
624
|
-
if (now - state.lastFlush >= topicThrottleMs) {
|
|
625
|
-
// Claim the cadence slot synchronously so subsequent broadcasts
|
|
626
|
-
// in the same JS pass take the trailing path and accumulate.
|
|
627
|
-
state.lastFlush = now;
|
|
628
|
-
dirtyTopics.delete(topic);
|
|
629
|
-
if (!state.pendingMicroflush) {
|
|
630
|
-
state.pendingMicroflush = true;
|
|
631
|
-
queueMicrotask(() => {
|
|
632
|
-
state.pendingMicroflush = false;
|
|
633
|
-
// flushBoth clears `dirty` and `inboundDirty` internally
|
|
634
|
-
// and early-returns on empty; this guard saves the
|
|
635
|
-
// for-of entry cost on the empty case.
|
|
636
|
-
if (state.dirty.size === 0 && state.inboundDirty.size === 0) return;
|
|
637
|
-
flushBoth(topic, state);
|
|
638
|
-
});
|
|
639
|
-
}
|
|
640
|
-
return;
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
// Within window: trailing-edge flush via the scheduler tick.
|
|
644
632
|
dirtyTopics.add(topic);
|
|
645
|
-
|
|
633
|
+
|
|
634
|
+
const elapsed = Date.now() - state.lastFlush;
|
|
635
|
+
const delay = elapsed >= topicThrottleMs ? 0 : topicThrottleMs - elapsed;
|
|
636
|
+
armTick(delay);
|
|
646
637
|
}
|
|
647
638
|
|
|
648
639
|
/**
|
|
@@ -669,31 +660,19 @@ export function createCursor(client, options = {}) {
|
|
|
669
660
|
|
|
670
661
|
let state = topicFlush.get(topic);
|
|
671
662
|
if (!state) {
|
|
672
|
-
state = { dirty: new Map(), inboundDirty: new Map(), lastFlush:
|
|
663
|
+
state = { dirty: new Map(), inboundDirty: new Map(), lastFlush: Date.now() - topicThrottleMs };
|
|
673
664
|
topicFlush.set(topic, state);
|
|
674
665
|
}
|
|
675
666
|
state.inboundDirty.set(key, { data, platform });
|
|
676
|
-
|
|
677
|
-
const now = Date.now();
|
|
678
|
-
if (now - state.lastFlush >= topicThrottleMs) {
|
|
679
|
-
// Symmetric with `broadcast()`: same shared `pendingMicroflush`
|
|
680
|
-
// per topic state, so if a local broadcast already claimed the
|
|
681
|
-
// slot the peer-relay entry just appends and ships with it.
|
|
682
|
-
state.lastFlush = now;
|
|
683
|
-
dirtyTopics.delete(topic);
|
|
684
|
-
if (!state.pendingMicroflush) {
|
|
685
|
-
state.pendingMicroflush = true;
|
|
686
|
-
queueMicrotask(() => {
|
|
687
|
-
state.pendingMicroflush = false;
|
|
688
|
-
if (state.dirty.size === 0 && state.inboundDirty.size === 0) return;
|
|
689
|
-
flushBoth(topic, state);
|
|
690
|
-
});
|
|
691
|
-
}
|
|
692
|
-
return;
|
|
693
|
-
}
|
|
694
|
-
|
|
695
667
|
dirtyTopics.add(topic);
|
|
696
|
-
|
|
668
|
+
|
|
669
|
+
// Symmetric with `broadcast()` always-tick: both source paths share
|
|
670
|
+
// the same `state` and the same tracker-wide tick timer, so a local
|
|
671
|
+
// broadcast and a peer-relayed inbound landing in the same loop
|
|
672
|
+
// iteration ship together as one combined frame at the next tick.
|
|
673
|
+
const elapsed = Date.now() - state.lastFlush;
|
|
674
|
+
const delay = elapsed >= topicThrottleMs ? 0 : topicThrottleMs - elapsed;
|
|
675
|
+
armTick(delay);
|
|
697
676
|
}
|
|
698
677
|
|
|
699
678
|
async function broadcastRemove(topic, key, platform) {
|