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 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 Zod or Valibot schema before the function runs.
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
- On the client, validated exports work like regular `live()` calls. Validation errors are thrown as `RpcError` with `code: 'VALIDATION'` and an `issues` array. Valibot schemas are also supported -- the adapter detects the schema type automatically.
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 Zod/Valibot input validation |
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelte-realtime",
3
- "version": "0.4.18",
3
+ "version": "0.4.20",
4
4
  "description": "Realtime RPC and reactive subscriptions for SvelteKit, built on svelte-adapter-uws",
5
5
  "author": "Kevin Radziszewski",
6
6
  "license": "MIT",
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 Zod and Valibot schemas.
373
+ * Supports any [Standard Schema](https://standardschema.dev/)-compatible schema,
374
+ * including Zod, ArkType, Valibot v1+, and others.
374
375
  *
375
- * @param schema - Zod or Valibot schema
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 Zod (.safeParse method on schema) and Valibot (safeParse as standalone).
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 or Valibot schema
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 Zod or Valibot schema.
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
- // Zod-style: schema has .safeParse method
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-style: schema is passed to a standalone safeParse
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 + '\x00' + args.map(a => String(a).replace(/\x00/g, '')).join('\x00');
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 + '\x00' + args.map(a => String(a).replace(/\x00/g, '')).join('\x00');
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 : [];