ripple 0.3.25 → 0.3.26

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 +38 -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
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @import { Component, Dependency, Derived, Tracked, Block, TryBlockWithCatch } from '#server';
2
+ * @import { Component, Dependency, Block, TryBlockWithCatch } from '#server';
3
3
  * @import { NestedArray } from '#helpers';
4
4
  * @import { Props } from '#public';
5
5
  * @import { RenderResult, BaseRenderOptions, RenderStreamResult, Stream, StreamSink } from 'ripple/server';
@@ -13,6 +13,10 @@
13
13
  /** @typedef {{ tag: string; parent: undefined | ElementContext; filename: undefined | string; line: number; column: number; }} ElementContext */
14
14
  /** @typedef {{ cancel: () => void }} RegisteredAsyncOperation */
15
15
 
16
+ // Both
17
+ /** @typedef {TrackedValue} Tracked */
18
+ /** @typedef {DerivedValue} Derived */
19
+
16
20
  import {
17
21
  DERIVED,
18
22
  UNINITIALIZED,
@@ -22,8 +26,9 @@ import {
22
26
  ASYNC_DERIVED_READ_THROWN,
23
27
  DERIVED_UPDATED,
24
28
  } from '../client/constants.js';
29
+ import { DEV } from 'esm-env';
25
30
  import { is_ripple_object, array_slice } from '../client/utils.js';
26
- import { escape } from '../../../utils/escaping.js';
31
+ import { escape, escape_script } from '../../../utils/escaping.js';
27
32
  import { is_boolean_attribute } from '../../../utils/attributes.js';
28
33
  import { clsx } from 'clsx';
29
34
  import { normalize_css_property_name } from '../../../utils/normalize_css_property_name.js';
@@ -34,6 +39,8 @@ import {
34
39
  is_tag_valid_with_ancestor,
35
40
  } from '../../../html-tree-validation.js';
36
41
  import { get_async_track_result } from '../../../utils/async.js';
42
+ import { get_track_async_script_id } from '../../../utils/track-async-serialization.js';
43
+ import * as devalue from 'devalue';
37
44
  import {
38
45
  cancel_async_operations,
39
46
  component_block,
@@ -50,6 +57,24 @@ export { try_block, component_block, regular_block } from './blocks.js';
50
57
  export { array_slice };
51
58
  export { tsrx_element, normalize_children };
52
59
 
60
+ /** @extends Error */
61
+ export class TrackAsyncRunError extends Error {
62
+ /** @type {Tracked} */
63
+ tracked;
64
+ /** @type {Error} */
65
+ cause;
66
+ /**
67
+ * @param {string} message
68
+ * @param {{tracked: Tracked, cause: Error}} options
69
+ */
70
+ constructor(message, options) {
71
+ super(message);
72
+ this.name = 'TrackAsyncRunError';
73
+ this.tracked = options.tracked;
74
+ this.cause = options.cause;
75
+ }
76
+ }
77
+
53
78
  export function noop() {}
54
79
 
55
80
  /**
@@ -400,9 +425,11 @@ export class Output {
400
425
 
401
426
  /**
402
427
  * @param {string} str
428
+ * @param {boolean} [is_root=false]
429
+ * @param {boolean} [is_prepend=false]
403
430
  * @returns {void}
404
431
  */
405
- push(str) {
432
+ #push(str, is_root = false, is_prepend = false) {
406
433
  if (this.isStreamMode() && !this.isSyncRun()) {
407
434
  // TODO - we need to wrap the resulting block output into something that
408
435
  // the client-side can understand and append them appropriately,
@@ -413,12 +440,49 @@ export class Output {
413
440
  return;
414
441
  }
415
442
 
416
- if (this.target === 'head') {
417
- this.#head.push(str);
443
+ var instance = is_root ? this.#root : this;
444
+
445
+ // we never write to `head` in the root instance
446
+ if (instance !== this.#root && instance.target === 'head') {
447
+ if (is_prepend) {
448
+ instance.#head.unshift(str);
449
+ } else {
450
+ instance.#head.push(str);
451
+ }
418
452
  return;
419
453
  }
420
454
 
421
- this.#body.push(str);
455
+ if (is_prepend) {
456
+ instance.#body.unshift(str);
457
+ } else {
458
+ instance.#body.push(str);
459
+ }
460
+ }
461
+
462
+ /**
463
+ * @param {string} str
464
+ * @returns {void}
465
+ */
466
+ push(str) {
467
+ this.#push(str);
468
+ }
469
+
470
+ /**
471
+ * @param {string} str
472
+ * @returns {void}
473
+ */
474
+ push_serialized_error(str) {
475
+ // prepend to the root block to avoid messing up the hydration markers
476
+ // writing to the root to avoid being cleared in the local instance when an error occurs
477
+ this.#push(str, true, true);
478
+ }
479
+
480
+ /**
481
+ * @param {string} str
482
+ * @returns {void}
483
+ */
484
+ push_serialized_result(str) {
485
+ this.#push(str);
422
486
  }
423
487
 
424
488
  clear() {
@@ -674,6 +738,14 @@ export function output_push(str) {
674
738
  /** @type {Block} */ (active_block).o.push(str);
675
739
  }
676
740
 
741
+ /**
742
+ * @param {string} str
743
+ * @returns {void}
744
+ */
745
+ export function output_push_serialized_error(str) {
746
+ /** @type {Block} */ (active_block).o.push_serialized_error(str);
747
+ }
748
+
677
749
  /**
678
750
  * @param {Output['target']} target
679
751
  */
@@ -958,31 +1030,43 @@ export function spread_attrs(attrs, css_hash) {
958
1030
 
959
1031
  var empty_get_set = { get: undefined, set: undefined };
960
1032
 
961
- /** @type {Tracked} */
962
1033
  class TrackedValue {
963
1034
  /**
964
1035
  * @param {any} v
965
1036
  * @param {{ get?: Function; set?: Function }} a
1037
+ * @param {string} hash
966
1038
  */
967
- constructor(v, a) {
1039
+ constructor(v, a, hash) {
1040
+ /** @type {{ get?: Function; set?: Function }} */
968
1041
  this.a = a;
969
1042
  /** @type {AbortController | null} */
970
1043
  this.aa = null;
971
1044
  /** @type {PromiseLike<any> | null} */
972
1045
  this.ap = null;
1046
+ /** @type {Block} */
1047
+ this.b = /** @type {Block} */ (active_block);
1048
+ /** @type {number} */
973
1049
  this.c = 0;
1050
+ /** @type {number} */
974
1051
  this.f = TRACKED;
1052
+ /** @type {string} */
1053
+ this.h = hash;
1054
+ /** @type {any} */
975
1055
  this.v = v;
976
1056
  }
1057
+ /** @returns {any} */
977
1058
  get [0]() {
978
1059
  return get(/** @type {Tracked} */ (this));
979
1060
  }
1061
+ /** @param {any} v */
980
1062
  set [0](v) {
981
1063
  set(/** @type {Tracked} */ (this), v);
982
1064
  }
1065
+ /** @returns {Tracked} */
983
1066
  get [1]() {
984
- return this;
1067
+ return /** @type {Tracked} */ (this);
985
1068
  }
1069
+ /** @returns {any} */
986
1070
  get value() {
987
1071
  return get(/** @type {Tracked} */ (this));
988
1072
  }
@@ -994,40 +1078,53 @@ class TrackedValue {
994
1078
  get length() {
995
1079
  return 2;
996
1080
  }
1081
+ /** @returns {Iterator<any | Tracked>} */
997
1082
  *[Symbol.iterator]() {
998
1083
  yield get(/** @type {Tracked} */ (this));
999
1084
  yield this;
1000
1085
  }
1001
1086
  }
1002
1087
 
1003
- /** @type {Derived} */
1004
1088
  class DerivedValue {
1005
1089
  /**
1006
1090
  * @param {Function} fn
1007
1091
  * @param {{ get?: Function; set?: Function }} a
1092
+ * @param {string} hash
1008
1093
  */
1009
- constructor(fn, a) {
1094
+ constructor(fn, a, hash) {
1095
+ /** @type {{ get?: Function; set?: Function }} */
1010
1096
  this.a = a;
1011
1097
  // we always should have an active block
1012
- // even in async we rerun blocks so we can rely on this
1098
+ /** @type {Block} */
1013
1099
  this.b = /** @type {Block} */ (active_block);
1100
+ /** @type {number} */
1014
1101
  this.c = 0;
1102
+ /** @type {Component | null} */
1015
1103
  this.co = active_component;
1016
1104
  /** @type {Dependency | null} */
1017
1105
  this.d = null;
1106
+ /** @type {number} */
1018
1107
  this.f = DERIVED;
1108
+ /** @type {Function} */
1019
1109
  this.fn = fn;
1110
+ /** @type {string} */
1111
+ this.h = hash;
1112
+ /** @type {any} */
1020
1113
  this.v = UNINITIALIZED;
1021
1114
  }
1115
+ /** @returns {any} */
1022
1116
  get [0]() {
1023
1117
  return get(/** @type {Derived} */ (this));
1024
1118
  }
1119
+ /** @param {any} v */
1025
1120
  set [0](v) {
1026
1121
  set(/** @type {Derived} */ (this), v);
1027
1122
  }
1123
+ /** @returns {Derived} */
1028
1124
  get [1]() {
1029
- return this;
1125
+ return /** @type {Derived} */ (this);
1030
1126
  }
1127
+ /** @returns {any} */
1031
1128
  get value() {
1032
1129
  return get(/** @type {Derived} */ (this));
1033
1130
  }
@@ -1039,6 +1136,7 @@ class DerivedValue {
1039
1136
  get length() {
1040
1137
  return 2;
1041
1138
  }
1139
+ /** @returns {Iterator<any | Derived>} */
1042
1140
  *[Symbol.iterator]() {
1043
1141
  yield get(/** @type {Derived} */ (this));
1044
1142
  yield this;
@@ -1047,12 +1145,15 @@ class DerivedValue {
1047
1145
 
1048
1146
  /**
1049
1147
  * @param {any} v
1148
+ * @param {string} hash
1050
1149
  * @param {(value: any) => any} [get]
1051
1150
  * @param {(next: any, prev: any) => any} [set]
1052
1151
  * @returns {Tracked}
1053
1152
  */
1054
- function tracked(v, get, set) {
1055
- return /** @type {Tracked} */ (new TrackedValue(v, get || set ? { get, set } : empty_get_set));
1153
+ function tracked(v, hash, get, set) {
1154
+ return /** @type {Tracked} */ (
1155
+ new TrackedValue(v, get || set ? { get, set } : empty_get_set, hash)
1156
+ );
1056
1157
  }
1057
1158
 
1058
1159
  /**
@@ -1075,21 +1176,25 @@ export function exclude_from_object(obj, exclude_keys) {
1075
1176
 
1076
1177
  /**
1077
1178
  * @param {any} v
1179
+ * @param {string} hash
1078
1180
  * @param {(value: any) => any} [get]
1079
1181
  * @param {(next: any, prev: any) => any} [set]
1080
1182
  * @returns {Derived}
1081
1183
  */
1082
- function derived(v, get, set) {
1083
- return /** @type {Derived} */ (new DerivedValue(v, get || set ? { get, set } : empty_get_set));
1184
+ function derived(v, hash, get, set) {
1185
+ return /** @type {Derived} */ (
1186
+ new DerivedValue(v, get || set ? { get, set } : empty_get_set, hash)
1187
+ );
1084
1188
  }
1085
1189
 
1086
1190
  /**
1087
1191
  * @param {any} v
1192
+ * @param {string} hash
1088
1193
  * @param {(value: any) => any} [get]
1089
1194
  * @param {(next: any, prev: any) => any} [set]
1090
1195
  * @returns {Tracked | Derived}
1091
1196
  */
1092
- export function track(v, get, set) {
1197
+ export function track(v, hash, get, set) {
1093
1198
  var is_tracked = is_ripple_object(v);
1094
1199
 
1095
1200
  if (is_tracked) {
@@ -1097,10 +1202,118 @@ export function track(v, get, set) {
1097
1202
  }
1098
1203
 
1099
1204
  if (typeof v === 'function') {
1100
- return derived(v, get, set);
1205
+ return derived(v, hash, get, set);
1101
1206
  }
1102
1207
 
1103
- return tracked(v, get, set);
1208
+ return tracked(v, hash, get, set);
1209
+ }
1210
+
1211
+ /**
1212
+ * Serializes a resolved trackAsync result as a script tag for hydration.
1213
+ * @param {OutputInterface} output - The output push function captured at call time
1214
+ * @param {string} hash - The unique hash for this trackAsync call
1215
+ * @param {any} value - The resolved value
1216
+ * @param {string[] | null} [deps] - Hashes of direct reactive dependencies read by fn()
1217
+ * @returns {void}
1218
+ */
1219
+ function serialize_track_async_result(output, hash, value, deps) {
1220
+ /** @type {{ ok: true, payload: string, deps?: string[] }} */
1221
+ var envelope = { ok: true, payload: devalue.stringify(value) };
1222
+ if (deps && deps.length > 0) {
1223
+ envelope.deps = deps;
1224
+ }
1225
+ push_script_for_hydration((str) => output.push_serialized_result(str), hash, envelope);
1226
+ }
1227
+
1228
+ /**
1229
+ * Serializes a rejected trackAsync error as a script tag for hydration.
1230
+ * Must be called after route_error_to_catch_block so active_block is the catch block.
1231
+ * @param {string} hash
1232
+ * @param {any} error
1233
+ * @returns {void}
1234
+ */
1235
+ export function serialize_track_async_error(hash, error) {
1236
+ var error_message = get_public_track_async_error_message(error);
1237
+
1238
+ // we can just use the output_push_serialized directly so it's added to the root block
1239
+ // if we here then the try's block failed to render and the output was cleared
1240
+ // so we're writing to the root otherwise it will be cleared in the local output
1241
+ push_script_for_hydration(output_push_serialized_error, hash, {
1242
+ ok: false,
1243
+ error: { message: error_message },
1244
+ });
1245
+ }
1246
+
1247
+ /**
1248
+ * @param {string} hash
1249
+ * @param {any} error
1250
+ * @returns {void}
1251
+ */
1252
+ export function route_track_async_error_to_catch_block(hash, error) {
1253
+ route_track_async_error_to_catch_block_with_boundary(
1254
+ get_closest_catch_block(/** @type {Block} */ (active_block)),
1255
+ hash,
1256
+ error,
1257
+ );
1258
+ }
1259
+
1260
+ /**
1261
+ * @param {any} error
1262
+ * @returns {any}
1263
+ */
1264
+ export function create_public_track_async_error(error) {
1265
+ if (DEV) {
1266
+ return error;
1267
+ }
1268
+
1269
+ return new Error(get_public_track_async_error_message(error));
1270
+ }
1271
+
1272
+ /**
1273
+ * We avoid leaking arbitrary server errors in production while still keeping
1274
+ * rich error messages in development and tests.
1275
+ * @param {any} error
1276
+ * @returns {string}
1277
+ */
1278
+ function get_public_track_async_error_message(error) {
1279
+ if (DEV) {
1280
+ return error?.message ?? String(error);
1281
+ }
1282
+ return 'An error occurred during async rendering';
1283
+ }
1284
+
1285
+ /**
1286
+ * Routes trackAsync errors to a catch boundary and serializes the same
1287
+ * public error for hydration, preventing SSR/hydration message mismatches.
1288
+ * @param {TryBlockWithCatch} catch_block
1289
+ * @param {string} hash
1290
+ * @param {any} error
1291
+ * @returns {void}
1292
+ */
1293
+ function route_track_async_error_to_catch_block_with_boundary(catch_block, hash, error) {
1294
+ var public_error = create_public_track_async_error(error);
1295
+ route_error_to_catch_block(catch_block, public_error);
1296
+ // has to run after routing as it sets the active_block to the catch block
1297
+ serialize_track_async_error(hash, public_error);
1298
+ }
1299
+
1300
+ /**
1301
+ * @param {(str: string) => void} push_fn
1302
+ * @param {string} hash
1303
+ * @param {object} envelope - The envelope containing the serialized data
1304
+ * @envelope {ok: boolean, payload?: any, error?: { message: string } }
1305
+ * @returns {void}
1306
+ */
1307
+ function push_script_for_hydration(push_fn, hash, envelope) {
1308
+ var serialized_envelope = escape_script(JSON.stringify(envelope));
1309
+
1310
+ push_fn(
1311
+ '<script id="' +
1312
+ get_track_async_script_id(hash) +
1313
+ '" type="application/json">' +
1314
+ serialized_envelope +
1315
+ '</script>',
1316
+ );
1104
1317
  }
1105
1318
 
1106
1319
  /**
@@ -1123,16 +1336,22 @@ function run_track_async(t, fn, block, dr, dj) {
1123
1336
  var result;
1124
1337
  /** @type {Dependency | null} */
1125
1338
  var caught_dep = null;
1339
+ /** @type {Dependency | null} */
1340
+ var direct_deps = null;
1126
1341
  var caught = false;
1127
1342
 
1128
1343
  try {
1129
1344
  result = fn();
1345
+ direct_deps = active_dependency;
1130
1346
  } catch (error) {
1131
1347
  caught_dep = active_dependency;
1132
1348
  caught = true;
1133
1349
 
1134
1350
  if (error !== ASYNC_DERIVED_READ_THROWN) {
1135
- throw error;
1351
+ throw new TrackAsyncRunError('Error thrown during trackAsync execution', {
1352
+ cause: /** @type {Error} */ (error),
1353
+ tracked: t,
1354
+ });
1136
1355
  }
1137
1356
  } finally {
1138
1357
  tracking = previous_tracking;
@@ -1175,7 +1394,11 @@ function run_track_async(t, fn, block, dr, dj) {
1175
1394
  if (dj) {
1176
1395
  dj(error);
1177
1396
  }
1178
- route_error_to_catch_block(get_closest_catch_block(block), error);
1397
+ route_track_async_error_to_catch_block_with_boundary(
1398
+ get_closest_catch_block(block),
1399
+ t.h,
1400
+ error,
1401
+ );
1179
1402
  },
1180
1403
  );
1181
1404
  return;
@@ -1185,12 +1408,15 @@ function run_track_async(t, fn, block, dr, dj) {
1185
1408
  return;
1186
1409
  }
1187
1410
 
1411
+ var dep_hashes = collect_dep_hashes(direct_deps);
1412
+
1188
1413
  // Handle the result
1189
1414
  var async_result = get_async_track_result(result);
1190
1415
 
1191
1416
  if (async_result === null) {
1192
1417
  // Sync result
1193
1418
  update_tracked_value_clock(t, result);
1419
+ serialize_track_async_result(t.b.o, t.h, result, dep_hashes);
1194
1420
  if (dr) {
1195
1421
  dr(result);
1196
1422
  }
@@ -1207,6 +1433,7 @@ function run_track_async(t, fn, block, dr, dj) {
1207
1433
  async_result.promise.then(
1208
1434
  (resolved) => {
1209
1435
  update_tracked_value_clock(t, resolved);
1436
+ serialize_track_async_result(t.b.o, t.h, resolved, dep_hashes);
1210
1437
  if (dr) {
1211
1438
  dr(resolved);
1212
1439
  }
@@ -1216,16 +1443,42 @@ function run_track_async(t, fn, block, dr, dj) {
1216
1443
  if (dj) {
1217
1444
  dj(error);
1218
1445
  }
1219
- route_error_to_catch_block(get_closest_catch_block(block), error);
1446
+ route_track_async_error_to_catch_block_with_boundary(
1447
+ get_closest_catch_block(block),
1448
+ t.h,
1449
+ error,
1450
+ );
1220
1451
  },
1221
1452
  );
1222
1453
  }
1223
1454
 
1455
+ /**
1456
+ * Walks a dependency chain and collects the hashes of dependencies that have
1457
+ * one (i.e. were created from a compile-time track/trackAsync call).
1458
+ * @param {Dependency | null} head
1459
+ * @returns {string[] | null}
1460
+ */
1461
+ function collect_dep_hashes(head) {
1462
+ /** @type {string[] | null} */
1463
+ var hashes = null;
1464
+ var dep = head;
1465
+ while (dep !== null) {
1466
+ var h = /** @type {{ h?: string }} */ (dep.t).h;
1467
+ if (h !== undefined) {
1468
+ if (hashes === null) hashes = [];
1469
+ hashes.push(h);
1470
+ }
1471
+ dep = dep.n;
1472
+ }
1473
+ return hashes;
1474
+ }
1475
+
1224
1476
  /**
1225
1477
  * @param {any} v
1478
+ * @param {string} hash - Unique hash for SSR serialization/hydration
1226
1479
  * @returns {Tracked | void}
1227
1480
  */
1228
- export function track_async(v) {
1481
+ export function track_async(v, hash) {
1229
1482
  if (is_ripple_object(v)) {
1230
1483
  return v;
1231
1484
  }
@@ -1236,22 +1489,21 @@ export function track_async(v) {
1236
1489
  );
1237
1490
  }
1238
1491
 
1239
- var t = tracked(SUSPENSE_PENDING);
1240
- var block = /** @type {Block} */ (active_block);
1241
- run_track_async(t, v, block, null, null);
1492
+ var t = tracked(SUSPENSE_PENDING, hash);
1493
+ run_track_async(t, v, t.b, null, null);
1242
1494
  return t;
1243
1495
  }
1244
1496
 
1245
1497
  /**
1246
- * @param {(Derived | Tracked) | (() => any)} tracked
1498
+ * @param {(Derived | Tracked) | (() => any)} t
1247
1499
  * @returns {boolean}
1248
1500
  */
1249
- export function is_tracked_pending(tracked) {
1501
+ export function is_tracked_pending(t) {
1250
1502
  try {
1251
- if (typeof tracked === 'function') {
1252
- tracked();
1503
+ if (typeof t === 'function') {
1504
+ t();
1253
1505
  } else {
1254
- get(tracked);
1506
+ get(t);
1255
1507
  }
1256
1508
  return false;
1257
1509
  } catch (error) {
@@ -1332,14 +1584,27 @@ function register_block_rerun(block) {
1332
1584
  run_block(block);
1333
1585
  try_catch_block.o.resolveAsync(operation);
1334
1586
  } catch (error) {
1335
- route_error_to_catch_block(try_catch_block, error);
1587
+ if (error instanceof TrackAsyncRunError) {
1588
+ var {
1589
+ cause,
1590
+ tracked: { h: hash },
1591
+ } = /** @type {InstanceType<typeof TrackAsyncRunError>} */ (error);
1592
+ error = cause;
1593
+ route_track_async_error_to_catch_block_with_boundary(try_catch_block, hash, error);
1594
+ } else {
1595
+ route_error_to_catch_block(try_catch_block, error);
1596
+ }
1336
1597
  }
1337
1598
  },
1338
1599
  (error) => {
1339
1600
  if (cancelled) {
1340
1601
  return;
1341
1602
  }
1342
- route_error_to_catch_block(try_catch_block, error);
1603
+ route_track_async_error_to_catch_block_with_boundary(
1604
+ try_catch_block,
1605
+ /** @type {Tracked} */ (t).h,
1606
+ error,
1607
+ );
1343
1608
  },
1344
1609
  );
1345
1610
  // clear all output buffers as we'll rerun the block rendering
@@ -1,7 +1,9 @@
1
1
  import type { Context } from './context.js';
2
- import type { OutputInterface } from './index.js';
2
+ import type { OutputInterface, Derived, Tracked } from './index.js';
3
3
  import type { BlockFunction, CatchFunction, PendingFunction } from './blocks.js';
4
4
 
5
+ export { Tracked, Derived };
6
+
5
7
  export type Component = {
6
8
  c: null | Map<Context<any>, any>;
7
9
  p: null | Component;
@@ -13,36 +15,6 @@ export type Dependency = {
13
15
  n: null | Dependency;
14
16
  };
15
17
 
16
- export type Derived = {
17
- a: { get?: Function; set?: Function };
18
- b: Block;
19
- c: number;
20
- co: null | Component;
21
- d: null | Dependency;
22
- f: number;
23
- fn: Function;
24
- v: any;
25
- readonly [0]: any;
26
- [1]: Derived;
27
- value: any;
28
- readonly length: 2;
29
- [Symbol.iterator](): Iterator<any | Derived>;
30
- };
31
-
32
- export type Tracked = {
33
- a: { get?: Function; set?: Function };
34
- c: number;
35
- f: number;
36
- v: any;
37
- aa: AbortController | null;
38
- ap: PromiseLike<any> | null;
39
- readonly [0]: any;
40
- [1]: Tracked;
41
- value: any;
42
- readonly length: 2;
43
- [Symbol.iterator](): Iterator<any | Tracked>;
44
- };
45
-
46
18
  export type Block = {
47
19
  co: Component | null;
48
20
  f: number;
@@ -21,6 +21,7 @@ export const ReactiveValue = /** @type {any} */ (
21
21
  return derived(
22
22
  fn,
23
23
  block,
24
+ undefined,
24
25
  () => {
25
26
  s();
26
27
  return fn();
@@ -1,5 +1,7 @@
1
1
  const ATTR_REGEX = /[&"<]/g;
2
2
  const CONTENT_REGEX = /[&<]/g;
3
+ const OPEN_TAG_REGEX = /</g;
4
+ const CLOSE_TAG_REGEX = />/g;
3
5
 
4
6
  /**
5
7
  * @template V
@@ -24,3 +26,12 @@ export function escape(value, is_attr) {
24
26
 
25
27
  return escaped + str.substring(last);
26
28
  }
29
+
30
+ /**
31
+ * Escapes characters that can prematurely terminate inline script tags.
32
+ * @param {string} str
33
+ * @returns {string}
34
+ */
35
+ export function escape_script(str) {
36
+ return str.replace(OPEN_TAG_REGEX, '\\u003c').replace(CLOSE_TAG_REGEX, '\\u003e');
37
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Get the script element ID used for serializing/hydrating trackAsync results.
3
+ * Used by both server and client runtimes.
4
+ * @param {string} hash - The unique hash for this trackAsync call
5
+ * @returns {string}
6
+ */
7
+ export function get_track_async_script_id(hash) {
8
+ return `__tsrx_ta_${hash}`;
9
+ }
@@ -562,7 +562,17 @@ describe('async suspense', () => {
562
562
 
563
563
  render(App);
564
564
 
565
- await new Promise((resolve) => setTimeout(resolve, 10));
565
+ for (let i = 0; i < 50; i++) {
566
+ await new Promise((resolve) => setTimeout(resolve, 0));
567
+ flushSync();
568
+ if (
569
+ !container.innerHTML.includes('Loading...') &&
570
+ container.querySelector('.double')?.textContent === '0' &&
571
+ container.querySelector('.quadruple')?.textContent === '0'
572
+ ) {
573
+ break;
574
+ }
575
+ }
566
576
 
567
577
  expect(container.innerHTML).not.toContain('Loading...');
568
578
  expect(container.querySelector('.double')!.textContent).toBe('0');