svelte-realtime 0.5.2 → 0.5.4
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 +3 -3
- package/server.d.ts +17 -0
- package/server.js +214 -48
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "svelte-realtime",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.4",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"tag": "latest"
|
|
6
6
|
},
|
|
@@ -89,12 +89,12 @@
|
|
|
89
89
|
"peerDependencies": {
|
|
90
90
|
"@sveltejs/kit": "^2.0.0",
|
|
91
91
|
"svelte": "^4.0.0 || ^5.0.0",
|
|
92
|
-
"svelte-adapter-uws": "^0.5.
|
|
92
|
+
"svelte-adapter-uws": "^0.5.1"
|
|
93
93
|
},
|
|
94
94
|
"devDependencies": {
|
|
95
95
|
"@playwright/test": "^1.59.1",
|
|
96
96
|
"fast-check": "^4.7.0",
|
|
97
|
-
"svelte-adapter-uws-extensions": "^0.5.
|
|
97
|
+
"svelte-adapter-uws-extensions": "^0.5.1",
|
|
98
98
|
"vitest": "^4.0.18"
|
|
99
99
|
},
|
|
100
100
|
"keywords": [
|
package/server.d.ts
CHANGED
|
@@ -619,6 +619,23 @@ export const combineMerge: <T extends object>(...buckets: Array<T | undefined>)
|
|
|
619
619
|
*/
|
|
620
620
|
export const MAX_AGGREGATE_BUCKETS: number;
|
|
621
621
|
|
|
622
|
+
/**
|
|
623
|
+
* Maximum entries in the per-userId connection registry that backs
|
|
624
|
+
* `live.push({ userId })` / `live.notify`. Saturation behaviour is
|
|
625
|
+
* WARN-once and skip new registrations; existing entries continue to
|
|
626
|
+
* route. Default 10,000,000.
|
|
627
|
+
*/
|
|
628
|
+
export const MAX_PUSH_REGISTRY: number;
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Maximum entries in the in-memory presence-ref map that backs
|
|
632
|
+
* `live.room({ presence })` on the single-instance / dev path. When
|
|
633
|
+
* `platform.presence.list` is wired (e.g. via the Redis presence
|
|
634
|
+
* extension), this cap is bypassed. Saturation behaviour is WARN-once
|
|
635
|
+
* and skip new entries. Default 1,000,000.
|
|
636
|
+
*/
|
|
637
|
+
export const MAX_PRESENCE_REF: number;
|
|
638
|
+
|
|
622
639
|
export type TopicEntry = string | ((...args: any[]) => string);
|
|
623
640
|
|
|
624
641
|
/**
|
package/server.js
CHANGED
|
@@ -982,8 +982,16 @@ const _publishRateConfig = {
|
|
|
982
982
|
const _publishRateWarned = new Set();
|
|
983
983
|
/** @type {WeakMap<any, ReturnType<typeof setInterval>>} */
|
|
984
984
|
const _publishRateSamplers = new WeakMap();
|
|
985
|
-
/**
|
|
986
|
-
|
|
985
|
+
/**
|
|
986
|
+
* Bumped by `_resetPublishRateWarning` and by `live.publishRateWarning(false)`.
|
|
987
|
+
* Each sampler captures its activation-time epoch and self-clears on the next
|
|
988
|
+
* fire when the epoch no longer matches. Pattern used in place of the prior
|
|
989
|
+
* strong-reference `Set<platform>` because that Set held every dev-mode
|
|
990
|
+
* platform alive across the process lifetime, defeating the WeakMap above and
|
|
991
|
+
* leaking the platform + all captured helpers/closures on every per-call
|
|
992
|
+
* wrap pattern (e.g. cron tick wrapping a fresh `bus.wrap(platform)` per fire).
|
|
993
|
+
*/
|
|
994
|
+
let _publishRateEpoch = 0;
|
|
987
995
|
|
|
988
996
|
/**
|
|
989
997
|
* Activate the dev-mode publish-rate warning sampler for one platform.
|
|
@@ -993,6 +1001,15 @@ const _publishRateActivePlatforms = new Set();
|
|
|
993
1001
|
* underscore prefix so tests can drive activation deterministically
|
|
994
1002
|
* without going through the async RPC path.
|
|
995
1003
|
*
|
|
1004
|
+
* The sampler closure must NOT strongly capture `platform`. Node's timer
|
|
1005
|
+
* queue holds the `setInterval` Timer alive until clearInterval fires; if
|
|
1006
|
+
* the closure captured `platform` directly, every platform ever passed in
|
|
1007
|
+
* would stay reachable forever, leaking the entire helpers+closures graph.
|
|
1008
|
+
* The `WeakRef` wrapper here breaks that retention: on each tick the
|
|
1009
|
+
* sampler derefs, and a null deref (platform GC'd elsewhere) self-clears
|
|
1010
|
+
* the timer. Net effect: at most one stale tick after platform GC, then
|
|
1011
|
+
* the entry vanishes.
|
|
1012
|
+
*
|
|
996
1013
|
* @param {any} platform
|
|
997
1014
|
*/
|
|
998
1015
|
export function _activatePublishRateWarning(platform) {
|
|
@@ -1000,8 +1017,22 @@ export function _activatePublishRateWarning(platform) {
|
|
|
1000
1017
|
if (!_publishRateConfig.enabled) return;
|
|
1001
1018
|
if (_publishRateSamplers.has(platform)) return;
|
|
1002
1019
|
if (typeof platform?.pressure !== 'object' || platform.pressure === null) return;
|
|
1020
|
+
const platformRef = new WeakRef(platform);
|
|
1021
|
+
const epoch = _publishRateEpoch;
|
|
1003
1022
|
const sampler = setInterval(() => {
|
|
1004
|
-
|
|
1023
|
+
// Self-clear on disable / reset / platform-GC. Any of the three
|
|
1024
|
+
// makes the sampler stale; clearInterval here lets Node drop the
|
|
1025
|
+
// Timer from the queue on the next event-loop turn.
|
|
1026
|
+
if (!_publishRateConfig.enabled || epoch !== _publishRateEpoch) {
|
|
1027
|
+
clearInterval(sampler);
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
const p = platformRef.deref();
|
|
1031
|
+
if (!p) {
|
|
1032
|
+
clearInterval(sampler);
|
|
1033
|
+
return;
|
|
1034
|
+
}
|
|
1035
|
+
const top = p.pressure?.topPublishers;
|
|
1005
1036
|
if (!Array.isArray(top)) return;
|
|
1006
1037
|
for (const entry of top) {
|
|
1007
1038
|
if (!entry || typeof entry.topic !== 'string') continue;
|
|
@@ -1029,24 +1060,24 @@ export function _activatePublishRateWarning(platform) {
|
|
|
1029
1060
|
}, _publishRateConfig.intervalMs);
|
|
1030
1061
|
if (typeof sampler.unref === 'function') sampler.unref();
|
|
1031
1062
|
_publishRateSamplers.set(platform, sampler);
|
|
1032
|
-
_publishRateActivePlatforms.add(platform);
|
|
1033
1063
|
}
|
|
1034
1064
|
|
|
1035
1065
|
/**
|
|
1036
1066
|
* Reset the dev-mode publish-rate warning state. Tests only. Clears the
|
|
1037
|
-
* one-shot warned set
|
|
1038
|
-
*
|
|
1039
|
-
*
|
|
1040
|
-
*
|
|
1067
|
+
* one-shot warned set and bumps the per-process epoch so every existing
|
|
1068
|
+
* sampler self-clears on its next fire. Stale samplers stop within one
|
|
1069
|
+
* `intervalMs` of the reset (default 5s); a same-platform re-activation
|
|
1070
|
+
* after reset gets a fresh sampler because the old WeakMap entry's
|
|
1071
|
+
* sampler will self-clear on its next tick and never write state again.
|
|
1072
|
+
*
|
|
1073
|
+
* If a test needs synchronous teardown (e.g. to assert no extra warns
|
|
1074
|
+
* fire after reset within the same tick), call this AND assert that
|
|
1075
|
+
* `_publishRateConfig.enabled` is false; samplers short-circuit on the
|
|
1076
|
+
* disabled flag without doing any work.
|
|
1041
1077
|
*/
|
|
1042
1078
|
export function _resetPublishRateWarning() {
|
|
1043
1079
|
_publishRateWarned.clear();
|
|
1044
|
-
|
|
1045
|
-
const sampler = _publishRateSamplers.get(platform);
|
|
1046
|
-
if (sampler) clearInterval(sampler);
|
|
1047
|
-
_publishRateSamplers.delete(platform);
|
|
1048
|
-
}
|
|
1049
|
-
_publishRateActivePlatforms.clear();
|
|
1080
|
+
_publishRateEpoch++;
|
|
1050
1081
|
}
|
|
1051
1082
|
|
|
1052
1083
|
/**
|
|
@@ -2183,13 +2214,13 @@ export function _resetRateLimits() {
|
|
|
2183
2214
|
live.publishRateWarning = function publishRateWarning(config) {
|
|
2184
2215
|
if (config === false) {
|
|
2185
2216
|
_publishRateConfig.enabled = false;
|
|
2186
|
-
//
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2217
|
+
// Existing samplers self-clear on their next fire via the
|
|
2218
|
+
// `_publishRateConfig.enabled` check at the top of the callback.
|
|
2219
|
+
// Bumping the epoch is belt-and-suspenders: a sampler whose
|
|
2220
|
+
// callback is in flight when the flag flips still sees the
|
|
2221
|
+
// epoch mismatch on its NEXT scheduled fire. Worst case is one
|
|
2222
|
+
// stale interval (default 5s) before the timer goes idle.
|
|
2223
|
+
_publishRateEpoch++;
|
|
2193
2224
|
return;
|
|
2194
2225
|
}
|
|
2195
2226
|
if (config === undefined || config === true) {
|
|
@@ -4518,6 +4549,131 @@ export function _getIdentityKey(ctx) {
|
|
|
4518
4549
|
return guestId;
|
|
4519
4550
|
}
|
|
4520
4551
|
|
|
4552
|
+
/**
|
|
4553
|
+
* Cluster-shared presence-ref store. Used by `live.room({ presence })` when
|
|
4554
|
+
* `platform.redis` is wired by the host app (raw ioredis-shaped client with
|
|
4555
|
+
* hincrby / hset / hdel / hgetall / expire). Falls back to the in-process
|
|
4556
|
+
* `_presenceRef` Map when absent, preserving zero-config dev behavior.
|
|
4557
|
+
*
|
|
4558
|
+
* Storage layout (one Redis HASH per topic):
|
|
4559
|
+
* key: `__live-presence:{topic}`
|
|
4560
|
+
* fields: 'c:{userKey}' -> integer (cluster-wide subscriber count)
|
|
4561
|
+
* 'd:{userKey}' -> JSON-stringified presence data
|
|
4562
|
+
*
|
|
4563
|
+
* Cluster semantics:
|
|
4564
|
+
* - First replica to take a user's count from 0->1 publishes 'join' (cluster-
|
|
4565
|
+
* wide isFirst). Subsequent replicas just increment.
|
|
4566
|
+
* - Last replica to take a user's count from 1->0 publishes 'leave'.
|
|
4567
|
+
* - Loader returns the full HGETALL view so any replica's new subscriber sees
|
|
4568
|
+
* all users from all replicas, not just the locally-attached ones.
|
|
4569
|
+
*
|
|
4570
|
+
* Per-replica refcount (multiple tabs of the same user on the same replica)
|
|
4571
|
+
* and grace-timer behavior stay in the existing _presenceRef Map. The cluster
|
|
4572
|
+
* helpers only fire at the local 0<->1 transitions, so a quick reconnect
|
|
4573
|
+
* burst on a single replica doesn't churn the cluster counter.
|
|
4574
|
+
*/
|
|
4575
|
+
const _PRESENCE_KEY_PREFIX = '__live-presence:';
|
|
4576
|
+
const _PRESENCE_TTL_SEC = 3600;
|
|
4577
|
+
|
|
4578
|
+
/**
|
|
4579
|
+
* Bump the cluster-wide count for (topic, key). Returns isFirst=true when
|
|
4580
|
+
* this acquire took the count from 0 to 1 cluster-wide, signaling that the
|
|
4581
|
+
* caller should publish a 'join' event. Falls through to a no-op stub when
|
|
4582
|
+
* platform.redis is missing (single-replica dev path).
|
|
4583
|
+
*/
|
|
4584
|
+
async function _clusterPresenceAcquire(platform, topic, key, data) {
|
|
4585
|
+
const redis = platform && platform.redis;
|
|
4586
|
+
if (!redis || typeof redis.hincrby !== 'function') return { isFirst: true };
|
|
4587
|
+
const hKey = _PRESENCE_KEY_PREFIX + topic;
|
|
4588
|
+
const countField = 'c:' + key;
|
|
4589
|
+
const dataField = 'd:' + key;
|
|
4590
|
+
let serialized;
|
|
4591
|
+
try { serialized = JSON.stringify(data); } catch { serialized = 'null'; }
|
|
4592
|
+
try {
|
|
4593
|
+
// Write the data field BEFORE bumping the count. A concurrent
|
|
4594
|
+
// `_clusterPresenceList` (e.g. the same user's own :presence stream
|
|
4595
|
+
// loader racing the data stream's acquire) reads data fields only;
|
|
4596
|
+
// if HINCRBY ran first the loader could observe a count without a
|
|
4597
|
+
// data field and return an empty roster, missing the user's own
|
|
4598
|
+
// entry. Writing data first guarantees the loader sees the entry
|
|
4599
|
+
// as soon as the count is visible.
|
|
4600
|
+
await redis.hset(hKey, dataField, serialized);
|
|
4601
|
+
const count = await redis.hincrby(hKey, countField, 1);
|
|
4602
|
+
if (count === 1) {
|
|
4603
|
+
await redis.expire(hKey, _PRESENCE_TTL_SEC);
|
|
4604
|
+
return { isFirst: true };
|
|
4605
|
+
}
|
|
4606
|
+
// Refresh TTL on activity so the hash doesn't expire under a busy room.
|
|
4607
|
+
try { await redis.expire(hKey, _PRESENCE_TTL_SEC); } catch { /* best-effort */ }
|
|
4608
|
+
return { isFirst: false };
|
|
4609
|
+
} catch {
|
|
4610
|
+
// Redis blip: treat as first so we publish a join. Worst case a duplicate
|
|
4611
|
+
// 'join' merges idempotently by key on the client.
|
|
4612
|
+
return { isFirst: true };
|
|
4613
|
+
}
|
|
4614
|
+
}
|
|
4615
|
+
|
|
4616
|
+
/**
|
|
4617
|
+
* Decrement the cluster-wide count for (topic, key). Returns isLast=true when
|
|
4618
|
+
* this release took the count from 1 to 0 cluster-wide, signaling that the
|
|
4619
|
+
* caller should publish a 'leave' event. Falls through to isLast=true when
|
|
4620
|
+
* platform.redis is missing (single-replica dev path treats every grace-timer
|
|
4621
|
+
* expiry as the final leave).
|
|
4622
|
+
*/
|
|
4623
|
+
async function _clusterPresenceRelease(platform, topic, key) {
|
|
4624
|
+
const redis = platform && platform.redis;
|
|
4625
|
+
if (!redis || typeof redis.hincrby !== 'function') return { isLast: true };
|
|
4626
|
+
const hKey = _PRESENCE_KEY_PREFIX + topic;
|
|
4627
|
+
const countField = 'c:' + key;
|
|
4628
|
+
const dataField = 'd:' + key;
|
|
4629
|
+
try {
|
|
4630
|
+
const count = await redis.hincrby(hKey, countField, -1);
|
|
4631
|
+
if (count <= 0) {
|
|
4632
|
+
await redis.hdel(hKey, countField, dataField);
|
|
4633
|
+
return { isLast: true };
|
|
4634
|
+
}
|
|
4635
|
+
return { isLast: false };
|
|
4636
|
+
} catch {
|
|
4637
|
+
// Redis blip: assume last so we publish a leave. A late observer reading
|
|
4638
|
+
// HGETALL might still see the stale field until the next acquire repairs
|
|
4639
|
+
// the count (or the hash TTL expires).
|
|
4640
|
+
return { isLast: true };
|
|
4641
|
+
}
|
|
4642
|
+
}
|
|
4643
|
+
|
|
4644
|
+
/**
|
|
4645
|
+
* Return the cluster-wide presence roster for a topic as `[{key, data}, ...]`.
|
|
4646
|
+
* Falls through to the local _presenceRef iteration when platform.redis is
|
|
4647
|
+
* missing.
|
|
4648
|
+
*/
|
|
4649
|
+
async function _clusterPresenceList(platform, topic) {
|
|
4650
|
+
const redis = platform && platform.redis;
|
|
4651
|
+
if (!redis || typeof redis.hgetall !== 'function') {
|
|
4652
|
+
const prefix = topic + '\0';
|
|
4653
|
+
const out = [];
|
|
4654
|
+
for (const [refKey, ref] of _presenceRef) {
|
|
4655
|
+
if (!refKey.startsWith(prefix)) continue;
|
|
4656
|
+
if (ref.data == null) continue;
|
|
4657
|
+
out.push({ key: refKey.slice(prefix.length), data: ref.data });
|
|
4658
|
+
}
|
|
4659
|
+
return out;
|
|
4660
|
+
}
|
|
4661
|
+
const hKey = _PRESENCE_KEY_PREFIX + topic;
|
|
4662
|
+
try {
|
|
4663
|
+
const all = await redis.hgetall(hKey);
|
|
4664
|
+
const out = [];
|
|
4665
|
+
for (const field of Object.keys(all)) {
|
|
4666
|
+
if (field.length < 3 || field[0] !== 'd' || field[1] !== ':') continue;
|
|
4667
|
+
const key = field.slice(2);
|
|
4668
|
+
try { out.push({ key, data: JSON.parse(all[field]) }); }
|
|
4669
|
+
catch { /* skip corrupt entry */ }
|
|
4670
|
+
}
|
|
4671
|
+
return out;
|
|
4672
|
+
} catch {
|
|
4673
|
+
return [];
|
|
4674
|
+
}
|
|
4675
|
+
}
|
|
4676
|
+
|
|
4521
4677
|
live.room = function room(config) {
|
|
4522
4678
|
const {
|
|
4523
4679
|
topic: topicFn,
|
|
@@ -4562,7 +4718,7 @@ live.room = function room(config) {
|
|
|
4562
4718
|
}, {
|
|
4563
4719
|
merge: mergeMode,
|
|
4564
4720
|
key: keyField,
|
|
4565
|
-
onSubscribe: presenceFn ? (ctx, topic) => {
|
|
4721
|
+
onSubscribe: presenceFn ? async (ctx, topic) => {
|
|
4566
4722
|
const userId = _getIdentityKey(ctx);
|
|
4567
4723
|
const refKey = topic + '\0' + userId;
|
|
4568
4724
|
|
|
@@ -4582,7 +4738,13 @@ live.room = function room(config) {
|
|
|
4582
4738
|
if (r.timer) {
|
|
4583
4739
|
clearTimeout(r.timer);
|
|
4584
4740
|
const [t, u] = k.split('\0');
|
|
4585
|
-
|
|
4741
|
+
// Cluster release runs eagerly here too: an evicted entry
|
|
4742
|
+
// would otherwise leak a phantom counter on Redis.
|
|
4743
|
+
_clusterPresenceRelease(ctx.platform, t, u).then((res) => {
|
|
4744
|
+
if (res.isLast) {
|
|
4745
|
+
ctx.publish(t + ':presence', 'leave', { key: u });
|
|
4746
|
+
}
|
|
4747
|
+
}).catch(() => {});
|
|
4586
4748
|
if (onLeave) {
|
|
4587
4749
|
Promise.resolve().then(() => onLeave(ctx, t)).catch(() => {});
|
|
4588
4750
|
}
|
|
@@ -4595,7 +4757,7 @@ live.room = function room(config) {
|
|
|
4595
4757
|
console.warn(
|
|
4596
4758
|
"[svelte-realtime] presence-ref map reached MAX_PRESENCE_REF=" + _maxPresenceRef +
|
|
4597
4759
|
"; new joiners will not appear in any subscriber's roster until existing entries clear.\n" +
|
|
4598
|
-
" For multi-instance deploys, wire `platform.
|
|
4760
|
+
" For multi-instance deploys, wire `platform.redis` (raw ioredis client) so the cluster-shared Redis presence is bypasses the in-memory cap.\n" +
|
|
4599
4761
|
" See: https://svti.me/presence"
|
|
4600
4762
|
);
|
|
4601
4763
|
}
|
|
@@ -4609,8 +4771,14 @@ live.room = function room(config) {
|
|
|
4609
4771
|
// to the :presence topic.
|
|
4610
4772
|
const presenceData = presenceFn(ctx);
|
|
4611
4773
|
_presenceRef.set(refKey, { count: 1, timer: null, data: presenceData });
|
|
4774
|
+
// Cluster transition: bump shared count; only the first replica to
|
|
4775
|
+
// reach 1 publishes 'join'. With no platform.redis the helper
|
|
4776
|
+
// returns isFirst=true unconditionally, matching the in-memory path.
|
|
4612
4777
|
if (presenceData) {
|
|
4613
|
-
ctx.
|
|
4778
|
+
const { isFirst } = await _clusterPresenceAcquire(ctx.platform, topic, userId, presenceData);
|
|
4779
|
+
if (isFirst) {
|
|
4780
|
+
ctx.publish(topic + ':presence', 'join', { key: userId, data: presenceData });
|
|
4781
|
+
}
|
|
4614
4782
|
}
|
|
4615
4783
|
} : undefined,
|
|
4616
4784
|
onUnsubscribe: presenceFn ? (ctx, topic) => {
|
|
@@ -4623,11 +4791,17 @@ live.room = function room(config) {
|
|
|
4623
4791
|
ref.count--;
|
|
4624
4792
|
if (ref.count > 0) return;
|
|
4625
4793
|
|
|
4626
|
-
// On rollback (failed stream init), skip grace and
|
|
4794
|
+
// On rollback (failed stream init), skip grace and release
|
|
4795
|
+
// immediately. Cluster release decides whether this was the LAST
|
|
4796
|
+
// subscriber across the cluster - only then do we publish 'leave'.
|
|
4627
4797
|
if (ctx.ws && _rollingBack.has(ctx.ws)) {
|
|
4628
4798
|
if (ref.timer) clearTimeout(ref.timer);
|
|
4629
4799
|
_presenceRef.delete(refKey);
|
|
4630
|
-
ctx.
|
|
4800
|
+
_clusterPresenceRelease(ctx.platform, topic, userId).then((res) => {
|
|
4801
|
+
if (res.isLast) {
|
|
4802
|
+
ctx.publish(topic + ':presence', 'leave', { key: userId });
|
|
4803
|
+
}
|
|
4804
|
+
}).catch(() => {});
|
|
4631
4805
|
if (onLeave) {
|
|
4632
4806
|
Promise.resolve().then(() => onLeave(ctx, topic)).catch(() => {});
|
|
4633
4807
|
}
|
|
@@ -4636,7 +4810,11 @@ live.room = function room(config) {
|
|
|
4636
4810
|
|
|
4637
4811
|
ref.timer = setTimeout(() => {
|
|
4638
4812
|
_presenceRef.delete(refKey);
|
|
4639
|
-
ctx.
|
|
4813
|
+
_clusterPresenceRelease(ctx.platform, topic, userId).then((res) => {
|
|
4814
|
+
if (res.isLast) {
|
|
4815
|
+
ctx.publish(topic + ':presence', 'leave', { key: userId });
|
|
4816
|
+
}
|
|
4817
|
+
}).catch(() => {});
|
|
4640
4818
|
if (onLeave) {
|
|
4641
4819
|
Promise.resolve().then(() => onLeave(ctx, topic)).catch(() => {});
|
|
4642
4820
|
}
|
|
@@ -4658,24 +4836,12 @@ live.room = function room(config) {
|
|
|
4658
4836
|
async (ctx, ...args) => {
|
|
4659
4837
|
if (guardFn) await guardFn(ctx, ...args);
|
|
4660
4838
|
const dataTopic = topicFn(ctx, ...args);
|
|
4661
|
-
|
|
4662
|
-
|
|
4663
|
-
|
|
4664
|
-
//
|
|
4665
|
-
//
|
|
4666
|
-
|
|
4667
|
-
// would never see its own join. Reconstructs the roster from
|
|
4668
|
-
// `_presenceRef`. Production wires `platform.presence.list`
|
|
4669
|
-
// (Redis-backed) for cluster-wide consistency and bypasses
|
|
4670
|
-
// this branch.
|
|
4671
|
-
const prefix = dataTopic + '\0';
|
|
4672
|
-
const result = [];
|
|
4673
|
-
for (const [refKey, ref] of _presenceRef) {
|
|
4674
|
-
if (!refKey.startsWith(prefix)) continue;
|
|
4675
|
-
if (ref.data == null) continue;
|
|
4676
|
-
result.push({ key: refKey.slice(prefix.length), data: ref.data });
|
|
4677
|
-
}
|
|
4678
|
-
return result;
|
|
4839
|
+
// Cluster-shared roster when `platform.redis` is wired; falls
|
|
4840
|
+
// back to the local _presenceRef iteration otherwise. The
|
|
4841
|
+
// loader reconstructs the roster even when this user's join
|
|
4842
|
+
// was published before they subscribed to :presence (the live
|
|
4843
|
+
// merge takes over from here).
|
|
4844
|
+
return _clusterPresenceList(ctx.platform, dataTopic);
|
|
4679
4845
|
},
|
|
4680
4846
|
{ merge: 'presence' }
|
|
4681
4847
|
);
|
|
@@ -7561,10 +7727,10 @@ function _respond(ws, platform, correlationId, payload) {
|
|
|
7561
7727
|
if (_IS_DEV) {
|
|
7562
7728
|
// Estimate size without double-serialization.
|
|
7563
7729
|
const data = payload.data;
|
|
7564
|
-
if ((Array.isArray(data) && data.length >
|
|
7730
|
+
if ((Array.isArray(data) && data.length > 5000) || (typeof data === 'string' && data.length > 800_000)) {
|
|
7565
7731
|
console.warn(
|
|
7566
7732
|
`[svelte-realtime] RPC response for '${correlationId}' contains ${data.length} items - ` +
|
|
7567
|
-
|
|
7733
|
+
"large responses may exceed maxPayloadLength (default 1 MB; raise `websocket.maxPayloadLength` in svelte.config.js if needed).\n See: https://svti.me/adapter-config"
|
|
7568
7734
|
);
|
|
7569
7735
|
}
|
|
7570
7736
|
}
|