svelte-adapter-uws-extensions 0.5.2 → 0.5.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 +2 -2
- package/redis/cursor.js +53 -0
- package/redis/presence.js +36 -2
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.3",
|
|
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.3"
|
|
158
158
|
},
|
|
159
159
|
"dependencies": {
|
|
160
160
|
"ioredis": "^5.0.0"
|
package/redis/cursor.js
CHANGED
|
@@ -760,7 +760,23 @@ export function createCursor(client, options = {}) {
|
|
|
760
760
|
message(ws, { data, platform }) {
|
|
761
761
|
if (data && data.type === 'cursor' && data.topic && data.data !== undefined) {
|
|
762
762
|
tracker.update(ws, data.topic, data.data, platform);
|
|
763
|
+
return;
|
|
763
764
|
}
|
|
765
|
+
// Client-initiated reconnect-snapshot. The cursor plugin client
|
|
766
|
+
// sends `{type:'cursor-snapshot', topic}` on every status==='open'
|
|
767
|
+
// (initial connect + reconnect). Pre-fix, this text frame had no
|
|
768
|
+
// server handler and was a dead wire frame; the snapshot path
|
|
769
|
+
// only fired through `hooks.subscribe` -> `tracker.snapshot` when
|
|
770
|
+
// the ws subscribed to the `__cursor:{topic}` channel. With this
|
|
771
|
+
// branch, the snapshot also re-emits on the explicit frame so a
|
|
772
|
+
// reconnecting tab that resubscribes via `subscribe-batch` (which
|
|
773
|
+
// the adapter dedups when the topic is already in the user data
|
|
774
|
+
// set) still gets a fresh catalog + bulk.
|
|
775
|
+
if (data && data.type === 'cursor-snapshot' && typeof data.topic === 'string') {
|
|
776
|
+
tracker.snapshot(ws, data.topic, platform);
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
_warnCursorHooksMessageShape(data);
|
|
764
780
|
},
|
|
765
781
|
close(ws, { platform }) {
|
|
766
782
|
return tracker.remove(ws, platform);
|
|
@@ -770,3 +786,40 @@ export function createCursor(client, options = {}) {
|
|
|
770
786
|
|
|
771
787
|
return tracker;
|
|
772
788
|
}
|
|
789
|
+
|
|
790
|
+
/**
|
|
791
|
+
* One-time dev-warn dedup for `cursor.hooks.message` shape misuse. The most
|
|
792
|
+
* common cause is wiring the hook against `createMessage({ onUnhandled })`
|
|
793
|
+
* which passes raw bytes, not a parsed envelope. The fix is to switch to
|
|
794
|
+
* `createMessage({ onJsonMessage(ws, msg, platform) { ... } })` (svelte-
|
|
795
|
+
* realtime >= 0.5.9 + svelte-adapter-uws >= 0.5.3), which forwards the
|
|
796
|
+
* parsed object directly.
|
|
797
|
+
*/
|
|
798
|
+
let _cursorHooksMessageBadShapeWarned = false;
|
|
799
|
+
|
|
800
|
+
/**
|
|
801
|
+
* @param {any} data
|
|
802
|
+
*/
|
|
803
|
+
function _warnCursorHooksMessageShape(data) {
|
|
804
|
+
if (_cursorHooksMessageBadShapeWarned) return;
|
|
805
|
+
_cursorHooksMessageBadShapeWarned = true;
|
|
806
|
+
const got = data instanceof ArrayBuffer
|
|
807
|
+
? 'ArrayBuffer (raw bytes -- did you wire this from createMessage({onUnhandled}) ?)'
|
|
808
|
+
: Array.isArray(data)
|
|
809
|
+
? 'Array'
|
|
810
|
+
: data === null
|
|
811
|
+
? 'null'
|
|
812
|
+
: typeof data === 'object'
|
|
813
|
+
? 'object with data.type=' + String(data.type)
|
|
814
|
+
: typeof data;
|
|
815
|
+
console.warn(
|
|
816
|
+
'[redis/cursor] hooks.message called with unexpected shape (' + got + '). ' +
|
|
817
|
+
'Expected a parsed object {type:"cursor", topic, data} or ' +
|
|
818
|
+
'{type:"cursor-snapshot", topic}. ' +
|
|
819
|
+
'If you wired this from `createMessage({ onUnhandled })` and got raw bytes, ' +
|
|
820
|
+
'switch to `createMessage({ onJsonMessage(ws, msg, platform) { ... } })` ' +
|
|
821
|
+
'which forwards the parsed JSON envelope. ' +
|
|
822
|
+
'This warning fires once per process.\n' +
|
|
823
|
+
' See: https://svti.me/cursor-hooks-message'
|
|
824
|
+
);
|
|
825
|
+
}
|
package/redis/presence.js
CHANGED
|
@@ -522,8 +522,22 @@ export function createPresence(client, options = {}) {
|
|
|
522
522
|
pipe.hpexpire(topicHash, presenceTtlMs, 'FIELDS', 1, userKey);
|
|
523
523
|
}
|
|
524
524
|
if (activePlatform) {
|
|
525
|
-
|
|
526
|
-
|
|
525
|
+
// Publish a `{userKey: data}` map (instead of a key-only
|
|
526
|
+
// array) so a client whose entry aged out between
|
|
527
|
+
// heartbeats can re-add it from the heartbeat alone.
|
|
528
|
+
// Pre-fix, the wire carried only `keys` and the client
|
|
529
|
+
// handler could only refresh `existing` entries; an
|
|
530
|
+
// entry the client swept (cross-replica relay latency,
|
|
531
|
+
// brief backpressure, JS thread saturation) could never
|
|
532
|
+
// be recovered without a presence_diff for that user.
|
|
533
|
+
// Older clients fall back gracefully: they see an
|
|
534
|
+
// object instead of an array and skip the legacy
|
|
535
|
+
// "refresh-existing" branch, but the next presence_diff
|
|
536
|
+
// or presence_state still reconciles them.
|
|
537
|
+
/** @type {Record<string, any>} */
|
|
538
|
+
const dataMap = {};
|
|
539
|
+
for (const [userKey, entry] of data) dataMap[userKey] = entry.data;
|
|
540
|
+
activePlatform.publish('__presence:' + topic, 'heartbeat', dataMap);
|
|
527
541
|
}
|
|
528
542
|
}
|
|
529
543
|
}
|
|
@@ -1360,6 +1374,26 @@ export function createPresence(client, options = {}) {
|
|
|
1360
1374
|
}
|
|
1361
1375
|
await tracker.join(ws, topic, platform);
|
|
1362
1376
|
},
|
|
1377
|
+
message(ws, { data, platform }) {
|
|
1378
|
+
// Client-initiated reconnect-snapshot. The presence plugin
|
|
1379
|
+
// client sends `{type:'presence-snapshot', topic}` on every
|
|
1380
|
+
// status==='open' (initial connect + reconnect). Re-emits
|
|
1381
|
+
// `presence_state` to the requesting ws via `tracker.sync`,
|
|
1382
|
+
// which is the same path that fires on a fresh subscribe.
|
|
1383
|
+
// Symmetric to cursor's `cursor-snapshot` text frame.
|
|
1384
|
+
//
|
|
1385
|
+
// Without this, board-scoped presence stayed stale across
|
|
1386
|
+
// reconnects: a tab that had joined via an RPC saw no
|
|
1387
|
+
// presence_diff during the disconnect window, and on
|
|
1388
|
+
// reconnect its in-memory map was whatever it last knew.
|
|
1389
|
+
// Global presence accidentally self-healed because most
|
|
1390
|
+
// apps call `presence.join('global')` from the `open` hook
|
|
1391
|
+
// which fires on every reconnect; per-board presence does
|
|
1392
|
+
// not have an equivalent auto-rejoin.
|
|
1393
|
+
if (data && data.type === 'presence-snapshot' && typeof data.topic === 'string') {
|
|
1394
|
+
tracker.sync(ws, data.topic, platform).catch(() => { /* surfaced via breaker */ });
|
|
1395
|
+
}
|
|
1396
|
+
},
|
|
1363
1397
|
async unsubscribe(ws, topic, { platform }) {
|
|
1364
1398
|
if (topic.startsWith('__presence:')) {
|
|
1365
1399
|
const realTopic = topic.slice('__presence:'.length);
|