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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelte-adapter-uws-extensions",
3
- "version": "0.5.2",
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.2"
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
- const keys = [...data.keys()];
526
- activePlatform.publish('__presence:' + topic, 'heartbeat', keys);
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);