svelte-realtime 0.4.14 → 0.4.16
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 +37 -3
- package/client.d.ts +11 -0
- package/client.js +14 -2
- package/package.json +1 -1
- package/server.d.ts +23 -3
- package/server.js +206 -14
- package/vite.js +27 -7
package/README.md
CHANGED
|
@@ -261,7 +261,7 @@ The `merge` option on `live.stream()` controls how live pub/sub events are appli
|
|
|
261
261
|
|
|
262
262
|
### crud (default)
|
|
263
263
|
|
|
264
|
-
Handles `created`, `updated`, `deleted` events. The store maintains an array, keyed by `id` (configurable with `key`).
|
|
264
|
+
Handles `created`, `updated`, `deleted` events. The store maintains an array, keyed by `id` (configurable with `key`). Set `max` to cap the buffer size and drop the oldest items when exceeded (useful for live feeds with `prepend: true`).
|
|
265
265
|
|
|
266
266
|
```js
|
|
267
267
|
// Server
|
|
@@ -361,7 +361,7 @@ Events: `update` (add/update by key), `remove` (remove by key), `set` (replace a
|
|
|
361
361
|
| `merge` | `'crud'` | Merge strategy: `'crud'`, `'latest'`, `'set'`, `'presence'`, `'cursor'` |
|
|
362
362
|
| `key` | `'id'` | Key field for `crud` mode |
|
|
363
363
|
| `prepend` | `false` | Prepend new items instead of appending (`crud` mode) |
|
|
364
|
-
| `max` | `50` | Max items to keep
|
|
364
|
+
| `max` | `50` / `0` | Max items to keep. Defaults to 50 for `latest`, 0 (unlimited) for `crud`. Oldest items are dropped when exceeded |
|
|
365
365
|
| `replay` | `false` | Enable seq-based replay for gap-free reconnection |
|
|
366
366
|
| `onSubscribe` | -- | Callback `(ctx, topic)` fired when a client subscribes |
|
|
367
367
|
| `onUnsubscribe` | -- | Callback `(ctx, topic)` fired when a client disconnects |
|
|
@@ -1222,6 +1222,40 @@ On the client, derived streams work like regular streams:
|
|
|
1222
1222
|
<p>Orders: {$dashboardStats?.totalOrders}</p>
|
|
1223
1223
|
```
|
|
1224
1224
|
|
|
1225
|
+
### Dynamic derived streams
|
|
1226
|
+
|
|
1227
|
+
When source topics depend on runtime arguments (e.g., an org ID, a room ID), pass a source factory function instead of a static array. The factory receives the same args the client passes at subscribe time:
|
|
1228
|
+
|
|
1229
|
+
```js
|
|
1230
|
+
export const orgStats = live.derived(
|
|
1231
|
+
(orgId) => [`memberships:${orgId}`, `emails:${orgId}`, `audit:${orgId}`],
|
|
1232
|
+
async (ctx, orgId) => {
|
|
1233
|
+
const [members, emails, auditCount] = await Promise.all([
|
|
1234
|
+
db.query('SELECT count(*) FROM memberships WHERE org_id = $1', [orgId]),
|
|
1235
|
+
db.query('SELECT count(*) FROM emails WHERE org_id = $1', [orgId]),
|
|
1236
|
+
db.query('SELECT count(*) FROM audit_log WHERE org_id = $1', [orgId])
|
|
1237
|
+
]);
|
|
1238
|
+
return { members, emails, auditCount };
|
|
1239
|
+
},
|
|
1240
|
+
{ debounce: 100 }
|
|
1241
|
+
);
|
|
1242
|
+
```
|
|
1243
|
+
|
|
1244
|
+
On the client, dynamic derived streams are called like functions:
|
|
1245
|
+
|
|
1246
|
+
```svelte
|
|
1247
|
+
<script>
|
|
1248
|
+
import { orgStats } from '$live/dashboard';
|
|
1249
|
+
let { orgId } = $props();
|
|
1250
|
+
</script>
|
|
1251
|
+
|
|
1252
|
+
<p>Members: {$orgStats(orgId)?.members}</p>
|
|
1253
|
+
```
|
|
1254
|
+
|
|
1255
|
+
Each unique set of args creates an independent instance with its own source subscriptions. Instances are created when the first subscriber connects and cleaned up when the last subscriber disconnects.
|
|
1256
|
+
|
|
1257
|
+
### Activation
|
|
1258
|
+
|
|
1225
1259
|
Call `_activateDerived(platform)` in your `open` hook to enable derived stream listeners:
|
|
1226
1260
|
|
|
1227
1261
|
```js
|
|
@@ -1916,7 +1950,7 @@ Import from `svelte-realtime/server`.
|
|
|
1916
1950
|
| `live.binary(fn, options?)` | Mark a function as a binary RPC handler (`maxSize` limits payload, default 10MB) |
|
|
1917
1951
|
| `live.validated(schema, fn)` | RPC with Zod/Valibot input validation |
|
|
1918
1952
|
| `live.cron(schedule, topic, fn)` | Server-side scheduled function |
|
|
1919
|
-
| `live.derived(sources, fn, options?)` | Server-side computed stream |
|
|
1953
|
+
| `live.derived(sources, fn, options?)` | Server-side computed stream (static or dynamic sources) |
|
|
1920
1954
|
| `live.effect(sources, fn, options?)` | Server-side reactive side effect |
|
|
1921
1955
|
| `live.aggregate(source, reducers, options)` | Real-time incremental aggregation |
|
|
1922
1956
|
| `live.room(config)` | Collaborative room (data + presence + cursors + actions) |
|
package/client.d.ts
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import type { Readable } from 'svelte/store';
|
|
2
2
|
import type { WSEvent } from 'svelte-adapter-uws/client';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* A store that always holds `undefined`. Use as a fallback for conditional
|
|
6
|
+
* streams so you don't need to import `readable` from `svelte/store`:
|
|
7
|
+
*
|
|
8
|
+
* ```svelte
|
|
9
|
+
* import { todos, empty } from '$live/todos';
|
|
10
|
+
* const items = $derived(user ? todos(orgId) : empty);
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
export const empty: Readable<undefined>;
|
|
14
|
+
|
|
4
15
|
/**
|
|
5
16
|
* Typed error for RPC failures.
|
|
6
17
|
* Contains a `code` field for programmatic handling.
|
package/client.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
import { connect as _connect, on, status } from 'svelte-adapter-uws/client';
|
|
3
|
-
import { writable } from 'svelte/store';
|
|
3
|
+
import { writable, readable } from 'svelte/store';
|
|
4
|
+
|
|
5
|
+
/** @type {import('svelte/store').Readable<undefined>} */
|
|
6
|
+
export const empty = readable(undefined);
|
|
4
7
|
|
|
5
8
|
const _textEncoder = new TextEncoder();
|
|
6
9
|
|
|
@@ -499,7 +502,7 @@ function _createStream(path, options, dynamicArgs) {
|
|
|
499
502
|
let merge = options?.merge || 'crud';
|
|
500
503
|
let key = options?.key || 'id';
|
|
501
504
|
let prepend = options?.prepend || false;
|
|
502
|
-
let max = options?.max || 50;
|
|
505
|
+
let max = options?.max || (merge === 'latest' ? 50 : 0);
|
|
503
506
|
|
|
504
507
|
/** @type {any} */
|
|
505
508
|
let currentValue;
|
|
@@ -637,9 +640,18 @@ function _createStream(path, options, dynamicArgs) {
|
|
|
637
640
|
currentValue.unshift(data);
|
|
638
641
|
for (const [k, i] of _index) _index.set(k, i + 1);
|
|
639
642
|
_index.set(data[key], 0);
|
|
643
|
+
if (max && currentValue.length > max) {
|
|
644
|
+
const removed = currentValue.splice(max);
|
|
645
|
+
for (const item of removed) _index.delete(item[key]);
|
|
646
|
+
}
|
|
640
647
|
} else {
|
|
641
648
|
_index.set(data[key], currentValue.length);
|
|
642
649
|
currentValue.push(data);
|
|
650
|
+
if (max && currentValue.length > max) {
|
|
651
|
+
const removed = currentValue.splice(0, currentValue.length - max);
|
|
652
|
+
for (const item of removed) _index.delete(item[key]);
|
|
653
|
+
_rebuildIndex();
|
|
654
|
+
}
|
|
643
655
|
}
|
|
644
656
|
} else if (event === 'updated') {
|
|
645
657
|
const idx = _index.get(data[key]);
|
package/package.json
CHANGED
package/server.d.ts
CHANGED
|
@@ -67,8 +67,10 @@ export interface StreamOptions {
|
|
|
67
67
|
prepend?: boolean;
|
|
68
68
|
|
|
69
69
|
/**
|
|
70
|
-
* Maximum items to keep
|
|
71
|
-
*
|
|
70
|
+
* Maximum items to keep. When the buffer exceeds this size, the oldest
|
|
71
|
+
* items are dropped. Works with `crud` and `latest` merge strategies.
|
|
72
|
+
*
|
|
73
|
+
* For `latest`, defaults to 50. For `crud`, defaults to 0 (unlimited).
|
|
72
74
|
*/
|
|
73
75
|
max?: number;
|
|
74
76
|
|
|
@@ -475,17 +477,35 @@ export namespace live {
|
|
|
475
477
|
/**
|
|
476
478
|
* Create a server-side computed stream that recomputes when any source topic publishes.
|
|
477
479
|
*
|
|
478
|
-
*
|
|
480
|
+
* Static form: pass an array of topic names to watch.
|
|
481
|
+
* Dynamic form: pass a function that receives runtime args and returns topic names.
|
|
482
|
+
*
|
|
483
|
+
* @param sources - Topic names to watch, or a factory function that receives runtime args
|
|
479
484
|
* @param fn - Async function that computes the derived value
|
|
480
485
|
* @param options - Merge mode and debounce settings
|
|
481
486
|
*
|
|
482
487
|
* @example
|
|
483
488
|
* ```js
|
|
489
|
+
* // Static sources
|
|
484
490
|
* export const summary = live.derived(['orders', 'inventory'], async () => {
|
|
485
491
|
* return { totalOrders: await db.orders.count(), totalItems: await db.inventory.count() };
|
|
486
492
|
* });
|
|
493
|
+
*
|
|
494
|
+
* // Dynamic sources (parameterized)
|
|
495
|
+
* export const stats = live.derived(
|
|
496
|
+
* (orgId) => [`memberships:${orgId}`, `emails:${orgId}`],
|
|
497
|
+
* async (ctx, orgId) => {
|
|
498
|
+
* return { members: await countMembers(orgId), emails: await countEmails(orgId) };
|
|
499
|
+
* },
|
|
500
|
+
* { debounce: 100 }
|
|
501
|
+
* );
|
|
487
502
|
* ```
|
|
488
503
|
*/
|
|
504
|
+
function derived<T extends (...args: any[]) => any>(
|
|
505
|
+
sources: (...args: any[]) => string[],
|
|
506
|
+
fn: T,
|
|
507
|
+
options?: { merge?: string; debounce?: number }
|
|
508
|
+
): T;
|
|
489
509
|
function derived<T extends () => any>(
|
|
490
510
|
sources: string[],
|
|
491
511
|
fn: T,
|
package/server.js
CHANGED
|
@@ -108,6 +108,14 @@ function _copyStreamMeta(target, source) {
|
|
|
108
108
|
if (source.__streamVersion !== undefined) target.__streamVersion = source.__streamVersion;
|
|
109
109
|
if (source.__streamMigrate) target.__streamMigrate = source.__streamMigrate;
|
|
110
110
|
if (source.__isChannel) target.__isChannel = source.__isChannel;
|
|
111
|
+
if (source.__isDerived) target.__isDerived = source.__isDerived;
|
|
112
|
+
if (source.__derivedDynamic) {
|
|
113
|
+
target.__derivedDynamic = source.__derivedDynamic;
|
|
114
|
+
target.__derivedSourceFactory = source.__derivedSourceFactory;
|
|
115
|
+
target.__derivedTopicArgs = source.__derivedTopicArgs;
|
|
116
|
+
target.__derivedDebounce = source.__derivedDebounce;
|
|
117
|
+
}
|
|
118
|
+
if (source.__derivedSources) target.__derivedSources = source.__derivedSources;
|
|
111
119
|
if (source.__isGated) {
|
|
112
120
|
target.__isGated = true;
|
|
113
121
|
target.__gatePredicate = source.__gatePredicate;
|
|
@@ -720,28 +728,69 @@ const derivedRegistry = new Map();
|
|
|
720
728
|
/**
|
|
721
729
|
* Create a server-side computed stream that recomputes when any source topic publishes.
|
|
722
730
|
*
|
|
723
|
-
*
|
|
731
|
+
* Static form: sources is a string[] of topic names.
|
|
732
|
+
* Dynamic form: sources is a function (...args) => string[] that resolves topics at subscribe time.
|
|
733
|
+
*
|
|
734
|
+
* @param {string[] | Function} sources - Topic names to watch, or a factory that receives runtime args
|
|
724
735
|
* @param {Function} fn - Async function that computes the derived value
|
|
725
736
|
* @param {{ merge?: string, debounce?: number }} [options]
|
|
726
737
|
* @returns {Function}
|
|
727
738
|
*/
|
|
728
739
|
live.derived = function derived(sources, fn, options) {
|
|
729
|
-
const
|
|
740
|
+
const baseTopic = /** @type {any} */ (fn).__derivedTopic || ('__derived:' + (_derivedIdCounter++));
|
|
730
741
|
const merge = options?.merge || 'set';
|
|
731
742
|
const debounce = options?.debounce || 0;
|
|
743
|
+
const dynamic = typeof sources === 'function';
|
|
732
744
|
|
|
733
745
|
/** @type {any} */ (fn).__isDerived = true;
|
|
734
746
|
/** @type {any} */ (fn).__isStream = true;
|
|
735
747
|
/** @type {any} */ (fn).__isLive = true;
|
|
736
|
-
/** @type {any} */ (fn).__streamTopic = topic;
|
|
737
748
|
/** @type {any} */ (fn).__streamOptions = { merge, key: 'id' };
|
|
738
|
-
/** @type {any} */ (fn).__derivedSources = sources;
|
|
739
749
|
/** @type {any} */ (fn).__derivedDebounce = debounce;
|
|
750
|
+
|
|
751
|
+
if (dynamic) {
|
|
752
|
+
/** @type {any} */ (fn).__derivedDynamic = true;
|
|
753
|
+
/** @type {any} */ (fn).__derivedSourceFactory = sources;
|
|
754
|
+
/** @type {Map<string, any[]>} */
|
|
755
|
+
const topicArgs = new Map();
|
|
756
|
+
const topicFn = (...args) => {
|
|
757
|
+
const t = baseTopic + '\x00' + args.map(a => String(a).replace(/\x00/g, '')).join('\x00');
|
|
758
|
+
topicArgs.set(t, args);
|
|
759
|
+
if (topicArgs.size > 10000) {
|
|
760
|
+
const iter = topicArgs.keys();
|
|
761
|
+
topicArgs.delete(iter.next().value);
|
|
762
|
+
}
|
|
763
|
+
return t;
|
|
764
|
+
};
|
|
765
|
+
/** @type {any} */ (topicFn).__topicUsesCtx = false;
|
|
766
|
+
/** @type {any} */ (fn).__streamTopic = topicFn;
|
|
767
|
+
/** @type {any} */ (fn).__derivedTopicArgs = topicArgs;
|
|
768
|
+
|
|
769
|
+
/** @type {any} */ (fn).__onSubscribe = function (_ctx, resolvedTopic) {
|
|
770
|
+
_activateDynamicDerived(fn, resolvedTopic);
|
|
771
|
+
};
|
|
772
|
+
/** @type {any} */ (fn).__onUnsubscribe = function (_ctx, resolvedTopic) {
|
|
773
|
+
_deactivateDynamicDerived(fn, resolvedTopic);
|
|
774
|
+
};
|
|
775
|
+
} else {
|
|
776
|
+
/** @type {any} */ (fn).__streamTopic = baseTopic;
|
|
777
|
+
/** @type {any} */ (fn).__derivedSources = sources;
|
|
778
|
+
}
|
|
779
|
+
|
|
740
780
|
return fn;
|
|
741
781
|
};
|
|
742
782
|
|
|
743
783
|
let _derivedIdCounter = 0;
|
|
744
784
|
|
|
785
|
+
/** @type {boolean} Whether any dynamic derived streams have been registered */
|
|
786
|
+
let _hasDynamicDerived = false;
|
|
787
|
+
|
|
788
|
+
/** @type {Map<Function, object>} O(1) lookup from fn reference to dynamic derived registry entry */
|
|
789
|
+
const _dynamicDerivedByFn = new Map();
|
|
790
|
+
|
|
791
|
+
/** @type {import('svelte-adapter-uws').Platform | null} Captured platform for dynamic derived recomputation */
|
|
792
|
+
let _derivedPlatform = null;
|
|
793
|
+
|
|
745
794
|
/** @type {Map<string, { sources: string[], fn: Function, debounce: number, timer: ReturnType<typeof setTimeout> | null }>} */
|
|
746
795
|
const effectRegistry = new Map();
|
|
747
796
|
|
|
@@ -1316,6 +1365,20 @@ export function __registerDerived(path, fn) {
|
|
|
1316
1365
|
_lazyQueue.push({ type: 'derived', path, loader: fn });
|
|
1317
1366
|
return;
|
|
1318
1367
|
}
|
|
1368
|
+
|
|
1369
|
+
if (/** @type {any} */ (fn).__derivedDynamic) {
|
|
1370
|
+
const sourceFactory = /** @type {any} */ (fn).__derivedSourceFactory;
|
|
1371
|
+
const debounce = /** @type {any} */ (fn).__derivedDebounce || 0;
|
|
1372
|
+
const entry = {
|
|
1373
|
+
sources: null, sourceFactory, fn, topic: /** @type {any} */ (fn).__streamTopic,
|
|
1374
|
+
debounce, timer: null, dynamic: true, instances: new Map()
|
|
1375
|
+
};
|
|
1376
|
+
derivedRegistry.set(path, entry);
|
|
1377
|
+
_dynamicDerivedByFn.set(fn, entry);
|
|
1378
|
+
_hasDynamicDerived = true;
|
|
1379
|
+
return;
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1319
1382
|
const sources = /** @type {any} */ (fn).__derivedSources;
|
|
1320
1383
|
const topic = /** @type {any} */ (fn).__streamTopic;
|
|
1321
1384
|
const debounce = /** @type {any} */ (fn).__derivedDebounce || 0;
|
|
@@ -1339,14 +1402,19 @@ export function __registerDerived(path, fn) {
|
|
|
1339
1402
|
const _activatedPlatforms = new WeakSet();
|
|
1340
1403
|
|
|
1341
1404
|
export function _activateDerived(platform) {
|
|
1405
|
+
_derivedPlatform = platform;
|
|
1342
1406
|
if (_activatedPlatforms.has(platform)) return;
|
|
1343
1407
|
|
|
1344
1408
|
// Only wrap platform.publish if there are actual reactive registrations
|
|
1345
|
-
if (_derivedBySource.size === 0 && _effectBySource.size === 0 && _aggregateBySource.size === 0) {
|
|
1409
|
+
if (_derivedBySource.size === 0 && _effectBySource.size === 0 && _aggregateBySource.size === 0 && !_hasDynamicDerived) {
|
|
1346
1410
|
return;
|
|
1347
1411
|
}
|
|
1348
1412
|
|
|
1349
1413
|
_activatedPlatforms.add(platform);
|
|
1414
|
+
_wrapPlatformPublish(platform);
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
function _wrapPlatformPublish(platform) {
|
|
1350
1418
|
|
|
1351
1419
|
const originalPublish = platform.publish.bind(platform);
|
|
1352
1420
|
|
|
@@ -1426,20 +1494,121 @@ export function _activateDerived(platform) {
|
|
|
1426
1494
|
|
|
1427
1495
|
/**
|
|
1428
1496
|
* Recompute a derived stream and publish the result.
|
|
1429
|
-
*
|
|
1497
|
+
* For dynamic instances, entry.args holds the runtime args and a ctx is built from the platform.
|
|
1498
|
+
* @param {{ fn: Function, topic: string, args?: any[] }} entry
|
|
1430
1499
|
* @param {import('svelte-adapter-uws').Platform} platform
|
|
1431
1500
|
*/
|
|
1432
1501
|
async function _recomputeDerived(entry, platform) {
|
|
1433
1502
|
try {
|
|
1434
|
-
|
|
1503
|
+
let result;
|
|
1504
|
+
if (entry.args) {
|
|
1505
|
+
const _h = _getCtxHelpers(platform);
|
|
1506
|
+
const ctx = _buildCtx(null, null, platform, _h, null);
|
|
1507
|
+
result = await entry.fn(ctx, ...entry.args);
|
|
1508
|
+
} else {
|
|
1509
|
+
result = await entry.fn();
|
|
1510
|
+
}
|
|
1435
1511
|
platform.publish(entry.topic, 'set', result);
|
|
1436
1512
|
} catch (err) {
|
|
1437
|
-
if (
|
|
1513
|
+
if (_serverErrorHandler) {
|
|
1514
|
+
try { _serverErrorHandler('derived', err); } catch {}
|
|
1515
|
+
} else if (typeof process !== 'undefined' && process.env.NODE_ENV !== 'production') {
|
|
1438
1516
|
console.error(`[svelte-realtime] Derived stream '${entry.topic}' error:`, err);
|
|
1439
1517
|
}
|
|
1440
1518
|
}
|
|
1441
1519
|
}
|
|
1442
1520
|
|
|
1521
|
+
/**
|
|
1522
|
+
* Activate a dynamic derived instance for a resolved topic.
|
|
1523
|
+
* Wires the instance's resolved sources into _derivedBySource so publishes trigger recomputation.
|
|
1524
|
+
* @param {Function} fn - The derived compute function
|
|
1525
|
+
* @param {string} resolvedTopic - The resolved output topic (e.g. '__derived:5:org_123')
|
|
1526
|
+
*/
|
|
1527
|
+
function _activateDynamicDerived(fn, resolvedTopic) {
|
|
1528
|
+
const entry = _dynamicDerivedByFn.get(fn);
|
|
1529
|
+
if (!entry) return;
|
|
1530
|
+
|
|
1531
|
+
const existing = entry.instances.get(resolvedTopic);
|
|
1532
|
+
if (existing) {
|
|
1533
|
+
existing.refCount++;
|
|
1534
|
+
return;
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
// Late activation: if _activateDerived returned early before dynamic entries existed,
|
|
1538
|
+
// wrap platform.publish now that we have something to watch.
|
|
1539
|
+
if (_derivedPlatform && !_activatedPlatforms.has(_derivedPlatform)) {
|
|
1540
|
+
_activatedPlatforms.add(_derivedPlatform);
|
|
1541
|
+
_wrapPlatformPublish(_derivedPlatform);
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
const topicArgs = /** @type {any} */ (fn).__derivedTopicArgs;
|
|
1545
|
+
const args = topicArgs && topicArgs.get(resolvedTopic);
|
|
1546
|
+
if (!args) return;
|
|
1547
|
+
|
|
1548
|
+
const resolvedSources = entry.sourceFactory(...args);
|
|
1549
|
+
if (!Array.isArray(resolvedSources) || resolvedSources.length === 0) {
|
|
1550
|
+
if (typeof process !== 'undefined' && process.env.NODE_ENV !== 'production') {
|
|
1551
|
+
console.warn(`[svelte-realtime] Dynamic derived sourceFactory returned empty sources for topic '${resolvedTopic}'`);
|
|
1552
|
+
}
|
|
1553
|
+
return;
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
const instance = {
|
|
1557
|
+
fn: entry.fn,
|
|
1558
|
+
args,
|
|
1559
|
+
topic: resolvedTopic,
|
|
1560
|
+
resolvedSources,
|
|
1561
|
+
debounce: entry.debounce,
|
|
1562
|
+
timer: null,
|
|
1563
|
+
refCount: 1
|
|
1564
|
+
};
|
|
1565
|
+
|
|
1566
|
+
entry.instances.set(resolvedTopic, instance);
|
|
1567
|
+
|
|
1568
|
+
for (const src of resolvedSources) {
|
|
1569
|
+
let set = _derivedBySource.get(src);
|
|
1570
|
+
if (!set) { set = new Set(); _derivedBySource.set(src, set); }
|
|
1571
|
+
set.add(instance);
|
|
1572
|
+
_watchedTopics.add(src);
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
/**
|
|
1577
|
+
* Deactivate a dynamic derived instance when the last subscriber disconnects.
|
|
1578
|
+
* Removes the instance from _derivedBySource and cleans up.
|
|
1579
|
+
* @param {Function} fn - The derived compute function
|
|
1580
|
+
* @param {string} resolvedTopic - The resolved output topic
|
|
1581
|
+
*/
|
|
1582
|
+
function _deactivateDynamicDerived(fn, resolvedTopic) {
|
|
1583
|
+
const entry = _dynamicDerivedByFn.get(fn);
|
|
1584
|
+
if (!entry) return;
|
|
1585
|
+
|
|
1586
|
+
const instance = entry.instances.get(resolvedTopic);
|
|
1587
|
+
if (!instance) return;
|
|
1588
|
+
|
|
1589
|
+
instance.refCount--;
|
|
1590
|
+
if (instance.refCount > 0) return;
|
|
1591
|
+
|
|
1592
|
+
if (instance.timer) clearTimeout(instance.timer);
|
|
1593
|
+
|
|
1594
|
+
for (const src of instance.resolvedSources) {
|
|
1595
|
+
const set = _derivedBySource.get(src);
|
|
1596
|
+
if (set) {
|
|
1597
|
+
set.delete(instance);
|
|
1598
|
+
if (set.size === 0) {
|
|
1599
|
+
_derivedBySource.delete(src);
|
|
1600
|
+
if (!_effectBySource.has(src) && !_aggregateBySource.has(src)) {
|
|
1601
|
+
_watchedTopics.delete(src);
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
entry.instances.delete(resolvedTopic);
|
|
1608
|
+
const topicArgs = /** @type {any} */ (fn).__derivedTopicArgs;
|
|
1609
|
+
if (topicArgs) topicArgs.delete(resolvedTopic);
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1443
1612
|
/**
|
|
1444
1613
|
* Fire an effect handler. Errors are caught and routed to the error handler.
|
|
1445
1614
|
* @param {{ fn: Function }} entry
|
|
@@ -1606,7 +1775,12 @@ export function _prepareHmr() {
|
|
|
1606
1775
|
};
|
|
1607
1776
|
|
|
1608
1777
|
// Clear debounce timers
|
|
1609
|
-
for (const e of derivedRegistry.values()) {
|
|
1778
|
+
for (const e of derivedRegistry.values()) {
|
|
1779
|
+
if (e.timer) clearTimeout(e.timer);
|
|
1780
|
+
if (e.instances) {
|
|
1781
|
+
for (const inst of e.instances.values()) { if (inst.timer) clearTimeout(inst.timer); }
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1610
1784
|
for (const e of effectRegistry.values()) { if (e.timer) clearTimeout(e.timer); }
|
|
1611
1785
|
for (const e of aggregateRegistry.values()) { if (e.timer) clearTimeout(e.timer); }
|
|
1612
1786
|
|
|
@@ -1636,6 +1810,8 @@ export function _prepareHmr() {
|
|
|
1636
1810
|
_aggregateByTopic.clear();
|
|
1637
1811
|
_watchedTopics.clear();
|
|
1638
1812
|
_streamsWithUnsubscribe.clear();
|
|
1813
|
+
_hasDynamicDerived = false;
|
|
1814
|
+
_dynamicDerivedByFn.clear();
|
|
1639
1815
|
|
|
1640
1816
|
return snap;
|
|
1641
1817
|
}
|
|
@@ -1662,11 +1838,27 @@ export function _restoreHmr(snap) {
|
|
|
1662
1838
|
for (const [k, v] of snap.derived) {
|
|
1663
1839
|
v.timer = null;
|
|
1664
1840
|
derivedRegistry.set(k, v);
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1841
|
+
if (v.dynamic) {
|
|
1842
|
+
_hasDynamicDerived = true;
|
|
1843
|
+
_dynamicDerivedByFn.set(v.fn, v);
|
|
1844
|
+
if (v.instances) {
|
|
1845
|
+
for (const inst of v.instances.values()) {
|
|
1846
|
+
inst.timer = null;
|
|
1847
|
+
for (const src of inst.resolvedSources) {
|
|
1848
|
+
let set = _derivedBySource.get(src);
|
|
1849
|
+
if (!set) { set = new Set(); _derivedBySource.set(src, set); }
|
|
1850
|
+
set.add(inst);
|
|
1851
|
+
_watchedTopics.add(src);
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
} else {
|
|
1856
|
+
for (const src of v.sources) {
|
|
1857
|
+
let set = _derivedBySource.get(src);
|
|
1858
|
+
if (!set) { set = new Set(); _derivedBySource.set(src, set); }
|
|
1859
|
+
set.add(v);
|
|
1860
|
+
_watchedTopics.add(src);
|
|
1861
|
+
}
|
|
1670
1862
|
}
|
|
1671
1863
|
}
|
|
1672
1864
|
|
package/vite.js
CHANGED
|
@@ -12,6 +12,7 @@ const DYNAMIC_STREAM_RE = /export\s+const\s+(\w+)\s*=\s*live\.stream\s*\(\s*(?:\
|
|
|
12
12
|
const CRON_EXPORT_RE = /export\s+const\s+(\w+)\s*=\s*live\.cron\s*\(/g;
|
|
13
13
|
const BINARY_EXPORT_RE = /export\s+const\s+(\w+)\s*=\s*live\.binary\s*\(/g;
|
|
14
14
|
const DERIVED_EXPORT_RE = /export\s+const\s+(\w+)\s*=\s*live\.derived\s*\(/g;
|
|
15
|
+
const DYNAMIC_DERIVED_RE = /export\s+const\s+(\w+)\s*=\s*live\.derived\s*\(\s*(?:\([^)]*\)|[a-zA-Z_$][\w$]*)\s*=>/g;
|
|
15
16
|
const ROOM_EXPORT_RE = /export\s+const\s+(\w+)\s*=\s*live\.room\s*\(/g;
|
|
16
17
|
const WEBHOOK_EXPORT_RE = /export\s+const\s+(\w+)\s*=\s*live\.webhook\s*\(/g;
|
|
17
18
|
const CHANNEL_EXPORT_RE = /export\s+const\s+(\w+)\s*=\s*live\.channel\s*\(/g;
|
|
@@ -518,8 +519,8 @@ function _generateSsrStubs(filePath, modulePath) {
|
|
|
518
519
|
const dynamicNames = new Set();
|
|
519
520
|
let match;
|
|
520
521
|
|
|
521
|
-
// Detect dynamic (function-returning) streams and
|
|
522
|
-
for (const re of [DYNAMIC_STREAM_RE, DYNAMIC_CHANNEL_RE]) {
|
|
522
|
+
// Detect dynamic (function-returning) streams, channels, and derived
|
|
523
|
+
for (const re of [DYNAMIC_STREAM_RE, DYNAMIC_CHANNEL_RE, DYNAMIC_DERIVED_RE]) {
|
|
523
524
|
re.lastIndex = 0;
|
|
524
525
|
while ((match = re.exec(source)) !== null) {
|
|
525
526
|
dynamicNames.add(match[1]);
|
|
@@ -683,8 +684,12 @@ function _generateClientStubs(filePath, modulePath, dir) {
|
|
|
683
684
|
if (!exportedNames.has(name)) {
|
|
684
685
|
exportedNames.add(name);
|
|
685
686
|
imports.add('__stream');
|
|
686
|
-
|
|
687
|
-
|
|
687
|
+
const isDynamic = _isDynamicExport(source, name, 'live\\.derived');
|
|
688
|
+
if (isDynamic) {
|
|
689
|
+
lines.push(`export const ${name} = __stream('${modulePath}/${name}', ${JSON.stringify({ merge: 'set', key: 'id' })}, true);`);
|
|
690
|
+
} else {
|
|
691
|
+
lines.push(`export const ${name} = __stream('${modulePath}/${name}', ${JSON.stringify({ merge: 'set', key: 'id' })});`);
|
|
692
|
+
}
|
|
688
693
|
}
|
|
689
694
|
}
|
|
690
695
|
|
|
@@ -785,7 +790,9 @@ function _generateClientStubs(filePath, modulePath, dir) {
|
|
|
785
790
|
? `import { ${[...imports].join(', ')} } from 'svelte-realtime/client';\n`
|
|
786
791
|
: '';
|
|
787
792
|
|
|
788
|
-
|
|
793
|
+
const reexport = `export { empty } from 'svelte-realtime/client';\n`;
|
|
794
|
+
|
|
795
|
+
return importLine + reexport + lines.join('\n') + '\n';
|
|
789
796
|
}
|
|
790
797
|
|
|
791
798
|
/**
|
|
@@ -1643,14 +1650,25 @@ function _generateTypeDeclarations(liveDir, dir) {
|
|
|
1643
1650
|
}
|
|
1644
1651
|
}
|
|
1645
1652
|
|
|
1646
|
-
// Detect live.derived() exports (read-only stream)
|
|
1653
|
+
// Detect live.derived() exports (read-only stream, static or dynamic)
|
|
1647
1654
|
DERIVED_EXPORT_RE.lastIndex = 0;
|
|
1648
1655
|
while ((match = DERIVED_EXPORT_RE.exec(source)) !== null) {
|
|
1649
1656
|
const name = match[1];
|
|
1650
1657
|
handledNames.add(name);
|
|
1651
1658
|
if (!exports.some(e => e.includes(`export const ${name}:`))) {
|
|
1652
1659
|
needsStreamStore = true;
|
|
1653
|
-
|
|
1660
|
+
const isDynamic = _isDynamicExport(source, name, 'live\\.derived');
|
|
1661
|
+
const loadSig = `{ load(platform: any, options?: { args?: any[]; user?: any }): Promise<any> }`;
|
|
1662
|
+
if (isDynamic) {
|
|
1663
|
+
if (isTS) {
|
|
1664
|
+
const factoryParams = _extractDynamicFactoryParams(source, name, 'live\\.derived');
|
|
1665
|
+
exports.push(` export const ${name}: (${factoryParams} => StreamStore<any>) & ${loadSig};`);
|
|
1666
|
+
} else {
|
|
1667
|
+
exports.push(` export const ${name}: ((...args: any[]) => StreamStore<any>) & ${loadSig};`);
|
|
1668
|
+
}
|
|
1669
|
+
} else {
|
|
1670
|
+
exports.push(` export const ${name}: StreamStore<any> & ${loadSig};`);
|
|
1671
|
+
}
|
|
1654
1672
|
}
|
|
1655
1673
|
}
|
|
1656
1674
|
|
|
@@ -1712,10 +1730,12 @@ function _generateTypeDeclarations(liveDir, dir) {
|
|
|
1712
1730
|
if (needsRpcError) clientImports.push('RpcError');
|
|
1713
1731
|
declarations.push(` import type { ${clientImports.join(', ')} } from 'svelte-realtime/client';`);
|
|
1714
1732
|
}
|
|
1733
|
+
declarations.push(` import type { Readable } from 'svelte/store';`);
|
|
1715
1734
|
if (needsStreamStore || needsRpcError) {
|
|
1716
1735
|
declarations.push('');
|
|
1717
1736
|
}
|
|
1718
1737
|
declarations.push(...exports);
|
|
1738
|
+
declarations.push(` export const empty: Readable<undefined>;`);
|
|
1719
1739
|
declarations.push('}');
|
|
1720
1740
|
declarations.push('');
|
|
1721
1741
|
}
|