@zubra/runtime 5.9.0 → 5.9.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.
- package/README.md +7 -1
- package/dist/production/effect.js +13 -10
- package/dist/production/element.js +44 -14
- package/dist/production/signal-impl.js +235 -165
- package/package.json +1 -1
- package/src/devtools.js +1 -1
- package/src/effect.js +13 -10
- package/src/element.js +44 -14
- package/src/signal-impl.js +235 -165
package/README.md
CHANGED
|
@@ -46,7 +46,7 @@ including values, recompute counts, the order watchers are notified, the order `
|
|
|
46
46
|
|
|
47
47
|
### Deliberate differences from the reference polyfill
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
The first three only affect programs that break a rule the proposal states, so correct programs
|
|
50
50
|
behave the same on both. All are tested in `test/signal.test.js`.
|
|
51
51
|
|
|
52
52
|
1. **Reads and writes inside `watched`/`unwatched` hooks throw.** The proposal says hooks run
|
|
@@ -57,6 +57,12 @@ behave the same on both. All are tested in `test/signal.test.js`.
|
|
|
57
57
|
keeps throwing it after the notification is over.
|
|
58
58
|
3. **`new Signal.Computed(x)` requires a function** (TypeError), instead of failing on first read.
|
|
59
59
|
|
|
60
|
+
One difference is visible to correct programs: **`Watcher.getPending()` isn't in watch order.**
|
|
61
|
+
The watcher keeps the computeds it watches as they're marked dirty, instead of scanning everything
|
|
62
|
+
it watches, so `getPending()` costs O(pending), not O(watched), and lists them roughly in the order
|
|
63
|
+
they were marked. The proposal doesn't state an order (the reference's is the watch order); the
|
|
64
|
+
computeds, and how many times each is listed, are the same. Also tested in `test/signal.test.js`.
|
|
65
|
+
|
|
60
66
|
## Effects
|
|
61
67
|
|
|
62
68
|
```js
|
|
@@ -32,7 +32,7 @@ let owner = null; // the Effect or Scope currently running
|
|
|
32
32
|
let nextId = 0;
|
|
33
33
|
let scheduled = null; // promise for the pending microtask flush
|
|
34
34
|
let flushing = false;
|
|
35
|
-
const
|
|
35
|
+
const EFFECT = Symbol('effect');
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
38
|
* `Symbol.dispose`, or, where the engine doesn't have it yet, the symbol TypeScript's downlevel
|
|
@@ -85,15 +85,18 @@ function report(error) {
|
|
|
85
85
|
class Owner {
|
|
86
86
|
constructor(parent) {
|
|
87
87
|
this.parent = parent;
|
|
88
|
-
this.children =
|
|
88
|
+
this.children = null; // created on first child: most effects have none
|
|
89
89
|
this.disposed = false;
|
|
90
90
|
this.paused = false;
|
|
91
|
-
parent
|
|
91
|
+
if (parent) (parent.children ??= new Set()).add(this);
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
disposeChildren() {
|
|
95
|
+
const children = this.children;
|
|
96
|
+
if (children === null || children.size === 0) return;
|
|
97
|
+
this.children = null;
|
|
95
98
|
batch(() => {
|
|
96
|
-
for (const child of
|
|
99
|
+
for (const child of children) { child.parent = null; child.dispose(); }
|
|
97
100
|
});
|
|
98
101
|
}
|
|
99
102
|
|
|
@@ -101,7 +104,7 @@ class Owner {
|
|
|
101
104
|
if (this.disposed) return;
|
|
102
105
|
this.disposed = true;
|
|
103
106
|
this.disposeChildren();
|
|
104
|
-
this.parent?.children
|
|
107
|
+
this.parent?.children?.delete(this);
|
|
105
108
|
}
|
|
106
109
|
|
|
107
110
|
[DISPOSE]() {
|
|
@@ -111,7 +114,7 @@ class Owner {
|
|
|
111
114
|
pause() {
|
|
112
115
|
if (this.paused || this.disposed) return;
|
|
113
116
|
this.paused = true;
|
|
114
|
-
batch(() => {
|
|
117
|
+
if (this.children) batch(() => {
|
|
115
118
|
for (const child of [...this.children]) child.pause();
|
|
116
119
|
});
|
|
117
120
|
}
|
|
@@ -121,7 +124,7 @@ class Owner {
|
|
|
121
124
|
this.paused = false;
|
|
122
125
|
// Children may be replaced while their parent resumes, so iterate a snapshot and skip
|
|
123
126
|
// anything that was disposed along the way.
|
|
124
|
-
for (const child of [...this.children]) if (!child.disposed) child.resume();
|
|
127
|
+
if (this.children) for (const child of [...this.children]) if (!child.disposed) child.resume();
|
|
125
128
|
}
|
|
126
129
|
}
|
|
127
130
|
|
|
@@ -142,7 +145,7 @@ class Effect extends Owner {
|
|
|
142
145
|
owner = previous;
|
|
143
146
|
}
|
|
144
147
|
});
|
|
145
|
-
|
|
148
|
+
this.computed[EFFECT] = this;
|
|
146
149
|
watcher.watch(this.computed);
|
|
147
150
|
}
|
|
148
151
|
|
|
@@ -177,7 +180,7 @@ class Effect extends Owner {
|
|
|
177
180
|
this.paused = false;
|
|
178
181
|
watchAgain(this.computed);
|
|
179
182
|
try { this.run(); } catch (error) { report(error); }
|
|
180
|
-
for (const child of [...this.children]) if (!child.disposed) child.resume();
|
|
183
|
+
if (this.children) for (const child of [...this.children]) if (!child.disposed) child.resume();
|
|
181
184
|
}
|
|
182
185
|
}
|
|
183
186
|
|
|
@@ -239,7 +242,7 @@ export function flush() {
|
|
|
239
242
|
report(new Error('zubra: effects keep re-triggering each other; stopped after 100 rounds'));
|
|
240
243
|
return;
|
|
241
244
|
}
|
|
242
|
-
const effects = pending.map((computed) =>
|
|
245
|
+
const effects = pending.map((computed) => computed[EFFECT]).sort((a, b) => a.id - b.id);
|
|
243
246
|
for (const e of effects) {
|
|
244
247
|
if (e.disposed || e.paused) continue;
|
|
245
248
|
try { e.run(); } catch (error) { report(error); }
|
|
@@ -540,6 +540,25 @@ function blueprint(source, parent = null) {
|
|
|
540
540
|
return content;
|
|
541
541
|
}
|
|
542
542
|
|
|
543
|
+
// For a blueprint with no custom elements: a copy owned by this document, made once, which rows
|
|
544
|
+
// clone. Its clones need no adopting when they're inserted, and nothing in it has to upgrade.
|
|
545
|
+
const imported = new WeakMap();
|
|
546
|
+
const upgrades = (content) =>
|
|
547
|
+
[...content.querySelectorAll('*')].some((element) => element.localName.includes('-') || element.hasAttribute('is'));
|
|
548
|
+
|
|
549
|
+
/** A fresh copy of `block`'s template for `parent`, ready for its parts to be found and bound. */
|
|
550
|
+
function freshCopy(block, parent) {
|
|
551
|
+
const content = blueprint(block, parent);
|
|
552
|
+
let copy = imported.get(content);
|
|
553
|
+
if (copy === undefined) {
|
|
554
|
+
// importNode: custom elements inside upgrade now, so their properties exist before parts
|
|
555
|
+
// bind (a plain clone stays in the template's inert document).
|
|
556
|
+
copy = upgrades(content) ? null : document.importNode(content, true);
|
|
557
|
+
imported.set(content, copy);
|
|
558
|
+
}
|
|
559
|
+
return copy === null ? document.importNode(content, true) : copy.cloneNode(true);
|
|
560
|
+
}
|
|
561
|
+
|
|
543
562
|
function clearBetween(open, close) {
|
|
544
563
|
while (open.nextSibling && open.nextSibling !== close) open.nextSibling.remove();
|
|
545
564
|
}
|
|
@@ -600,10 +619,8 @@ function mountBlock(block, args, open, close, hydrating, context) {
|
|
|
600
619
|
clearBetween(open, close);
|
|
601
620
|
// Find and bind the parts in the fragment, then insert it: child components inside see
|
|
602
621
|
// their props from the start, and can't move slotted nodes before they're found.
|
|
603
|
-
// importNode, not cloneNode: defined custom elements upgrade now, so their properties exist
|
|
604
|
-
// before parts bind (a clone stays in the template's inert document).
|
|
605
622
|
const parent = parentOf(close);
|
|
606
|
-
const fragment =
|
|
623
|
+
const fragment = freshCopy(block, parent);
|
|
607
624
|
fragmentParents.set(fragment, parent);
|
|
608
625
|
const fresh = locate(childRange(fragment), parts);
|
|
609
626
|
parts.forEach((part, index) => bind(part, fresh[index], false, context));
|
|
@@ -731,7 +748,12 @@ function bind(part, node, hydrating, context) {
|
|
|
731
748
|
if (part.text) {
|
|
732
749
|
effect(() => {
|
|
733
750
|
const text = part.text();
|
|
734
|
-
|
|
751
|
+
// A lone Text child is compared and written in place (`.data`): reading `textContent`
|
|
752
|
+
// walks the subtree, and setting it replaces the Text node.
|
|
753
|
+
const child = node.firstChild;
|
|
754
|
+
if (child !== null && child === node.lastChild && child.nodeType === TEXT) {
|
|
755
|
+
if (child.data !== text) child.data = text;
|
|
756
|
+
} else if (node.textContent !== text) node.textContent = text;
|
|
735
757
|
});
|
|
736
758
|
} else if (part.anchor) {
|
|
737
759
|
effect(() => {
|
|
@@ -1289,19 +1311,27 @@ function bindEach(part, open, hydrating, context) {
|
|
|
1289
1311
|
const rowsOwner = scope();
|
|
1290
1312
|
let rows = [];
|
|
1291
1313
|
|
|
1314
|
+
// A row is the nodes from `open` to `close`. Rows the server rendered keep the markers it put
|
|
1315
|
+
// around them. A row made here is its block's own nodes, from first to last, with no markers:
|
|
1316
|
+
// what a block's nodes are at the top never changes after it's bound (a region inside one keeps
|
|
1317
|
+
// its own markers), so the first and last stay the row's ends. Only an empty block needs one.
|
|
1292
1318
|
const createRow = (value, index, key, rowOpen, rowClose, hydrateRow) => {
|
|
1293
|
-
if (!rowOpen) {
|
|
1294
|
-
// A new row is assembled in a fragment, then placed.
|
|
1295
|
-
rowOpen = document.createComment('[');
|
|
1296
|
-
rowClose = document.createComment(']');
|
|
1297
|
-
const fragment = new DocumentFragment();
|
|
1298
|
-
fragmentParents.set(fragment, parentOf(open));
|
|
1299
|
-
fragment.append(rowOpen, rowClose);
|
|
1300
|
-
}
|
|
1301
1319
|
const item = new Signal.State(value);
|
|
1302
1320
|
const position = new Signal.State(index);
|
|
1303
|
-
|
|
1304
|
-
|
|
1321
|
+
if (rowOpen) {
|
|
1322
|
+
const owner = rowsOwner.run(() => scope(() => mountBlock(part.block, [item, position], rowOpen, rowClose, hydrateRow, context)));
|
|
1323
|
+
return { key, item, position, open: rowOpen, close: rowClose, owner };
|
|
1324
|
+
}
|
|
1325
|
+
const parent = parentOf(open);
|
|
1326
|
+
const fragment = freshCopy(part.block, parent);
|
|
1327
|
+
fragmentParents.set(fragment, parent);
|
|
1328
|
+
const owner = rowsOwner.run(() => scope(() => {
|
|
1329
|
+
const parts = part.block.parts(item, position);
|
|
1330
|
+
const nodes = locate(childRange(fragment), parts);
|
|
1331
|
+
parts.forEach((each, at) => bind(each, nodes[at], false, context));
|
|
1332
|
+
}));
|
|
1333
|
+
if (!fragment.firstChild) fragment.append(document.createComment(''));
|
|
1334
|
+
return { key, item, position, open: fragment.firstChild, close: fragment.lastChild, owner };
|
|
1305
1335
|
};
|
|
1306
1336
|
const nodesOf = (row) => {
|
|
1307
1337
|
const nodes = [];
|