@zubra/runtime 5.9.0 → 5.9.2

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/src/effect.js CHANGED
@@ -31,7 +31,7 @@ let owner = null; // the Effect or Scope currently running
31
31
  let nextId = 0;
32
32
  let scheduled = null; // promise for the pending microtask flush
33
33
  let flushing = false;
34
- const effectOf = new WeakMap();
34
+ const EFFECT = Symbol('effect');
35
35
 
36
36
  /**
37
37
  * `Symbol.dispose`, or, where the engine doesn't have it yet, the symbol TypeScript's downlevel
@@ -92,15 +92,18 @@ function report(error) {
92
92
  class Owner {
93
93
  constructor(parent) {
94
94
  this.parent = parent;
95
- this.children = new Set();
95
+ this.children = null; // created on first child: most effects have none
96
96
  this.disposed = false;
97
97
  this.paused = false;
98
- parent?.children.add(this);
98
+ if (parent) (parent.children ??= new Set()).add(this);
99
99
  }
100
100
 
101
101
  disposeChildren() {
102
+ const children = this.children;
103
+ if (children === null || children.size === 0) return;
104
+ this.children = null;
102
105
  batch(() => {
103
- for (const child of [...this.children]) child.dispose();
106
+ for (const child of children) { child.parent = null; child.dispose(); }
104
107
  });
105
108
  }
106
109
 
@@ -108,7 +111,7 @@ class Owner {
108
111
  if (this.disposed) return;
109
112
  this.disposed = true;
110
113
  this.disposeChildren();
111
- this.parent?.children.delete(this);
114
+ this.parent?.children?.delete(this);
112
115
  }
113
116
 
114
117
  [DISPOSE]() {
@@ -118,7 +121,7 @@ class Owner {
118
121
  pause() {
119
122
  if (this.paused || this.disposed) return;
120
123
  this.paused = true;
121
- batch(() => {
124
+ if (this.children) batch(() => {
122
125
  for (const child of [...this.children]) child.pause();
123
126
  });
124
127
  }
@@ -128,7 +131,7 @@ class Owner {
128
131
  this.paused = false;
129
132
  // Children may be replaced while their parent resumes, so iterate a snapshot and skip
130
133
  // anything that was disposed along the way.
131
- for (const child of [...this.children]) if (!child.disposed) child.resume();
134
+ if (this.children) for (const child of [...this.children]) if (!child.disposed) child.resume();
132
135
  }
133
136
  }
134
137
 
@@ -152,7 +155,7 @@ class Effect extends Owner {
152
155
  owner = previous;
153
156
  }
154
157
  });
155
- effectOf.set(this.computed, this);
158
+ this.computed[EFFECT] = this;
156
159
  watcher.watch(this.computed);
157
160
  }
158
161
 
@@ -187,7 +190,7 @@ class Effect extends Owner {
187
190
  this.paused = false;
188
191
  watchAgain(this.computed);
189
192
  try { this.run(); } catch (error) { report(error); }
190
- for (const child of [...this.children]) if (!child.disposed) child.resume();
193
+ if (this.children) for (const child of [...this.children]) if (!child.disposed) child.resume();
191
194
  }
192
195
  }
193
196
 
@@ -255,7 +258,7 @@ export function flush() {
255
258
  report(new Error('zubra: effects keep re-triggering each other; stopped after 100 rounds'));
256
259
  return;
257
260
  }
258
- const effects = pending.map((computed) => effectOf.get(computed)).sort((a, b) => a.id - b.id);
261
+ const effects = pending.map((computed) => computed[EFFECT]).sort((a, b) => a.id - b.id);
259
262
  for (const e of effects) {
260
263
  if (e.disposed || e.paused) continue;
261
264
  try { e.run(); } catch (error) { report(error); }
package/src/element.js CHANGED
@@ -559,6 +559,25 @@ function blueprint(source, parent = null) {
559
559
  return content;
560
560
  }
561
561
 
562
+ // For a blueprint with no custom elements: a copy owned by this document, made once, which rows
563
+ // clone. Its clones need no adopting when they're inserted, and nothing in it has to upgrade.
564
+ const imported = new WeakMap();
565
+ const upgrades = (content) =>
566
+ [...content.querySelectorAll('*')].some((element) => element.localName.includes('-') || element.hasAttribute('is'));
567
+
568
+ /** A fresh copy of `block`'s template for `parent`, ready for its parts to be found and bound. */
569
+ function freshCopy(block, parent) {
570
+ const content = blueprint(block, parent);
571
+ let copy = imported.get(content);
572
+ if (copy === undefined) {
573
+ // importNode: custom elements inside upgrade now, so their properties exist before parts
574
+ // bind (a plain clone stays in the template's inert document).
575
+ copy = upgrades(content) ? null : document.importNode(content, true);
576
+ imported.set(content, copy);
577
+ }
578
+ return copy === null ? document.importNode(content, true) : copy.cloneNode(true);
579
+ }
580
+
562
581
  function clearBetween(open, close) {
563
582
  while (open.nextSibling && open.nextSibling !== close) open.nextSibling.remove();
564
583
  }
@@ -619,10 +638,8 @@ function mountBlock(block, args, open, close, hydrating, context) {
619
638
  clearBetween(open, close);
620
639
  // Find and bind the parts in the fragment, then insert it: child components inside see
621
640
  // their props from the start, and can't move slotted nodes before they're found.
622
- // importNode, not cloneNode: defined custom elements upgrade now, so their properties exist
623
- // before parts bind (a clone stays in the template's inert document).
624
641
  const parent = parentOf(close);
625
- const fragment = document.importNode(blueprint(block, parent), true);
642
+ const fragment = freshCopy(block, parent);
626
643
  fragmentParents.set(fragment, parent);
627
644
  const fresh = locate(childRange(fragment), parts);
628
645
  parts.forEach((part, index) => bind(part, fresh[index], false, context));
@@ -756,7 +773,12 @@ function bind(part, node, hydrating, context) {
756
773
  if (part.text) {
757
774
  effect(() => {
758
775
  const text = part.text();
759
- if (node.textContent !== text) node.textContent = text;
776
+ // A lone Text child is compared and written in place (`.data`): reading `textContent`
777
+ // walks the subtree, and setting it replaces the Text node.
778
+ const child = node.firstChild;
779
+ if (child !== null && child === node.lastChild && child.nodeType === TEXT) {
780
+ if (child.data !== text) child.data = text;
781
+ } else if (node.textContent !== text) node.textContent = text;
760
782
  });
761
783
  } else if (part.anchor) {
762
784
  effect(() => {
@@ -1317,19 +1339,27 @@ function bindEach(part, open, hydrating, context) {
1317
1339
  const rowsOwner = scope();
1318
1340
  let rows = [];
1319
1341
 
1342
+ // A row is the nodes from `open` to `close`. Rows the server rendered keep the markers it put
1343
+ // around them. A row made here is its block's own nodes, from first to last, with no markers:
1344
+ // what a block's nodes are at the top never changes after it's bound (a region inside one keeps
1345
+ // its own markers), so the first and last stay the row's ends. Only an empty block needs one.
1320
1346
  const createRow = (value, index, key, rowOpen, rowClose, hydrateRow) => {
1321
- if (!rowOpen) {
1322
- // A new row is assembled in a fragment, then placed.
1323
- rowOpen = document.createComment('[');
1324
- rowClose = document.createComment(']');
1325
- const fragment = new DocumentFragment();
1326
- fragmentParents.set(fragment, parentOf(open));
1327
- fragment.append(rowOpen, rowClose);
1328
- }
1329
1347
  const item = new Signal.State(value);
1330
1348
  const position = new Signal.State(index);
1331
- const owner = rowsOwner.run(() => scope(() => mountBlock(part.block, [item, position], rowOpen, rowClose, hydrateRow, context)));
1332
- return { key, item, position, open: rowOpen, close: rowClose, owner };
1349
+ if (rowOpen) {
1350
+ const owner = rowsOwner.run(() => scope(() => mountBlock(part.block, [item, position], rowOpen, rowClose, hydrateRow, context)));
1351
+ return { key, item, position, open: rowOpen, close: rowClose, owner };
1352
+ }
1353
+ const parent = parentOf(open);
1354
+ const fragment = freshCopy(part.block, parent);
1355
+ fragmentParents.set(fragment, parent);
1356
+ const owner = rowsOwner.run(() => scope(() => {
1357
+ const parts = part.block.parts(item, position);
1358
+ const nodes = locate(childRange(fragment), parts);
1359
+ parts.forEach((each, at) => bind(each, nodes[at], false, context));
1360
+ }));
1361
+ if (!fragment.firstChild) fragment.append(document.createComment(''));
1362
+ return { key, item, position, open: fragment.firstChild, close: fragment.lastChild, owner };
1333
1363
  };
1334
1364
  const nodesOf = (row) => {
1335
1365
  const nodes = [];