svelte-realtime 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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/server.js +138 -99
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelte-realtime",
3
- "version": "0.5.6",
3
+ "version": "0.5.7",
4
4
  "publishConfig": {
5
5
  "tag": "latest"
6
6
  },
package/server.js CHANGED
@@ -3461,15 +3461,6 @@ let _bus = null;
3461
3461
  */
3462
3462
  let _cronBus = null;
3463
3463
 
3464
- /**
3465
- * Sentinel attached to platforms that have been wrapped with the
3466
- * process-wide bus. Lets the framework detect already-wrapped inputs
3467
- * and skip re-wrapping, so a user who passes a manually `bus.wrap`-ed
3468
- * platform via `createMessage({ platform })` is not double-wrapped by
3469
- * the auto-wrap path.
3470
- */
3471
- const _BUS_WRAPPED = Symbol.for('svelte-realtime.busWrapped');
3472
-
3473
3464
  /**
3474
3465
  * Write the process-wide bus. Validated like `configureCron({ bus })`
3475
3466
  * - must expose `.wrap(platform)` or be `null`. Mirrored into the
@@ -5166,43 +5157,73 @@ export function __registerDerived(path, fn) {
5166
5157
  * triggered externally when the platform fires publish.
5167
5158
  * @param {import('svelte-adapter-uws').Platform} platform
5168
5159
  */
5169
- /** @type {WeakSet<object>} Guard against double-wrapping platform.publish during HMR */
5160
+ /**
5161
+ * Tracks platforms whose `publish` has been swapped to `derivedPublish`
5162
+ * by `_wrapPlatformPublish`. WeakSet so per-connection platform clones
5163
+ * inherit the mutation via prototype chain without forcing the base
5164
+ * platform to live longer than the adapter intends - entries clear
5165
+ * naturally when the platform itself becomes GC-eligible. The WeakSet
5166
+ * is the single source of truth for "is this platform's publish path
5167
+ * framework-owned?" - consulted by `_ensureWrap` (the universal idempotent
5168
+ * installer), referenced indirectly by every publish surface (RPC, cron,
5169
+ * reactive, top-level `publish()`).
5170
+ *
5171
+ * @type {WeakSet<object>}
5172
+ */
5170
5173
  const _activatedPlatforms = new WeakSet();
5171
5174
 
5172
- export function _activateDerived(platform) {
5173
- _derivedPlatform = platform;
5174
- _activateDerivedCalled = true;
5175
-
5176
- // Only wrap platform.publish if there are actual reactive registrations,
5177
- // OR a lazy push has signaled "registrations are coming." Without the
5178
- // `_hasLazyReactive` clause, calling `_activateDerived` from
5179
- // `init({ platform })` (the README's recommended call site) would
5180
- // early-return on a still-empty registry and leave the wrap uninstalled
5181
- // - so a cron-driven publish that fires before the lazy queue resolves
5182
- // (or before the first WS connection) silently bypasses every watcher.
5183
- if (
5184
- _derivedBySource.size === 0
5185
- && _effectBySource.size === 0
5186
- && _aggregateBySource.size === 0
5187
- && !_hasDynamicDerived
5188
- && !_hasLazyReactive
5189
- ) {
5190
- return;
5191
- }
5192
-
5175
+ /**
5176
+ * Universal install point for the framework's publish wrap. Idempotent
5177
+ * against `_activatedPlatforms`, safe under HMR, called from every site
5178
+ * that captures or first sees a platform reference:
5179
+ * - `setCronPlatform(platform)` - call from `realtime().init` or
5180
+ * directly from `hooks.ws.js`'s `init({ platform })`.
5181
+ * - `_activateDerived(platform)` - same call site, alternative entry.
5182
+ * - The default `message` hook + `createMessage` returned hook - first
5183
+ * message per platform installs the wrap, so apps that wire only
5184
+ * `setBus(bus)` and re-export `message` (no init hook, no
5185
+ * `_activateDerived` call) still get cluster routing on first RPC.
5186
+ *
5187
+ * Single install site eliminates the entire class of "outer wrap stacks
5188
+ * on inner wrap" bugs: there is only ONE `bus.wrap(...)` call in the
5189
+ * whole framework (inside `_wrapPlatformPublish`'s `_refreshBusCache`)
5190
+ * and it's composed with everything else (reactive watchers, batched
5191
+ * fast path, replay routing) at publish time via the mutated
5192
+ * `derivedPublish` / `derivedPublishBatched`.
5193
+ *
5194
+ * @param {any} platform
5195
+ */
5196
+ function _ensureWrap(platform) {
5197
+ if (!platform) return;
5193
5198
  // svelte-adapter-uws hands hooks a per-connection platform created via
5194
- // Object.create(basePlatform). Wrapping that per-connection object leaves
5195
- // every other connection's inherited publish / publishBatched untouched,
5196
- // because their lookups walk the prototype chain to the original base.
5197
- // Resolve to the base prototype so the wrap is visible to all connections
5198
- // that share it. Test mocks pass plain objects whose proto is
5199
- // Object.prototype - in that case wrap the object itself.
5199
+ // Object.create(basePlatform). Wrapping that per-connection object would
5200
+ // leave every other connection's inherited publish / publishBatched
5201
+ // untouched, because their lookups walk the prototype chain to the
5202
+ // original base. Resolve to the base prototype so the wrap is visible
5203
+ // to all connections that share it. Test mocks pass plain objects whose
5204
+ // proto is Object.prototype - in that case wrap the object itself.
5200
5205
  const target = _resolveWrapTarget(platform);
5201
5206
  if (_activatedPlatforms.has(target)) return;
5202
5207
  _activatedPlatforms.add(target);
5203
5208
  _wrapPlatformPublish(target);
5204
5209
  }
5205
5210
 
5211
+ export function _activateDerived(platform) {
5212
+ _derivedPlatform = platform;
5213
+ _activateDerivedCalled = true;
5214
+ // Install the framework's publish wrap unconditionally. Pre-0.5.7 this
5215
+ // was gated on "any reactive primitives registered?" to avoid wrap
5216
+ // overhead on apps that didn't use derived/effect/aggregate. With the
5217
+ // wrap now also responsible for bus routing (every publish surface
5218
+ // consults `_getBus()` via `derivedPublish`), gating would create a
5219
+ // window where a publish escapes routing - the late-activation race
5220
+ // from the 0.5.6 audit. The per-publish overhead of an empty wrap is
5221
+ // one function call plus a `Map.has` check on an empty Map (`O(1)`,
5222
+ // branch-predicted to false); the install cost is one closure scope
5223
+ // per platform, paid once at init.
5224
+ _ensureWrap(platform);
5225
+ }
5226
+
5206
5227
  /**
5207
5228
  * Install the publish wrap retroactively if `_activateDerived(platform)`
5208
5229
  * was called against an empty registry and a registration has now landed
@@ -5221,10 +5242,7 @@ export function _activateDerived(platform) {
5221
5242
  */
5222
5243
  function _maybeLateActivate() {
5223
5244
  if (!_derivedPlatform) return;
5224
- const target = _resolveWrapTarget(_derivedPlatform);
5225
- if (_activatedPlatforms.has(target)) return;
5226
- _activatedPlatforms.add(target);
5227
- _wrapPlatformPublish(target);
5245
+ _ensureWrap(_derivedPlatform);
5228
5246
  }
5229
5247
 
5230
5248
  /**
@@ -5282,10 +5300,6 @@ function _wrapPlatformPublish(platform) {
5282
5300
  surrogate.publish = derivedPublishLocal;
5283
5301
  if (originalPublishBatched) surrogate.publishBatched = derivedPublishBatchedLocal;
5284
5302
  const wrapped = bus.wrap(surrogate);
5285
- // Tag so a downstream auto-wrap pass (e.g. message hook) can
5286
- // detect "already wrapped by us" and skip re-wrapping. The tag
5287
- // records the bus identity so a later swap re-wraps cleanly.
5288
- /** @type {any} */ (wrapped)[_BUS_WRAPPED] = bus;
5289
5303
  _busPublish = typeof wrapped.publish === 'function' ? wrapped.publish.bind(wrapped) : null;
5290
5304
  _busPublishBatched = typeof /** @type {any} */ (wrapped).publishBatched === 'function'
5291
5305
  ? /** @type {any} */ (wrapped).publishBatched.bind(wrapped)
@@ -5648,6 +5662,12 @@ export function setCronPlatform(platform) {
5648
5662
  // Re-arm the dedup so a subsequent platform-loss (defensive only --
5649
5663
  // platform never goes null in practice) gets one fresh warning.
5650
5664
  _cronPlatformWarnFired = false;
5665
+ // Install the framework's publish wrap here too: pure-cron apps that
5666
+ // never call `_activateDerived` (no reactive primitives wired) still
5667
+ // need cluster routing when a bus is configured. The wrap is idempotent
5668
+ // via `_activatedPlatforms`, so when `realtime().init` calls both
5669
+ // `setCronPlatform` and `_activateDerived` the second call is a no-op.
5670
+ if (platform) _ensureWrap(platform);
5651
5671
  }
5652
5672
 
5653
5673
  /**
@@ -6115,17 +6135,15 @@ export async function _tickCron() {
6115
6135
  }
6116
6136
  return;
6117
6137
  }
6118
- // Cluster fan-out: when a bus is wired via
6119
- // `configureCron({ bus })`, route the cron fire's
6120
- // publishes through `bus.wrap(platform)` so other cluster
6121
- // instances see them too. Without a bus, leader-only ticks
6122
- // only reach subscribers on the leader worker. Fresh wrap
6123
- // per fire is cheap (object literal allocation) and
6124
- // avoids any caching staleness around platform / bus
6125
- // mutation. Falls through to the raw platform when no bus
6126
- // is configured (single-instance dev, the canonical
6127
- // happy path).
6128
- const cronPub = _cronBus ? _cronBus.wrap(_cronPlatform) : _cronPlatform;
6138
+ // Cluster fan-out is the framework's publish wrap's job
6139
+ // now (one wrap site for the whole framework, installed
6140
+ // by `_ensureWrap` from `setCronPlatform`). The cron tick
6141
+ // uses the captured `_cronPlatform` directly - its
6142
+ // `publish` is `derivedPublish`, which consults the
6143
+ // process-wide bus at publish time. No outer `bus.wrap(...)`
6144
+ // here, which eliminates the 0.5.6 double-relay class of
6145
+ // bugs by construction.
6146
+ const cronPub = _cronPlatform;
6129
6147
  const _h = _getCtxHelpers(cronPub);
6130
6148
  const ctx = _buildCtx(null, null, cronPub, _h, null);
6131
6149
  const result = await entry.fn(ctx);
@@ -8181,50 +8199,43 @@ export function close(ws, { platform, subscriptions }) {
8181
8199
  }
8182
8200
 
8183
8201
  /**
8184
- * Per-platform cache of the bus-wrapped surrogate used by the RPC hook
8185
- * (`message` / `createMessage`). Keyed on the raw adapter platform, with
8186
- * the bus identity stored alongside so a `setBus(differentBus)` swap is
8187
- * detected and re-wrapped without holding a strong reference to the old
8188
- * bus. WeakMap so the entry clears when the platform is GC-eligible.
8189
- * @type {WeakMap<object, { bus: any, wrapped: any, epoch: number }>}
8202
+ * One-shot dev-mode flag for the "createMessage({ platform: callback })
8203
+ * is redundant" warning. A user-supplied `platform` callback in
8204
+ * `createMessage` was the pre-0.5.6 way to wire bus.wrap into the RPC
8205
+ * hook. With 0.5.7+ the framework installs a single publish wrap on the
8206
+ * adapter platform (via `_ensureWrap`, called from
8207
+ * `setCronPlatform` / `_activateDerived` / first message), and that
8208
+ * wrap is the sole `bus.wrap(...)` site. A manual callback that wraps
8209
+ * with `bus.wrap` stacks an outer relay on top of the inner one and
8210
+ * double-delivers every RPC publish to other replicas. We can't detect
8211
+ * the manual-wrap case from the callback's output (user-built wraps
8212
+ * don't carry our sentinel), but the input platform is the activated
8213
+ * adapter platform, so we warn at receive time when both conditions
8214
+ * hold. Module-level so a user creating multiple message hooks sees
8215
+ * one warning total.
8190
8216
  */
8191
- const _rpcBusWrapCache = new WeakMap();
8217
+ let _manualPlatformCallbackWarnFired = false;
8192
8218
 
8193
8219
  /**
8194
- * Resolve the platform handed to `handleRpc` from the WS message path.
8195
- * When a process-wide bus is configured (via `setBus`,
8196
- * `configureCron({ bus })`, or `realtime({ bus })`), the raw adapter
8197
- * platform is wrapped on first use and memoized for subsequent
8198
- * messages on the same platform. When the user manually pre-wraps via
8199
- * `createMessage({ platform })`, this is bypassed (their callback
8200
- * runs first) so we never double-wrap.
8201
- *
8202
- * @param {import('svelte-adapter-uws').Platform} platform
8203
- * @returns {import('svelte-adapter-uws').Platform}
8220
+ * Reset the one-shot dev-warn flag for tests. Production deployments
8221
+ * don't need this - the warning is meant to fire once per process and
8222
+ * the flag never needs resetting outside test isolation.
8204
8223
  */
8205
- function _autoBusWrap(platform) {
8206
- const bus = _getBus();
8207
- if (!bus) return platform;
8208
- // Idempotence: if the input has already been wrapped by this
8209
- // framework against the current bus, return it untouched.
8210
- if (/** @type {any} */ (platform)[_BUS_WRAPPED] === bus) return platform;
8211
- const entry = _rpcBusWrapCache.get(/** @type {any} */ (platform));
8212
- if (entry && entry.bus === bus && entry.epoch === _busEpoch) return entry.wrapped;
8213
- const wrapped = bus.wrap(platform);
8214
- /** @type {any} */ (wrapped)[_BUS_WRAPPED] = bus;
8215
- _rpcBusWrapCache.set(/** @type {any} */ (platform), { bus, wrapped, epoch: _busEpoch });
8216
- return wrapped;
8224
+ export function _resetManualPlatformCallbackWarn() {
8225
+ _manualPlatformCallbackWarnFired = false;
8217
8226
  }
8218
8227
 
8219
8228
  /**
8220
- * Ready-made message hook. Re-export from hooks.ws.js for zero-config RPC routing.
8229
+ * Ready-made message hook. Re-export from hooks.ws.js for zero-config
8230
+ * RPC routing.
8221
8231
  *
8222
- * When a process-wide bus is configured (via `setBus`,
8223
- * `configureCron({ bus })`, or `realtime({ bus })`), this hook auto-
8224
- * wraps the adapter platform so RPC `ctx.publish` relays to other
8225
- * cluster instances without any per-hook wiring. Without a bus,
8226
- * publishes stay local - the single-replica default with zero
8227
- * overhead.
8232
+ * First call per platform installs the framework's publish wrap via
8233
+ * `_ensureWrap` (idempotent), so apps that wire `setBus(bus)` and
8234
+ * re-export `message` but never call `_activateDerived` /
8235
+ * `setCronPlatform` themselves still get cluster routing on first
8236
+ * RPC. Subsequent calls are no-ops on the wrap path. Without a bus,
8237
+ * the wrap's per-publish overhead is one function call plus a
8238
+ * `Map.has` check on an empty Map - well below noise.
8228
8239
  *
8229
8240
  * Signature matches the adapter's message hook exactly.
8230
8241
  *
@@ -8232,7 +8243,8 @@ function _autoBusWrap(platform) {
8232
8243
  * @param {{ data: ArrayBuffer, platform: import('svelte-adapter-uws').Platform }} ctx
8233
8244
  */
8234
8245
  export function message(ws, { data, platform }) {
8235
- handleRpc(ws, data, _autoBusWrap(platform));
8246
+ _ensureWrap(platform);
8247
+ handleRpc(ws, data, platform);
8236
8248
  }
8237
8249
 
8238
8250
  /**
@@ -8253,12 +8265,39 @@ export function createMessage(options) {
8253
8265
  const hasRpcOpts = beforeExecute || onError;
8254
8266
 
8255
8267
  return function customMessage(ws, { data, platform }) {
8256
- // User-supplied `platform` callback signals "I am wiring the
8257
- // transform myself"; we run it as-is and skip the auto bus
8258
- // wrap so we never double-wrap. Without the callback, we
8259
- // route through `_autoBusWrap` so the process-wide bus
8260
- // reaches RPC handlers with zero per-hook config.
8261
- const p = transformPlatform ? transformPlatform(platform) : _autoBusWrap(platform);
8268
+ // Install the framework's publish wrap on the platform (idempotent
8269
+ // per platform). After this returns, `platform.publish` is
8270
+ // `derivedPublish`, which is the single bus-routing site for the
8271
+ // whole framework. Done BEFORE any transform callback so the
8272
+ // callback sees the wrapped publish path (correct ordering for
8273
+ // non-bus transforms like metrics instrumentation; double-wrap
8274
+ // detected and warned for legacy bus.wrap callbacks).
8275
+ _ensureWrap(platform);
8276
+ let p;
8277
+ if (transformPlatform) {
8278
+ // Dev-only nudge: a `platform` callback against an
8279
+ // already-activated platform with a process-wide bus wired
8280
+ // almost always means a legacy `(p) => bus.wrap(p)` callback
8281
+ // is layered on top of `derivedPublish`'s inner bus.wrap,
8282
+ // which double-relays every RPC publish. Warn once per
8283
+ // process; users with a non-bus transform (e.g. metrics
8284
+ // instrumentation) can ignore.
8285
+ if (_IS_DEV
8286
+ && !_manualPlatformCallbackWarnFired
8287
+ && _getBus()
8288
+ ) {
8289
+ _manualPlatformCallbackWarnFired = true;
8290
+ console.warn(
8291
+ "[svelte-realtime] createMessage({ platform: callback }) is redundant when `setBus(...)` is wired: " +
8292
+ "the framework already routes ctx.publish through the bus, so a manual `bus.wrap(p)` callback double-relays every RPC publish to other replicas. " +
8293
+ "Drop the `platform` option to fix. If your callback does a non-bus transform (e.g. metrics) you can ignore this warning.\n" +
8294
+ " See: https://svti.me/cluster-relay"
8295
+ );
8296
+ }
8297
+ p = transformPlatform(platform);
8298
+ } else {
8299
+ p = platform;
8300
+ }
8262
8301
  const handled = handleRpc(ws, data, p, hasRpcOpts ? rpcOpts : undefined);
8263
8302
  if (!handled && onUnhandled) {
8264
8303
  onUnhandled(ws, data, p);