ripple 0.3.127 → 0.3.128

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 (39) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/package.json +2 -2
  3. package/src/runtime/internal/client/blocks.js +20 -24
  4. package/src/runtime/internal/client/constants.js +5 -5
  5. package/src/runtime/internal/client/expression.js +142 -101
  6. package/src/runtime/internal/client/for.js +217 -145
  7. package/src/runtime/internal/client/index.js +5 -1
  8. package/src/runtime/internal/client/operations.js +24 -10
  9. package/src/runtime/internal/client/render.js +22 -3
  10. package/src/runtime/internal/client/runtime.js +246 -182
  11. package/src/runtime/internal/client/selector.js +74 -0
  12. package/src/runtime/internal/client/types.d.ts +11 -0
  13. package/src/runtime/internal/server/index.js +2 -0
  14. package/tests/client/basic/basic.reactivity.test.tsrx +33 -0
  15. package/tests/client/for-selector.test.tsrx +221 -0
  16. package/tests/hydration/compiled/client/basic.js +44 -44
  17. package/tests/hydration/compiled/client/composite.js +3 -3
  18. package/tests/hydration/compiled/client/events.js +56 -29
  19. package/tests/hydration/compiled/client/for.js +76 -70
  20. package/tests/hydration/compiled/client/head.js +11 -8
  21. package/tests/hydration/compiled/client/hmr.js +4 -4
  22. package/tests/hydration/compiled/client/html-in-template.js +3 -3
  23. package/tests/hydration/compiled/client/html.js +141 -141
  24. package/tests/hydration/compiled/client/if-children.js +29 -29
  25. package/tests/hydration/compiled/client/if-fragment-controlflow.js +16 -16
  26. package/tests/hydration/compiled/client/if.js +10 -10
  27. package/tests/hydration/compiled/client/mixed-control-flow.js +26 -22
  28. package/tests/hydration/compiled/client/nested-control-flow.js +114 -92
  29. package/tests/hydration/compiled/client/portal.js +8 -8
  30. package/tests/hydration/compiled/client/reactivity.js +38 -15
  31. package/tests/hydration/compiled/client/streaming.js +16 -16
  32. package/tests/hydration/compiled/client/switch.js +4 -4
  33. package/tests/hydration/compiled/client/track-async-serialization.js +18 -18
  34. package/tests/hydration/compiled/client/try.js +8 -8
  35. package/tests/hydration/compiled/server/events.js +5 -53
  36. package/tests/hydration/compiled/server/for.js +2 -18
  37. package/tests/hydration/compiled/server/head.js +1 -9
  38. package/tests/hydration/compiled/server/html.js +1 -9
  39. package/tests/hydration/compiled/server/reactivity.js +2 -34
package/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # ripple
2
2
 
3
+ ## 0.3.128
4
+
5
+ ### Patch Changes
6
+
7
+ - [#1450](https://github.com/Ripple-TS/ripple/pull/1450)
8
+ [`26be014`](https://github.com/Ripple-TS/ripple/commit/26be014dfd06da9eae7e2d9e0fbd332ddf1bc9ca)
9
+ Thanks [@leonidaz](https://github.com/leonidaz)! - Faster updates and list
10
+ rendering in the client runtime. Tracked values now keep a subscriber list, so a
11
+ write schedules exactly the blocks that read it and the flush no longer scans
12
+ the owner's subtree; scheduled blocks run in creation order from a queue. Text
13
+ expressions, keyed list items, and class updates allocate less per row, and
14
+ `set_class` skips DOM writes when the class is unchanged. Adds the `selector` /
15
+ `selector_match` internals the compiler uses to share one subscription across
16
+ `outer === item` comparisons in `@for` templates. Keyed list reconciliation
17
+ matches moved end items before falling back to the map and LIS, so reversals,
18
+ rotations, and swaps complete with plain moves. An empty dynamic `class` no
19
+ longer writes a `class=""` attribute; the server omits it as well. Exposes
20
+ `hydrating`, `hydrate_child`, and `hydrate_sibling` from the internal client
21
+ entry for the compiler's inline traversal. When most surviving items of a keyed
22
+ list would have to move, the list is re-laid in order before a fixed node, which
23
+ is cheaper than moving each item into place.
24
+
25
+ - Updated dependencies
26
+ [[`26be014`](https://github.com/Ripple-TS/ripple/commit/26be014dfd06da9eae7e2d9e0fbd332ddf1bc9ca)]:
27
+ - @tsrx/ripple@0.1.65
28
+
3
29
  ## 0.3.127
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.127",
6
+ "version": "0.3.128",
7
7
  "type": "module",
8
8
  "module": "src/runtime/index-client.js",
9
9
  "main": "src/runtime/index-client.js",
@@ -74,7 +74,7 @@
74
74
  "devalue": "^5.8.1",
75
75
  "esm-env": "^1.2.2",
76
76
  "@tsrx/core": "^0.1.67",
77
- "@tsrx/ripple": "0.1.64"
77
+ "@tsrx/ripple": "0.1.65"
78
78
  },
79
79
  "devDependencies": {
80
80
  "@types/estree": "^1.0.8",
@@ -4,7 +4,6 @@ import {
4
4
  BLOCK_HAS_RUN,
5
5
  BRANCH_BLOCK,
6
6
  DERIVED,
7
- CONTAINS_TEARDOWN,
8
7
  DESTROYED,
9
8
  EFFECT_BLOCK,
10
9
  PAUSED,
@@ -25,6 +24,7 @@ import {
25
24
  active_reaction,
26
25
  create_component_ctx,
27
26
  is_block_dirty,
27
+ remove_dependencies,
28
28
  run_block,
29
29
  run_teardown,
30
30
  schedule_update,
@@ -197,6 +197,9 @@ export function boundary_fn_running_block(fn, flags = 0, state = null) {
197
197
  return branch(fn, DIRECT_CHILD_BLOCK | flags, state);
198
198
  }
199
199
 
200
+ /** Creation counter: a parent always has a lower id than its descendants. */
201
+ var block_id = 0;
202
+
200
203
  /**
201
204
  * @param {Block} block
202
205
  * @param {Block} parent_block
@@ -227,6 +230,7 @@ export function block(flags, fn, state = null, co) {
227
230
  first: null,
228
231
  f: flags,
229
232
  fn,
233
+ i: ++block_id,
230
234
  last: null,
231
235
  next: null,
232
236
  p: active_block,
@@ -247,7 +251,7 @@ export function block(flags, fn, state = null, co) {
247
251
  if ((flags & EFFECT_BLOCK) !== 0) {
248
252
  schedule_update(block);
249
253
  } else {
250
- run_block(block);
254
+ run_block(block, true);
251
255
  block.f ^= BLOCK_HAS_RUN;
252
256
  }
253
257
 
@@ -262,12 +266,10 @@ export function destroy_block_children(parent, remove_dom = false) {
262
266
  var block = parent.first;
263
267
  parent.first = parent.last = null;
264
268
 
265
- if (remove_dom || (parent.f & CONTAINS_TEARDOWN) !== 0) {
266
- while (block !== null) {
267
- var next = block.next;
268
- destroy_block(block, remove_dom);
269
- block = next;
270
- }
269
+ while (block !== null) {
270
+ var next = block.next;
271
+ destroy_block(block, remove_dom);
272
+ block = next;
271
273
  }
272
274
  }
273
275
 
@@ -278,20 +280,12 @@ export function destroy_block_children(parent, remove_dom = false) {
278
280
  export function destroy_non_branch_children(parent, remove_dom = false) {
279
281
  var block = parent.first;
280
282
 
281
- if (
282
- (parent.f & CONTAINS_TEARDOWN) === 0 &&
283
- parent.first !== null &&
284
- (parent.first.f & BRANCH_BLOCK) === 0
285
- ) {
286
- parent.first = parent.last = null;
287
- } else {
288
- while (block !== null) {
289
- var next = block.next;
290
- if ((block.f & BRANCH_BLOCK) === 0) {
291
- destroy_block(block, remove_dom);
292
- }
293
- block = next;
283
+ while (block !== null) {
284
+ var next = block.next;
285
+ if ((block.f & BRANCH_BLOCK) === 0) {
286
+ destroy_block(block, remove_dom);
294
287
  }
288
+ block = next;
295
289
  }
296
290
  }
297
291
 
@@ -487,10 +481,8 @@ export function get_last_node(block) {
487
481
  * @param {boolean} [remove_dom]
488
482
  */
489
483
  export function destroy_block(block, remove_dom = true) {
490
- block.f ^= DESTROYED;
491
-
484
+ var f = (block.f |= DESTROYED);
492
485
  var removed = false;
493
- var f = block.f;
494
486
 
495
487
  if (
496
488
  (remove_dom && (f & (BRANCH_BLOCK | ROOT_BLOCK)) !== 0 && (f & TRY_BLOCK) === 0) ||
@@ -507,6 +499,10 @@ export function destroy_block(block, remove_dom = true) {
507
499
 
508
500
  run_teardown(block);
509
501
 
502
+ if (block.d !== null) {
503
+ remove_dependencies(block);
504
+ }
505
+
510
506
  var parent = block.p;
511
507
 
512
508
  // If the parent doesn't have any children, then skip this work altogether
@@ -11,7 +11,6 @@ export var ASYNC_BLOCK = 1 << 10;
11
11
  export var HEAD_BLOCK = 1 << 11;
12
12
  export var PRE_EFFECT_BLOCK = 1 << 12;
13
13
  export var DIRECT_CHILD_BLOCK = 1 << 13;
14
- export var CONTAINS_UPDATE = 1 << 14;
15
14
  export var CONTAINS_TEARDOWN = 1 << 15;
16
15
  export var BLOCK_HAS_RUN = 1 << 16;
17
16
  export var TRACKED = 1 << 17;
@@ -19,10 +18,11 @@ export var DERIVED = 1 << 18;
19
18
  export var DEFERRED = 1 << 19;
20
19
  export var PAUSED = 1 << 20;
21
20
  export var DESTROYED = 1 << 21;
22
- // Marks a block that was directly scheduled (the owner of a mutated tracked).
23
- // Its whole subtree is scanned for dirty subscribers during flush; blocks above
24
- // it are only traversed along the CONTAINS_UPDATE routing path.
25
- export var UPDATE_SOURCE = 1 << 22;
21
+ // Marks a block that is queued for the next flush.
22
+ export var SCHEDULED = 1 << 22;
23
+ // A per-key match value created by a selector; its map entry is dropped once
24
+ // the last subscriber unlinks.
25
+ export var SELECTOR = 1 << 23;
26
26
 
27
27
  export var CONTROL_FLOW_BLOCK = FOR_BLOCK | IF_BLOCK | SWITCH_BLOCK | TRY_BLOCK | COMPOSITE_BLOCK;
28
28
 
@@ -167,68 +167,99 @@ function render_tsrx_collection_text(value, anchor, assign = false) {
167
167
  set_hydrate_node(node ?? anchor);
168
168
  }
169
169
 
170
+ /**
171
+ * @typedef {{
172
+ * start: Node | null;
173
+ * end: Node | null;
174
+ * a: ChildNode;
175
+ * n: number;
176
+ * g: () => any;
177
+ * b: Block | null;
178
+ * m: Comment | null;
179
+ * t: Text | null;
180
+ * v: string | import('../../element.js').TSRXElement | typeof UNINITIALIZED;
181
+ * e: boolean;
182
+ * i: boolean;
183
+ * p: Block | null;
184
+ * o: Node | null;
185
+ * }} ExpressionState
186
+ */
187
+
170
188
  /**
171
189
  * @param {Node} node
172
190
  * @param {() => any} get_value
173
191
  * @returns {void}
174
192
  */
175
193
  export function expression(node, get_value) {
176
- var anchor = /** @type {ChildNode} */ (node);
177
- /** @type {Block | null} */
178
- var child_block = null;
179
- /** @type {Comment | null} */
180
- var end = null;
181
- /** @type {Text | null} */
182
- var text = null;
183
- /** @type {string | import('../../element.js').TSRXElement | typeof UNINITIALIZED} */
184
- var value = UNINITIALIZED;
185
- var is_element = false;
186
- var initialized = false;
187
- /** @type {Block | null} */
188
- var modified_parent_branch = null;
189
- /** @type {Node | null} */
190
- var original_parent_start = null;
191
-
192
- render(() => {
193
- var next_value = get_value();
194
+ // State lives on the render block instead of a per-expression closure, and
195
+ // the anchor's node type is read once since the anchor never changes.
196
+ render(run_expression, {
197
+ start: null,
198
+ end: null,
199
+ a: /** @type {ChildNode} */ (node),
200
+ n: node.nodeType,
201
+ g: get_value,
202
+ // child block rendering a TSRX element / collection
203
+ b: null,
204
+ // hydration end marker
205
+ m: null,
206
+ // text node inserted before a comment anchor
207
+ t: null,
208
+ // last rendered value
209
+ v: UNINITIALIZED,
210
+ // whether the last value was an element / collection
211
+ e: false,
212
+ // whether the block has rendered once
213
+ i: false,
214
+ // enclosing branch whose start was moved to include element content
215
+ p: null,
216
+ // that branch's original start node
217
+ o: null,
218
+ });
219
+ }
220
+
221
+ /**
222
+ * @param {ExpressionState} s
223
+ * @returns {void}
224
+ */
225
+ function run_expression(s) {
226
+ var next_value = s.g();
227
+ var anchor = s.a;
228
+ var type = typeof next_value;
229
+ var is_hydration_marker =
230
+ hydrating && s.n === COMMENT_NODE && /** @type {Comment} */ (anchor).data === HYDRATION_START;
231
+
232
+ if (is_hydration_marker) {
233
+ s.m ??= ensure_expression_end(anchor);
234
+ }
235
+
236
+ var end = s.m;
237
+
238
+ if (next_value !== null && (type === 'object' || type === 'function')) {
194
239
  var next_is_collection = is_array(next_value);
195
- var next_is_element = next_is_collection || is_tsrx_element(next_value);
196
- var is_hydration_marker =
197
- hydrating &&
198
- anchor.nodeType === COMMENT_NODE &&
199
- /** @type {Comment} */ (anchor).data === HYDRATION_START;
200
-
201
- if (is_hydration_marker) {
202
- end ??= ensure_expression_end(anchor);
203
- }
204
240
 
205
- if (next_is_element) {
206
- if (initialized && is_element && value === next_value) {
241
+ if (next_is_collection || is_tsrx_element(next_value)) {
242
+ if (s.i && s.e && s.v === next_value) {
207
243
  if (end !== null) {
208
244
  advance_hydration(end);
209
245
  }
210
246
  return;
211
247
  }
212
248
 
213
- if (anchor.nodeType === TEXT_NODE) {
249
+ if (s.n === TEXT_NODE) {
214
250
  /** @type {Text} */ (anchor).nodeValue = '';
215
- } else if (text !== null) {
216
- text.remove();
217
- text = null;
251
+ } else if (s.t !== null) {
252
+ s.t.remove();
253
+ s.t = null;
218
254
  }
219
255
 
220
- if (child_block !== null) {
221
- destroy_block(child_block);
222
- child_block = null;
223
- // Restore parent branch's start since we may update it again below
224
- if (modified_parent_branch !== null && modified_parent_branch.s !== null) {
225
- modified_parent_branch.s.start = original_parent_start;
226
- modified_parent_branch = null;
227
- original_parent_start = null;
228
- }
256
+ if (s.b !== null) {
257
+ destroy_block(s.b);
258
+ s.b = null;
259
+ restore_parent_start(s);
229
260
  }
230
261
 
231
- if (end !== null && (initialized || !hydrating)) {
262
+ if (end !== null && (s.i || !hydrating)) {
232
263
  clear_expression_range(anchor, end);
233
264
  }
234
265
 
@@ -236,18 +267,18 @@ export function expression(node, get_value) {
236
267
  set_hydrate_node(get_next_sibling(anchor) ?? end);
237
268
  }
238
269
 
239
- // Find the enclosing branch block BEFORE creating child_block
270
+ // Find the enclosing branch block BEFORE creating the child block
240
271
  // so we can update its s.start to include content inserted before anchor
241
272
  var parent_branch = find_enclosing_branch(active_block);
242
273
 
243
- child_block = branch(() => {
274
+ var child_block = (s.b = branch(() => {
244
275
  var block = /** @type {Block} */ (active_block);
245
276
  if (next_is_collection) {
246
277
  render_tsrx_collection(next_value, end ?? anchor, block);
247
278
  } else {
248
279
  render_tsrx_element(next_value, end ?? anchor, block);
249
280
  }
250
- });
281
+ }));
251
282
 
252
283
  // Update parent branch's s.start to include content inserted before anchor.
253
284
  // This ensures that when the parent branch is destroyed, the full DOM range
@@ -267,81 +298,91 @@ export function expression(node, get_value) {
267
298
  // update it to include the child's content
268
299
  if (parent_start === anchor || parent_start === end) {
269
300
  // Save original so we can restore it when switching to non-TSRXElement
270
- if (modified_parent_branch === null) {
271
- modified_parent_branch = parent_branch;
272
- original_parent_start = parent_start;
301
+ if (s.p === null) {
302
+ s.p = parent_branch;
303
+ s.o = parent_start;
273
304
  }
274
305
  parent_branch.s.start = child_start;
275
306
  }
276
307
  }
277
308
 
278
- value = next_value;
279
- is_element = true;
280
- initialized = true;
309
+ s.v = next_value;
310
+ s.e = true;
311
+ s.i = true;
281
312
  if (end !== null) {
282
313
  advance_hydration(end);
283
314
  }
284
315
  return;
285
316
  }
317
+ }
286
318
 
287
- var next_text = (next_value ?? '') + '';
319
+ var next_text = next_value == null ? '' : next_value + '';
288
320
 
289
- if (initialized && !is_element && value === next_text) {
290
- if (end !== null) {
291
- advance_hydration(end);
292
- }
293
- return;
321
+ if (s.i && !s.e && s.v === next_text) {
322
+ if (end !== null) {
323
+ advance_hydration(end);
294
324
  }
325
+ return;
326
+ }
295
327
 
296
- if (child_block !== null) {
297
- destroy_block(child_block);
298
- child_block = null;
299
- // Restore parent branch's start to original value since the child's DOM nodes
300
- // have been removed and the old start reference would be stale
301
- if (modified_parent_branch !== null && modified_parent_branch.s !== null) {
302
- modified_parent_branch.s.start = original_parent_start;
303
- modified_parent_branch = null;
304
- original_parent_start = null;
305
- }
306
- }
328
+ if (s.b !== null) {
329
+ destroy_block(s.b);
330
+ s.b = null;
331
+ // Restore parent branch's start to original value since the child's DOM nodes
332
+ // have been removed and the old start reference would be stale
333
+ restore_parent_start(s);
334
+ }
307
335
 
308
- if (is_hydration_marker) {
309
- text = get_hydrated_text(anchor, /** @type {Comment} */ (end));
336
+ if (is_hydration_marker) {
337
+ var text = (s.t = get_hydrated_text(anchor, /** @type {Comment} */ (end)));
310
338
 
311
- if (next_text === '') {
312
- if (text !== null) {
313
- text.remove();
314
- text = null;
315
- }
316
- } else if (text === null) {
317
- text = create_text(next_text);
318
- /** @type {Comment} */ (end).before(text);
319
- } else if (text.nodeValue !== next_text) {
320
- text.nodeValue = next_text;
339
+ if (next_text === '') {
340
+ if (text !== null) {
341
+ text.remove();
342
+ s.t = null;
321
343
  }
322
- } else if (anchor.nodeType === COMMENT_NODE) {
323
- if (next_text === '') {
324
- if (text !== null) {
325
- text.remove();
326
- text = null;
327
- }
328
- } else if (text === null) {
329
- text = create_text(next_text);
330
- (end ?? anchor).before(text);
331
- } else if (text.nodeValue !== next_text) {
332
- text.nodeValue = next_text;
344
+ } else if (text === null) {
345
+ text = s.t = create_text(next_text);
346
+ /** @type {Comment} */ (end).before(text);
347
+ } else if (text.nodeValue !== next_text) {
348
+ text.nodeValue = next_text;
349
+ }
350
+ } else if (s.n === COMMENT_NODE) {
351
+ var text = s.t;
352
+ if (next_text === '') {
353
+ if (text !== null) {
354
+ text.remove();
355
+ s.t = null;
333
356
  }
334
- } else if (anchor.nodeType === TEXT_NODE) {
335
- /** @type {Text} */ (anchor).nodeValue = next_text;
357
+ } else if (text === null) {
358
+ text = s.t = create_text(next_text);
359
+ (end ?? anchor).before(text);
360
+ } else if (text.nodeValue !== next_text) {
361
+ text.nodeValue = next_text;
336
362
  }
363
+ } else if (s.n === TEXT_NODE) {
364
+ /** @type {Text} */ (anchor).nodeValue = next_text;
365
+ }
337
366
 
338
- value = next_text;
339
- is_element = false;
340
- initialized = true;
341
- if (end !== null) {
342
- advance_hydration(end);
343
- }
344
- });
367
+ s.v = next_text;
368
+ s.e = false;
369
+ s.i = true;
370
+ if (end !== null) {
371
+ advance_hydration(end);
372
+ }
373
+ }
374
+
375
+ /**
376
+ * @param {ExpressionState} s
377
+ * @returns {void}
378
+ */
379
+ function restore_parent_start(s) {
380
+ var parent = s.p;
381
+ if (parent !== null && parent.s !== null) {
382
+ parent.s.start = s.o;
383
+ s.p = null;
384
+ s.o = null;
385
+ }
345
386
  }
346
387
 
347
388
  /**