svelte-realtime 0.5.3 → 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.
Files changed (3) hide show
  1. package/package.json +1 -1
  2. package/server.d.ts +17 -0
  3. package/server.js +54 -23
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelte-realtime",
3
- "version": "0.5.3",
3
+ "version": "0.5.4",
4
4
  "publishConfig": {
5
5
  "tag": "latest"
6
6
  },
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
- /** @type {Set<any>} Tracks platforms with active samplers so reset can clear them. */
986
- const _publishRateActivePlatforms = new Set();
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
- const top = platform.pressure?.topPublishers;
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, stops every active sampler, and removes the
1038
- * activation marker so the next ctx-helpers cache miss for a previously
1039
- * seen platform re-attaches a sampler. Does NOT reset config back to
1040
- * defaults - tests that mutate config should restore it themselves.
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
- for (const platform of _publishRateActivePlatforms) {
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
- // Stop active samplers so disable takes effect immediately.
2187
- for (const platform of _publishRateActivePlatforms) {
2188
- const sampler = _publishRateSamplers.get(platform);
2189
- if (sampler) clearInterval(sampler);
2190
- _publishRateSamplers.delete(platform);
2191
- }
2192
- _publishRateActivePlatforms.clear();
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) {
@@ -7696,10 +7727,10 @@ function _respond(ws, platform, correlationId, payload) {
7696
7727
  if (_IS_DEV) {
7697
7728
  // Estimate size without double-serialization.
7698
7729
  const data = payload.data;
7699
- if ((Array.isArray(data) && data.length > 100) || (typeof data === 'string' && data.length > 12000)) {
7730
+ if ((Array.isArray(data) && data.length > 5000) || (typeof data === 'string' && data.length > 800_000)) {
7700
7731
  console.warn(
7701
7732
  `[svelte-realtime] RPC response for '${correlationId}' contains ${data.length} items - ` +
7702
- 'large responses may exceed maxPayloadLength (16KB). Increase maxPayloadLength in adapter config if needed.\n See: https://svti.me/adapter-config'
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"
7703
7734
  );
7704
7735
  }
7705
7736
  }