svelte-realtime 0.4.19 → 0.4.21
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 -18
- package/client.d.ts +7 -1
- package/client.js +38 -5
- package/package.json +1 -1
- package/server.js +25 -6
package/README.md
CHANGED
|
@@ -147,8 +147,6 @@ export const messages = live.stream('messages', async (ctx) => {
|
|
|
147
147
|
|
|
148
148
|
{#if $messages === undefined}
|
|
149
149
|
<p>Loading...</p>
|
|
150
|
-
{:else if $messages?.error}
|
|
151
|
-
<p>Failed to load: {$messages.error.message}</p>
|
|
152
150
|
{:else}
|
|
153
151
|
{#each $messages as msg (msg.id)}
|
|
154
152
|
<p><b>{msg.userId}:</b> {msg.text}</p>
|
|
@@ -378,21 +376,19 @@ When the WebSocket reconnects, streams automatically refetch initial data and re
|
|
|
378
376
|
|
|
379
377
|
## Error handling
|
|
380
378
|
|
|
381
|
-
|
|
379
|
+
The data store value never changes shape. It is always your data type or `undefined` while loading. Errors and connection status live on separate reactive stores so a network failure can never crash your UI:
|
|
382
380
|
|
|
383
|
-
|
|
|
384
|
-
|
|
385
|
-
| `undefined` |
|
|
386
|
-
| `
|
|
387
|
-
| `
|
|
381
|
+
| Property | Type | Description |
|
|
382
|
+
|---|---|---|
|
|
383
|
+
| `$store` | `T \| undefined` | Your data. Never replaced by an error object. On failure, the last loaded value is preserved. |
|
|
384
|
+
| `store.error` | `Readable<RpcError \| null>` | Current error, or `null` when healthy. |
|
|
385
|
+
| `store.status` | `Readable<'loading' \| 'connected' \| 'reconnecting' \| 'error'>` | Connection status. |
|
|
388
386
|
|
|
389
|
-
Handle
|
|
387
|
+
Handle loading in your template:
|
|
390
388
|
|
|
391
389
|
```svelte
|
|
392
390
|
{#if $messages === undefined}
|
|
393
391
|
<p>Loading...</p>
|
|
394
|
-
{:else if $messages?.error}
|
|
395
|
-
<p>Failed: {$messages.error.message}</p>
|
|
396
392
|
{:else}
|
|
397
393
|
{#each $messages as msg (msg.id)}
|
|
398
394
|
<p>{msg.text}</p>
|
|
@@ -400,6 +396,27 @@ Handle all three in your template:
|
|
|
400
396
|
{/if}
|
|
401
397
|
```
|
|
402
398
|
|
|
399
|
+
To show errors, subscribe to the `.error` store:
|
|
400
|
+
|
|
401
|
+
```svelte
|
|
402
|
+
<script>
|
|
403
|
+
import { messages } from '$live/chat';
|
|
404
|
+
|
|
405
|
+
const err = messages.error;
|
|
406
|
+
const status = messages.status;
|
|
407
|
+
</script>
|
|
408
|
+
|
|
409
|
+
{#if $err}
|
|
410
|
+
<p>Error: {$err.message} ({$err.code})</p>
|
|
411
|
+
{/if}
|
|
412
|
+
|
|
413
|
+
{#if $status === 'reconnecting'}
|
|
414
|
+
<p>Reconnecting...</p>
|
|
415
|
+
{/if}
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
Defensive patterns like `($store ?? []).filter(...)` work correctly because `$store` is always an array or `undefined`.
|
|
419
|
+
|
|
403
420
|
For RPC calls, errors are thrown as `RpcError` with a `code` field:
|
|
404
421
|
|
|
405
422
|
```js
|
|
@@ -421,22 +438,23 @@ try {
|
|
|
421
438
|
When the adapter's `ready()` promise rejects (terminal close codes 1008, 4401, 4403, exhausted retries, or explicit `close()`), svelte-realtime:
|
|
422
439
|
|
|
423
440
|
- Rejects all pending RPCs immediately with `RpcError('CONNECTION_CLOSED', ...)`
|
|
424
|
-
- Sets
|
|
441
|
+
- Sets `.error` on all active stream stores (the data value is preserved)
|
|
425
442
|
- Drains the offline queue with errors
|
|
426
443
|
|
|
427
444
|
RPCs called after a terminal close reject immediately without sending.
|
|
428
445
|
|
|
429
446
|
### Reusable error boundary component
|
|
430
447
|
|
|
431
|
-
For Svelte 5, you can build a reusable boundary that handles
|
|
448
|
+
For Svelte 5, you can build a reusable boundary that handles loading and error states:
|
|
432
449
|
|
|
433
450
|
```svelte
|
|
434
451
|
<!-- src/lib/StreamView.svelte -->
|
|
435
452
|
<script>
|
|
436
|
-
/** @type {{ store:
|
|
453
|
+
/** @type {{ store: any, children: import('svelte').Snippet, loading?: import('svelte').Snippet, error?: import('svelte').Snippet<[any]> }} */
|
|
437
454
|
let { store, children, loading, error } = $props();
|
|
438
455
|
|
|
439
456
|
let value = $derived($store);
|
|
457
|
+
const err = store.error;
|
|
440
458
|
</script>
|
|
441
459
|
|
|
442
460
|
{#if value === undefined}
|
|
@@ -445,11 +463,11 @@ For Svelte 5, you can build a reusable boundary that handles all three stream st
|
|
|
445
463
|
{:else}
|
|
446
464
|
<p>Loading...</p>
|
|
447
465
|
{/if}
|
|
448
|
-
{:else if
|
|
466
|
+
{:else if $err}
|
|
449
467
|
{#if error}
|
|
450
|
-
{@render error(
|
|
468
|
+
{@render error($err)}
|
|
451
469
|
{:else}
|
|
452
|
-
<p>Error: {
|
|
470
|
+
<p>Error: {$err.message}</p>
|
|
453
471
|
{/if}
|
|
454
472
|
{:else}
|
|
455
473
|
{@render children()}
|
|
@@ -492,7 +510,7 @@ With default slots, the minimal version is just:
|
|
|
492
510
|
</StreamView>
|
|
493
511
|
```
|
|
494
512
|
|
|
495
|
-
This removes the `{#if}/{:else
|
|
513
|
+
This removes the `{#if}/{:else}` boilerplate from every page that uses a stream.
|
|
496
514
|
|
|
497
515
|
---
|
|
498
516
|
|
|
@@ -673,6 +691,32 @@ export async function load({ platform, locals }) {
|
|
|
673
691
|
|
|
674
692
|
The hydrated store still subscribes for live updates on first render. It keeps the SSR data visible instead of showing `undefined` during the initial fetch. Guards still run during `.load()` calls. Pass `{ user }` as the second argument if your guard or init function needs user data.
|
|
675
693
|
|
|
694
|
+
For dynamic streams (streams with a topic function), call the stream first to get the store, then hydrate:
|
|
695
|
+
|
|
696
|
+
```js
|
|
697
|
+
// src/routes/team/[id]/+page.server.js
|
|
698
|
+
export async function load({ platform, locals, params }) {
|
|
699
|
+
const { invitations } = await import('$live/invitation');
|
|
700
|
+
const data = await invitations.load(platform, { args: [params.id], user: locals.user });
|
|
701
|
+
return { invitations: data };
|
|
702
|
+
}
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
```svelte
|
|
706
|
+
<!-- src/routes/team/[id]/+page.svelte -->
|
|
707
|
+
<script>
|
|
708
|
+
import { invitations } from '$live/invitation';
|
|
709
|
+
import { page } from '$app/state';
|
|
710
|
+
let { data } = $props();
|
|
711
|
+
|
|
712
|
+
const invites = invitations(page.params.id).hydrate(data.invitations);
|
|
713
|
+
</script>
|
|
714
|
+
|
|
715
|
+
{#each $invites as invite (invite.id)}
|
|
716
|
+
<p>{invite.email}</p>
|
|
717
|
+
{/each}
|
|
718
|
+
```
|
|
719
|
+
|
|
676
720
|
---
|
|
677
721
|
|
|
678
722
|
## Batching
|
|
@@ -1281,6 +1325,10 @@ export function open(ws, { platform }) {
|
|
|
1281
1325
|
}
|
|
1282
1326
|
```
|
|
1283
1327
|
|
|
1328
|
+
Without this call, derived streams will still serve their initial SSR data but will never receive live updates. In dev mode, a console warning is emitted when a client subscribes to a derived stream and `_activateDerived` has not been called.
|
|
1329
|
+
|
|
1330
|
+
Dynamic derived compute functions receive `ctx.user` from the subscribing client, so auth checks like `if (orgId !== ctx.user.organization_id) throw new LiveError("FORBIDDEN")` work the same as they do in regular stream handlers.
|
|
1331
|
+
|
|
1284
1332
|
| Option | Default | Description |
|
|
1285
1333
|
|---|---|---|
|
|
1286
1334
|
| `merge` | `'set'` | Merge strategy for the derived topic |
|
|
@@ -2009,6 +2057,8 @@ Import from `svelte-realtime/client`.
|
|
|
2009
2057
|
|
|
2010
2058
|
| Method/Property | Description |
|
|
2011
2059
|
|---|---|
|
|
2060
|
+
| `error` | `Readable<RpcError \| null>` -- current error, or `null` when healthy |
|
|
2061
|
+
| `status` | `Readable<'loading' \| 'connected' \| 'reconnecting' \| 'error'>` -- connection status |
|
|
2012
2062
|
| `optimistic(event, data)` | Apply instant UI update, returns rollback function |
|
|
2013
2063
|
| `hydrate(initialData)` | Pre-populate with SSR data |
|
|
2014
2064
|
| `loadMore(...extraArgs)` | Load next page (cursor-based) |
|
package/client.d.ts
CHANGED
|
@@ -45,7 +45,9 @@ export function __rpc(path: string): ((...args: any[]) => Promise<any>) & {
|
|
|
45
45
|
* - `undefined` while loading
|
|
46
46
|
* - The initial data once loaded
|
|
47
47
|
* - Automatically updated with live pub/sub events
|
|
48
|
-
*
|
|
48
|
+
*
|
|
49
|
+
* Errors never replace the store value. On failure, the last known data
|
|
50
|
+
* is preserved and the error is available via the `.error` store.
|
|
49
51
|
*
|
|
50
52
|
* @param path - Stream path (e.g. `'chat/messages'`)
|
|
51
53
|
* @param options - Merge strategy and options
|
|
@@ -57,6 +59,10 @@ export function __rpc(path: string): ((...args: any[]) => Promise<any>) & {
|
|
|
57
59
|
* SSR hydration, and cursor-based pagination.
|
|
58
60
|
*/
|
|
59
61
|
export interface StreamStore<T = any> extends Readable<T> {
|
|
62
|
+
/** Reactive store holding the current error, or null when healthy. Errors never replace the data store value. */
|
|
63
|
+
readonly error: Readable<RpcError | null>;
|
|
64
|
+
/** Reactive store holding the connection status: 'loading', 'connected', 'reconnecting', or 'error'. */
|
|
65
|
+
readonly status: Readable<'loading' | 'connected' | 'reconnecting' | 'error'>;
|
|
60
66
|
/** Apply an instant UI update. Returns a rollback function. */
|
|
61
67
|
optimistic(event: string, data: any): () => void;
|
|
62
68
|
/** Pre-populate with SSR data to avoid loading spinners. */
|
package/client.js
CHANGED
|
@@ -508,6 +508,28 @@ function _createStream(path, options, dynamicArgs) {
|
|
|
508
508
|
let currentValue;
|
|
509
509
|
const store = writable(undefined);
|
|
510
510
|
|
|
511
|
+
/** @type {RpcError | null} */
|
|
512
|
+
let _error = null;
|
|
513
|
+
const _errorStore = writable(null);
|
|
514
|
+
|
|
515
|
+
/** @type {'loading' | 'connected' | 'reconnecting' | 'error'} */
|
|
516
|
+
let _status = 'loading';
|
|
517
|
+
const _statusStore = writable(/** @type {'loading' | 'connected' | 'reconnecting' | 'error'} */ ('loading'));
|
|
518
|
+
|
|
519
|
+
function _setError(/** @type {RpcError} */ err) {
|
|
520
|
+
_error = err;
|
|
521
|
+
_errorStore.set(err);
|
|
522
|
+
_status = 'error';
|
|
523
|
+
_statusStore.set('error');
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function _clearError() {
|
|
527
|
+
if (_error !== null) {
|
|
528
|
+
_error = null;
|
|
529
|
+
_errorStore.set(null);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
511
533
|
/** @type {string | null} */
|
|
512
534
|
let topic = null;
|
|
513
535
|
|
|
@@ -841,7 +863,7 @@ function _createStream(path, options, dynamicArgs) {
|
|
|
841
863
|
function fetchAndSubscribe() {
|
|
842
864
|
if (fetching) return;
|
|
843
865
|
if (_terminated) {
|
|
844
|
-
|
|
866
|
+
_setError(new RpcError('CONNECTION_CLOSED', 'Connection permanently closed'));
|
|
845
867
|
return;
|
|
846
868
|
}
|
|
847
869
|
fetching = true;
|
|
@@ -871,13 +893,13 @@ function _createStream(path, options, dynamicArgs) {
|
|
|
871
893
|
pending.delete(id);
|
|
872
894
|
pendingId = null;
|
|
873
895
|
fetching = false;
|
|
874
|
-
|
|
896
|
+
_setError(new RpcError('DISCONNECTED', 'Connection interrupted (device sleep)'));
|
|
875
897
|
return;
|
|
876
898
|
}
|
|
877
899
|
pending.delete(id);
|
|
878
900
|
pendingId = null;
|
|
879
901
|
fetching = false;
|
|
880
|
-
|
|
902
|
+
_setError(new RpcError('TIMEOUT', `Stream '${path}' timed out after 30s`));
|
|
881
903
|
}, _getTimeout());
|
|
882
904
|
|
|
883
905
|
pending.set(id, {
|
|
@@ -886,6 +908,9 @@ function _createStream(path, options, dynamicArgs) {
|
|
|
886
908
|
fetching = false;
|
|
887
909
|
pendingId = null;
|
|
888
910
|
_reconnectAttempts = 0;
|
|
911
|
+
_clearError();
|
|
912
|
+
_status = 'connected';
|
|
913
|
+
_statusStore.set('connected');
|
|
889
914
|
topic = response.topic || null;
|
|
890
915
|
|
|
891
916
|
// Track sequence number for replay
|
|
@@ -985,7 +1010,7 @@ function _createStream(path, options, dynamicArgs) {
|
|
|
985
1010
|
reject(err) {
|
|
986
1011
|
fetching = false;
|
|
987
1012
|
pendingId = null;
|
|
988
|
-
|
|
1013
|
+
_setError(err instanceof RpcError ? err : new RpcError('STREAM_ERROR', err?.message || 'Stream failed'));
|
|
989
1014
|
},
|
|
990
1015
|
timer
|
|
991
1016
|
});
|
|
@@ -1035,6 +1060,10 @@ function _createStream(path, options, dynamicArgs) {
|
|
|
1035
1060
|
buffer = [];
|
|
1036
1061
|
currentValue = undefined;
|
|
1037
1062
|
store.set(undefined);
|
|
1063
|
+
_error = null;
|
|
1064
|
+
_errorStore.set(null);
|
|
1065
|
+
_status = 'loading';
|
|
1066
|
+
_statusStore.set('loading');
|
|
1038
1067
|
_index.clear();
|
|
1039
1068
|
_history = [];
|
|
1040
1069
|
_historyIndex = -1;
|
|
@@ -1046,6 +1075,8 @@ function _createStream(path, options, dynamicArgs) {
|
|
|
1046
1075
|
let _pendingCleanup = false;
|
|
1047
1076
|
|
|
1048
1077
|
return {
|
|
1078
|
+
error: { subscribe: _errorStore.subscribe },
|
|
1079
|
+
status: { subscribe: _statusStore.subscribe },
|
|
1049
1080
|
subscribe(fn) {
|
|
1050
1081
|
if (subCount++ === 0) {
|
|
1051
1082
|
if (_pendingCleanup) {
|
|
@@ -1064,6 +1095,8 @@ function _createStream(path, options, dynamicArgs) {
|
|
|
1064
1095
|
return;
|
|
1065
1096
|
}
|
|
1066
1097
|
if (s === 'open' && subCount > 0) {
|
|
1098
|
+
_status = 'reconnecting';
|
|
1099
|
+
_statusStore.set('reconnecting');
|
|
1067
1100
|
if (_reconnectTimer) clearTimeout(_reconnectTimer);
|
|
1068
1101
|
let delay;
|
|
1069
1102
|
if (_reconnectAttempts < 2) {
|
|
@@ -1093,7 +1126,7 @@ function _createStream(path, options, dynamicArgs) {
|
|
|
1093
1126
|
if (conn && typeof conn.ready === 'function') {
|
|
1094
1127
|
conn.ready().catch((/** @type {any} */ err) => {
|
|
1095
1128
|
if (subCount > 0) {
|
|
1096
|
-
|
|
1129
|
+
_setError(new RpcError(err?.code || 'CONNECTION_CLOSED', err?.message || 'Connection permanently closed'));
|
|
1097
1130
|
}
|
|
1098
1131
|
});
|
|
1099
1132
|
}
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -778,7 +778,7 @@ live.derived = function derived(sources, fn, options) {
|
|
|
778
778
|
/** @type {Map<string, any[]>} */
|
|
779
779
|
const topicArgs = new Map();
|
|
780
780
|
const topicFn = (...args) => {
|
|
781
|
-
const t = baseTopic + '
|
|
781
|
+
const t = baseTopic + '~' + args.map(a => String(a).replace(/~/g, '')).join('~');
|
|
782
782
|
topicArgs.set(t, args);
|
|
783
783
|
if (topicArgs.size > 10000) {
|
|
784
784
|
const iter = topicArgs.keys();
|
|
@@ -791,7 +791,7 @@ live.derived = function derived(sources, fn, options) {
|
|
|
791
791
|
/** @type {any} */ (fn).__derivedTopicArgs = topicArgs;
|
|
792
792
|
|
|
793
793
|
/** @type {any} */ (fn).__onSubscribe = function (_ctx, resolvedTopic) {
|
|
794
|
-
_activateDynamicDerived(fn, resolvedTopic);
|
|
794
|
+
_activateDynamicDerived(fn, resolvedTopic, _ctx && _ctx.user);
|
|
795
795
|
};
|
|
796
796
|
/** @type {any} */ (fn).__onUnsubscribe = function (_ctx, resolvedTopic) {
|
|
797
797
|
_deactivateDynamicDerived(fn, resolvedTopic);
|
|
@@ -815,6 +815,12 @@ const _dynamicDerivedByFn = new Map();
|
|
|
815
815
|
/** @type {import('svelte-adapter-uws').Platform | null} Captured platform for dynamic derived recomputation */
|
|
816
816
|
let _derivedPlatform = null;
|
|
817
817
|
|
|
818
|
+
/** @type {boolean} Whether _activateDerived has been called at least once */
|
|
819
|
+
let _activateDerivedCalled = false;
|
|
820
|
+
|
|
821
|
+
/** @type {boolean} Whether the missing _activateDerived warning has already fired */
|
|
822
|
+
let _warnedActivateDerived = false;
|
|
823
|
+
|
|
818
824
|
/** @type {Map<string, { sources: string[], fn: Function, debounce: number, timer: ReturnType<typeof setTimeout> | null }>} */
|
|
819
825
|
const effectRegistry = new Map();
|
|
820
826
|
|
|
@@ -1387,6 +1393,7 @@ live.breaker = function breaker(options, fn) {
|
|
|
1387
1393
|
export function __registerDerived(path, fn) {
|
|
1388
1394
|
if (/** @type {any} */ (fn).__lazy) {
|
|
1389
1395
|
_lazyQueue.push({ type: 'derived', path, loader: fn });
|
|
1396
|
+
_hasDynamicDerived = true;
|
|
1390
1397
|
return;
|
|
1391
1398
|
}
|
|
1392
1399
|
|
|
@@ -1397,7 +1404,7 @@ export function __registerDerived(path, fn) {
|
|
|
1397
1404
|
/** @type {Map<string, any[]>} */
|
|
1398
1405
|
const topicArgs = new Map();
|
|
1399
1406
|
const topicFn = (...args) => {
|
|
1400
|
-
const t = path + '
|
|
1407
|
+
const t = path + '~' + args.map(a => String(a).replace(/~/g, '')).join('~');
|
|
1401
1408
|
topicArgs.set(t, args);
|
|
1402
1409
|
if (topicArgs.size > 10000) {
|
|
1403
1410
|
const iter = topicArgs.keys();
|
|
@@ -1443,6 +1450,7 @@ const _activatedPlatforms = new WeakSet();
|
|
|
1443
1450
|
|
|
1444
1451
|
export function _activateDerived(platform) {
|
|
1445
1452
|
_derivedPlatform = platform;
|
|
1453
|
+
_activateDerivedCalled = true;
|
|
1446
1454
|
if (_activatedPlatforms.has(platform)) return;
|
|
1447
1455
|
|
|
1448
1456
|
// Only wrap platform.publish if there are actual reactive registrations
|
|
@@ -1543,7 +1551,7 @@ async function _recomputeDerived(entry, platform) {
|
|
|
1543
1551
|
let result;
|
|
1544
1552
|
if (entry.args) {
|
|
1545
1553
|
const _h = _getCtxHelpers(platform);
|
|
1546
|
-
const ctx = _buildCtx(null, null, platform, _h, null);
|
|
1554
|
+
const ctx = _buildCtx(entry.user || null, null, platform, _h, null);
|
|
1547
1555
|
result = await entry.fn(ctx, ...entry.args);
|
|
1548
1556
|
} else {
|
|
1549
1557
|
result = await entry.fn();
|
|
@@ -1563,8 +1571,9 @@ async function _recomputeDerived(entry, platform) {
|
|
|
1563
1571
|
* Wires the instance's resolved sources into _derivedBySource so publishes trigger recomputation.
|
|
1564
1572
|
* @param {Function} fn - The derived compute function
|
|
1565
1573
|
* @param {string} resolvedTopic - The resolved output topic (e.g. '__derived:5:org_123')
|
|
1574
|
+
* @param {any} [user] - The subscribing client's user data, used for ctx during recomputation
|
|
1566
1575
|
*/
|
|
1567
|
-
function _activateDynamicDerived(fn, resolvedTopic) {
|
|
1576
|
+
function _activateDynamicDerived(fn, resolvedTopic, user) {
|
|
1568
1577
|
const entry = _dynamicDerivedByFn.get(fn);
|
|
1569
1578
|
if (!entry) return;
|
|
1570
1579
|
|
|
@@ -1600,7 +1609,8 @@ function _activateDynamicDerived(fn, resolvedTopic) {
|
|
|
1600
1609
|
resolvedSources,
|
|
1601
1610
|
debounce: entry.debounce,
|
|
1602
1611
|
timer: null,
|
|
1603
|
-
refCount: 1
|
|
1612
|
+
refCount: 1,
|
|
1613
|
+
user: user || null
|
|
1604
1614
|
};
|
|
1605
1615
|
|
|
1606
1616
|
entry.instances.set(resolvedTopic, instance);
|
|
@@ -1852,6 +1862,8 @@ export function _prepareHmr() {
|
|
|
1852
1862
|
_streamsWithUnsubscribe.clear();
|
|
1853
1863
|
_hasDynamicDerived = false;
|
|
1854
1864
|
_dynamicDerivedByFn.clear();
|
|
1865
|
+
_activateDerivedCalled = false;
|
|
1866
|
+
_warnedActivateDerived = false;
|
|
1855
1867
|
|
|
1856
1868
|
return snap;
|
|
1857
1869
|
}
|
|
@@ -2334,6 +2346,13 @@ async function _executeSingleRpc(ws, msg, platform, options) {
|
|
|
2334
2346
|
try { await /** @type {any} */ (fn).__onSubscribe(ctx, topic); } catch {}
|
|
2335
2347
|
}
|
|
2336
2348
|
|
|
2349
|
+
if (/** @type {any} */ (fn).__isDerived && !_activateDerivedCalled && !_warnedActivateDerived) {
|
|
2350
|
+
if (typeof process !== 'undefined' && process.env.NODE_ENV !== 'production') {
|
|
2351
|
+
_warnedActivateDerived = true;
|
|
2352
|
+
console.warn('[svelte-realtime] live.derived() subscribed but _activateDerived(platform) was never called. Derived streams will not receive live updates.\n Call _activateDerived(platform) in your WebSocket open hook.\n See: https://svti.me/derived');
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2337
2356
|
// Channel fast-path
|
|
2338
2357
|
if (/** @type {any} */ (fn).__isChannel) {
|
|
2339
2358
|
const emptyValue = streamOpts.merge === 'set' ? null : [];
|