svelte-realtime 0.4.18 → 0.4.20
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 +22 -3
- package/package.json +1 -1
- package/server.d.ts +13 -2
- package/server.js +55 -12
package/README.md
CHANGED
|
@@ -574,7 +574,7 @@ Same arguments return the same cached store instance. The cache is cleaned up wh
|
|
|
574
574
|
|
|
575
575
|
## Schema validation
|
|
576
576
|
|
|
577
|
-
Use `live.validated(schema, fn)` to validate the first argument against a
|
|
577
|
+
Use `live.validated(schema, fn)` to validate the first argument against a schema before the function runs. Any [Standard Schema](https://standardschema.dev/)-compatible validator is supported, including Zod, ArkType, Valibot, and others.
|
|
578
578
|
|
|
579
579
|
```js
|
|
580
580
|
import { z } from 'zod';
|
|
@@ -592,7 +592,22 @@ export const addTodo = live.validated(CreateTodo, async (ctx, input) => {
|
|
|
592
592
|
});
|
|
593
593
|
```
|
|
594
594
|
|
|
595
|
-
|
|
595
|
+
Because `live.validated()` uses the [Standard Schema](https://standardschema.dev/) interface, you can swap in any compatible validator:
|
|
596
|
+
|
|
597
|
+
```js
|
|
598
|
+
import { type } from 'arktype';
|
|
599
|
+
import { live } from 'svelte-realtime/server';
|
|
600
|
+
|
|
601
|
+
const CreateTodo = type({ text: 'string>0', priority: '"low"|"medium"|"high"|undefined' });
|
|
602
|
+
|
|
603
|
+
export const addTodo = live.validated(CreateTodo, async (ctx, input) => {
|
|
604
|
+
const todo = await db.todos.insert({ ...input, userId: ctx.user.id });
|
|
605
|
+
ctx.publish('todos', 'created', todo);
|
|
606
|
+
return todo;
|
|
607
|
+
});
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
On the client, validated exports work like regular `live()` calls. Validation errors are thrown as `RpcError` with `code: 'VALIDATION'` and an `issues` array.
|
|
596
611
|
|
|
597
612
|
---
|
|
598
613
|
|
|
@@ -1266,6 +1281,10 @@ export function open(ws, { platform }) {
|
|
|
1266
1281
|
}
|
|
1267
1282
|
```
|
|
1268
1283
|
|
|
1284
|
+
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.
|
|
1285
|
+
|
|
1286
|
+
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.
|
|
1287
|
+
|
|
1269
1288
|
| Option | Default | Description |
|
|
1270
1289
|
|---|---|---|
|
|
1271
1290
|
| `merge` | `'set'` | Merge strategy for the derived topic |
|
|
@@ -1948,7 +1967,7 @@ Import from `svelte-realtime/server`.
|
|
|
1948
1967
|
| `live.stream(topic, initFn, options?)` | Create a reactive stream |
|
|
1949
1968
|
| `live.channel(topic, options?)` | Create an ephemeral pub/sub channel |
|
|
1950
1969
|
| `live.binary(fn, options?)` | Mark a function as a binary RPC handler (`maxSize` limits payload, default 10MB) |
|
|
1951
|
-
| `live.validated(schema, fn)` | RPC with
|
|
1970
|
+
| `live.validated(schema, fn)` | RPC with [Standard Schema](https://standardschema.dev/) input validation (Zod, ArkType, Valibot, etc.) |
|
|
1952
1971
|
| `live.cron(schedule, topic, fn)` | Server-side scheduled function |
|
|
1953
1972
|
| `live.derived(sources, fn, options?)` | Server-side computed stream (static or dynamic sources) |
|
|
1954
1973
|
| `live.effect(sources, fn, options?)` | Server-side reactive side effect |
|
package/package.json
CHANGED
package/server.d.ts
CHANGED
|
@@ -370,18 +370,29 @@ export namespace live {
|
|
|
370
370
|
/**
|
|
371
371
|
* Mark a function as RPC-callable with schema validation.
|
|
372
372
|
* Validates args[0] against the schema before calling fn.
|
|
373
|
-
* Supports
|
|
373
|
+
* Supports any [Standard Schema](https://standardschema.dev/)-compatible schema,
|
|
374
|
+
* including Zod, ArkType, Valibot v1+, and others.
|
|
374
375
|
*
|
|
375
|
-
* @param schema - Zod
|
|
376
|
+
* @param schema - Zod, ArkType, Valibot, or any Standard Schema-compatible schema
|
|
376
377
|
* @param fn - Handler function (ctx, validatedInput, ...rest)
|
|
377
378
|
*
|
|
378
379
|
* @example
|
|
379
380
|
* ```js
|
|
381
|
+
* import { z } from 'zod';
|
|
380
382
|
* const SendSchema = z.object({ text: z.string().min(1) });
|
|
381
383
|
* export const send = live.validated(SendSchema, async (ctx, input) => {
|
|
382
384
|
* // input is validated and typed
|
|
383
385
|
* });
|
|
384
386
|
* ```
|
|
387
|
+
*
|
|
388
|
+
* @example
|
|
389
|
+
* ```js
|
|
390
|
+
* import { type } from 'arktype';
|
|
391
|
+
* const SendSchema = type({ text: 'string>0' });
|
|
392
|
+
* export const send = live.validated(SendSchema, async (ctx, input) => {
|
|
393
|
+
* // works with any Standard Schema-compatible validator
|
|
394
|
+
* });
|
|
395
|
+
* ```
|
|
385
396
|
*/
|
|
386
397
|
function validated<S, T extends (ctx: LiveContext<any>, input: any, ...args: any[]) => any>(
|
|
387
398
|
schema: S,
|
package/server.js
CHANGED
|
@@ -600,9 +600,10 @@ live.rateLimit = function rateLimit(config, fn) {
|
|
|
600
600
|
/**
|
|
601
601
|
* Mark a function as RPC-callable with schema validation.
|
|
602
602
|
* Validates args[0] against the schema before calling fn.
|
|
603
|
-
* Supports
|
|
603
|
+
* Supports any Standard Schema-compatible schema (https://standardschema.dev/),
|
|
604
|
+
* including Zod, ArkType, Valibot v1+, and others.
|
|
604
605
|
*
|
|
605
|
-
* @param {any} schema - Zod
|
|
606
|
+
* @param {any} schema - Zod, ArkType, Valibot, or any Standard Schema-compatible schema
|
|
606
607
|
* @param {Function} fn - Handler function (ctx, validatedInput, ...rest)
|
|
607
608
|
* @returns {Function}
|
|
608
609
|
*/
|
|
@@ -626,13 +627,36 @@ live.validated = function validated(schema, fn) {
|
|
|
626
627
|
};
|
|
627
628
|
|
|
628
629
|
/**
|
|
629
|
-
* Validate input against a
|
|
630
|
+
* Validate input against a Standard Schema-compatible schema, with legacy Zod/Valibot fallbacks.
|
|
630
631
|
* @param {any} schema
|
|
631
632
|
* @param {any} input
|
|
632
633
|
* @returns {{ ok: true, data: any } | { ok: false, message: string, issues: Array<{ path: string[], message: string }> }}
|
|
633
634
|
*/
|
|
634
635
|
function _validate(schema, input) {
|
|
635
|
-
//
|
|
636
|
+
// Standard Schema: schema exposes `~standard.validate` (https://standardschema.dev/)
|
|
637
|
+
if (schema?.['~standard'] && typeof schema['~standard'].validate === 'function') {
|
|
638
|
+
const result = schema['~standard'].validate(input);
|
|
639
|
+
if (result instanceof Promise) {
|
|
640
|
+
return {
|
|
641
|
+
ok: false,
|
|
642
|
+
message: 'Async schemas are not supported in live.validated(). Use a synchronous schema.',
|
|
643
|
+
issues: [{ path: [], message: 'Async schema not supported' }]
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
if (result.issues == null) {
|
|
647
|
+
return { ok: true, data: result.value };
|
|
648
|
+
}
|
|
649
|
+
const issues = result.issues.map((/** @type {any} */ i) => ({
|
|
650
|
+
path: (i.path || []).map((/** @type {any} */ p) => {
|
|
651
|
+
const key = typeof p === 'object' && p !== null && 'key' in p ? p.key : p;
|
|
652
|
+
return key != null ? String(key) : '';
|
|
653
|
+
}).filter((k) => k !== ''),
|
|
654
|
+
message: i.message || 'Validation failed'
|
|
655
|
+
}));
|
|
656
|
+
return { ok: false, message: 'Validation failed', issues };
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// Zod legacy fallback: schema has .safeParse method
|
|
636
660
|
if (typeof schema?.safeParse === 'function') {
|
|
637
661
|
const result = schema.safeParse(input);
|
|
638
662
|
if (result.success) {
|
|
@@ -649,7 +673,7 @@ function _validate(schema, input) {
|
|
|
649
673
|
};
|
|
650
674
|
}
|
|
651
675
|
|
|
652
|
-
// Valibot
|
|
676
|
+
// Valibot legacy fallback: schema is passed to a standalone safeParse
|
|
653
677
|
// In Valibot v1, schemas have a ._run or .pipe method
|
|
654
678
|
// Try to use the schema directly as a Valibot schema
|
|
655
679
|
if (schema?._run || schema?.pipe || schema?.type) {
|
|
@@ -675,7 +699,7 @@ function _validate(schema, input) {
|
|
|
675
699
|
// Unknown schema type -- reject. Passing unvalidated input through is a security risk.
|
|
676
700
|
return {
|
|
677
701
|
ok: false,
|
|
678
|
-
message: 'Unrecognized schema type passed to live.validated(). Supported: Zod (.safeParse), Valibot (._run).',
|
|
702
|
+
message: 'Unrecognized schema type passed to live.validated(). Supported: Standard Schema (https://standardschema.dev/), Zod (.safeParse), Valibot (._run).',
|
|
679
703
|
issues: [{ path: [], message: 'Unrecognized schema type' }]
|
|
680
704
|
};
|
|
681
705
|
}
|
|
@@ -754,7 +778,7 @@ live.derived = function derived(sources, fn, options) {
|
|
|
754
778
|
/** @type {Map<string, any[]>} */
|
|
755
779
|
const topicArgs = new Map();
|
|
756
780
|
const topicFn = (...args) => {
|
|
757
|
-
const t = baseTopic + '
|
|
781
|
+
const t = baseTopic + '~' + args.map(a => String(a).replace(/~/g, '')).join('~');
|
|
758
782
|
topicArgs.set(t, args);
|
|
759
783
|
if (topicArgs.size > 10000) {
|
|
760
784
|
const iter = topicArgs.keys();
|
|
@@ -767,7 +791,7 @@ live.derived = function derived(sources, fn, options) {
|
|
|
767
791
|
/** @type {any} */ (fn).__derivedTopicArgs = topicArgs;
|
|
768
792
|
|
|
769
793
|
/** @type {any} */ (fn).__onSubscribe = function (_ctx, resolvedTopic) {
|
|
770
|
-
_activateDynamicDerived(fn, resolvedTopic);
|
|
794
|
+
_activateDynamicDerived(fn, resolvedTopic, _ctx && _ctx.user);
|
|
771
795
|
};
|
|
772
796
|
/** @type {any} */ (fn).__onUnsubscribe = function (_ctx, resolvedTopic) {
|
|
773
797
|
_deactivateDynamicDerived(fn, resolvedTopic);
|
|
@@ -791,6 +815,12 @@ const _dynamicDerivedByFn = new Map();
|
|
|
791
815
|
/** @type {import('svelte-adapter-uws').Platform | null} Captured platform for dynamic derived recomputation */
|
|
792
816
|
let _derivedPlatform = null;
|
|
793
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
|
+
|
|
794
824
|
/** @type {Map<string, { sources: string[], fn: Function, debounce: number, timer: ReturnType<typeof setTimeout> | null }>} */
|
|
795
825
|
const effectRegistry = new Map();
|
|
796
826
|
|
|
@@ -1363,6 +1393,7 @@ live.breaker = function breaker(options, fn) {
|
|
|
1363
1393
|
export function __registerDerived(path, fn) {
|
|
1364
1394
|
if (/** @type {any} */ (fn).__lazy) {
|
|
1365
1395
|
_lazyQueue.push({ type: 'derived', path, loader: fn });
|
|
1396
|
+
_hasDynamicDerived = true;
|
|
1366
1397
|
return;
|
|
1367
1398
|
}
|
|
1368
1399
|
|
|
@@ -1373,7 +1404,7 @@ export function __registerDerived(path, fn) {
|
|
|
1373
1404
|
/** @type {Map<string, any[]>} */
|
|
1374
1405
|
const topicArgs = new Map();
|
|
1375
1406
|
const topicFn = (...args) => {
|
|
1376
|
-
const t = path + '
|
|
1407
|
+
const t = path + '~' + args.map(a => String(a).replace(/~/g, '')).join('~');
|
|
1377
1408
|
topicArgs.set(t, args);
|
|
1378
1409
|
if (topicArgs.size > 10000) {
|
|
1379
1410
|
const iter = topicArgs.keys();
|
|
@@ -1419,6 +1450,7 @@ const _activatedPlatforms = new WeakSet();
|
|
|
1419
1450
|
|
|
1420
1451
|
export function _activateDerived(platform) {
|
|
1421
1452
|
_derivedPlatform = platform;
|
|
1453
|
+
_activateDerivedCalled = true;
|
|
1422
1454
|
if (_activatedPlatforms.has(platform)) return;
|
|
1423
1455
|
|
|
1424
1456
|
// Only wrap platform.publish if there are actual reactive registrations
|
|
@@ -1519,7 +1551,7 @@ async function _recomputeDerived(entry, platform) {
|
|
|
1519
1551
|
let result;
|
|
1520
1552
|
if (entry.args) {
|
|
1521
1553
|
const _h = _getCtxHelpers(platform);
|
|
1522
|
-
const ctx = _buildCtx(null, null, platform, _h, null);
|
|
1554
|
+
const ctx = _buildCtx(entry.user || null, null, platform, _h, null);
|
|
1523
1555
|
result = await entry.fn(ctx, ...entry.args);
|
|
1524
1556
|
} else {
|
|
1525
1557
|
result = await entry.fn();
|
|
@@ -1539,8 +1571,9 @@ async function _recomputeDerived(entry, platform) {
|
|
|
1539
1571
|
* Wires the instance's resolved sources into _derivedBySource so publishes trigger recomputation.
|
|
1540
1572
|
* @param {Function} fn - The derived compute function
|
|
1541
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
|
|
1542
1575
|
*/
|
|
1543
|
-
function _activateDynamicDerived(fn, resolvedTopic) {
|
|
1576
|
+
function _activateDynamicDerived(fn, resolvedTopic, user) {
|
|
1544
1577
|
const entry = _dynamicDerivedByFn.get(fn);
|
|
1545
1578
|
if (!entry) return;
|
|
1546
1579
|
|
|
@@ -1576,7 +1609,8 @@ function _activateDynamicDerived(fn, resolvedTopic) {
|
|
|
1576
1609
|
resolvedSources,
|
|
1577
1610
|
debounce: entry.debounce,
|
|
1578
1611
|
timer: null,
|
|
1579
|
-
refCount: 1
|
|
1612
|
+
refCount: 1,
|
|
1613
|
+
user: user || null
|
|
1580
1614
|
};
|
|
1581
1615
|
|
|
1582
1616
|
entry.instances.set(resolvedTopic, instance);
|
|
@@ -1828,6 +1862,8 @@ export function _prepareHmr() {
|
|
|
1828
1862
|
_streamsWithUnsubscribe.clear();
|
|
1829
1863
|
_hasDynamicDerived = false;
|
|
1830
1864
|
_dynamicDerivedByFn.clear();
|
|
1865
|
+
_activateDerivedCalled = false;
|
|
1866
|
+
_warnedActivateDerived = false;
|
|
1831
1867
|
|
|
1832
1868
|
return snap;
|
|
1833
1869
|
}
|
|
@@ -2310,6 +2346,13 @@ async function _executeSingleRpc(ws, msg, platform, options) {
|
|
|
2310
2346
|
try { await /** @type {any} */ (fn).__onSubscribe(ctx, topic); } catch {}
|
|
2311
2347
|
}
|
|
2312
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
|
+
|
|
2313
2356
|
// Channel fast-path
|
|
2314
2357
|
if (/** @type {any} */ (fn).__isChannel) {
|
|
2315
2358
|
const emptyValue = streamOpts.merge === 'set' ? null : [];
|