svelte-realtime 0.5.5 → 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.
- package/README.md +68 -15
- package/package.json +1 -1
- package/server.d.ts +120 -0
- package/server.js +473 -72
package/README.md
CHANGED
|
@@ -2239,7 +2239,7 @@ On older adapters (`open(ws, platform)` is the only available hand-off point), c
|
|
|
2239
2239
|
|
|
2240
2240
|
Each worker process runs its own cron tick. In a single-process deployment that's exactly what you want. In a clustered deployment - whether `CLUSTER_MODE=reuseport` on Linux (N kernel workers per replica), acceptor mode on Windows / macOS (N internal workers per process), or N Docker replicas, or any combination - every worker fires every job in parallel by default. For "send the daily summary at 9am" jobs, that's almost certainly wrong.
|
|
2241
2241
|
|
|
2242
|
-
Wire a cluster-wide leader gate via `configureCron({ leader, bus })
|
|
2242
|
+
Wire a cluster-wide leader gate via `configureCron({ leader, bus })` (or via the higher-level `realtime({ bus, leader })` factory described in [Redis multi-instance](#redis-multi-instance) - same outcome, one fewer wiring step). The canonical leader implementation lives in `svelte-adapter-uws-extensions/redis/leader` (Redis SETNX lease) and the canonical bus is `svelte-adapter-uws-extensions/redis/pubsub`:
|
|
2243
2243
|
|
|
2244
2244
|
```js
|
|
2245
2245
|
// src/hooks.ws.js (clustered, with extensions)
|
|
@@ -3014,28 +3014,70 @@ If `replay: true` is declared but `platform.replay` is never set, dev-mode logs
|
|
|
3014
3014
|
|
|
3015
3015
|
## Redis multi-instance
|
|
3016
3016
|
|
|
3017
|
-
|
|
3017
|
+
One declaration of cluster intent reaches every framework publish surface. `realtime({ bus, leader })` (added in 0.5.6) wires `ctx.publish` for RPC, the cron tick, the reactive watcher path (`live.effect`, `live.derived`, `live.aggregate`, `live.webhook`), and the top-level `publish()` helper in one call. Handler code (`src/live/*.js`) is byte-identical between single-replica and cluster - you opt in by passing `bus` and `leader` at the top of `hooks.ws.js`, nothing else changes.
|
|
3018
3018
|
|
|
3019
3019
|
```js
|
|
3020
3020
|
// src/hooks.ws.js
|
|
3021
|
-
import {
|
|
3022
|
-
import { createRedis, createPubSubBus } from 'svelte-adapter-uws-extensions/redis';
|
|
3021
|
+
import { realtime } from 'svelte-realtime/server';
|
|
3022
|
+
import { createRedis, createPubSubBus, createLeader } from 'svelte-adapter-uws-extensions/redis';
|
|
3023
3023
|
|
|
3024
3024
|
const redis = createRedis();
|
|
3025
3025
|
const bus = createPubSubBus(redis);
|
|
3026
|
+
const leader = createLeader(redis);
|
|
3026
3027
|
|
|
3027
|
-
export
|
|
3028
|
-
bus
|
|
3028
|
+
export const { open, close, message, init } = realtime({
|
|
3029
|
+
bus,
|
|
3030
|
+
leader: leader.isLeader,
|
|
3031
|
+
});
|
|
3032
|
+
|
|
3033
|
+
export function upgrade({ cookies }) {
|
|
3034
|
+
return validateSession(cookies.session_id) || false;
|
|
3029
3035
|
}
|
|
3036
|
+
```
|
|
3030
3037
|
|
|
3038
|
+
Single-replica is the same file with no config:
|
|
3039
|
+
|
|
3040
|
+
```js
|
|
3041
|
+
// src/hooks.ws.js (single-replica)
|
|
3042
|
+
import { realtime } from 'svelte-realtime/server';
|
|
3043
|
+
export const { open, close, message, init } = realtime();
|
|
3031
3044
|
export function upgrade({ cookies }) {
|
|
3032
3045
|
return validateSession(cookies.session_id) || false;
|
|
3033
3046
|
}
|
|
3047
|
+
```
|
|
3048
|
+
|
|
3049
|
+
### Layer 1: manual wiring (experts)
|
|
3050
|
+
|
|
3051
|
+
`realtime()` is sugar over the existing primitives. If you want fine control - per-route bus, custom hook composition, conditional cluster wiring - drop down to the building blocks. The pre-0.5.6 pattern still works unchanged, and as of 0.5.6 it routes through the same compose-at-publish-time pipeline, so reactive handlers (`live.effect`, `live.derived`, `live.aggregate`) pick up cluster relay automatically once a bus is wired:
|
|
3052
|
+
|
|
3053
|
+
```js
|
|
3054
|
+
// src/hooks.ws.js (manual primitives)
|
|
3055
|
+
import { setBus, setCronPlatform, _activateDerived, configureCron, pushHooks, message } from 'svelte-realtime/server';
|
|
3056
|
+
import { createRedis, createPubSubBus, createLeader } from 'svelte-adapter-uws-extensions/redis';
|
|
3057
|
+
|
|
3058
|
+
const redis = createRedis();
|
|
3059
|
+
const bus = createPubSubBus(redis);
|
|
3060
|
+
const leader = createLeader(redis);
|
|
3061
|
+
|
|
3062
|
+
setBus(bus); // process-wide bus; reactive seam + RPC auto-wrap + publish() helper all use it
|
|
3063
|
+
configureCron({ leader: leader.isLeader });
|
|
3064
|
+
|
|
3065
|
+
export { message };
|
|
3066
|
+
export const open = pushHooks.open;
|
|
3067
|
+
export const close = pushHooks.close;
|
|
3068
|
+
export function init({ platform }) {
|
|
3069
|
+
setCronPlatform(platform);
|
|
3070
|
+
_activateDerived(platform);
|
|
3071
|
+
}
|
|
3034
3072
|
|
|
3035
|
-
export
|
|
3073
|
+
export function upgrade({ cookies }) {
|
|
3074
|
+
return validateSession(cookies.session_id) || false;
|
|
3075
|
+
}
|
|
3036
3076
|
```
|
|
3037
3077
|
|
|
3038
|
-
|
|
3078
|
+
`setBus(bus)` and `configureCron({ bus })` write the same backing state; pick whichever reads better at the call site. `getBus()`, `getPlatform()`, and `publish(topic, event, data)` are exported for diagnostics and for publishing from outside a framework handler (e.g. a `+server.js` HTTP endpoint).
|
|
3079
|
+
|
|
3080
|
+
No changes needed in your live modules either way. `ctx.publish` delegates to whatever composed platform reached the handler, so cluster relay is transparent to user code.
|
|
3039
3081
|
|
|
3040
3082
|
If you already run Postgres and don't need Redis, you can use the [LISTEN/NOTIFY bridge](#postgres-notify) instead for cross-instance pub/sub.
|
|
3041
3083
|
|
|
@@ -3051,19 +3093,23 @@ When you add the Redis extensions from [svelte-adapter-uws-extensions](https://g
|
|
|
3051
3093
|
|
|
3052
3094
|
### Combined: Redis + rate limiting
|
|
3053
3095
|
|
|
3096
|
+
`realtime()` returns the standard hook set; for the cross-cutting `beforeExecute` rate-limit gate, swap `realtime()`'s `message` for a `createMessage` you composed yourself. The bus is still wired once via `setBus`, so the reactive seam and cron tick stay cluster-correct without a per-hook bus callback:
|
|
3097
|
+
|
|
3054
3098
|
```js
|
|
3055
|
-
import { createMessage, LiveError } from 'svelte-realtime/server';
|
|
3056
|
-
import { createRedis, createPubSubBus, createRateLimit } from 'svelte-adapter-uws-extensions/redis';
|
|
3099
|
+
import { realtime, createMessage, setBus, LiveError } from 'svelte-realtime/server';
|
|
3100
|
+
import { createRedis, createPubSubBus, createLeader, createRateLimit } from 'svelte-adapter-uws-extensions/redis';
|
|
3057
3101
|
|
|
3058
3102
|
const redis = createRedis();
|
|
3059
3103
|
const bus = createPubSubBus(redis);
|
|
3104
|
+
const leader = createLeader(redis);
|
|
3060
3105
|
const limiter = createRateLimit(redis, { points: 30, interval: 10000 });
|
|
3061
3106
|
|
|
3062
|
-
|
|
3107
|
+
setBus(bus);
|
|
3108
|
+
|
|
3109
|
+
export const { open, close, init } = realtime({ leader: leader.isLeader });
|
|
3063
3110
|
export function upgrade({ cookies }) { return validateSession(cookies.session_id) || false; }
|
|
3064
3111
|
|
|
3065
3112
|
export const message = createMessage({
|
|
3066
|
-
platform: (p) => bus.wrap(p),
|
|
3067
3113
|
async beforeExecute(ws, rpcPath) {
|
|
3068
3114
|
const { allowed, resetMs } = await limiter.consume(ws);
|
|
3069
3115
|
if (!allowed)
|
|
@@ -3072,6 +3118,8 @@ export const message = createMessage({
|
|
|
3072
3118
|
});
|
|
3073
3119
|
```
|
|
3074
3120
|
|
|
3121
|
+
Without a `platform` callback, `createMessage` auto-wraps with whatever `setBus(...)` wired - one source of truth, no double-wrap.
|
|
3122
|
+
|
|
3075
3123
|
---
|
|
3076
3124
|
|
|
3077
3125
|
## Postgres NOTIFY
|
|
@@ -3586,13 +3634,18 @@ Import from `svelte-realtime/server`.
|
|
|
3586
3634
|
| `guard(...fns)` | Per-module auth middleware |
|
|
3587
3635
|
| `LiveError(code, message?)` | Typed error (propagates to client) |
|
|
3588
3636
|
| `handleRpc(ws, data, platform, options?)` | Low-level RPC handler |
|
|
3589
|
-
| `message` | Ready-made message hook |
|
|
3590
|
-
| `createMessage(options?)` | Custom message hook factory |
|
|
3637
|
+
| `message` | Ready-made message hook (auto bus-wraps when `setBus` is wired) |
|
|
3638
|
+
| `createMessage(options?)` | Custom message hook factory (auto bus-wraps unless `options.platform` is provided) |
|
|
3639
|
+
| `realtime(config?)` | One-call setup returning `{ open, close, message, init, upgrade? }` - wires bus + leader + platform from a single declaration of cluster intent |
|
|
3640
|
+
| `setBus(bus)` | Configure the process-wide bus consulted by every framework publish surface (alias for `configureCron({ bus })`) |
|
|
3641
|
+
| `getBus()` | Read the process-wide bus, or `null` |
|
|
3642
|
+
| `getPlatform()` | Read the framework-owned composed platform after `init` has captured it |
|
|
3643
|
+
| `publish(topic, event, data, options?)` | Top-level publish helper (routes through the composed platform; relays via bus when wired) |
|
|
3591
3644
|
| `pipe(stream, ...transforms)` | Composable stream transforms |
|
|
3592
3645
|
| `close` | Ready-made close hook (fires onUnsubscribe for remaining topics) |
|
|
3593
3646
|
| `unsubscribe` | Ready-made unsubscribe hook (fires onUnsubscribe in real time) |
|
|
3594
3647
|
| `setCronPlatform(platform)` | Capture platform for cron jobs (call from `init({ platform })`) |
|
|
3595
|
-
| `configureCron({ leader })` | Cluster-mode leader gate for cron (default: every worker fires) |
|
|
3648
|
+
| `configureCron({ leader, bus })` | Cluster-mode leader gate for cron (default: every worker fires); `bus` also wires the process-wide bus |
|
|
3596
3649
|
| `onError(handler)` | Global error handler for cron, effects, and derived |
|
|
3597
3650
|
| `onCronError(handler)` | Deprecated alias for `onError` |
|
|
3598
3651
|
| `enableSignals(ws)` | Enable point-to-point signal delivery |
|
package/package.json
CHANGED
package/server.d.ts
CHANGED
|
@@ -2445,3 +2445,123 @@ export function close(
|
|
|
2445
2445
|
ws: WebSocket<any>,
|
|
2446
2446
|
ctx: { platform: Platform; subscriptions?: Set<string> | string[] }
|
|
2447
2447
|
): void;
|
|
2448
|
+
|
|
2449
|
+
// ---------------------------------------------------------------------------
|
|
2450
|
+
// Cluster wiring + realtime() convenience factory
|
|
2451
|
+
// ---------------------------------------------------------------------------
|
|
2452
|
+
|
|
2453
|
+
/**
|
|
2454
|
+
* Cluster bus contract. Any object exposing `wrap(platform)` matches.
|
|
2455
|
+
* The canonical implementation is `redisBus()` from
|
|
2456
|
+
* `svelte-adapter-uws-extensions/redis/pubsub`; sharded-pubsub and
|
|
2457
|
+
* test buses conform to the same shape.
|
|
2458
|
+
*/
|
|
2459
|
+
export interface ClusterBus {
|
|
2460
|
+
wrap(platform: Platform): Platform;
|
|
2461
|
+
}
|
|
2462
|
+
|
|
2463
|
+
/**
|
|
2464
|
+
* Configure the process-wide cluster bus. One declaration of cluster
|
|
2465
|
+
* intent reaches every framework publish surface in lockstep: RPC
|
|
2466
|
+
* `ctx.publish` (auto-wrapped by `message` / `createMessage`), cron
|
|
2467
|
+
* tick publishes, the reactive watcher publish wrap (`live.effect`,
|
|
2468
|
+
* `live.derived`, `live.aggregate`, `live.webhook`), and the top-level
|
|
2469
|
+
* `publish()` helper.
|
|
2470
|
+
*
|
|
2471
|
+
* Pass `null` to clear and revert to single-replica behaviour.
|
|
2472
|
+
*
|
|
2473
|
+
* `configureCron({ bus })` is equivalent for the bus field - they
|
|
2474
|
+
* write the same backing state. Most apps reach for
|
|
2475
|
+
* `realtime({ bus, leader })` instead and never call this directly.
|
|
2476
|
+
*/
|
|
2477
|
+
export function setBus(bus: ClusterBus | null): void;
|
|
2478
|
+
|
|
2479
|
+
/** Read the process-wide bus, or `null` when none is configured. */
|
|
2480
|
+
export function getBus(): ClusterBus | null;
|
|
2481
|
+
|
|
2482
|
+
/**
|
|
2483
|
+
* Read the framework-owned composed platform - the same reference
|
|
2484
|
+
* handed to every reactive handler. Returns `null` before the adapter's
|
|
2485
|
+
* `init({ platform })` hook has captured it on this worker.
|
|
2486
|
+
*/
|
|
2487
|
+
export function getPlatform(): Platform | null;
|
|
2488
|
+
|
|
2489
|
+
/**
|
|
2490
|
+
* Publish from outside a framework handler (e.g. a `+server.js` HTTP
|
|
2491
|
+
* handler). Routes through the composed platform so the publish reaches
|
|
2492
|
+
* every local subscriber, fires every reactive watcher, and relays to
|
|
2493
|
+
* other cluster instances when a bus is wired. Throws when called
|
|
2494
|
+
* before the platform has been captured.
|
|
2495
|
+
*/
|
|
2496
|
+
export function publish(topic: string, event: string, data?: unknown, options?: unknown): void;
|
|
2497
|
+
|
|
2498
|
+
/**
|
|
2499
|
+
* Configuration accepted by `realtime()`.
|
|
2500
|
+
*/
|
|
2501
|
+
export interface RealtimeConfig {
|
|
2502
|
+
/**
|
|
2503
|
+
* Cluster bus. Pass `redisBus()` or any object exposing
|
|
2504
|
+
* `wrap(platform)` to enable cluster fan-out. Omit (or pass `null`)
|
|
2505
|
+
* for single-replica.
|
|
2506
|
+
*/
|
|
2507
|
+
bus?: ClusterBus | null;
|
|
2508
|
+
/**
|
|
2509
|
+
* Cluster leader gate for cron's "exactly once across the cluster"
|
|
2510
|
+
* semantics. Pass `redisLeader().isLeader` (or any function returning
|
|
2511
|
+
* the current leader status). Omit for single-replica or for the
|
|
2512
|
+
* "every worker fires" default.
|
|
2513
|
+
*/
|
|
2514
|
+
leader?: (() => boolean) | null;
|
|
2515
|
+
/**
|
|
2516
|
+
* Optional `upgrade` hook handed straight back out as part of the
|
|
2517
|
+
* returned hook set. Lets you write a single one-import-one-
|
|
2518
|
+
* destructure `hooks.ws.js`. Omit and export your own `upgrade`
|
|
2519
|
+
* separately if you prefer.
|
|
2520
|
+
*/
|
|
2521
|
+
upgrade?: (...args: any[]) => any;
|
|
2522
|
+
/**
|
|
2523
|
+
* Optional error handler for cron / effect / derived failures.
|
|
2524
|
+
* Equivalent to calling `onError(handler)`.
|
|
2525
|
+
*/
|
|
2526
|
+
onError?: (path: string, error: unknown) => void;
|
|
2527
|
+
}
|
|
2528
|
+
|
|
2529
|
+
/**
|
|
2530
|
+
* Shape returned by `realtime()`. Spread into `hooks.ws.js` exports.
|
|
2531
|
+
* `upgrade` is present only when the caller passed one in the config.
|
|
2532
|
+
*/
|
|
2533
|
+
export interface RealtimeHooks {
|
|
2534
|
+
open(ws: any, ctx: { platform: Platform }): void;
|
|
2535
|
+
close(ws: any, ctx: { platform: Platform; subscriptions?: Set<string> | string[] }): void;
|
|
2536
|
+
message(ws: any, ctx: { data: ArrayBuffer; platform: Platform }): void;
|
|
2537
|
+
init(ctx: { platform: Platform }): void;
|
|
2538
|
+
upgrade?: (...args: any[]) => any;
|
|
2539
|
+
}
|
|
2540
|
+
|
|
2541
|
+
/**
|
|
2542
|
+
* One-call setup that wires every framework seam from a single
|
|
2543
|
+
* declaration of cluster intent. Returns the standard adapter hook
|
|
2544
|
+
* set so `hooks.ws.js` is a one-import-one-destructure file.
|
|
2545
|
+
*
|
|
2546
|
+
* Single-replica:
|
|
2547
|
+
* ```js
|
|
2548
|
+
* import { realtime } from 'svelte-realtime/server';
|
|
2549
|
+
* export const { open, close, message, init } = realtime();
|
|
2550
|
+
* export function upgrade({ cookies }) { ... }
|
|
2551
|
+
* ```
|
|
2552
|
+
*
|
|
2553
|
+
* Cluster:
|
|
2554
|
+
* ```js
|
|
2555
|
+
* import { realtime } from 'svelte-realtime/server';
|
|
2556
|
+
* import { redisBus, redisLeader } from 'svelte-adapter-uws-extensions/redis';
|
|
2557
|
+
* export const { open, close, message, init } = realtime({
|
|
2558
|
+
* bus: redisBus(),
|
|
2559
|
+
* leader: redisLeader().isLeader,
|
|
2560
|
+
* });
|
|
2561
|
+
* export function upgrade({ cookies }) { ... }
|
|
2562
|
+
* ```
|
|
2563
|
+
*
|
|
2564
|
+
* Handler-level code is byte-identical between the two modes; the only
|
|
2565
|
+
* difference is whether `bus` and `leader` are passed at the top.
|
|
2566
|
+
*/
|
|
2567
|
+
export function realtime(config?: RealtimeConfig): RealtimeHooks;
|
package/server.js
CHANGED
|
@@ -3426,23 +3426,71 @@ let _cronPlatform = null;
|
|
|
3426
3426
|
let _cronLeader = null;
|
|
3427
3427
|
|
|
3428
3428
|
/**
|
|
3429
|
-
*
|
|
3430
|
-
*
|
|
3431
|
-
*
|
|
3432
|
-
*
|
|
3433
|
-
*
|
|
3434
|
-
*
|
|
3435
|
-
*
|
|
3436
|
-
*
|
|
3437
|
-
*
|
|
3438
|
-
*
|
|
3439
|
-
*
|
|
3440
|
-
*
|
|
3429
|
+
* Process-wide cluster bus. Single source of truth consulted by every
|
|
3430
|
+
* publish surface in the framework (RPC `ctx.publish`, cron tick,
|
|
3431
|
+
* reactive watchers' publish wrap, top-level `publish()` helper). When
|
|
3432
|
+
* set, outbound publishes relay to other cluster instances via
|
|
3433
|
+
* `bus.wrap(platform).publish`; inbound relays from other instances
|
|
3434
|
+
* arrive through the bus's own subscriber and are broadcast on this
|
|
3435
|
+
* instance via the wrapped surrogate's `publish`.
|
|
3436
|
+
*
|
|
3437
|
+
* Written by `setBus(bus)`, `configureCron({ bus })`, and the
|
|
3438
|
+
* `realtime({ bus })` factory. Read via `getBus()` and consulted at
|
|
3439
|
+
* publish-time by every framework seam so a single declaration of
|
|
3440
|
+
* deployment intent covers all of them. Without a bus, every seam
|
|
3441
|
+
* publishes locally (the single-replica default).
|
|
3442
|
+
*
|
|
3443
|
+
* Composition discipline: `bus.wrap(...)` is applied at publish time
|
|
3444
|
+
* over a snapshot of the raw adapter publish (the "surrogate"). The
|
|
3445
|
+
* reactive seam mutates `platform.publish` to a `derivedPublish` that
|
|
3446
|
+
* routes through the wrapped surrogate when a bus is configured, so
|
|
3447
|
+
* the same publish call fires reactive watchers AND relays to the
|
|
3448
|
+
* cluster in one step. Inbound relays from other instances arrive via
|
|
3449
|
+
* the wrapped surrogate's publish (which runs the local broadcast + the
|
|
3450
|
+
* watcher fan-out without re-relaying), so derived / effect /
|
|
3451
|
+
* aggregate handlers on receiving instances see the cross-cluster
|
|
3452
|
+
* stream the same way they see local publishes.
|
|
3441
3453
|
*
|
|
3442
3454
|
* @type {{ wrap: (platform: any) => any } | null}
|
|
3443
3455
|
*/
|
|
3456
|
+
let _bus = null;
|
|
3457
|
+
/**
|
|
3458
|
+
* Legacy alias retained so the existing `_cronBus` references in the
|
|
3459
|
+
* cron tick read the canonical bus without surgery. Always equal to
|
|
3460
|
+
* `_bus` (the setter writes both in lockstep). Treat as read-only.
|
|
3461
|
+
*/
|
|
3444
3462
|
let _cronBus = null;
|
|
3445
3463
|
|
|
3464
|
+
/**
|
|
3465
|
+
* Write the process-wide bus. Validated like `configureCron({ bus })`
|
|
3466
|
+
* - must expose `.wrap(platform)` or be `null`. Mirrored into the
|
|
3467
|
+
* legacy `_cronBus` alias so the existing cron tick keeps reading the
|
|
3468
|
+
* canonical value without surgery. Bumps `_busEpoch` so memoized
|
|
3469
|
+
* `bus.wrap(...)` caches (per-platform, computed lazily by the
|
|
3470
|
+
* reactive wrap and the RPC message hooks) invalidate on swap.
|
|
3471
|
+
* @param {{ wrap: (platform: any) => any } | null} bus
|
|
3472
|
+
*/
|
|
3473
|
+
function _setBus(bus) {
|
|
3474
|
+
if (bus !== null && (typeof bus !== 'object' || typeof bus.wrap !== 'function')) {
|
|
3475
|
+
throw new Error('[svelte-realtime] setBus: bus must expose a .wrap(platform) method or be null');
|
|
3476
|
+
}
|
|
3477
|
+
_bus = bus;
|
|
3478
|
+
_cronBus = bus;
|
|
3479
|
+
_busEpoch++;
|
|
3480
|
+
}
|
|
3481
|
+
|
|
3482
|
+
/** Read the process-wide bus (or null when no cluster intent is wired). */
|
|
3483
|
+
function _getBus() {
|
|
3484
|
+
return _bus;
|
|
3485
|
+
}
|
|
3486
|
+
|
|
3487
|
+
/**
|
|
3488
|
+
* Monotonic counter bumped on every bus swap. Used by per-platform
|
|
3489
|
+
* `bus.wrap(...)` caches to detect "the bus changed under me, re-wrap"
|
|
3490
|
+
* without holding a strong reference to the old bus.
|
|
3491
|
+
*/
|
|
3492
|
+
let _busEpoch = 0;
|
|
3493
|
+
|
|
3446
3494
|
/**
|
|
3447
3495
|
* One-shot flag for the "configureCron leader without bus" warning.
|
|
3448
3496
|
* Setting `leader` declares cluster intent; not also wiring a bus
|
|
@@ -5109,43 +5157,73 @@ export function __registerDerived(path, fn) {
|
|
|
5109
5157
|
* triggered externally when the platform fires publish.
|
|
5110
5158
|
* @param {import('svelte-adapter-uws').Platform} platform
|
|
5111
5159
|
*/
|
|
5112
|
-
/**
|
|
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
|
+
*/
|
|
5113
5173
|
const _activatedPlatforms = new WeakSet();
|
|
5114
5174
|
|
|
5115
|
-
|
|
5116
|
-
|
|
5117
|
-
|
|
5118
|
-
|
|
5119
|
-
|
|
5120
|
-
|
|
5121
|
-
|
|
5122
|
-
|
|
5123
|
-
|
|
5124
|
-
|
|
5125
|
-
|
|
5126
|
-
|
|
5127
|
-
|
|
5128
|
-
|
|
5129
|
-
|
|
5130
|
-
|
|
5131
|
-
|
|
5132
|
-
|
|
5133
|
-
|
|
5134
|
-
|
|
5135
|
-
|
|
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;
|
|
5136
5198
|
// svelte-adapter-uws hands hooks a per-connection platform created via
|
|
5137
|
-
// Object.create(basePlatform). Wrapping that per-connection object
|
|
5138
|
-
// every other connection's inherited publish / publishBatched
|
|
5139
|
-
// because their lookups walk the prototype chain to the
|
|
5140
|
-
// Resolve to the base prototype so the wrap is visible
|
|
5141
|
-
// that share it. Test mocks pass plain objects whose
|
|
5142
|
-
// 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.
|
|
5143
5205
|
const target = _resolveWrapTarget(platform);
|
|
5144
5206
|
if (_activatedPlatforms.has(target)) return;
|
|
5145
5207
|
_activatedPlatforms.add(target);
|
|
5146
5208
|
_wrapPlatformPublish(target);
|
|
5147
5209
|
}
|
|
5148
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
|
+
|
|
5149
5227
|
/**
|
|
5150
5228
|
* Install the publish wrap retroactively if `_activateDerived(platform)`
|
|
5151
5229
|
* was called against an empty registry and a registration has now landed
|
|
@@ -5164,10 +5242,7 @@ export function _activateDerived(platform) {
|
|
|
5164
5242
|
*/
|
|
5165
5243
|
function _maybeLateActivate() {
|
|
5166
5244
|
if (!_derivedPlatform) return;
|
|
5167
|
-
|
|
5168
|
-
if (_activatedPlatforms.has(target)) return;
|
|
5169
|
-
_activatedPlatforms.add(target);
|
|
5170
|
-
_wrapPlatformPublish(target);
|
|
5245
|
+
_ensureWrap(_derivedPlatform);
|
|
5171
5246
|
}
|
|
5172
5247
|
|
|
5173
5248
|
/**
|
|
@@ -5192,6 +5267,45 @@ function _wrapPlatformPublish(platform) {
|
|
|
5192
5267
|
? /** @type {any} */ (platform).publishBatched.bind(platform)
|
|
5193
5268
|
: null;
|
|
5194
5269
|
|
|
5270
|
+
// Memoized bus-wrapped surrogate. Recomputed when the process-wide
|
|
5271
|
+
// bus changes (detected via `_busEpoch`). The surrogate's `publish`
|
|
5272
|
+
// is `derivedPublishLocal` (local broadcast + watcher fan-out, no
|
|
5273
|
+
// re-relay), so inbound bus deliveries fire watchers on the
|
|
5274
|
+
// receiving instance without bouncing the message back out onto the
|
|
5275
|
+
// bus. The wrapped surrogate's `publish` (set up by the extension's
|
|
5276
|
+
// `bus.wrap`) does relay + delegate-to-surrogate; outbound publishes
|
|
5277
|
+
// from user code go through `derivedPublish` below, which routes via
|
|
5278
|
+
// this cache when a bus is configured.
|
|
5279
|
+
let _cachedBusEpoch = -1;
|
|
5280
|
+
/** @type {((topic: string, event: string, data: any, opts?: any) => any) | null} */
|
|
5281
|
+
let _busPublish = null;
|
|
5282
|
+
/** @type {((batch: any) => any) | null} */
|
|
5283
|
+
let _busPublishBatched = null;
|
|
5284
|
+
function _refreshBusCache() {
|
|
5285
|
+
if (_cachedBusEpoch === _busEpoch) return;
|
|
5286
|
+
_cachedBusEpoch = _busEpoch;
|
|
5287
|
+
const bus = _getBus();
|
|
5288
|
+
if (!bus) {
|
|
5289
|
+
_busPublish = null;
|
|
5290
|
+
_busPublishBatched = null;
|
|
5291
|
+
return;
|
|
5292
|
+
}
|
|
5293
|
+
// Surrogate holds derivedPublishLocal as its publish so inbound
|
|
5294
|
+
// cluster relays still fire reactive watchers on this instance
|
|
5295
|
+
// but do not bounce back out. Spread carries the rest of the
|
|
5296
|
+
// platform surface (subscribe, send, redis, replay, ...) so the
|
|
5297
|
+
// extensions's bus.wrap sees a complete platform shape.
|
|
5298
|
+
/** @type {any} */
|
|
5299
|
+
const surrogate = Object.assign(Object.create(Object.getPrototypeOf(platform)), platform);
|
|
5300
|
+
surrogate.publish = derivedPublishLocal;
|
|
5301
|
+
if (originalPublishBatched) surrogate.publishBatched = derivedPublishBatchedLocal;
|
|
5302
|
+
const wrapped = bus.wrap(surrogate);
|
|
5303
|
+
_busPublish = typeof wrapped.publish === 'function' ? wrapped.publish.bind(wrapped) : null;
|
|
5304
|
+
_busPublishBatched = typeof /** @type {any} */ (wrapped).publishBatched === 'function'
|
|
5305
|
+
? /** @type {any} */ (wrapped).publishBatched.bind(wrapped)
|
|
5306
|
+
: null;
|
|
5307
|
+
}
|
|
5308
|
+
|
|
5195
5309
|
let _publishDepth = 0;
|
|
5196
5310
|
|
|
5197
5311
|
function fireWatchers(topic, event, data) {
|
|
@@ -5297,10 +5411,37 @@ function _wrapPlatformPublish(platform) {
|
|
|
5297
5411
|
_publishDepth--;
|
|
5298
5412
|
}
|
|
5299
5413
|
|
|
5300
|
-
|
|
5414
|
+
// Inner publish used by the bus-wrap surrogate. Does the local
|
|
5415
|
+
// broadcast + watcher fan-out but NEVER relays - relay is the
|
|
5416
|
+
// outer `derivedPublish`'s job (via the wrapped surrogate). This
|
|
5417
|
+
// is also what runs when an inbound message arrives from another
|
|
5418
|
+
// instance, so cluster-relayed events fire derived / effect /
|
|
5419
|
+
// aggregate watchers on the receiving instance.
|
|
5420
|
+
function derivedPublishLocal(topic, event, data, opts) {
|
|
5301
5421
|
const result = originalPublish(topic, event, data, opts);
|
|
5302
5422
|
fireWatchers(topic, event, data);
|
|
5303
5423
|
return result;
|
|
5424
|
+
}
|
|
5425
|
+
|
|
5426
|
+
function derivedPublishBatchedLocal(batch) {
|
|
5427
|
+
const result = originalPublishBatched ? originalPublishBatched(batch) : undefined;
|
|
5428
|
+
if (Array.isArray(batch) && _watchedTopics.size > 0) {
|
|
5429
|
+
for (const item of batch) {
|
|
5430
|
+
if (!item || typeof item.topic !== 'string') continue;
|
|
5431
|
+
fireWatchers(item.topic, item.event, item.data);
|
|
5432
|
+
}
|
|
5433
|
+
}
|
|
5434
|
+
return result;
|
|
5435
|
+
}
|
|
5436
|
+
|
|
5437
|
+
// Outbound user-facing publish. When a bus is configured, routes
|
|
5438
|
+
// through the wrapped surrogate so the publish both broadcasts
|
|
5439
|
+
// locally (with watchers) and relays to the cluster in one step.
|
|
5440
|
+
// Without a bus, identical to the legacy local-only path.
|
|
5441
|
+
platform.publish = function derivedPublish(topic, event, data, opts) {
|
|
5442
|
+
_refreshBusCache();
|
|
5443
|
+
if (_busPublish) return _busPublish(topic, event, data, opts);
|
|
5444
|
+
return derivedPublishLocal(topic, event, data, opts);
|
|
5304
5445
|
};
|
|
5305
5446
|
|
|
5306
5447
|
if (originalPublishBatched) {
|
|
@@ -5311,14 +5452,9 @@ function _wrapPlatformPublish(platform) {
|
|
|
5311
5452
|
// publishes from the batched path - they only fire from the unbatched
|
|
5312
5453
|
// platform.publish path that some test mocks happen to use.
|
|
5313
5454
|
/** @type {any} */ (platform).publishBatched = function derivedPublishBatched(batch) {
|
|
5314
|
-
|
|
5315
|
-
if (
|
|
5316
|
-
|
|
5317
|
-
if (!item || typeof item.topic !== 'string') continue;
|
|
5318
|
-
fireWatchers(item.topic, item.event, item.data);
|
|
5319
|
-
}
|
|
5320
|
-
}
|
|
5321
|
-
return result;
|
|
5455
|
+
_refreshBusCache();
|
|
5456
|
+
if (_busPublishBatched) return _busPublishBatched(batch);
|
|
5457
|
+
return derivedPublishBatchedLocal(batch);
|
|
5322
5458
|
};
|
|
5323
5459
|
}
|
|
5324
5460
|
}
|
|
@@ -5526,6 +5662,12 @@ export function setCronPlatform(platform) {
|
|
|
5526
5662
|
// Re-arm the dedup so a subsequent platform-loss (defensive only --
|
|
5527
5663
|
// platform never goes null in practice) gets one fresh warning.
|
|
5528
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);
|
|
5529
5671
|
}
|
|
5530
5672
|
|
|
5531
5673
|
/**
|
|
@@ -5589,7 +5731,7 @@ export function setCronPlatform(platform) {
|
|
|
5589
5731
|
export function configureCron(config) {
|
|
5590
5732
|
if (config === null) {
|
|
5591
5733
|
_cronLeader = null;
|
|
5592
|
-
|
|
5734
|
+
_setBus(null);
|
|
5593
5735
|
return;
|
|
5594
5736
|
}
|
|
5595
5737
|
if (typeof config !== 'object') {
|
|
@@ -5608,13 +5750,15 @@ export function configureCron(config) {
|
|
|
5608
5750
|
}
|
|
5609
5751
|
}
|
|
5610
5752
|
if (config.bus !== undefined) {
|
|
5611
|
-
|
|
5612
|
-
|
|
5613
|
-
|
|
5753
|
+
// Routes through `_setBus` so the canonical `_bus` (consulted by
|
|
5754
|
+
// the reactive wrap, the RPC auto-wrap, and the top-level
|
|
5755
|
+
// `publish()` helper) stays in lockstep with the legacy
|
|
5756
|
+
// `_cronBus` alias - one declaration of cluster intent covers
|
|
5757
|
+
// every framework seam, not just cron.
|
|
5758
|
+
if (config.bus !== null && (typeof config.bus !== 'object' || typeof config.bus.wrap !== 'function')) {
|
|
5614
5759
|
throw new Error('[svelte-realtime] configureCron: bus must expose a .wrap(platform) method or be null');
|
|
5615
|
-
} else {
|
|
5616
|
-
_cronBus = config.bus;
|
|
5617
5760
|
}
|
|
5761
|
+
_setBus(config.bus);
|
|
5618
5762
|
}
|
|
5619
5763
|
// Diagnostic: cluster intent (leader) without cluster fan-out (bus)
|
|
5620
5764
|
// is almost always a misconfig. Leader-only cron ticks publish on the
|
|
@@ -5991,17 +6135,15 @@ export async function _tickCron() {
|
|
|
5991
6135
|
}
|
|
5992
6136
|
return;
|
|
5993
6137
|
}
|
|
5994
|
-
// Cluster fan-out
|
|
5995
|
-
//
|
|
5996
|
-
//
|
|
5997
|
-
//
|
|
5998
|
-
//
|
|
5999
|
-
//
|
|
6000
|
-
//
|
|
6001
|
-
//
|
|
6002
|
-
|
|
6003
|
-
// happy path).
|
|
6004
|
-
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;
|
|
6005
6147
|
const _h = _getCtxHelpers(cronPub);
|
|
6006
6148
|
const ctx = _buildCtx(null, null, cronPub, _h, null);
|
|
6007
6149
|
const result = await entry.fn(ctx);
|
|
@@ -8057,7 +8199,43 @@ export function close(ws, { platform, subscriptions }) {
|
|
|
8057
8199
|
}
|
|
8058
8200
|
|
|
8059
8201
|
/**
|
|
8060
|
-
*
|
|
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.
|
|
8216
|
+
*/
|
|
8217
|
+
let _manualPlatformCallbackWarnFired = false;
|
|
8218
|
+
|
|
8219
|
+
/**
|
|
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.
|
|
8223
|
+
*/
|
|
8224
|
+
export function _resetManualPlatformCallbackWarn() {
|
|
8225
|
+
_manualPlatformCallbackWarnFired = false;
|
|
8226
|
+
}
|
|
8227
|
+
|
|
8228
|
+
/**
|
|
8229
|
+
* Ready-made message hook. Re-export from hooks.ws.js for zero-config
|
|
8230
|
+
* RPC routing.
|
|
8231
|
+
*
|
|
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.
|
|
8061
8239
|
*
|
|
8062
8240
|
* Signature matches the adapter's message hook exactly.
|
|
8063
8241
|
*
|
|
@@ -8065,6 +8243,7 @@ export function close(ws, { platform, subscriptions }) {
|
|
|
8065
8243
|
* @param {{ data: ArrayBuffer, platform: import('svelte-adapter-uws').Platform }} ctx
|
|
8066
8244
|
*/
|
|
8067
8245
|
export function message(ws, { data, platform }) {
|
|
8246
|
+
_ensureWrap(platform);
|
|
8068
8247
|
handleRpc(ws, data, platform);
|
|
8069
8248
|
}
|
|
8070
8249
|
|
|
@@ -8086,10 +8265,232 @@ export function createMessage(options) {
|
|
|
8086
8265
|
const hasRpcOpts = beforeExecute || onError;
|
|
8087
8266
|
|
|
8088
8267
|
return function customMessage(ws, { data, platform }) {
|
|
8089
|
-
|
|
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
|
+
}
|
|
8090
8301
|
const handled = handleRpc(ws, data, p, hasRpcOpts ? rpcOpts : undefined);
|
|
8091
8302
|
if (!handled && onUnhandled) {
|
|
8092
8303
|
onUnhandled(ws, data, p);
|
|
8093
8304
|
}
|
|
8094
8305
|
};
|
|
8095
8306
|
}
|
|
8307
|
+
|
|
8308
|
+
// ---------------------------------------------------------------------------
|
|
8309
|
+
// Cluster wiring: process-wide bus + composed-platform accessors
|
|
8310
|
+
// ---------------------------------------------------------------------------
|
|
8311
|
+
|
|
8312
|
+
/**
|
|
8313
|
+
* Configure the process-wide cluster bus. Consumed by every framework
|
|
8314
|
+
* publish surface in lockstep: RPC `ctx.publish` (via the `message` /
|
|
8315
|
+
* `createMessage` auto-wrap), cron tick publishes, reactive watchers'
|
|
8316
|
+
* publish wrap (`live.effect`, `live.derived`, `live.aggregate`,
|
|
8317
|
+
* `live.webhook`), and the top-level `publish()` helper. One
|
|
8318
|
+
* declaration of cluster intent covers all of them.
|
|
8319
|
+
*
|
|
8320
|
+
* Pass a bus exposing `.wrap(platform)` (e.g. `redisBus()` from
|
|
8321
|
+
* `svelte-adapter-uws-extensions/redis/pubsub`) to enable cluster
|
|
8322
|
+
* fan-out. Pass `null` to clear and revert to single-replica behaviour.
|
|
8323
|
+
*
|
|
8324
|
+
* `configureCron({ bus })` is equivalent to `setBus(bus)` for the bus
|
|
8325
|
+
* field - they write the same backing state. Pick whichever reads more
|
|
8326
|
+
* naturally at the call site; the typical app uses
|
|
8327
|
+
* `realtime({ bus, leader })` instead and never calls either directly.
|
|
8328
|
+
*
|
|
8329
|
+
* @param {{ wrap: (platform: any) => any } | null} bus
|
|
8330
|
+
*
|
|
8331
|
+
* @example
|
|
8332
|
+
* ```js
|
|
8333
|
+
* // hooks.ws.js (Layer 1 / expert wiring)
|
|
8334
|
+
* import { setBus, setCronPlatform, _activateDerived, configureCron, message } from 'svelte-realtime/server';
|
|
8335
|
+
* import { redisBus, redisLeader } from 'svelte-adapter-uws-extensions/redis';
|
|
8336
|
+
*
|
|
8337
|
+
* const bus = redisBus();
|
|
8338
|
+
* setBus(bus);
|
|
8339
|
+
* configureCron({ leader: redisLeader() });
|
|
8340
|
+
*
|
|
8341
|
+
* export { message };
|
|
8342
|
+
* export function init({ platform }) {
|
|
8343
|
+
* setCronPlatform(platform);
|
|
8344
|
+
* _activateDerived(platform);
|
|
8345
|
+
* }
|
|
8346
|
+
* ```
|
|
8347
|
+
*/
|
|
8348
|
+
export function setBus(bus) {
|
|
8349
|
+
_setBus(bus);
|
|
8350
|
+
}
|
|
8351
|
+
|
|
8352
|
+
/**
|
|
8353
|
+
* Read the process-wide bus, or `null` when none is configured. Useful
|
|
8354
|
+
* for diagnostics, conditional cluster-only wiring, and tests.
|
|
8355
|
+
*
|
|
8356
|
+
* @returns {{ wrap: (platform: any) => any } | null}
|
|
8357
|
+
*/
|
|
8358
|
+
export function getBus() {
|
|
8359
|
+
return _getBus();
|
|
8360
|
+
}
|
|
8361
|
+
|
|
8362
|
+
/**
|
|
8363
|
+
* Read the framework-owned composed platform - the same reference handed
|
|
8364
|
+
* to every `live.effect` / `live.derived` / `live.aggregate` handler and
|
|
8365
|
+
* threaded through `ctx.platform` in RPC / cron / webhook contexts.
|
|
8366
|
+
*
|
|
8367
|
+
* Returns `null` before `setCronPlatform(platform)` /
|
|
8368
|
+
* `_activateDerived(platform)` / `realtime().init({ platform })` has
|
|
8369
|
+
* captured the adapter platform on this worker.
|
|
8370
|
+
*
|
|
8371
|
+
* Use for publish from outside a framework handler (e.g. a `+server.js`
|
|
8372
|
+
* HTTP handler) when you want the same cluster semantics. Most callers
|
|
8373
|
+
* should reach for the top-level `publish()` helper instead.
|
|
8374
|
+
*
|
|
8375
|
+
* @returns {import('svelte-adapter-uws').Platform | null}
|
|
8376
|
+
*/
|
|
8377
|
+
export function getPlatform() {
|
|
8378
|
+
return _derivedPlatform || _cronPlatform || null;
|
|
8379
|
+
}
|
|
8380
|
+
|
|
8381
|
+
/**
|
|
8382
|
+
* Publish from outside a framework handler. Routes through the
|
|
8383
|
+
* framework-owned composed platform, so the same publish reaches every
|
|
8384
|
+
* local subscriber, fires every reactive watcher (`live.effect`,
|
|
8385
|
+
* `live.derived`, `live.aggregate`), and relays to other cluster
|
|
8386
|
+
* instances when a bus is wired - identical semantics to a publish
|
|
8387
|
+
* inside an RPC, cron, or effect handler.
|
|
8388
|
+
*
|
|
8389
|
+
* Throws when the platform has not yet been captured (called before
|
|
8390
|
+
* the adapter's `init({ platform })` hook fires, or in a process where
|
|
8391
|
+
* no svelte-realtime wiring ran). For the rare case where you genuinely
|
|
8392
|
+
* want a no-op when the platform is absent (e.g. a shared utility
|
|
8393
|
+
* that may run in non-realtime contexts), guard with `getPlatform()`.
|
|
8394
|
+
*
|
|
8395
|
+
* @param {string} topic
|
|
8396
|
+
* @param {string} event
|
|
8397
|
+
* @param {unknown} data
|
|
8398
|
+
* @param {unknown} [options]
|
|
8399
|
+
*
|
|
8400
|
+
* @example
|
|
8401
|
+
* ```js
|
|
8402
|
+
* // src/routes/webhooks/+server.js
|
|
8403
|
+
* import { publish } from 'svelte-realtime/server';
|
|
8404
|
+
*
|
|
8405
|
+
* export async function POST({ request }) {
|
|
8406
|
+
* const payload = await request.json();
|
|
8407
|
+
* publish('audit', 'webhook', payload);
|
|
8408
|
+
* return new Response();
|
|
8409
|
+
* }
|
|
8410
|
+
* ```
|
|
8411
|
+
*/
|
|
8412
|
+
export function publish(topic, event, data, options) {
|
|
8413
|
+
const platform = getPlatform();
|
|
8414
|
+
if (!platform) {
|
|
8415
|
+
throw new Error('[svelte-realtime] publish: platform has not been captured yet. Wire `realtime({ ... }).init` (or `setCronPlatform` + `_activateDerived`) from your hooks.ws.js init({ platform }) hook before calling publish() at module scope.');
|
|
8416
|
+
}
|
|
8417
|
+
return platform.publish(topic, event, data, options);
|
|
8418
|
+
}
|
|
8419
|
+
|
|
8420
|
+
// ---------------------------------------------------------------------------
|
|
8421
|
+
// realtime() - Layer 2 convenience factory
|
|
8422
|
+
// ---------------------------------------------------------------------------
|
|
8423
|
+
|
|
8424
|
+
/**
|
|
8425
|
+
* One-call setup that wires every framework seam from a single
|
|
8426
|
+
* declaration of cluster intent. Returns the standard adapter hook
|
|
8427
|
+
* set (`open`, `close`, `message`, `init`) plus optional `upgrade`,
|
|
8428
|
+
* so `hooks.ws.js` is a one-import-one-destructure file:
|
|
8429
|
+
*
|
|
8430
|
+
* ```js
|
|
8431
|
+
* // src/hooks.ws.js (single-replica)
|
|
8432
|
+
* import { realtime } from 'svelte-realtime/server';
|
|
8433
|
+
* export const { upgrade, open, close, message, init } = realtime({
|
|
8434
|
+
* upgrade: ({ cookies }) => validate(cookies),
|
|
8435
|
+
* });
|
|
8436
|
+
* ```
|
|
8437
|
+
*
|
|
8438
|
+
* ```js
|
|
8439
|
+
* // src/hooks.ws.js (cluster)
|
|
8440
|
+
* import { realtime } from 'svelte-realtime/server';
|
|
8441
|
+
* import { redisBus, redisLeader } from 'svelte-adapter-uws-extensions/redis';
|
|
8442
|
+
*
|
|
8443
|
+
* export const { upgrade, open, close, message, init } = realtime({
|
|
8444
|
+
* bus: redisBus(),
|
|
8445
|
+
* leader: redisLeader().isLeader,
|
|
8446
|
+
* upgrade: ({ cookies }) => validate(cookies),
|
|
8447
|
+
* });
|
|
8448
|
+
* ```
|
|
8449
|
+
*
|
|
8450
|
+
* Handler-level code (`live.rpc`, `live.effect`, `live.derived`,
|
|
8451
|
+
* `live.aggregate`, `live.cron`, `live.webhook`, ...) is byte-identical
|
|
8452
|
+
* between the two modes - the only difference between single-replica
|
|
8453
|
+
* and cluster is whether `bus` and `leader` are passed at the top.
|
|
8454
|
+
*
|
|
8455
|
+
* `realtime()` is sugar over the existing primitives. Internally it
|
|
8456
|
+
* calls `setBus(bus)`, `configureCron({ leader })`,
|
|
8457
|
+
* `setCronPlatform(platform)`, and `_activateDerived(platform)` in the
|
|
8458
|
+
* right order when the adapter's `init` hook fires. Mixing `realtime()`
|
|
8459
|
+
* with direct calls to those primitives is supported - the primitives
|
|
8460
|
+
* remain first-class and write the same backing state.
|
|
8461
|
+
*
|
|
8462
|
+
* @param {{
|
|
8463
|
+
* bus?: { wrap: (platform: any) => any } | null,
|
|
8464
|
+
* leader?: (() => boolean) | null,
|
|
8465
|
+
* upgrade?: (...args: any[]) => any,
|
|
8466
|
+
* onError?: (path: string, error: unknown) => void,
|
|
8467
|
+
* }} [config]
|
|
8468
|
+
*/
|
|
8469
|
+
export function realtime(config) {
|
|
8470
|
+
const cfg = config || {};
|
|
8471
|
+
const { bus, leader, upgrade: upgradeFn, onError } = cfg;
|
|
8472
|
+
|
|
8473
|
+
if (bus !== undefined) _setBus(bus);
|
|
8474
|
+
if (leader !== undefined) configureCron({ leader });
|
|
8475
|
+
if (typeof onError === 'function') {
|
|
8476
|
+
// Routes through the existing module-level setter so the
|
|
8477
|
+
// behaviour matches a direct `onError(handler)` call - one
|
|
8478
|
+
// source of truth for the cron / effect / derived error path.
|
|
8479
|
+
_serverErrorHandler = onError;
|
|
8480
|
+
}
|
|
8481
|
+
|
|
8482
|
+
const hooks = {
|
|
8483
|
+
open: pushHooks.open,
|
|
8484
|
+
close: pushHooks.close,
|
|
8485
|
+
message,
|
|
8486
|
+
init(ctx) {
|
|
8487
|
+
if (!ctx || !ctx.platform) {
|
|
8488
|
+
throw new Error('[svelte-realtime] realtime().init: missing platform on hook context (expected adapter init({ platform }) signature)');
|
|
8489
|
+
}
|
|
8490
|
+
setCronPlatform(ctx.platform);
|
|
8491
|
+
_activateDerived(ctx.platform);
|
|
8492
|
+
},
|
|
8493
|
+
};
|
|
8494
|
+
if (typeof upgradeFn === 'function') /** @type {any} */ (hooks).upgrade = upgradeFn;
|
|
8495
|
+
return hooks;
|
|
8496
|
+
}
|