ripple 0.3.25 → 1.0.1

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.
Files changed (49) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/package.json +5 -5
  3. package/src/runtime/index-client.js +4 -0
  4. package/src/runtime/internal/client/hmr.js +1 -1
  5. package/src/runtime/internal/client/hydration.js +14 -0
  6. package/src/runtime/internal/client/runtime.js +127 -31
  7. package/src/runtime/internal/client/types.d.ts +3 -33
  8. package/src/runtime/internal/server/blocks.js +21 -1
  9. package/src/runtime/internal/server/index.js +299 -34
  10. package/src/runtime/internal/server/types.d.ts +3 -31
  11. package/src/runtime/reactive-value.js +1 -0
  12. package/src/utils/escaping.js +11 -0
  13. package/src/utils/track-async-serialization.js +9 -0
  14. package/tests/client/async-suspend.test.tsrx +11 -1
  15. package/tests/client/compiler/compiler.basic.test.tsrx +18 -3
  16. package/tests/client/track-async-hydration.test.tsrx +54 -0
  17. package/tests/hydration/compiled/client/basic.js +1 -1
  18. package/tests/hydration/compiled/client/events.js +8 -8
  19. package/tests/hydration/compiled/client/for.js +22 -24
  20. package/tests/hydration/compiled/client/head.js +6 -6
  21. package/tests/hydration/compiled/client/hmr.js +1 -1
  22. package/tests/hydration/compiled/client/html.js +1 -1
  23. package/tests/hydration/compiled/client/if-children.js +7 -7
  24. package/tests/hydration/compiled/client/if.js +5 -5
  25. package/tests/hydration/compiled/client/mixed-control-flow.js +4 -4
  26. package/tests/hydration/compiled/client/portal.js +1 -1
  27. package/tests/hydration/compiled/client/reactivity.js +9 -9
  28. package/tests/hydration/compiled/client/return.js +23 -23
  29. package/tests/hydration/compiled/client/switch.js +4 -4
  30. package/tests/hydration/compiled/client/track-async-serialization.js +390 -0
  31. package/tests/hydration/compiled/client/try.js +2 -2
  32. package/tests/hydration/compiled/server/basic.js +1 -1
  33. package/tests/hydration/compiled/server/events.js +8 -8
  34. package/tests/hydration/compiled/server/for.js +34 -28
  35. package/tests/hydration/compiled/server/head.js +6 -6
  36. package/tests/hydration/compiled/server/hmr.js +1 -1
  37. package/tests/hydration/compiled/server/html.js +1 -1
  38. package/tests/hydration/compiled/server/if-children.js +7 -7
  39. package/tests/hydration/compiled/server/if.js +5 -5
  40. package/tests/hydration/compiled/server/mixed-control-flow.js +4 -4
  41. package/tests/hydration/compiled/server/portal.js +1 -1
  42. package/tests/hydration/compiled/server/reactivity.js +9 -9
  43. package/tests/hydration/compiled/server/return.js +11 -11
  44. package/tests/hydration/compiled/server/switch.js +4 -4
  45. package/tests/hydration/compiled/server/track-async-serialization.js +502 -0
  46. package/tests/hydration/compiled/server/try.js +2 -2
  47. package/tests/hydration/components/track-async-serialization.tsrx +116 -0
  48. package/tests/hydration/track-async-serialization.test.js +127 -0
  49. package/tests/server/track-async-serialization.test.tsrx +185 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # ripple
2
2
 
3
+ ## 1.0.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#886](https://github.com/Ripple-TS/ripple/pull/886)
8
+ [`316cba1`](https://github.com/Ripple-TS/ripple/commit/316cba18614e5ef59dce15e0de6e720eb922955f)
9
+ Thanks [@leonidaz](https://github.com/leonidaz)! - Add SSR-to-client
10
+ serialization/hydration for trackAsync by emitting per-call JSON <script>
11
+ envelopes (resolved payload + direct dependency hashes, or sanitized error
12
+ message) and consuming/removing them during client hydration to avoid re-running
13
+ the user async function. Add proper error handling routing to catch blocks with
14
+ actual error messages in DEV and safe production error messages, all with
15
+ correct hydration support
16
+ - Updated dependencies
17
+ [[`316cba1`](https://github.com/Ripple-TS/ripple/commit/316cba18614e5ef59dce15e0de6e720eb922955f)]:
18
+ - ripple@1.0.1
19
+ - @tsrx/ripple@0.0.8
20
+
21
+ ## 1.0.0
22
+
23
+ ### Patch Changes
24
+
25
+ - Updated dependencies []:
26
+ - ripple@1.0.0
27
+ - @tsrx/ripple@0.0.7
28
+
3
29
  ## 0.3.25
4
30
 
5
31
  ### Patch Changes
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "Ripple is an elegant TypeScript UI framework",
4
4
  "license": "MIT",
5
5
  "author": "Dominic Gannaway",
6
- "version": "0.3.25",
6
+ "version": "1.0.1",
7
7
  "type": "module",
8
8
  "module": "src/runtime/index-client.js",
9
9
  "main": "src/runtime/index-client.js",
@@ -72,11 +72,11 @@
72
72
  },
73
73
  "dependencies": {
74
74
  "clsx": "^2.1.1",
75
- "devalue": "^5.6.3",
75
+ "devalue": "^5.7.1",
76
76
  "esm-env": "^1.2.2",
77
77
  "@types/estree": "^1.0.8",
78
78
  "@types/estree-jsx": "^1.0.5",
79
- "@tsrx/ripple": "0.0.6"
79
+ "@tsrx/ripple": "0.0.8"
80
80
  },
81
81
  "devDependencies": {
82
82
  "@types/node": "^24.3.0",
@@ -84,9 +84,9 @@
84
84
  "typescript": "^5.9.3",
85
85
  "@volar/language-core": "~2.4.28",
86
86
  "vscode-languageserver-types": "^3.17.5",
87
- "@tsrx/core": "0.0.5"
87
+ "@tsrx/core": "0.0.6"
88
88
  },
89
89
  "peerDependencies": {
90
- "ripple": "0.3.25"
90
+ "ripple": "1.0.1"
91
91
  }
92
92
  }
@@ -11,6 +11,7 @@ import { active_block } from './internal/client/runtime.js';
11
11
  import { create_anchor } from './internal/client/utils.js';
12
12
  import { remove_ssr_css } from './internal/client/css.js';
13
13
  import {
14
+ clear_track_hash_reference,
14
15
  hydrate_next,
15
16
  hydrate_node,
16
17
  hydrating,
@@ -108,6 +109,9 @@ export function hydrate(component, options) {
108
109
  } finally {
109
110
  set_hydrating(was_hydrating);
110
111
  set_hydrate_node(previous_hydrate_node, true);
112
+ if (!was_hydrating) {
113
+ clear_track_hash_reference();
114
+ }
111
115
  }
112
116
 
113
117
  return () => {
@@ -19,7 +19,7 @@ import { active_block, get, set, tracked } from './runtime.js';
19
19
  * @returns {ComponentWrapper}
20
20
  */
21
21
  export function hmr(fn) {
22
- /** @type {Tracked<Component> | undefined} */
22
+ /** @type {Tracked | undefined} */
23
23
  var current;
24
24
 
25
25
  /**
@@ -1,3 +1,5 @@
1
+ /** @import { Derived, Tracked } from '#client' */
2
+
1
3
  import {
2
4
  COMMENT_NODE,
3
5
  HYDRATION_END,
@@ -11,6 +13,18 @@ export let hydrating = false;
11
13
  /** @type {Node | null} */
12
14
  export let hydrate_node = null;
13
15
 
16
+ /**
17
+ * Map of hash -> Tracked/Derived registered during hydration. Allows a
18
+ * hydrating trackAsync to look up its serialized dependencies by hash and
19
+ * wire up reactivity without re-running the user's async fn.
20
+ * @type {Map<string, Tracked | Derived>}
21
+ */
22
+ export const track_hash_reference = new Map();
23
+
24
+ export function clear_track_hash_reference() {
25
+ track_hash_reference.clear();
26
+ }
27
+
14
28
  /**
15
29
  * @param {boolean} value
16
30
  */
@@ -1,5 +1,7 @@
1
- /** @import { Block, Component, Dependency, Derived, Tracked, BlockWithTryBoundaryAndCatch, DeferredTrackedEntry } from '#client' */
1
+ /** @import { Block, Component, Dependency, BlockWithTryBoundaryAndCatch, DeferredTrackedEntry } from '#client' */
2
2
  /** @import { NAMESPACE_URI } from './constants.js' */
3
+ /** @typedef {TrackedValue} Tracked */
4
+ /** @typedef {DerivedValue} Derived */
3
5
 
4
6
  import { DEV } from 'esm-env';
5
7
  import {
@@ -51,6 +53,9 @@ import {
51
53
  object_keys,
52
54
  } from './utils.js';
53
55
  import { get_async_track_result } from '../../../utils/async.js';
56
+ import { get_track_async_script_id } from '../../../utils/track-async-serialization.js';
57
+ import * as devalue from 'devalue';
58
+ import { hydrating, track_hash_reference } from './hydration.js';
54
59
 
55
60
  const FLUSH_MICROTASK = 0;
56
61
  const FLUSH_SYNC = 1;
@@ -383,31 +388,42 @@ function complete_deferred_boundaries(t, show_resolved = true) {
383
388
  }
384
389
  }
385
390
 
386
- /** @type {Tracked} */
387
391
  class TrackedValue {
388
392
  /**
389
393
  * @param {any} v
390
394
  * @param {Block} block
391
395
  * @param {{ get?: Function; set?: Function }} a
396
+ * @param {string} [hash]
392
397
  */
393
- constructor(v, block, a) {
398
+ constructor(v, block, a, hash) {
399
+ /** @type {{ get?: Function; set?: Function }} */
394
400
  this.a = a;
401
+ /** @type {Block} */
395
402
  this.b = block;
403
+ /** @type {number} */
396
404
  this.c = 0;
397
405
  /** @type {DeferredTrackedEntry[] | null} */
398
406
  this.d = null;
407
+ /** @type {number} */
399
408
  this.f = TRACKED;
409
+ /** @type {string | undefined} */
410
+ this.h = hash;
411
+ /** @type {any} */
400
412
  this.__v = v;
401
413
  }
414
+ /** @returns {any} */
402
415
  get [0]() {
403
416
  return get_tracked(this);
404
417
  }
418
+ /** @param {any} v */
405
419
  set [0](v) {
406
420
  set(this, v);
407
421
  }
422
+ /** @returns {Tracked} */
408
423
  get [1]() {
409
- return this;
424
+ return /** @type {Tracked} */ (this);
410
425
  }
426
+ /** @returns {any} */
411
427
  get value() {
412
428
  return get_tracked(this);
413
429
  }
@@ -419,41 +435,55 @@ class TrackedValue {
419
435
  get length() {
420
436
  return 2;
421
437
  }
438
+ /** @returns {Iterator<any | Tracked>} */
422
439
  *[Symbol.iterator]() {
423
440
  yield get_tracked(this);
424
441
  yield this;
425
442
  }
426
443
  }
427
444
 
428
- /** @type {Derived} */
429
445
  class DerivedValue {
430
446
  /**
431
447
  * @param {Function} fn
432
448
  * @param {Block} block
433
449
  * @param {{ get?: Function; set?: Function }} a
450
+ * @param {string} [hash]
434
451
  */
435
- constructor(fn, block, a) {
452
+ constructor(fn, block, a, hash) {
453
+ /** @type {{ get?: Function; set?: Function }} */
436
454
  this.a = a;
455
+ /** @type {Block} */
437
456
  this.b = block;
438
- /** @type {null | Block[]} */
457
+ /** @type {Block[] | null} */
439
458
  this.blocks = null;
459
+ /** @type {number} */
440
460
  this.c = 0;
461
+ /** @type {Component | null} */
441
462
  this.co = active_component;
442
- /** @type {null | Dependency} */
463
+ /** @type {Dependency | null} */
443
464
  this.d = null;
465
+ /** @type {number} */
444
466
  this.f = DERIVED;
467
+ /** @type {Function} */
445
468
  this.fn = fn;
469
+ /** @type {string | undefined} */
470
+ this.h = hash;
471
+ /** @type {any} */
446
472
  this.__v = UNINITIALIZED;
447
473
  }
474
+ /** @returns {any} */
448
475
  get [0]() {
449
476
  return get_derived(this);
450
477
  }
478
+ /** @param {any} v */
451
479
  set [0](v) {
452
480
  set(this, v);
453
481
  }
482
+ /** @returns {Derived} */
454
483
  get [1]() {
455
- return this;
484
+ return /** @type {Derived} */ (this);
456
485
  }
486
+ /** @returns {any} */
457
487
  get value() {
458
488
  return get_derived(this);
459
489
  }
@@ -465,6 +495,7 @@ class DerivedValue {
465
495
  get length() {
466
496
  return 2;
467
497
  }
498
+ /** @returns {Iterator<any | Derived>} */
468
499
  *[Symbol.iterator]() {
469
500
  yield get_derived(this);
470
501
  yield this;
@@ -480,37 +511,48 @@ if (DEV) {
480
511
  *
481
512
  * @param {any} v
482
513
  * @param {Block} block
514
+ * @param {string} [hash]
483
515
  * @param {(value: any) => any} [get]
484
516
  * @param {(next: any, prev: any) => any} [set]
485
517
  * @returns {Tracked}
486
518
  */
487
- export function tracked(v, block, get, set) {
488
- return /** @type {Tracked} */ (
489
- new TrackedValue(v, block || active_block, get || set ? { get, set } : empty_get_set)
519
+ export function tracked(v, block, hash, get, set) {
520
+ var t = /** @type {Tracked} */ (
521
+ new TrackedValue(v, block || active_block, get || set ? { get, set } : empty_get_set, hash)
490
522
  );
523
+ if (hydrating && hash !== undefined) {
524
+ track_hash_reference.set(hash, t);
525
+ }
526
+ return t;
491
527
  }
492
528
 
493
529
  /**
494
530
  * @param {any} fn
495
- * @param {any} block
531
+ * @param {Block} block
532
+ * @param {string} [hash]
496
533
  * @param {(value: any) => any} [get]
497
534
  * @param {(next: any, prev: any) => any} [set]
498
535
  * @returns {Derived}
499
536
  */
500
- export function derived(fn, block, get, set) {
501
- return /** @type {Derived} */ (
502
- new DerivedValue(fn, block || active_block, get || set ? { get, set } : empty_get_set)
537
+ export function derived(fn, block, hash, get, set) {
538
+ var d = /** @type {Derived} */ (
539
+ new DerivedValue(fn, block || active_block, get || set ? { get, set } : empty_get_set, hash)
503
540
  );
541
+ if (hydrating && hash !== undefined) {
542
+ track_hash_reference.set(hash, d);
543
+ }
544
+ return d;
504
545
  }
505
546
 
506
547
  /**
507
548
  * @param {any} v
508
- * @param {(value: any) => any | undefined} get
509
- * @param {(next: any, prev: any) => any | undefined} set
510
549
  * @param {Block} b
550
+ * @param {string} [hash]
551
+ * @param {(value: any) => any} [get]
552
+ * @param {(next: any, prev: any) => any} [set]
511
553
  * @returns {Tracked | Derived}
512
554
  */
513
- export function track(v, get, set, b) {
555
+ export function track(v, b, hash, get, set) {
514
556
  if (is_ripple_object(v)) {
515
557
  return v;
516
558
  }
@@ -519,17 +561,18 @@ export function track(v, get, set, b) {
519
561
  }
520
562
 
521
563
  if (typeof v === 'function') {
522
- return derived(v, b, get, set);
564
+ return derived(v, b, hash, get, set);
523
565
  }
524
- return tracked(v, b, get, set);
566
+ return tracked(v, b, hash, get, set);
525
567
  }
526
568
 
527
569
  /**
528
570
  * @param {any} fn
529
571
  * @param {Block} b
572
+ * @param {string} hash - Unique hash for SSR serialization/hydration
530
573
  * @returns {Tracked | void}
531
574
  */
532
- export function track_async(fn, b) {
575
+ export function track_async(fn, b, hash) {
533
576
  if (is_ripple_object(fn)) {
534
577
  return fn;
535
578
  }
@@ -545,7 +588,31 @@ export function track_async(fn, b) {
545
588
  );
546
589
  }
547
590
 
548
- var t = tracked(SUSPENSE_PENDING, target_block);
591
+ // During hydration, attempt to read serialized data from SSR
592
+ var had_hydration_data = false;
593
+ var hydration_value;
594
+ /** @type {string[] | undefined} */
595
+ var hydration_deps;
596
+
597
+ if (hydrating) {
598
+ var script_id = get_track_async_script_id(hash);
599
+ var script_el = document.getElementById(script_id);
600
+ if (script_el) {
601
+ var envelope = JSON.parse(/** @type {string} */ (script_el.textContent));
602
+ script_el.remove();
603
+
604
+ if (envelope.ok) {
605
+ had_hydration_data = true;
606
+ hydration_value = devalue.parse(envelope.payload);
607
+ hydration_deps = envelope.deps;
608
+ } else {
609
+ // trigger the catch block
610
+ throw new Error(envelope.error?.message ?? 'Unknown server error');
611
+ }
612
+ }
613
+ }
614
+
615
+ var t = tracked(had_hydration_data ? hydration_value : SUSPENSE_PENDING, target_block, hash);
549
616
 
550
617
  // Capture the call-site block for boundary lookups. target_block is the
551
618
  // component's block (passed by compiler), but the actual try/pending/catch
@@ -559,15 +626,44 @@ export function track_async(fn, b) {
559
626
  /** @type {Block | null} */
560
627
  var boundary = null;
561
628
 
629
+ // TODO: decide if instead of insisting on pending, we create our own boundary
630
+ // we currently require a pending block upstream but we could also
631
+ // create a try/pending/catch boundary at mount and hydration like
632
+ // we do on the server so that there is always a boundary present.
633
+ // It can handle global pending when none were provided.
634
+ // Not sure about the catch boundary because if none were provided,
635
+ // the whole app for any error will be unmounted with the catch block rendered
636
+
562
637
  // Find boundary from the call-site block.
563
638
  boundary = get_pending_boundary(active_block);
564
639
  if (boundary === null) {
565
640
  throw new Error('Missing parent `try { ... } pending { ... }` statement');
566
641
  }
567
642
 
568
- request_id = begin_boundary_request(boundary);
643
+ // If we hydrated with resolved data, the SSR already completed this request.
644
+ // Otherwise mark a pending request on the boundary for the client-side run.
645
+ if (!had_hydration_data) {
646
+ request_id = begin_boundary_request(boundary);
647
+ }
569
648
 
570
649
  pre_effect(() => {
650
+ if (had_hydration_data) {
651
+ // First run after hydration: skip fn() entirely (the SSR already
652
+ // produced the resolved value) and instead register the direct
653
+ // dependencies from the serialized deps list so future dep changes
654
+ // trigger a re-run via the normal async path.
655
+ had_hydration_data = false;
656
+ if (hydration_deps !== undefined) {
657
+ for (var i = 0; i < hydration_deps.length; i++) {
658
+ var dep_ref = track_hash_reference.get(hydration_deps[i]);
659
+ if (dep_ref !== undefined) {
660
+ get(dep_ref);
661
+ }
662
+ }
663
+ }
664
+ return;
665
+ }
666
+
571
667
  var current_version = ++version;
572
668
 
573
669
  // Abort previous in-flight request
@@ -583,7 +679,7 @@ export function track_async(fn, b) {
583
679
  request_id = begin_boundary_request(boundary);
584
680
  }
585
681
 
586
- // Set to pending before calling fn() in case it's sync
682
+ // Set to pending before calling fn() in case it's sync.
587
683
  if (t.__v !== SUSPENSE_PENDING) {
588
684
  update_tracked_value_clock(t, SUSPENSE_PENDING);
589
685
  schedule_update(t.b);
@@ -712,15 +808,15 @@ export function track_async(fn, b) {
712
808
  }
713
809
 
714
810
  /**
715
- * @param {(Derived | Tracked) | (() => any)} tracked
811
+ * @param {(Derived | Tracked) | (() => any)} t
716
812
  * @returns {boolean}
717
813
  */
718
- export function is_tracked_pending(tracked) {
814
+ export function is_tracked_pending(t) {
719
815
  try {
720
- if (typeof tracked === 'function') {
721
- tracked();
816
+ if (typeof t === 'function') {
817
+ t();
722
818
  } else {
723
- get(tracked);
819
+ get(t);
724
820
  }
725
821
  return false;
726
822
  } catch (error) {
@@ -1225,7 +1321,7 @@ export function spread_props(fn) {
1225
1321
  * @returns {Object}
1226
1322
  */
1227
1323
  export function proxy_props(fn) {
1228
- const memo = derived(fn, active_block);
1324
+ const memo = derived(fn, /** @type {Block} */ (active_block));
1229
1325
 
1230
1326
  return new Proxy(
1231
1327
  {},
@@ -1,5 +1,8 @@
1
+ import type { Tracked, Derived } from './runtime.js';
1
2
  import type { Context } from './context.js';
2
3
 
4
+ export { Tracked, Derived };
5
+
3
6
  export type Component = {
4
7
  b: null | Block;
5
8
  c: null | Map<Context<any>, any>;
@@ -18,44 +21,11 @@ export type Dependency = {
18
21
  n: null | Dependency;
19
22
  };
20
23
 
21
- export type Tracked<V = any> = {
22
- DO_NOT_ACCESS_THIS_OBJECT_DIRECTLY?: true;
23
- a: { get?: Function; set?: Function };
24
- b: Block;
25
- c: number;
26
- d: null | DeferredTrackedEntry[];
27
- f: number;
28
- __v: V;
29
- readonly [0]: V;
30
- [1]: Tracked<V>;
31
- value: V;
32
- readonly length: 2;
33
- [Symbol.iterator](): Iterator<V | Tracked<V>>;
34
- };
35
-
36
24
  export type DeferredTrackedEntry = {
37
25
  b: Block; // boundary block
38
26
  r: number; // request version id
39
27
  };
40
28
 
41
- export type Derived = {
42
- DO_NOT_ACCESS_THIS_OBJECT_DIRECTLY?: true;
43
- a: { get?: Function; set?: Function };
44
- b: Block;
45
- blocks: null | Block[];
46
- c: number;
47
- co: null | Component;
48
- d: null | Dependency;
49
- f: number;
50
- fn: Function;
51
- __v: any;
52
- readonly [0]: any;
53
- [1]: Derived;
54
- value: any;
55
- readonly length: 2;
56
- [Symbol.iterator](): Iterator<any | Derived>;
57
- };
58
-
59
29
  export type Block = {
60
30
  co: null | Component;
61
31
  d: null | Dependency;
@@ -18,7 +18,16 @@ import {
18
18
  ROOT_BLOCK,
19
19
  CAUGHT_ERROR,
20
20
  } from './constants.js';
21
- import { run_block, active_block, active_component, Output, set_active_block } from './index.js';
21
+ import {
22
+ run_block,
23
+ active_block,
24
+ active_component,
25
+ Output,
26
+ set_active_block,
27
+ TrackAsyncRunError,
28
+ create_public_track_async_error,
29
+ serialize_track_async_error,
30
+ } from './index.js';
22
31
 
23
32
  /**
24
33
  * @param {number} flags
@@ -90,6 +99,17 @@ export function try_block(try_fn, catch_fn = null, pending_fn = null) {
90
99
  if (created_block.f & TRY_CATCH_BLOCK) {
91
100
  created_block.o.clear();
92
101
  cancel_async_operations(created_block);
102
+
103
+ // make sure to serialize trackAsync error so the client can hydrate them properly
104
+ // needs to happen after clearing output
105
+ if (error instanceof TrackAsyncRunError) {
106
+ var { tracked: t, cause } = /** @type {InstanceType<typeof TrackAsyncRunError>} */ (error);
107
+ var public_error = create_public_track_async_error(cause);
108
+ catch_fn?.(public_error);
109
+ serialize_track_async_error(t.h, public_error);
110
+ return created_block;
111
+ }
112
+
93
113
  // render the catch
94
114
  catch_fn?.(/** @type {SSRError} */ (error));
95
115
  } else {