@b9g/crank 0.6.1 → 0.7.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/umd.js CHANGED
@@ -4,8 +4,252 @@
4
4
  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.Crank = {}));
5
5
  })(this, (function (exports) { 'use strict';
6
6
 
7
- const NOOP = () => { };
8
- const IDENTITY = (value) => value;
7
+ // EVENT PHASE CONSTANTS
8
+ // https://developer.mozilla.org/en-US/docs/Web/API/Event/eventPhase
9
+ const NONE = 0;
10
+ const CAPTURING_PHASE = 1;
11
+ const AT_TARGET = 2;
12
+ const BUBBLING_PHASE = 3;
13
+ function isEventTarget(value) {
14
+ return (value != null &&
15
+ typeof value.addEventListener === "function" &&
16
+ typeof value.removeEventListener === "function" &&
17
+ typeof value.dispatchEvent === "function");
18
+ }
19
+ function setEventProperty(ev, key, value) {
20
+ Object.defineProperty(ev, key, { value, writable: false, configurable: true });
21
+ }
22
+ function isListenerOrListenerObject(value) {
23
+ return (typeof value === "function" ||
24
+ (value !== null &&
25
+ typeof value === "object" &&
26
+ typeof value.handleEvent === "function"));
27
+ }
28
+ function normalizeListenerOptions(options) {
29
+ if (typeof options === "boolean") {
30
+ return { capture: options };
31
+ }
32
+ else if (options == null) {
33
+ return {};
34
+ }
35
+ return options;
36
+ }
37
+ const _parent = Symbol.for("CustomEventTarget.parent");
38
+ const _listeners = Symbol.for("CustomEventTarget.listeners");
39
+ const _delegates = Symbol.for("CustomEventTarget.delegates");
40
+ const _dispatchEventOnSelf = Symbol.for("CustomEventTarget.dispatchSelf");
41
+ class CustomEventTarget {
42
+ constructor(parent = null) {
43
+ this[_parent] = parent;
44
+ this[_listeners] = [];
45
+ this[_delegates] = new Set();
46
+ }
47
+ addEventListener(type, listener, options) {
48
+ if (!isListenerOrListenerObject(listener)) {
49
+ return;
50
+ }
51
+ const listeners = this[_listeners];
52
+ options = normalizeListenerOptions(options);
53
+ let callback;
54
+ if (typeof listener === "function") {
55
+ callback = listener;
56
+ }
57
+ else {
58
+ callback = (ev) => listener.handleEvent(ev);
59
+ }
60
+ const record = { type, listener, callback, options };
61
+ if (options.once) {
62
+ record.callback = function () {
63
+ const i = listeners.indexOf(record);
64
+ if (i !== -1) {
65
+ listeners.splice(i, 1);
66
+ }
67
+ return callback.apply(this, arguments);
68
+ };
69
+ }
70
+ if (listeners.some((record1) => record.type === record1.type &&
71
+ record.listener === record1.listener &&
72
+ !record.options.capture === !record1.options.capture)) {
73
+ return;
74
+ }
75
+ listeners.push(record);
76
+ for (const delegate of this[_delegates]) {
77
+ delegate.addEventListener(type, record.callback, record.options);
78
+ }
79
+ }
80
+ removeEventListener(type, listener, options) {
81
+ const listeners = this[_listeners];
82
+ if (listeners == null || !isListenerOrListenerObject(listener)) {
83
+ return;
84
+ }
85
+ const options1 = normalizeListenerOptions(options);
86
+ const i = listeners.findIndex((record) => record.type === type &&
87
+ record.listener === listener &&
88
+ !record.options.capture === !options1.capture);
89
+ if (i === -1) {
90
+ return;
91
+ }
92
+ const record = listeners[i];
93
+ listeners.splice(i, 1);
94
+ for (const delegate of this[_delegates]) {
95
+ delegate.removeEventListener(record.type, record.callback, record.options);
96
+ }
97
+ }
98
+ dispatchEvent(ev) {
99
+ const path = [];
100
+ for (let parent = this[_parent]; parent; parent = parent[_parent]) {
101
+ path.push(parent);
102
+ }
103
+ let cancelBubble = false;
104
+ let immediateCancelBubble = false;
105
+ const stopPropagation = ev.stopPropagation;
106
+ setEventProperty(ev, "stopPropagation", () => {
107
+ cancelBubble = true;
108
+ return stopPropagation.call(ev);
109
+ });
110
+ const stopImmediatePropagation = ev.stopImmediatePropagation;
111
+ setEventProperty(ev, "stopImmediatePropagation", () => {
112
+ immediateCancelBubble = true;
113
+ return stopImmediatePropagation.call(ev);
114
+ });
115
+ setEventProperty(ev, "target", this);
116
+ // The only possible errors in this block are errors thrown by callbacks,
117
+ // and dispatchEvent will only log these errors rather than throwing them.
118
+ // Therefore, we place all code in a try block, log errors in the catch
119
+ // block, and use an unsafe return statement in the finally block.
120
+ //
121
+ // Each early return within the try block returns true because while the
122
+ // return value is overridden in the finally block, TypeScript
123
+ // (justifiably) does not recognize the unsafe return statement.
124
+ try {
125
+ setEventProperty(ev, "eventPhase", CAPTURING_PHASE);
126
+ for (let i = path.length - 1; i >= 0; i--) {
127
+ const target = path[i];
128
+ const listeners = target[_listeners];
129
+ setEventProperty(ev, "currentTarget", target);
130
+ for (let i = 0; i < listeners.length; i++) {
131
+ const record = listeners[i];
132
+ if (record.type === ev.type && record.options.capture) {
133
+ try {
134
+ record.callback.call(target, ev);
135
+ }
136
+ catch (err) {
137
+ console.error(err);
138
+ }
139
+ if (immediateCancelBubble) {
140
+ return true;
141
+ }
142
+ }
143
+ }
144
+ if (cancelBubble) {
145
+ return true;
146
+ }
147
+ }
148
+ {
149
+ setEventProperty(ev, "eventPhase", AT_TARGET);
150
+ setEventProperty(ev, "currentTarget", this);
151
+ this[_dispatchEventOnSelf](ev);
152
+ if (immediateCancelBubble) {
153
+ return true;
154
+ }
155
+ const listeners = this[_listeners];
156
+ for (let i = 0; i < listeners.length; i++) {
157
+ const record = listeners[i];
158
+ if (record.type === ev.type) {
159
+ try {
160
+ record.callback.call(this, ev);
161
+ }
162
+ catch (err) {
163
+ console.error(err);
164
+ }
165
+ if (immediateCancelBubble) {
166
+ return true;
167
+ }
168
+ }
169
+ }
170
+ if (cancelBubble) {
171
+ return true;
172
+ }
173
+ }
174
+ if (ev.bubbles) {
175
+ setEventProperty(ev, "eventPhase", BUBBLING_PHASE);
176
+ for (let i = 0; i < path.length; i++) {
177
+ const target = path[i];
178
+ setEventProperty(ev, "currentTarget", target);
179
+ const listeners = target[_listeners];
180
+ for (let i = 0; i < listeners.length; i++) {
181
+ const record = listeners[i];
182
+ if (record.type === ev.type && !record.options.capture) {
183
+ try {
184
+ record.callback.call(target, ev);
185
+ }
186
+ catch (err) {
187
+ console.error(err);
188
+ }
189
+ if (immediateCancelBubble) {
190
+ return true;
191
+ }
192
+ }
193
+ }
194
+ if (cancelBubble) {
195
+ return true;
196
+ }
197
+ }
198
+ }
199
+ }
200
+ finally {
201
+ setEventProperty(ev, "eventPhase", NONE);
202
+ setEventProperty(ev, "currentTarget", null);
203
+ // eslint-disable-next-line no-unsafe-finally
204
+ return !ev.defaultPrevented;
205
+ }
206
+ }
207
+ [_dispatchEventOnSelf](_ev) { }
208
+ }
209
+ CustomEventTarget.dispatchEventOnSelf = _dispatchEventOnSelf;
210
+ function addEventTargetDelegates(target, delegates, include = (target1) => target === target1) {
211
+ const delegates1 = delegates.filter(isEventTarget);
212
+ for (let target1 = target; target1 && include(target1); target1 = target1[_parent]) {
213
+ for (let i = 0; i < delegates1.length; i++) {
214
+ const delegate = delegates1[i];
215
+ if (target1[_delegates].has(delegate)) {
216
+ continue;
217
+ }
218
+ target1[_delegates].add(delegate);
219
+ for (const record of target1[_listeners]) {
220
+ delegate.addEventListener(record.type, record.callback, record.options);
221
+ }
222
+ }
223
+ }
224
+ }
225
+ function removeEventTargetDelegates(target, delegates, include = (target1) => target === target1) {
226
+ const delegates1 = delegates.filter(isEventTarget);
227
+ for (let target1 = target; target1 && include(target1); target1 = target1[_parent]) {
228
+ for (let i = 0; i < delegates1.length; i++) {
229
+ const delegate = delegates1[i];
230
+ if (!target1[_delegates].has(delegate)) {
231
+ continue;
232
+ }
233
+ target1[_delegates].delete(delegate);
234
+ for (const record of target1[_listeners]) {
235
+ delegate.removeEventListener(record.type, record.callback, record.options);
236
+ }
237
+ }
238
+ }
239
+ }
240
+ function clearEventListeners(target) {
241
+ const listeners = target[_listeners];
242
+ const delegates = target[_delegates];
243
+ for (let i = 0; i < listeners.length; i++) {
244
+ const record = listeners[i];
245
+ for (const delegate of delegates) {
246
+ delegate.removeEventListener(record.type, record.callback, record.options);
247
+ }
248
+ }
249
+ listeners.length = 0;
250
+ delegates.clear();
251
+ }
252
+
9
253
  function wrap(value) {
10
254
  return value === undefined ? [] : Array.isArray(value) ? value : [value];
11
255
  }
@@ -35,11 +279,84 @@
35
279
  function isPromiseLike(value) {
36
280
  return value != null && typeof value.then === "function";
37
281
  }
38
- /***
39
- * SPECIAL TAGS
40
- *
41
- * Crank provides a couple tags which have special meaning for the renderer.
42
- ***/
282
+ function createRaceRecord(contender) {
283
+ const deferreds = new Set();
284
+ const record = { deferreds, settled: false };
285
+ // This call to `then` happens once for the lifetime of the value.
286
+ Promise.resolve(contender).then((value) => {
287
+ for (const { resolve } of deferreds) {
288
+ resolve(value);
289
+ }
290
+ deferreds.clear();
291
+ record.settled = true;
292
+ }, (err) => {
293
+ for (const { reject } of deferreds) {
294
+ reject(err);
295
+ }
296
+ deferreds.clear();
297
+ record.settled = true;
298
+ });
299
+ return record;
300
+ }
301
+ // Promise.race is memory unsafe. This is alternative which is. See:
302
+ // https://github.com/nodejs/node/issues/17469#issuecomment-685235106
303
+ // Keys are the values passed to race.
304
+ // Values are a record of data containing a set of deferreds and whether the
305
+ // value has settled.
306
+ const wm = new WeakMap();
307
+ function safeRace(contenders) {
308
+ let deferred;
309
+ const result = new Promise((resolve, reject) => {
310
+ deferred = { resolve, reject };
311
+ for (const contender of contenders) {
312
+ if (!isPromiseLike(contender)) {
313
+ // If the contender is a not a then-able, attempting to use it as a key
314
+ // in the weakmap would throw an error. Luckily, it is safe to call
315
+ // `Promise.resolve(contender).then` on regular values multiple
316
+ // times because the promise fulfills immediately.
317
+ Promise.resolve(contender).then(resolve, reject);
318
+ continue;
319
+ }
320
+ let record = wm.get(contender);
321
+ if (record === undefined) {
322
+ record = createRaceRecord(contender);
323
+ record.deferreds.add(deferred);
324
+ wm.set(contender, record);
325
+ }
326
+ else if (record.settled) {
327
+ // If the value has settled, it is safe to call
328
+ // `Promise.resolve(contender).then` on it.
329
+ Promise.resolve(contender).then(resolve, reject);
330
+ }
331
+ else {
332
+ record.deferreds.add(deferred);
333
+ }
334
+ }
335
+ });
336
+ // The finally callback executes when any value settles, preventing any of
337
+ // the unresolved values from retaining a reference to the resolved value.
338
+ return result.finally(() => {
339
+ for (const contender of contenders) {
340
+ if (isPromiseLike(contender)) {
341
+ const record = wm.get(contender);
342
+ if (record) {
343
+ record.deferreds.delete(deferred);
344
+ }
345
+ }
346
+ }
347
+ });
348
+ }
349
+
350
+ const NOOP = () => { };
351
+ function getTagName(tag) {
352
+ return typeof tag === "function"
353
+ ? tag.name || "Anonymous"
354
+ : typeof tag === "string"
355
+ ? tag
356
+ : // tag is symbol, using else branch to avoid typeof tag === "symbol"
357
+ tag.description || "Anonymous";
358
+ }
359
+ /*** SPECIAL TAGS ***/
43
360
  /**
44
361
  * A special tag for grouping multiple children within the same parent.
45
362
  *
@@ -51,8 +368,8 @@
51
368
  * reference this export.
52
369
  */
53
370
  const Fragment = "";
54
- // TODO: We assert the following symbol tags as any because TypeScript support
55
- // for symbol tags in JSX doesnt exist yet.
371
+ // TODO: We assert the following symbol tags as Components because TypeScript
372
+ // support for symbol tags in JSX doesn't exist yet.
56
373
  // https://github.com/microsoft/TypeScript/issues/38367
57
374
  /**
58
375
  * A special tag for rendering into a new root node via a root prop.
@@ -60,13 +377,13 @@
60
377
  * This tag is useful for creating element trees with multiple roots, for
61
378
  * things like modals or tooltips.
62
379
  *
63
- * Renderer.prototype.render() will implicitly wrap top-level element trees in
64
- * a Portal element.
380
+ * Renderer.prototype.render() implicitly wraps top-level in a Portal element
381
+ * with the root set to the second argument passed in.
65
382
  */
66
383
  const Portal = Symbol.for("crank.Portal");
67
384
  /**
68
385
  * A special tag which preserves whatever was previously rendered in the
69
- * elements position.
386
+ * element's position.
70
387
  *
71
388
  * Copy elements are useful for when you want to prevent a subtree from
72
389
  * rerendering as a performance optimization. Copy elements can also be keyed,
@@ -74,10 +391,13 @@
74
391
  */
75
392
  const Copy = Symbol.for("crank.Copy");
76
393
  /**
77
- * A special tag for injecting raw nodes or strings via a value prop.
394
+ * A special tag for rendering text nodes.
78
395
  *
79
- * Renderer.prototype.raw() is called with the value prop.
396
+ * Strings in the element tree are implicitly wrapped in a Text element with
397
+ * value set to the string.
80
398
  */
399
+ const Text = Symbol.for("crank.Text");
400
+ /** A special tag for injecting raw nodes or strings via a value prop. */
81
401
  const Raw = Symbol.for("crank.Raw");
82
402
  const ElementSymbol = Symbol.for("crank.Element");
83
403
  /**
@@ -105,15 +425,6 @@
105
425
  this.tag = tag;
106
426
  this.props = props;
107
427
  }
108
- get key() {
109
- return this.props.key;
110
- }
111
- get ref() {
112
- return this.props.ref;
113
- }
114
- get copy() {
115
- return !!this.props.copy;
116
- }
117
428
  }
118
429
  // See Element interface
119
430
  Element.prototype.$$typeof = ElementSymbol;
@@ -121,34 +432,34 @@
121
432
  return value != null && value.$$typeof === ElementSymbol;
122
433
  }
123
434
  const DEPRECATED_PROP_PREFIXES = ["crank-", "c-", "$"];
124
- const DEPRECATED_SPECIAL_PROP_BASES = ["key", "ref", "static"];
125
- const SPECIAL_PROPS = new Set(["children", "key", "ref", "copy"]);
126
- for (const propPrefix of DEPRECATED_PROP_PREFIXES) {
127
- for (const propBase of DEPRECATED_SPECIAL_PROP_BASES) {
128
- SPECIAL_PROPS.add(propPrefix + propBase);
129
- }
130
- }
435
+ const DEPRECATED_SPECIAL_PROP_BASES = ["key", "ref", "static", "copy"];
131
436
  /**
132
437
  * Creates an element with the specified tag, props and children.
133
438
  *
134
439
  * This function is usually used as a transpilation target for JSX transpilers,
135
440
  * but it can also be called directly. It additionally extracts special props so
136
- * they arent accessible to renderer methods or components, and assigns the
441
+ * they aren't accessible to renderer methods or components, and assigns the
137
442
  * children prop according to any additional arguments passed to the function.
138
443
  */
139
444
  function createElement(tag, props, ...children) {
140
445
  if (props == null) {
141
446
  props = {};
142
447
  }
448
+ if ("static" in props) {
449
+ console.error(`The \`static\` prop is deprecated. Use \`copy\` instead.`);
450
+ props["copy"] = props["static"];
451
+ delete props["static"];
452
+ }
143
453
  for (let i = 0; i < DEPRECATED_PROP_PREFIXES.length; i++) {
144
454
  const propPrefix = DEPRECATED_PROP_PREFIXES[i];
145
455
  for (let j = 0; j < DEPRECATED_SPECIAL_PROP_BASES.length; j++) {
146
456
  const propBase = DEPRECATED_SPECIAL_PROP_BASES[j];
147
457
  const deprecatedPropName = propPrefix + propBase;
148
- const targetPropBase = propBase === "static" ? "copy" : propBase;
149
458
  if (deprecatedPropName in props) {
150
- console.warn(`The \`${deprecatedPropName}\` prop is deprecated. Use \`${targetPropBase}\` instead.`);
459
+ const targetPropBase = propBase === "static" ? "copy" : propBase;
460
+ console.error(`The \`${deprecatedPropName}\` prop is deprecated. Use \`${targetPropBase}\` instead.`);
151
461
  props[targetPropBase] = props[deprecatedPropName];
462
+ delete props[deprecatedPropName];
152
463
  }
153
464
  }
154
465
  }
@@ -163,13 +474,13 @@
163
474
  /** Clones a given element, shallowly copying the props object. */
164
475
  function cloneElement(el) {
165
476
  if (!isElement(el)) {
166
- throw new TypeError("Cannot clone non-element");
477
+ throw new TypeError(`Cannot clone non-element: ${String(el)}`);
167
478
  }
168
479
  return new Element(el.tag, { ...el.props });
169
480
  }
170
481
  function narrow(value) {
171
482
  if (typeof value === "boolean" || value == null) {
172
- return undefined;
483
+ return;
173
484
  }
174
485
  else if (typeof value === "string" || isElement(value)) {
175
486
  return value;
@@ -179,137 +490,203 @@
179
490
  }
180
491
  return value.toString();
181
492
  }
182
- /**
183
- * Takes an array of element values and normalizes the output as an array of
184
- * nodes and strings.
185
- *
186
- * @returns Normalized array of nodes and/or strings.
187
- *
188
- * Normalize will flatten only one level of nested arrays, because it is
189
- * designed to be called once at each level of the tree. It will also
190
- * concatenate adjacent strings and remove all undefined values.
191
- */
192
- function normalize(values) {
193
- const result = [];
194
- let buffer;
195
- for (let i = 0; i < values.length; i++) {
196
- const value = values[i];
197
- if (!value) ;
198
- else if (typeof value === "string") {
199
- buffer = (buffer || "") + value;
200
- }
201
- else if (!Array.isArray(value)) {
202
- if (buffer) {
203
- result.push(buffer);
204
- buffer = undefined;
205
- }
206
- result.push(value);
207
- }
208
- else {
209
- // We could use recursion here but it’s just easier to do it inline.
210
- for (let j = 0; j < value.length; j++) {
211
- const value1 = value[j];
212
- if (!value1) ;
213
- else if (typeof value1 === "string") {
214
- buffer = (buffer || "") + value1;
215
- }
216
- else {
217
- if (buffer) {
218
- result.push(buffer);
219
- buffer = undefined;
220
- }
221
- result.push(value1);
222
- }
223
- }
224
- }
493
+ /*** RETAINER FLAGS ***/
494
+ const DidDiff = 1 << 0;
495
+ const DidCommit = 1 << 1;
496
+ const IsCopied = 1 << 2;
497
+ const IsUpdating = 1 << 3;
498
+ const IsExecuting = 1 << 4;
499
+ const IsRefreshing = 1 << 5;
500
+ const IsScheduling = 1 << 6;
501
+ const IsSchedulingFallback = 1 << 7;
502
+ const IsUnmounted = 1 << 8;
503
+ // TODO: Is this flag still necessary or can we use IsUnmounted?
504
+ const IsErrored = 1 << 9;
505
+ const IsResurrecting = 1 << 10;
506
+ // TODO: Maybe we can get rid of IsSyncGen and IsAsyncGen
507
+ const IsSyncGen = 1 << 11;
508
+ const IsAsyncGen = 1 << 12;
509
+ const IsInForOfLoop = 1 << 13;
510
+ const IsInForAwaitOfLoop = 1 << 14;
511
+ const NeedsToYield = 1 << 15;
512
+ const PropsAvailable = 1 << 16;
513
+ function getFlag(ret, flag) {
514
+ return !!(ret.f & flag);
515
+ }
516
+ function setFlag(ret, flag, value = true) {
517
+ if (value) {
518
+ ret.f |= flag;
225
519
  }
226
- if (buffer) {
227
- result.push(buffer);
520
+ else {
521
+ ret.f &= ~flag;
228
522
  }
229
- return result;
230
523
  }
231
524
  /**
232
525
  * @internal
233
- * The internal nodes which are cached and diffed against new elements when
234
- * rendering element trees.
526
+ * Retainers are objects which act as the internal representation of elements,
527
+ * mirroring the element tree.
235
528
  */
236
529
  class Retainer {
237
530
  constructor(el) {
531
+ this.f = 0;
238
532
  this.el = el;
239
533
  this.ctx = undefined;
240
534
  this.children = undefined;
535
+ this.fallback = undefined;
241
536
  this.value = undefined;
242
- this.cachedChildValues = undefined;
243
- this.fallbackValue = undefined;
244
- this.inflightValue = undefined;
245
- this.onNextValues = undefined;
537
+ this.oldProps = undefined;
538
+ this.pendingDiff = undefined;
539
+ this.onNextDiff = undefined;
540
+ this.graveyard = undefined;
541
+ this.lingerers = undefined;
246
542
  }
247
543
  }
544
+ function cloneRetainer(ret) {
545
+ const clone = new Retainer(ret.el);
546
+ clone.f = ret.f;
547
+ clone.ctx = ret.ctx;
548
+ clone.children = ret.children;
549
+ clone.fallback = ret.fallback;
550
+ clone.value = ret.value;
551
+ clone.scope = ret.scope;
552
+ clone.oldProps = ret.oldProps;
553
+ clone.pendingDiff = ret.pendingDiff;
554
+ clone.onNextDiff = ret.onNextDiff;
555
+ clone.graveyard = ret.graveyard;
556
+ clone.lingerers = ret.lingerers;
557
+ return clone;
558
+ }
248
559
  /**
249
560
  * Finds the value of the element according to its type.
250
561
  *
251
- * @returns The value of the element.
562
+ * @returns A node, an array of nodes or undefined.
252
563
  */
253
- function getValue(ret) {
254
- if (typeof ret.fallbackValue !== "undefined") {
255
- return typeof ret.fallbackValue === "object"
256
- ? getValue(ret.fallbackValue)
257
- : ret.fallbackValue;
564
+ function getValue(ret, isNested = false, index) {
565
+ if (getFlag(ret, IsScheduling) && isNested) {
566
+ return ret.fallback ? getValue(ret.fallback, isNested, index) : undefined;
567
+ }
568
+ else if (ret.fallback && !getFlag(ret, DidDiff)) {
569
+ return ret.fallback
570
+ ? getValue(ret.fallback, isNested, index)
571
+ : ret.fallback;
258
572
  }
259
573
  else if (ret.el.tag === Portal) {
260
574
  return;
261
575
  }
262
- else if (typeof ret.el.tag !== "function" && ret.el.tag !== Fragment) {
263
- return ret.value;
576
+ else if (ret.el.tag === Fragment || typeof ret.el.tag === "function") {
577
+ if (index != null && ret.ctx) {
578
+ ret.ctx.index = index;
579
+ }
580
+ return unwrap(getChildValues(ret, index));
264
581
  }
265
- return unwrap(getChildValues(ret));
582
+ return ret.value;
266
583
  }
267
584
  /**
268
- * Walks an elements children to find its child values.
585
+ * Walks an element's children to find its child values.
269
586
  *
270
- * @returns A normalized array of nodes and strings.
587
+ * @param ret - The retainer whose child values we are reading.
588
+ * @param startIndex - Starting index to thread through for context index updates.
589
+ *
590
+ * @returns An array of nodes.
271
591
  */
272
- function getChildValues(ret) {
273
- if (ret.cachedChildValues) {
274
- return wrap(ret.cachedChildValues);
275
- }
592
+ function getChildValues(ret, startIndex) {
276
593
  const values = [];
594
+ const lingerers = ret.lingerers;
277
595
  const children = wrap(ret.children);
596
+ let currentIndex = startIndex;
278
597
  for (let i = 0; i < children.length; i++) {
598
+ if (lingerers != null && lingerers[i] != null) {
599
+ const rets = lingerers[i];
600
+ for (const ret of rets) {
601
+ const value = getValue(ret, true, currentIndex);
602
+ if (Array.isArray(value)) {
603
+ for (let j = 0; j < value.length; j++) {
604
+ values.push(value[j]);
605
+ }
606
+ if (currentIndex != null) {
607
+ currentIndex += value.length;
608
+ }
609
+ }
610
+ else if (value) {
611
+ values.push(value);
612
+ if (currentIndex != null) {
613
+ currentIndex++;
614
+ }
615
+ }
616
+ }
617
+ }
279
618
  const child = children[i];
280
619
  if (child) {
281
- values.push(typeof child === "string" ? child : getValue(child));
620
+ const value = getValue(child, true, currentIndex);
621
+ if (Array.isArray(value)) {
622
+ for (let j = 0; j < value.length; j++) {
623
+ values.push(value[j]);
624
+ }
625
+ if (currentIndex != null) {
626
+ currentIndex += value.length;
627
+ }
628
+ }
629
+ else if (value) {
630
+ values.push(value);
631
+ if (currentIndex != null) {
632
+ currentIndex++;
633
+ }
634
+ }
282
635
  }
283
636
  }
284
- const values1 = normalize(values);
285
- const tag = ret.el.tag;
286
- if (typeof tag === "function" || (tag !== Fragment && tag !== Raw)) {
287
- ret.cachedChildValues = unwrap(values1);
637
+ if (lingerers != null && lingerers.length > children.length) {
638
+ for (let i = children.length; i < lingerers.length; i++) {
639
+ const rets = lingerers[i];
640
+ if (rets != null) {
641
+ for (const ret of rets) {
642
+ const value = getValue(ret, true, currentIndex);
643
+ if (Array.isArray(value)) {
644
+ for (let j = 0; j < value.length; j++) {
645
+ values.push(value[j]);
646
+ }
647
+ if (currentIndex != null) {
648
+ currentIndex += value.length;
649
+ }
650
+ }
651
+ else if (value) {
652
+ values.push(value);
653
+ if (currentIndex != null) {
654
+ currentIndex++;
655
+ }
656
+ }
657
+ }
658
+ }
659
+ }
288
660
  }
289
- return values1;
661
+ return values;
662
+ }
663
+ function stripSpecialProps(props) {
664
+ let _;
665
+ let result;
666
+ ({ key: _, ref: _, copy: _, hydrate: _, children: _, ...result } = props);
667
+ return result;
290
668
  }
291
- const defaultRendererImpl = {
669
+ const defaultAdapter = {
292
670
  create() {
293
- throw new Error("Not implemented");
671
+ throw new Error("adapter must implement create");
294
672
  },
295
- hydrate() {
296
- throw new Error("Not implemented");
673
+ adopt() {
674
+ throw new Error("adapter must implement adopt() for hydration");
297
675
  },
298
- scope: IDENTITY,
299
- read: IDENTITY,
300
- text: IDENTITY,
301
- raw: IDENTITY,
676
+ scope: ({ scope }) => scope,
677
+ read: (value) => value,
678
+ text: ({ value }) => value,
679
+ raw: ({ value }) => value,
302
680
  patch: NOOP,
303
681
  arrange: NOOP,
304
- dispose: NOOP,
305
- flush: NOOP,
682
+ remove: NOOP,
683
+ finalize: NOOP,
306
684
  };
307
- const _RendererImpl = Symbol.for("crank.RendererImpl");
308
685
  /**
309
686
  * An abstract class which is subclassed to render to different target
310
- * environments. Subclasses will typically call super() with a custom
311
- * RendererImpl. This class is responsible for kicking off the rendering
312
- * process and caching previous trees by root.
687
+ * environments. Subclasses call super() with a custom RenderAdapter object.
688
+ * This class is responsible for kicking off the rendering process and caching
689
+ * previous trees by root.
313
690
  *
314
691
  * @template TNode - The type of the node for a rendering environment.
315
692
  * @template TScope - Data which is passed down the tree.
@@ -317,125 +694,128 @@
317
694
  * @template TResult - The type of exposed values.
318
695
  */
319
696
  class Renderer {
320
- constructor(impl) {
697
+ constructor(adapter) {
321
698
  this.cache = new WeakMap();
322
- this[_RendererImpl] = {
323
- ...defaultRendererImpl,
324
- ...impl,
325
- };
699
+ this.adapter = { ...defaultAdapter, ...adapter };
326
700
  }
327
701
  /**
328
702
  * Renders an element tree into a specific root.
329
703
  *
330
- * @param children - An element tree. You can render null with a previously
331
- * used root to delete the previously rendered element tree from the cache.
332
- * @param root - The node to be rendered into. The renderer will cache
333
- * element trees per root.
334
- * @param bridge - An optional context that will be the ancestor context of all
335
- * elements in the tree. Useful for connecting different renderers so that
336
- * events/provisions properly propagate. The context for a given root must be
337
- * the same or an error will be thrown.
704
+ * @param children - An element tree. Rendering null deletes cached renders.
705
+ * @param root - The root to be rendered into. The renderer caches renders
706
+ * per root.
707
+ * @param bridge - An optional context that will be the ancestor context of
708
+ * all elements in the tree. Useful for connecting different renderers so
709
+ * that events/provisions/errors properly propagate. The context for a given
710
+ * root must be the same between renders.
338
711
  *
339
712
  * @returns The result of rendering the children, or a possible promise of
340
713
  * the result if the element tree renders asynchronously.
341
714
  */
342
715
  render(children, root, bridge) {
343
- let ret;
344
- const ctx = bridge && bridge[_ContextImpl];
345
- if (typeof root === "object" && root !== null) {
346
- ret = this.cache.get(root);
347
- }
348
- let oldProps;
349
- if (ret === undefined) {
350
- ret = new Retainer(createElement(Portal, { children, root }));
351
- ret.value = root;
352
- ret.ctx = ctx;
353
- if (typeof root === "object" && root !== null && children != null) {
354
- this.cache.set(root, ret);
355
- }
356
- }
357
- else if (ret.ctx !== ctx) {
358
- throw new Error("Context mismatch");
359
- }
360
- else {
361
- oldProps = ret.el.props;
362
- ret.el = createElement(Portal, { children, root });
363
- if (typeof root === "object" && root !== null && children == null) {
364
- this.cache.delete(root);
365
- }
366
- }
367
- const impl = this[_RendererImpl];
368
- const childValues = diffChildren(impl, root, ret, ctx, impl.scope(undefined, Portal, ret.el.props), ret, children, undefined);
369
- // We return the child values of the portal because portal elements
370
- // themselves have no readable value.
371
- if (isPromiseLike(childValues)) {
372
- return childValues.then((childValues) => commitRootRender(impl, root, ctx, ret, childValues, oldProps));
373
- }
374
- return commitRootRender(impl, root, ctx, ret, childValues, oldProps);
716
+ const ret = getRootRetainer(this, bridge, { children, root });
717
+ return renderRoot(this.adapter, root, ret, children);
375
718
  }
376
719
  hydrate(children, root, bridge) {
377
- const impl = this[_RendererImpl];
378
- const ctx = bridge && bridge[_ContextImpl];
379
- let ret;
380
- ret = this.cache.get(root);
381
- if (ret !== undefined) {
382
- // If there is a retainer for the root, hydration is not necessary.
383
- return this.render(children, root, bridge);
384
- }
385
- let oldProps;
386
- ret = new Retainer(createElement(Portal, { children, root }));
720
+ const ret = getRootRetainer(this, bridge, {
721
+ children,
722
+ root,
723
+ hydrate: true,
724
+ });
725
+ return renderRoot(this.adapter, root, ret, children);
726
+ }
727
+ }
728
+ /*** PRIVATE RENDERER FUNCTIONS ***/
729
+ function getRootRetainer(renderer, bridge, { children, root, hydrate, }) {
730
+ let ret;
731
+ const bridgeCtx = bridge && bridge[_ContextState];
732
+ if (typeof root === "object" && root !== null) {
733
+ ret = renderer.cache.get(root);
734
+ }
735
+ const adapter = renderer.adapter;
736
+ if (ret === undefined) {
737
+ ret = new Retainer(createElement(Portal, { children, root, hydrate }));
387
738
  ret.value = root;
739
+ ret.ctx = bridgeCtx;
740
+ ret.scope = adapter.scope({
741
+ tag: Portal,
742
+ tagName: getTagName(Portal),
743
+ props: stripSpecialProps(ret.el.props),
744
+ scope: undefined,
745
+ });
746
+ // remember that typeof null === "object"
388
747
  if (typeof root === "object" && root !== null && children != null) {
389
- this.cache.set(root, ret);
390
- }
391
- const hydrationData = impl.hydrate(Portal, root, {});
392
- const childValues = diffChildren(impl, root, ret, ctx, impl.scope(undefined, Portal, ret.el.props), ret, children, hydrationData);
393
- // We return the child values of the portal because portal elements
394
- // themselves have no readable value.
395
- if (isPromiseLike(childValues)) {
396
- return childValues.then((childValues) => commitRootRender(impl, root, ctx, ret, childValues, oldProps));
748
+ renderer.cache.set(root, ret);
397
749
  }
398
- return commitRootRender(impl, root, ctx, ret, childValues, oldProps);
399
750
  }
400
- }
401
- /*** PRIVATE RENDERER FUNCTIONS ***/
402
- function commitRootRender(renderer, root, ctx, ret, childValues, oldProps) {
403
- // element is a host or portal element
404
- if (root != null) {
405
- renderer.arrange(Portal, root, ret.el.props, childValues, oldProps, wrap(ret.cachedChildValues));
406
- flush(renderer, root);
751
+ else if (ret.ctx !== bridgeCtx) {
752
+ throw new Error("A previous call to render() was passed a different context");
407
753
  }
408
- ret.cachedChildValues = unwrap(childValues);
409
- if (root == null) {
410
- unmount(renderer, ret, ctx, ret);
754
+ else {
755
+ ret.el = createElement(Portal, { children, root, hydrate });
756
+ if (typeof root === "object" && root !== null && children == null) {
757
+ renderer.cache.delete(root);
758
+ }
411
759
  }
412
- return renderer.read(ret.cachedChildValues);
760
+ return ret;
413
761
  }
414
- function diffChildren(renderer, root, host, ctx, scope, parent, children, hydrationData) {
415
- const oldRetained = wrap(parent.children);
416
- const newRetained = [];
417
- const newChildren = arrayify(children);
418
- const values = [];
419
- let graveyard;
762
+ function renderRoot(adapter, root, ret, children) {
763
+ const diff = diffChildren(adapter, root, ret, ret.ctx, ret.scope, ret, children);
764
+ const schedulePromises = [];
765
+ if (isPromiseLike(diff)) {
766
+ return diff.then(() => {
767
+ commit(adapter, ret, ret, ret.ctx, ret.scope, 0, schedulePromises, undefined);
768
+ if (schedulePromises.length > 0) {
769
+ return Promise.all(schedulePromises).then(() => {
770
+ if (typeof root !== "object" || root === null) {
771
+ unmount(adapter, ret, ret.ctx, ret, false);
772
+ }
773
+ return adapter.read(unwrap(getChildValues(ret)));
774
+ });
775
+ }
776
+ if (typeof root !== "object" || root === null) {
777
+ unmount(adapter, ret, ret.ctx, ret, false);
778
+ }
779
+ return adapter.read(unwrap(getChildValues(ret)));
780
+ });
781
+ }
782
+ commit(adapter, ret, ret, ret.ctx, ret.scope, 0, schedulePromises, undefined);
783
+ if (schedulePromises.length > 0) {
784
+ return Promise.all(schedulePromises).then(() => {
785
+ if (typeof root !== "object" || root === null) {
786
+ unmount(adapter, ret, ret.ctx, ret, false);
787
+ }
788
+ return adapter.read(unwrap(getChildValues(ret)));
789
+ });
790
+ }
791
+ if (typeof root !== "object" || root === null) {
792
+ unmount(adapter, ret, ret.ctx, ret, false);
793
+ }
794
+ return adapter.read(unwrap(getChildValues(ret)));
795
+ }
796
+ function diffChildren(adapter, root, host, ctx, scope, parent, newChildren) {
797
+ const oldRetained = wrap(parent.children);
798
+ const newRetained = [];
799
+ const newChildren1 = arrayify(newChildren);
800
+ const diffs = [];
420
801
  let childrenByKey;
421
802
  let seenKeys;
422
803
  let isAsync = false;
423
- // When hydrating, sibling element trees must be rendered in order, because
424
- // we do not know how many DOM nodes an element will render.
425
- let hydrationBlock;
426
804
  let oi = 0;
427
805
  let oldLength = oldRetained.length;
428
- for (let ni = 0, newLength = newChildren.length; ni < newLength; ni++) {
806
+ let graveyard;
807
+ for (let ni = 0, newLength = newChildren1.length; ni < newLength; ni++) {
429
808
  // length checks to prevent index out of bounds deoptimizations.
430
809
  let ret = oi >= oldLength ? undefined : oldRetained[oi];
431
- let child = narrow(newChildren[ni]);
810
+ let child = narrow(newChildren1[ni]);
432
811
  {
433
812
  // aligning new children with old retainers
434
- let oldKey = typeof ret === "object" ? ret.el.key : undefined;
435
- let newKey = typeof child === "object" ? child.key : undefined;
813
+ let oldKey = typeof ret === "object" ? ret.el.props.key : undefined;
814
+ let newKey = typeof child === "object" ? child.props.key : undefined;
436
815
  if (newKey !== undefined && seenKeys && seenKeys.has(newKey)) {
437
- console.error("Duplicate key", newKey);
438
- newKey = undefined;
816
+ console.error(`Duplicate key found in <${getTagName(parent.el.tag)}>`, newKey);
817
+ child = cloneElement(child);
818
+ newKey = child.props.key = undefined;
439
819
  }
440
820
  if (oldKey === newKey) {
441
821
  if (childrenByKey !== undefined && newKey !== undefined) {
@@ -449,7 +829,7 @@
449
829
  while (ret !== undefined && oldKey !== undefined) {
450
830
  oi++;
451
831
  ret = oldRetained[oi];
452
- oldKey = typeof ret === "object" ? ret.el.key : undefined;
832
+ oldKey = typeof ret === "object" ? ret.el.props.key : undefined;
453
833
  }
454
834
  oi++;
455
835
  }
@@ -462,405 +842,748 @@
462
842
  }
463
843
  }
464
844
  }
465
- // Updating
466
- let value;
845
+ let diff = undefined;
467
846
  if (typeof child === "object") {
468
- if (child.tag === Copy || (typeof ret === "object" && ret.el === child)) {
469
- value = getInflightValue(ret);
847
+ let childCopied = false;
848
+ if (child.tag === Copy) {
849
+ childCopied = true;
850
+ }
851
+ else if (typeof ret === "object" &&
852
+ ret.el === child &&
853
+ getFlag(ret, DidCommit)) {
854
+ // If the child is the same as the retained element, we skip
855
+ // re-rendering.
856
+ childCopied = true;
470
857
  }
471
858
  else {
472
- let oldProps;
473
- let copy = false;
474
- if (typeof ret === "object" && ret.el.tag === child.tag) {
475
- oldProps = ret.el.props;
859
+ if (ret && ret.el.tag === child.tag) {
476
860
  ret.el = child;
477
- if (child.copy) {
478
- value = getInflightValue(ret);
479
- copy = true;
861
+ if (child.props.copy && typeof child.props.copy !== "string") {
862
+ childCopied = true;
480
863
  }
481
864
  }
482
- else {
483
- if (typeof ret === "object") {
484
- (graveyard = graveyard || []).push(ret);
865
+ else if (ret) {
866
+ // we do not need to add the retainer to the graveyard if it is the
867
+ // fallback of another retainer
868
+ // search for the tag in fallback chain
869
+ let candidateFound = false;
870
+ for (let predecessor = ret, candidate = ret.fallback; candidate; predecessor = candidate, candidate = candidate.fallback) {
871
+ if (candidate.el.tag === child.tag) {
872
+ // If we find a retainer in the fallback chain with the same tag,
873
+ // we reuse it rather than creating a new retainer to preserve
874
+ // state. This behavior is useful for when a Suspense component
875
+ // re-renders and the children are re-rendered quickly.
876
+ const clone = cloneRetainer(candidate);
877
+ setFlag(clone, IsResurrecting);
878
+ predecessor.fallback = clone;
879
+ const fallback = ret;
880
+ ret = candidate;
881
+ ret.el = child;
882
+ ret.fallback = fallback;
883
+ setFlag(ret, DidDiff, false);
884
+ candidateFound = true;
885
+ break;
886
+ }
887
+ }
888
+ if (!candidateFound) {
889
+ const fallback = ret;
890
+ ret = new Retainer(child);
891
+ ret.fallback = fallback;
485
892
  }
486
- const fallback = ret;
487
- ret = new Retainer(child);
488
- ret.fallbackValue = fallback;
489
893
  }
490
- if (copy) ;
491
- else if (child.tag === Raw) {
492
- value = hydrationBlock
493
- ? hydrationBlock.then(() => updateRaw(renderer, ret, scope, oldProps, hydrationData))
494
- : updateRaw(renderer, ret, scope, oldProps, hydrationData);
894
+ else {
895
+ ret = new Retainer(child);
495
896
  }
897
+ if (childCopied && getFlag(ret, DidCommit)) ;
898
+ else if (child.tag === Raw || child.tag === Text) ;
496
899
  else if (child.tag === Fragment) {
497
- value = hydrationBlock
498
- ? hydrationBlock.then(() => updateFragment(renderer, root, host, ctx, scope, ret, hydrationData))
499
- : updateFragment(renderer, root, host, ctx, scope, ret, hydrationData);
900
+ diff = diffChildren(adapter, root, host, ctx, scope, ret, ret.el.props.children);
500
901
  }
501
902
  else if (typeof child.tag === "function") {
502
- value = hydrationBlock
503
- ? hydrationBlock.then(() => updateComponent(renderer, root, host, ctx, scope, ret, oldProps, hydrationData))
504
- : updateComponent(renderer, root, host, ctx, scope, ret, oldProps, hydrationData);
903
+ diff = diffComponent(adapter, root, host, ctx, scope, ret);
505
904
  }
506
905
  else {
507
- value = hydrationBlock
508
- ? hydrationBlock.then(() => updateHost(renderer, root, ctx, scope, ret, oldProps, hydrationData))
509
- : updateHost(renderer, root, ctx, scope, ret, oldProps, hydrationData);
906
+ diff = diffHost(adapter, root, ctx, scope, ret);
510
907
  }
511
908
  }
512
- if (isPromiseLike(value)) {
909
+ if (typeof ret === "object") {
910
+ if (childCopied) {
911
+ setFlag(ret, IsCopied);
912
+ diff = getInflightDiff(ret);
913
+ }
914
+ else {
915
+ setFlag(ret, IsCopied, false);
916
+ }
917
+ }
918
+ if (isPromiseLike(diff)) {
513
919
  isAsync = true;
514
- if (hydrationData !== undefined) {
515
- hydrationBlock = value;
920
+ }
921
+ }
922
+ else if (typeof child === "string") {
923
+ if (typeof ret === "object" && ret.el.tag === Text) {
924
+ ret.el.props.value = child;
925
+ }
926
+ else {
927
+ if (typeof ret === "object") {
928
+ (graveyard = graveyard || []).push(ret);
516
929
  }
930
+ ret = new Retainer(createElement(Text, { value: child }));
517
931
  }
518
932
  }
519
933
  else {
520
- // child is a string or undefined
521
934
  if (typeof ret === "object") {
522
935
  (graveyard = graveyard || []).push(ret);
523
936
  }
524
- if (typeof child === "string") {
525
- value = ret = renderer.text(child, scope, hydrationData);
526
- }
527
- else {
528
- ret = undefined;
529
- }
937
+ ret = undefined;
530
938
  }
531
- values[ni] = value;
939
+ diffs[ni] = diff;
532
940
  newRetained[ni] = ret;
533
941
  }
534
942
  // cleanup remaining retainers
535
943
  for (; oi < oldLength; oi++) {
536
944
  const ret = oldRetained[oi];
537
945
  if (typeof ret === "object" &&
538
- (typeof ret.el.key === "undefined" ||
946
+ (typeof ret.el.props.key === "undefined" ||
539
947
  !seenKeys ||
540
- !seenKeys.has(ret.el.key))) {
948
+ !seenKeys.has(ret.el.props.key))) {
541
949
  (graveyard = graveyard || []).push(ret);
542
950
  }
543
951
  }
544
952
  if (childrenByKey !== undefined && childrenByKey.size > 0) {
545
- (graveyard = graveyard || []).push(...childrenByKey.values());
953
+ graveyard = graveyard || [];
954
+ for (const ret of childrenByKey.values()) {
955
+ graveyard.push(ret);
956
+ }
546
957
  }
547
958
  parent.children = unwrap(newRetained);
548
959
  if (isAsync) {
549
- let childValues1 = Promise.all(values).finally(() => {
960
+ const diffs1 = Promise.all(diffs)
961
+ .then(() => undefined)
962
+ .finally(() => {
963
+ setFlag(parent, DidDiff);
550
964
  if (graveyard) {
551
- for (let i = 0; i < graveyard.length; i++) {
552
- unmount(renderer, host, ctx, graveyard[i]);
965
+ if (parent.graveyard) {
966
+ for (let i = 0; i < graveyard.length; i++) {
967
+ parent.graveyard.push(graveyard[i]);
968
+ }
969
+ }
970
+ else {
971
+ parent.graveyard = graveyard;
553
972
  }
554
973
  }
555
974
  });
556
- let onChildValues;
557
- childValues1 = Promise.race([
558
- childValues1,
559
- new Promise((resolve) => (onChildValues = resolve)),
560
- ]);
561
- if (parent.onNextValues) {
562
- parent.onNextValues(childValues1);
563
- }
564
- parent.onNextValues = onChildValues;
565
- return childValues1.then((childValues) => {
566
- parent.inflightValue = parent.fallbackValue = undefined;
567
- return normalize(childValues);
568
- });
975
+ let onNextDiffs;
976
+ const diffs2 = (parent.pendingDiff = safeRace([
977
+ diffs1,
978
+ new Promise((resolve) => (onNextDiffs = resolve)),
979
+ ]));
980
+ if (parent.onNextDiff) {
981
+ parent.onNextDiff(diffs2);
982
+ }
983
+ parent.onNextDiff = onNextDiffs;
984
+ return diffs2;
569
985
  }
570
986
  else {
987
+ setFlag(parent, DidDiff);
571
988
  if (graveyard) {
572
- for (let i = 0; i < graveyard.length; i++) {
573
- unmount(renderer, host, ctx, graveyard[i]);
989
+ if (parent.graveyard) {
990
+ for (let i = 0; i < graveyard.length; i++) {
991
+ parent.graveyard.push(graveyard[i]);
992
+ }
993
+ }
994
+ else {
995
+ parent.graveyard = graveyard;
574
996
  }
575
997
  }
576
- if (parent.onNextValues) {
577
- parent.onNextValues(values);
578
- parent.onNextValues = undefined;
998
+ if (parent.onNextDiff) {
999
+ parent.onNextDiff(diffs);
1000
+ parent.onNextDiff = undefined;
579
1001
  }
580
- parent.inflightValue = parent.fallbackValue = undefined;
581
- // We can assert there are no promises in the array because isAsync is false
582
- return normalize(values);
1002
+ parent.pendingDiff = undefined;
1003
+ }
1004
+ }
1005
+ function getInflightDiff(ret) {
1006
+ // It is not enough to check pendingDiff because pendingDiff is the diff for
1007
+ // children, but not the diff of an async component retainer's current run.
1008
+ // For the latter we check ctx.inflight.
1009
+ if (ret.ctx && ret.ctx.inflight) {
1010
+ return ret.ctx.inflight[1];
1011
+ }
1012
+ else if (ret.pendingDiff) {
1013
+ return ret.pendingDiff;
583
1014
  }
584
1015
  }
585
1016
  function createChildrenByKey(children, offset) {
586
1017
  const childrenByKey = new Map();
587
1018
  for (let i = offset; i < children.length; i++) {
588
1019
  const child = children[i];
589
- if (typeof child === "object" && typeof child.el.key !== "undefined") {
590
- childrenByKey.set(child.el.key, child);
1020
+ if (typeof child === "object" &&
1021
+ typeof child.el.props.key !== "undefined") {
1022
+ childrenByKey.set(child.el.props.key, child);
591
1023
  }
592
1024
  }
593
1025
  return childrenByKey;
594
1026
  }
595
- function getInflightValue(child) {
596
- if (typeof child !== "object") {
597
- return child;
598
- }
599
- const ctx = typeof child.el.tag === "function" ? child.ctx : undefined;
600
- if (ctx && ctx.f & IsUpdating && ctx.inflightValue) {
601
- return ctx.inflightValue;
1027
+ function diffHost(adapter, root, ctx, scope, ret) {
1028
+ const el = ret.el;
1029
+ const tag = el.tag;
1030
+ if (el.tag === Portal) {
1031
+ root = ret.value = el.props.root;
602
1032
  }
603
- else if (child.inflightValue) {
604
- return child.inflightValue;
1033
+ if (getFlag(ret, DidCommit)) {
1034
+ scope = ret.scope;
605
1035
  }
606
- return getValue(child);
607
- }
608
- function updateRaw(renderer, ret, scope, oldProps, hydrationData) {
609
- const props = ret.el.props;
610
- if (!oldProps || oldProps.value !== props.value) {
611
- ret.value = renderer.raw(props.value, scope, hydrationData);
612
- if (typeof ret.el.ref === "function") {
613
- ret.el.ref(ret.value);
614
- }
1036
+ else {
1037
+ scope = ret.scope = adapter.scope({
1038
+ tag,
1039
+ tagName: getTagName(tag),
1040
+ props: el.props,
1041
+ scope,
1042
+ });
615
1043
  }
616
- return ret.value;
1044
+ return diffChildren(adapter, root, ret, ctx, scope, ret, ret.el.props.children);
617
1045
  }
618
- function updateFragment(renderer, root, host, ctx, scope, ret, hydrationData) {
619
- const childValues = diffChildren(renderer, root, host, ctx, scope, ret, ret.el.props.children, hydrationData);
620
- if (isPromiseLike(childValues)) {
621
- ret.inflightValue = childValues.then((childValues) => unwrap(childValues));
622
- return ret.inflightValue;
1046
+ function commit(adapter, host, ret, ctx, scope, index, schedulePromises, hydrationNodes) {
1047
+ if (getFlag(ret, IsCopied) && getFlag(ret, DidCommit)) {
1048
+ return getValue(ret);
623
1049
  }
624
- return unwrap(childValues);
625
- }
626
- function updateHost(renderer, root, ctx, scope, ret, oldProps, hydrationData) {
627
1050
  const el = ret.el;
628
1051
  const tag = el.tag;
629
- let hydrationValue;
630
- if (el.tag === Portal) {
631
- root = ret.value = el.props.root;
1052
+ if (typeof tag === "function" ||
1053
+ tag === Fragment ||
1054
+ tag === Portal ||
1055
+ tag === Raw ||
1056
+ tag === Text) {
1057
+ if (typeof el.props.copy === "string") {
1058
+ console.error(`String copy prop ignored for <${getTagName(tag)}>. Use booleans instead.`);
1059
+ }
1060
+ if (typeof el.props.hydrate === "string") {
1061
+ console.error(`String hydrate prop ignored for <${getTagName(tag)}>. Use booleans instead.`);
1062
+ }
1063
+ }
1064
+ let value;
1065
+ let skippedHydrationNodes;
1066
+ if (hydrationNodes &&
1067
+ el.props.hydrate != null &&
1068
+ !el.props.hydrate &&
1069
+ typeof el.props.hydrate !== "string") {
1070
+ skippedHydrationNodes = hydrationNodes;
1071
+ hydrationNodes = undefined;
1072
+ }
1073
+ if (typeof tag === "function") {
1074
+ ret.ctx.index = index;
1075
+ value = commitComponent(ret.ctx, schedulePromises, hydrationNodes);
632
1076
  }
633
1077
  else {
634
- if (hydrationData !== undefined) {
635
- const value = hydrationData.children.shift();
636
- hydrationValue = value;
1078
+ if (tag === Fragment) {
1079
+ value = commitChildren(adapter, host, ctx, scope, ret, index, schedulePromises, hydrationNodes);
1080
+ }
1081
+ else if (tag === Text) {
1082
+ value = commitText(adapter, ret, el, scope, hydrationNodes);
1083
+ }
1084
+ else if (tag === Raw) {
1085
+ value = commitRaw(adapter, host, ret, scope, hydrationNodes);
1086
+ }
1087
+ else {
1088
+ value = commitHost(adapter, ret, ctx, schedulePromises, hydrationNodes);
1089
+ }
1090
+ if (ret.fallback) {
1091
+ unmount(adapter, host, ctx, ret.fallback, false);
1092
+ ret.fallback = undefined;
1093
+ }
1094
+ }
1095
+ if (skippedHydrationNodes) {
1096
+ skippedHydrationNodes.splice(0, wrap(value).length);
1097
+ }
1098
+ if (!getFlag(ret, DidCommit)) {
1099
+ setFlag(ret, DidCommit);
1100
+ if (typeof tag !== "function" &&
1101
+ tag !== Fragment &&
1102
+ tag !== Portal &&
1103
+ typeof el.props.ref === "function") {
1104
+ el.props.ref(adapter.read(value));
1105
+ }
1106
+ }
1107
+ return value;
1108
+ }
1109
+ function commitChildren(adapter, host, ctx, scope, parent, index, schedulePromises, hydrationNodes) {
1110
+ let values = [];
1111
+ for (let i = 0, children = wrap(parent.children); i < children.length; i++) {
1112
+ let child = children[i];
1113
+ let schedulePromises1;
1114
+ let isSchedulingFallback = false;
1115
+ while (child &&
1116
+ ((!getFlag(child, DidDiff) && child.fallback) ||
1117
+ getFlag(child, IsScheduling))) {
1118
+ // If the child is scheduling, it is a component retainer so ctx will be
1119
+ // defined.
1120
+ if (getFlag(child, IsScheduling) && child.ctx.schedule) {
1121
+ (schedulePromises1 = schedulePromises1 || []).push(child.ctx.schedule.promise);
1122
+ isSchedulingFallback = true;
1123
+ }
1124
+ if (!getFlag(child, DidDiff) && getFlag(child, DidCommit)) {
1125
+ // If this child has not diffed but has committed, it means it is a
1126
+ // fallback that is being resurrected.
1127
+ for (const node of getChildValues(child)) {
1128
+ adapter.remove({
1129
+ node,
1130
+ parentNode: host.value,
1131
+ isNested: false,
1132
+ });
1133
+ }
1134
+ }
1135
+ child = child.fallback;
1136
+ // When a scheduling component is mounting asynchronously but diffs
1137
+ // immediately, it will cause previous async diffs to settle due to the
1138
+ // chasing mechanism. This would cause earlier renders to resolve sooner
1139
+ // than expected, because the render would be missing both its usual
1140
+ // children and the children of the scheduling render. Therefore, we need
1141
+ // to defer the settling of previous renders until either that render
1142
+ // settles, or the scheduling component finally finishes scheduling.
1143
+ //
1144
+ // To do this, we take advantage of the fact that commits for aborted
1145
+ // renders will still fire and walk the tree. During that commit walk,
1146
+ // when we encounter a scheduling element, we push a race of the
1147
+ // scheduling promise with the inflight diff of the async fallback
1148
+ // fallback to schedulePromises to delay the initiator.
1149
+ //
1150
+ // However, we need to make sure we only use the inflight diffs for the
1151
+ // fallback which we are trying to delay, in the case of multiple renders
1152
+ // and fallbacks. To do this, we take advantage of the fact that when
1153
+ // multiple renders race (e.g., render1->render2->render3->scheduling
1154
+ // component), the chasing mechanism will call stale commits in reverse
1155
+ // order.
1156
+ //
1157
+ // We can use this ordering to delay to find which fallbacks we need to
1158
+ // add to the race. Each commit call progressively marks an additional
1159
+ // fallback as a scheduling fallback, and does not contribute to the
1160
+ // scheduling promises if it is further than the last seen level.
1161
+ //
1162
+ // This prevents promise contamination where newer renders settle early
1163
+ // due to diffs from older renders.
1164
+ if (schedulePromises1 && isSchedulingFallback && child) {
1165
+ if (!getFlag(child, DidDiff)) {
1166
+ const inflightDiff = getInflightDiff(child);
1167
+ schedulePromises1.push(inflightDiff);
1168
+ }
1169
+ else {
1170
+ // If a scheduling component's fallback has already diffed, we do not
1171
+ // need delay the render.
1172
+ schedulePromises1 = undefined;
1173
+ }
1174
+ if (getFlag(child, IsSchedulingFallback)) {
1175
+ // This fallback was marked by a more recent commit - keep processing
1176
+ // deeper levels
1177
+ isSchedulingFallback = true;
1178
+ }
1179
+ else {
1180
+ // First unmarked fallback we've encountered - mark it and stop
1181
+ // contributing to schedulePromises1 for deeper levels.
1182
+ setFlag(child, IsSchedulingFallback, true);
1183
+ isSchedulingFallback = false;
1184
+ }
1185
+ }
1186
+ }
1187
+ if (schedulePromises1 && schedulePromises1.length > 1) {
1188
+ schedulePromises.push(safeRace(schedulePromises1));
1189
+ }
1190
+ if (child) {
1191
+ const value = commit(adapter, host, child, ctx, scope, index, schedulePromises, hydrationNodes);
1192
+ if (Array.isArray(value)) {
1193
+ for (let j = 0; j < value.length; j++) {
1194
+ values.push(value[j]);
1195
+ }
1196
+ index += value.length;
1197
+ }
1198
+ else if (value) {
1199
+ values.push(value);
1200
+ index++;
1201
+ }
637
1202
  }
638
1203
  }
639
- scope = renderer.scope(scope, tag, el.props);
640
- let childHydrationData;
641
- if (hydrationValue != null && typeof hydrationValue !== "string") {
642
- childHydrationData = renderer.hydrate(tag, hydrationValue, el.props);
643
- if (childHydrationData === undefined) {
644
- hydrationValue = undefined;
1204
+ if (parent.graveyard) {
1205
+ for (let i = 0; i < parent.graveyard.length; i++) {
1206
+ const child = parent.graveyard[i];
1207
+ unmount(adapter, host, ctx, child, false);
645
1208
  }
1209
+ parent.graveyard = undefined;
646
1210
  }
647
- const childValues = diffChildren(renderer, root, ret, ctx, scope, ret, ret.el.props.children, childHydrationData);
648
- if (isPromiseLike(childValues)) {
649
- ret.inflightValue = childValues.then((childValues) => commitHost(renderer, scope, ret, childValues, oldProps, hydrationValue));
650
- return ret.inflightValue;
1211
+ if (parent.lingerers) {
1212
+ // if parent.lingerers is set, a descendant component is unmounting
1213
+ // asynchronously, so we overwrite values to include lingerering DOM nodes.
1214
+ values = getChildValues(parent);
1215
+ }
1216
+ return values;
1217
+ }
1218
+ function commitText(adapter, ret, el, scope, hydrationNodes) {
1219
+ const value = adapter.text({
1220
+ value: el.props.value,
1221
+ scope,
1222
+ oldNode: ret.value,
1223
+ hydrationNodes,
1224
+ });
1225
+ ret.value = value;
1226
+ return value;
1227
+ }
1228
+ function commitRaw(adapter, host, ret, scope, hydrationNodes) {
1229
+ if (!ret.oldProps || ret.oldProps.value !== ret.el.props.value) {
1230
+ const oldNodes = wrap(ret.value);
1231
+ for (let i = 0; i < oldNodes.length; i++) {
1232
+ const oldNode = oldNodes[i];
1233
+ adapter.remove({
1234
+ node: oldNode,
1235
+ parentNode: host.value,
1236
+ isNested: false,
1237
+ });
1238
+ }
1239
+ ret.value = adapter.raw({
1240
+ value: ret.el.props.value,
1241
+ scope,
1242
+ hydrationNodes,
1243
+ });
651
1244
  }
652
- return commitHost(renderer, scope, ret, childValues, oldProps, hydrationValue);
1245
+ ret.oldProps = stripSpecialProps(ret.el.props);
1246
+ return ret.value;
653
1247
  }
654
- function commitHost(renderer, scope, ret, childValues, oldProps, hydrationValue) {
1248
+ function commitHost(adapter, ret, ctx, schedulePromises, hydrationNodes) {
1249
+ if (getFlag(ret, IsCopied) && getFlag(ret, DidCommit)) {
1250
+ return getValue(ret);
1251
+ }
655
1252
  const tag = ret.el.tag;
656
- let value = ret.value;
657
- if (hydrationValue != null) {
658
- value = ret.value = hydrationValue;
659
- if (typeof ret.el.ref === "function") {
660
- ret.el.ref(value);
1253
+ const props = stripSpecialProps(ret.el.props);
1254
+ const oldProps = ret.oldProps;
1255
+ let node = ret.value;
1256
+ let copyProps;
1257
+ let copyChildren = false;
1258
+ if (oldProps) {
1259
+ for (const propName in props) {
1260
+ if (props[propName] === Copy) {
1261
+ // The Copy tag can be used to skip the patching of a prop.
1262
+ // <div class={shouldPatchClass ? "class-name" : Copy} />
1263
+ props[propName] = oldProps[propName];
1264
+ (copyProps = copyProps || new Set()).add(propName);
1265
+ }
1266
+ }
1267
+ if (typeof ret.el.props.copy === "string") {
1268
+ const copyMetaProp = new MetaProp("copy", ret.el.props.copy);
1269
+ if (copyMetaProp.include) {
1270
+ for (const propName of copyMetaProp.props) {
1271
+ if (propName in oldProps) {
1272
+ props[propName] = oldProps[propName];
1273
+ (copyProps = copyProps || new Set()).add(propName);
1274
+ }
1275
+ }
1276
+ }
1277
+ else {
1278
+ for (const propName in oldProps) {
1279
+ if (!copyMetaProp.props.has(propName)) {
1280
+ props[propName] = oldProps[propName];
1281
+ (copyProps = copyProps || new Set()).add(propName);
1282
+ }
1283
+ }
1284
+ }
1285
+ copyChildren = copyMetaProp.includes("children");
661
1286
  }
662
1287
  }
663
- let props = ret.el.props;
664
- let copied;
665
- if (tag !== Portal) {
666
- if (value == null) {
667
- // This assumes that renderer.create does not return nullish values.
668
- value = ret.value = renderer.create(tag, props, scope);
669
- if (typeof ret.el.ref === "function") {
670
- ret.el.ref(value);
1288
+ const scope = ret.scope;
1289
+ let childHydrationNodes;
1290
+ let quietProps;
1291
+ let hydrationMetaProp;
1292
+ if (!getFlag(ret, DidCommit)) {
1293
+ if (tag === Portal) {
1294
+ if (ret.el.props.hydrate && typeof ret.el.props.hydrate !== "string") {
1295
+ childHydrationNodes = adapter.adopt({
1296
+ tag,
1297
+ tagName: getTagName(tag),
1298
+ node,
1299
+ props,
1300
+ scope,
1301
+ });
1302
+ if (childHydrationNodes) {
1303
+ for (let i = 0; i < childHydrationNodes.length; i++) {
1304
+ adapter.remove({
1305
+ node: childHydrationNodes[i],
1306
+ parentNode: node,
1307
+ isNested: false,
1308
+ });
1309
+ }
1310
+ }
671
1311
  }
672
1312
  }
673
- for (const propName in { ...oldProps, ...props }) {
674
- const propValue = props[propName];
675
- if (propValue === Copy) {
676
- // TODO: The Copy tag doubles as a way to skip the patching of a prop.
677
- // Not sure about this feature. Should probably be removed.
678
- (copied = copied || new Set()).add(propName);
1313
+ else {
1314
+ if (!node && hydrationNodes) {
1315
+ const nextChild = hydrationNodes.shift();
1316
+ if (typeof ret.el.props.hydrate === "string") {
1317
+ hydrationMetaProp = new MetaProp("hydration", ret.el.props.hydrate);
1318
+ if (hydrationMetaProp.include) {
1319
+ // if we're in inclusive mode, we add all props to quietProps and
1320
+ // remove props specified in the metaprop
1321
+ quietProps = new Set(Object.keys(props));
1322
+ for (const propName of hydrationMetaProp.props) {
1323
+ quietProps.delete(propName);
1324
+ }
1325
+ }
1326
+ else {
1327
+ quietProps = hydrationMetaProp.props;
1328
+ }
1329
+ }
1330
+ childHydrationNodes = adapter.adopt({
1331
+ tag,
1332
+ tagName: getTagName(tag),
1333
+ node: nextChild,
1334
+ props,
1335
+ scope,
1336
+ });
1337
+ if (childHydrationNodes) {
1338
+ node = nextChild;
1339
+ for (let i = 0; i < childHydrationNodes.length; i++) {
1340
+ adapter.remove({
1341
+ node: childHydrationNodes[i],
1342
+ parentNode: node,
1343
+ isNested: false,
1344
+ });
1345
+ }
1346
+ }
679
1347
  }
680
- else if (!SPECIAL_PROPS.has(propName)) {
681
- renderer.patch(tag, value, propName, propValue, oldProps && oldProps[propName], scope);
1348
+ // TODO: For some reason, there are cases where the node is already set
1349
+ // and the DidCommit flag is false. Not checking for node fails a test
1350
+ // where a child dispatches an event in a schedule callback, the parent
1351
+ // listens for this event and refreshes.
1352
+ if (!node) {
1353
+ node = adapter.create({
1354
+ tag,
1355
+ tagName: getTagName(tag),
1356
+ props,
1357
+ scope,
1358
+ });
682
1359
  }
1360
+ ret.value = node;
683
1361
  }
684
1362
  }
685
- if (copied) {
686
- props = { ...ret.el.props };
687
- for (const name of copied) {
688
- props[name] = oldProps && oldProps[name];
689
- }
690
- ret.el = new Element(tag, props);
1363
+ if (tag !== Portal) {
1364
+ adapter.patch({
1365
+ tag,
1366
+ tagName: getTagName(tag),
1367
+ node,
1368
+ props,
1369
+ oldProps,
1370
+ scope,
1371
+ copyProps,
1372
+ isHydrating: !!childHydrationNodes,
1373
+ quietProps,
1374
+ });
691
1375
  }
692
- renderer.arrange(tag, value, props, childValues, oldProps, wrap(ret.cachedChildValues));
693
- ret.cachedChildValues = unwrap(childValues);
1376
+ if (!copyChildren) {
1377
+ const children = commitChildren(adapter, ret, ctx, scope, ret, 0, schedulePromises, hydrationMetaProp && !hydrationMetaProp.includes("children")
1378
+ ? undefined
1379
+ : childHydrationNodes);
1380
+ adapter.arrange({
1381
+ tag,
1382
+ tagName: getTagName(tag),
1383
+ node: node,
1384
+ props,
1385
+ children,
1386
+ oldProps,
1387
+ });
1388
+ }
1389
+ ret.oldProps = props;
694
1390
  if (tag === Portal) {
695
- flush(renderer, ret.value);
1391
+ flush(adapter, ret.value);
1392
+ // The root passed to Portal elements are opaque to parents so we return
1393
+ // undefined here.
696
1394
  return;
697
1395
  }
698
- return value;
1396
+ return node;
699
1397
  }
700
- function flush(renderer, root, initiator) {
701
- renderer.flush(root);
702
- if (typeof root !== "object" || root === null) {
703
- return;
704
- }
705
- const flushMap = flushMaps.get(root);
706
- if (flushMap) {
707
- if (initiator) {
708
- const flushMap1 = new Map();
709
- for (let [ctx, callbacks] of flushMap) {
710
- if (!ctxContains(initiator, ctx)) {
711
- flushMap.delete(ctx);
712
- flushMap1.set(ctx, callbacks);
713
- }
1398
+ class MetaProp {
1399
+ constructor(propName, propValue) {
1400
+ this.include = true;
1401
+ this.props = new Set();
1402
+ let noBangs = true;
1403
+ let allBangs = true;
1404
+ const tokens = propValue.split(/[,\s]+/);
1405
+ for (let i = 0; i < tokens.length; i++) {
1406
+ const token = tokens[i].trim();
1407
+ if (!token) {
1408
+ continue;
714
1409
  }
715
- if (flushMap1.size) {
716
- flushMaps.set(root, flushMap1);
1410
+ else if (token.startsWith("!")) {
1411
+ noBangs = false;
1412
+ this.props.add(token.slice(1));
717
1413
  }
718
1414
  else {
719
- flushMaps.delete(root);
1415
+ allBangs = false;
1416
+ this.props.add(token);
720
1417
  }
721
1418
  }
1419
+ if (!allBangs && !noBangs) {
1420
+ console.error(`Invalid ${propName} prop "${propValue}".\nUse prop or !prop but not both.`);
1421
+ this.include = true;
1422
+ this.props.clear();
1423
+ }
1424
+ else {
1425
+ this.include = noBangs;
1426
+ }
1427
+ }
1428
+ includes(propName) {
1429
+ if (this.include) {
1430
+ return this.props.has(propName);
1431
+ }
1432
+ else {
1433
+ return !this.props.has(propName);
1434
+ }
1435
+ }
1436
+ }
1437
+ function contextContains(parent, child) {
1438
+ for (let current = child; current !== undefined; current = current.parent) {
1439
+ if (current === parent) {
1440
+ return true;
1441
+ }
1442
+ }
1443
+ return false;
1444
+ }
1445
+ // When rendering is done without a root, we use this special anonymous root to
1446
+ // make sure after callbacks are still called.
1447
+ const ANONYMOUS_ROOT = {};
1448
+ function flush(adapter, root, initiator) {
1449
+ if (root != null) {
1450
+ adapter.finalize(root);
1451
+ }
1452
+ if (typeof root !== "object" || root === null) {
1453
+ root = ANONYMOUS_ROOT;
1454
+ }
1455
+ // The initiator is the context which initiated the rendering process. If
1456
+ // initiator is defined we call and clear all flush callbacks which are
1457
+ // registered with the initiator or with a child context of the initiator,
1458
+ // because they are fully rendered.
1459
+ //
1460
+ // If no initiator is provided, we can call and clear all flush callbacks
1461
+ // which are not scheduling.
1462
+ const afterMap = afterMapByRoot.get(root);
1463
+ if (afterMap) {
1464
+ const afterMap1 = new Map();
1465
+ for (const [ctx, callbacks] of afterMap) {
1466
+ if (getFlag(ctx.ret, IsScheduling) ||
1467
+ (initiator && !contextContains(initiator, ctx))) {
1468
+ // copy over callbacks to the new map (defer them)
1469
+ afterMap.delete(ctx);
1470
+ afterMap1.set(ctx, callbacks);
1471
+ }
1472
+ }
1473
+ if (afterMap1.size) {
1474
+ afterMapByRoot.set(root, afterMap1);
1475
+ }
722
1476
  else {
723
- flushMaps.delete(root);
1477
+ afterMapByRoot.delete(root);
724
1478
  }
725
- for (const [ctx, callbacks] of flushMap) {
726
- const value = renderer.read(getValue(ctx.ret));
1479
+ for (const [ctx, callbacks] of afterMap) {
1480
+ const value = adapter.read(getValue(ctx.ret));
727
1481
  for (const callback of callbacks) {
728
1482
  callback(value);
729
1483
  }
730
1484
  }
731
1485
  }
732
1486
  }
733
- function unmount(renderer, host, ctx, ret) {
1487
+ function unmount(adapter, host, ctx, ret, isNested) {
1488
+ // TODO: set the IsUnmounted flag consistently for all retainers
1489
+ if (ret.fallback) {
1490
+ unmount(adapter, host, ctx, ret.fallback, isNested);
1491
+ ret.fallback = undefined;
1492
+ }
1493
+ if (getFlag(ret, IsResurrecting)) {
1494
+ return;
1495
+ }
1496
+ if (ret.lingerers) {
1497
+ for (let i = 0; i < ret.lingerers.length; i++) {
1498
+ const lingerers = ret.lingerers[i];
1499
+ if (lingerers) {
1500
+ for (const lingerer of lingerers) {
1501
+ unmount(adapter, host, ctx, lingerer, isNested);
1502
+ }
1503
+ }
1504
+ }
1505
+ ret.lingerers = undefined;
1506
+ }
734
1507
  if (typeof ret.el.tag === "function") {
735
- ctx = ret.ctx;
736
- unmountComponent(ctx);
1508
+ unmountComponent(ret.ctx, isNested);
1509
+ }
1510
+ else if (ret.el.tag === Fragment) {
1511
+ unmountChildren(adapter, host, ctx, ret, isNested);
737
1512
  }
738
1513
  else if (ret.el.tag === Portal) {
739
- host = ret;
740
- renderer.arrange(Portal, host.value, host.el.props, [], host.el.props, wrap(host.cachedChildValues));
741
- flush(renderer, host.value);
1514
+ unmountChildren(adapter, ret, ctx, ret, false);
1515
+ if (ret.value != null) {
1516
+ adapter.finalize(ret.value);
1517
+ }
742
1518
  }
743
- else if (ret.el.tag !== Fragment) {
744
- if (isEventTarget(ret.value)) {
745
- const records = getListenerRecords(ctx, host);
746
- for (let i = 0; i < records.length; i++) {
747
- const record = records[i];
748
- ret.value.removeEventListener(record.type, record.callback, record.options);
749
- }
1519
+ else {
1520
+ unmountChildren(adapter, ret, ctx, ret, true);
1521
+ if (getFlag(ret, DidCommit)) {
1522
+ if (ctx) {
1523
+ // Remove the value from every context which shares the same host.
1524
+ removeEventTargetDelegates(ctx.ctx, [ret.value], (ctx1) => ctx1[_ContextState].host === host);
1525
+ }
1526
+ adapter.remove({
1527
+ node: ret.value,
1528
+ parentNode: host.value,
1529
+ isNested,
1530
+ });
750
1531
  }
751
- renderer.dispose(ret.el.tag, ret.value, ret.el.props);
752
- host = ret;
753
1532
  }
754
- const children = wrap(ret.children);
755
- for (let i = 0; i < children.length; i++) {
1533
+ }
1534
+ function unmountChildren(adapter, host, ctx, ret, isNested) {
1535
+ if (ret.graveyard) {
1536
+ for (let i = 0; i < ret.graveyard.length; i++) {
1537
+ const child = ret.graveyard[i];
1538
+ unmount(adapter, host, ctx, child, isNested);
1539
+ }
1540
+ ret.graveyard = undefined;
1541
+ }
1542
+ for (let i = 0, children = wrap(ret.children); i < children.length; i++) {
756
1543
  const child = children[i];
757
1544
  if (typeof child === "object") {
758
- unmount(renderer, host, ctx, child);
1545
+ unmount(adapter, host, ctx, child, isNested);
759
1546
  }
760
1547
  }
761
1548
  }
762
- /*** CONTEXT FLAGS ***/
763
- /**
764
- * A flag which is true when the component is initialized or updated by an
765
- * ancestor component or the root render call.
766
- *
767
- * Used to determine things like whether the nearest host ancestor needs to be
768
- * rearranged.
769
- */
770
- const IsUpdating = 1 << 0;
771
- /**
772
- * A flag which is true when the component is synchronously executing.
773
- *
774
- * Used to guard against components triggering stack overflow or generator error.
775
- */
776
- const IsSyncExecuting = 1 << 1;
777
- /**
778
- * A flag which is true when the component is in a for...of loop.
779
- */
780
- const IsInForOfLoop = 1 << 2;
781
- /**
782
- * A flag which is true when the component is in a for await...of loop.
783
- */
784
- const IsInForAwaitOfLoop = 1 << 3;
785
- /**
786
- * A flag which is true when the component starts the render loop but has not
787
- * yielded yet.
788
- *
789
- * Used to make sure that components yield at least once per loop.
790
- */
791
- const NeedsToYield = 1 << 4;
792
- /**
793
- * A flag used by async generator components in conjunction with the
794
- * onAvailable callback to mark whether new props can be pulled via the context
795
- * async iterator. See the Symbol.asyncIterator method and the
796
- * resumeCtxIterator function.
797
- */
798
- const PropsAvailable = 1 << 5;
799
- /**
800
- * A flag which is set when a component errors.
801
- *
802
- * This is mainly used to prevent some false positives in "component yields or
803
- * returns undefined" warnings. The reason we’re using this versus IsUnmounted
804
- * is a very troubling test (cascades sync generator parent and sync generator
805
- * child) where synchronous code causes a stack overflow error in a
806
- * non-deterministic way. Deeply disturbing stuff.
807
- */
808
- const IsErrored = 1 << 6;
809
- /**
810
- * A flag which is set when the component is unmounted. Unmounted components
811
- * are no longer in the element tree and cannot refresh or rerender.
812
- */
813
- const IsUnmounted = 1 << 7;
814
- /**
815
- * A flag which indicates that the component is a sync generator component.
816
- */
817
- const IsSyncGen = 1 << 8;
818
- /**
819
- * A flag which indicates that the component is an async generator component.
820
- */
821
- const IsAsyncGen = 1 << 9;
822
- /**
823
- * A flag which is set while schedule callbacks are called.
824
- */
825
- const IsScheduling = 1 << 10;
826
- /**
827
- * A flag which is set when a schedule callback calls refresh.
828
- */
829
- const IsSchedulingRefresh = 1 << 11;
830
1549
  const provisionMaps = new WeakMap();
831
1550
  const scheduleMap = new WeakMap();
832
1551
  const cleanupMap = new WeakMap();
833
1552
  // keys are roots
834
- const flushMaps = new WeakMap();
1553
+ const afterMapByRoot = new WeakMap();
1554
+ // TODO: allow ContextState to be initialized for testing purposes
835
1555
  /**
836
1556
  * @internal
837
1557
  * The internal class which holds context data.
838
1558
  */
839
- class ContextImpl {
840
- constructor(renderer, root, host, parent, scope, ret) {
841
- this.f = 0;
842
- this.owner = new Context(this);
843
- this.renderer = renderer;
1559
+ class ContextState {
1560
+ constructor(adapter, root, host, parent, scope, ret) {
1561
+ this.adapter = adapter;
844
1562
  this.root = root;
845
1563
  this.host = host;
846
1564
  this.parent = parent;
1565
+ // This property must be set after this.parent is set because the Context
1566
+ // constructor reads this.parent.
1567
+ this.ctx = new Context(this);
847
1568
  this.scope = scope;
848
1569
  this.ret = ret;
849
1570
  this.iterator = undefined;
850
- this.inflightBlock = undefined;
851
- this.inflightValue = undefined;
852
- this.enqueuedBlock = undefined;
853
- this.enqueuedValue = undefined;
854
- this.onProps = undefined;
1571
+ this.inflight = undefined;
1572
+ this.enqueued = undefined;
1573
+ this.onPropsProvided = undefined;
855
1574
  this.onPropsRequested = undefined;
1575
+ this.pull = undefined;
1576
+ this.index = 0;
1577
+ this.schedule = undefined;
856
1578
  }
857
1579
  }
858
- const _ContextImpl = Symbol.for("crank.ContextImpl");
1580
+ const _ContextState = Symbol.for("crank.ContextState");
859
1581
  /**
860
1582
  * A class which is instantiated and passed to every component as its this
861
- * value. Contexts form a tree just like elements and all components in the
862
- * element tree are connected via contexts. Components can use this tree to
863
- * communicate data upwards via events and downwards via provisions.
1583
+ * value/second parameter. Contexts form a tree just like elements and all
1584
+ * components in the element tree are connected via contexts. Components can
1585
+ * use this tree to communicate data upwards via events and downwards via
1586
+ * provisions.
864
1587
  *
865
1588
  * @template [T=*] - The expected shape of the props passed to the component,
866
1589
  * or a component function. Used to strongly type the Context iterator methods.
@@ -868,17 +1591,18 @@
868
1591
  * places such as the return value of refresh and the argument passed to
869
1592
  * schedule and cleanup callbacks.
870
1593
  */
871
- class Context {
1594
+ class Context extends CustomEventTarget {
872
1595
  // TODO: If we could make the constructor function take a nicer value, it
873
1596
  // would be useful for testing purposes.
874
- constructor(impl) {
875
- this[_ContextImpl] = impl;
1597
+ constructor(state) {
1598
+ super(state.parent ? state.parent.ctx : null);
1599
+ this[_ContextState] = state;
876
1600
  }
877
1601
  /**
878
1602
  * The current props of the associated element.
879
1603
  */
880
1604
  get props() {
881
- return this[_ContextImpl].ret.el.props;
1605
+ return this[_ContextState].ret.el.props;
882
1606
  }
883
1607
  /**
884
1608
  * The current value of the associated element.
@@ -886,47 +1610,51 @@
886
1610
  * @deprecated
887
1611
  */
888
1612
  get value() {
889
- return this[_ContextImpl].renderer.read(getValue(this[_ContextImpl].ret));
1613
+ console.warn("Context.value is deprecated.");
1614
+ return this[_ContextState].adapter.read(getValue(this[_ContextState].ret));
1615
+ }
1616
+ get isExecuting() {
1617
+ return getFlag(this[_ContextState].ret, IsExecuting);
1618
+ }
1619
+ get isUnmounted() {
1620
+ return getFlag(this[_ContextState].ret, IsUnmounted);
890
1621
  }
891
1622
  *[Symbol.iterator]() {
892
- const ctx = this[_ContextImpl];
1623
+ const ctx = this[_ContextState];
1624
+ setFlag(ctx.ret, IsInForOfLoop);
893
1625
  try {
894
- ctx.f |= IsInForOfLoop;
895
- while (!(ctx.f & IsUnmounted)) {
896
- if (ctx.f & NeedsToYield) {
897
- throw new Error("Context iterated twice without a yield");
1626
+ while (!getFlag(ctx.ret, IsUnmounted) && !getFlag(ctx.ret, IsErrored)) {
1627
+ if (getFlag(ctx.ret, NeedsToYield)) {
1628
+ throw new Error(`<${getTagName(ctx.ret.el.tag)}> context iterated twice without a yield`);
898
1629
  }
899
1630
  else {
900
- ctx.f |= NeedsToYield;
1631
+ setFlag(ctx.ret, NeedsToYield);
901
1632
  }
902
1633
  yield ctx.ret.el.props;
903
1634
  }
904
1635
  }
905
1636
  finally {
906
- ctx.f &= -5;
1637
+ setFlag(ctx.ret, IsInForOfLoop, false);
907
1638
  }
908
1639
  }
909
1640
  async *[Symbol.asyncIterator]() {
910
- const ctx = this[_ContextImpl];
911
- if (ctx.f & IsSyncGen) {
912
- throw new Error("Use for...of in sync generator components");
913
- }
1641
+ const ctx = this[_ContextState];
1642
+ setFlag(ctx.ret, IsInForAwaitOfLoop);
914
1643
  try {
915
- ctx.f |= IsInForAwaitOfLoop;
916
- while (!(ctx.f & IsUnmounted)) {
917
- if (ctx.f & NeedsToYield) {
918
- throw new Error("Context iterated twice without a yield");
1644
+ while (!getFlag(ctx.ret, IsUnmounted) && !getFlag(ctx.ret, IsErrored)) {
1645
+ if (getFlag(ctx.ret, NeedsToYield)) {
1646
+ throw new Error(`<${getTagName(ctx.ret.el.tag)}> context iterated twice without a yield`);
919
1647
  }
920
1648
  else {
921
- ctx.f |= NeedsToYield;
1649
+ setFlag(ctx.ret, NeedsToYield);
922
1650
  }
923
- if (ctx.f & PropsAvailable) {
924
- ctx.f &= ~PropsAvailable;
1651
+ if (getFlag(ctx.ret, PropsAvailable)) {
1652
+ setFlag(ctx.ret, PropsAvailable, false);
925
1653
  yield ctx.ret.el.props;
926
1654
  }
927
1655
  else {
928
- const props = await new Promise((resolve) => (ctx.onProps = resolve));
929
- if (ctx.f & IsUnmounted) {
1656
+ const props = await new Promise((resolve) => (ctx.onPropsProvided = resolve));
1657
+ if (getFlag(ctx.ret, IsUnmounted) || getFlag(ctx.ret, IsErrored)) {
930
1658
  break;
931
1659
  }
932
1660
  yield props;
@@ -938,7 +1666,7 @@
938
1666
  }
939
1667
  }
940
1668
  finally {
941
- ctx.f &= -9;
1669
+ setFlag(ctx.ret, IsInForAwaitOfLoop, false);
942
1670
  if (ctx.onPropsRequested) {
943
1671
  ctx.onPropsRequested();
944
1672
  ctx.onPropsRequested = undefined;
@@ -948,37 +1676,108 @@
948
1676
  /**
949
1677
  * Re-executes a component.
950
1678
  *
951
- * @returns The rendered value of the component or a promise thereof if the
1679
+ * @param callback - Optional callback to execute before refresh
1680
+ * @returns The rendered result of the component or a promise thereof if the
952
1681
  * component or its children execute asynchronously.
953
- *
954
- * The refresh method works a little differently for async generator
955
- * components, in that it will resume the Context’s props async iterator
956
- * rather than resuming execution. This is because async generator components
957
- * are perpetually resumed independent of updates, and rely on the props
958
- * async iterator to suspend.
959
1682
  */
960
- refresh() {
961
- const ctx = this[_ContextImpl];
962
- if (ctx.f & IsUnmounted) {
963
- console.error("Component is unmounted");
964
- return ctx.renderer.read(undefined);
1683
+ refresh(callback) {
1684
+ const ctx = this[_ContextState];
1685
+ if (getFlag(ctx.ret, IsUnmounted)) {
1686
+ console.error(`Component <${getTagName(ctx.ret.el.tag)}> is unmounted. Check the isUnmounted property if necessary.`);
1687
+ return ctx.adapter.read(getValue(ctx.ret));
1688
+ }
1689
+ else if (getFlag(ctx.ret, IsExecuting)) {
1690
+ console.error(`Component <${getTagName(ctx.ret.el.tag)}> is already executing Check the isExecuting property if necessary.`);
1691
+ return ctx.adapter.read(getValue(ctx.ret));
1692
+ }
1693
+ if (callback) {
1694
+ const result = callback();
1695
+ if (isPromiseLike(result)) {
1696
+ return Promise.resolve(result).then(() => {
1697
+ if (!getFlag(ctx.ret, IsUnmounted)) {
1698
+ return this.refresh();
1699
+ }
1700
+ return ctx.adapter.read(getValue(ctx.ret));
1701
+ });
1702
+ }
1703
+ }
1704
+ let diff;
1705
+ const schedulePromises = [];
1706
+ try {
1707
+ setFlag(ctx.ret, IsRefreshing);
1708
+ diff = enqueueComponent(ctx);
1709
+ if (isPromiseLike(diff)) {
1710
+ return diff
1711
+ .then(() => ctx.adapter.read(commitComponent(ctx, schedulePromises)))
1712
+ .then((result) => {
1713
+ if (schedulePromises.length) {
1714
+ return Promise.all(schedulePromises).then(() => {
1715
+ return ctx.adapter.read(getValue(ctx.ret));
1716
+ });
1717
+ }
1718
+ return result;
1719
+ })
1720
+ .catch((err) => {
1721
+ const diff = propagateError(ctx, err, schedulePromises);
1722
+ if (diff) {
1723
+ return diff.then(() => {
1724
+ if (schedulePromises.length) {
1725
+ return Promise.all(schedulePromises).then(() => {
1726
+ return ctx.adapter.read(getValue(ctx.ret));
1727
+ });
1728
+ }
1729
+ return ctx.adapter.read(getValue(ctx.ret));
1730
+ });
1731
+ }
1732
+ if (schedulePromises.length) {
1733
+ return Promise.all(schedulePromises).then(() => {
1734
+ return ctx.adapter.read(getValue(ctx.ret));
1735
+ });
1736
+ }
1737
+ return ctx.adapter.read(getValue(ctx.ret));
1738
+ })
1739
+ .finally(() => setFlag(ctx.ret, IsRefreshing, false));
1740
+ }
1741
+ const result = ctx.adapter.read(commitComponent(ctx, schedulePromises));
1742
+ if (schedulePromises.length) {
1743
+ return Promise.all(schedulePromises).then(() => {
1744
+ return ctx.adapter.read(getValue(ctx.ret));
1745
+ });
1746
+ }
1747
+ return result;
965
1748
  }
966
- else if (ctx.f & IsSyncExecuting) {
967
- console.error("Component is already executing");
968
- return ctx.renderer.read(getValue(ctx.ret));
1749
+ catch (err) {
1750
+ // TODO: await schedulePromises
1751
+ const diff = propagateError(ctx, err, schedulePromises);
1752
+ if (diff) {
1753
+ return diff
1754
+ .then(() => {
1755
+ if (schedulePromises.length) {
1756
+ return Promise.all(schedulePromises).then(() => {
1757
+ return ctx.adapter.read(getValue(ctx.ret));
1758
+ });
1759
+ }
1760
+ })
1761
+ .then(() => ctx.adapter.read(getValue(ctx.ret)));
1762
+ }
1763
+ if (schedulePromises.length) {
1764
+ return Promise.all(schedulePromises).then(() => {
1765
+ return ctx.adapter.read(getValue(ctx.ret));
1766
+ });
1767
+ }
1768
+ return ctx.adapter.read(getValue(ctx.ret));
969
1769
  }
970
- const value = enqueueComponentRun(ctx);
971
- if (isPromiseLike(value)) {
972
- return value.then((value) => ctx.renderer.read(value));
1770
+ finally {
1771
+ if (!isPromiseLike(diff)) {
1772
+ setFlag(ctx.ret, IsRefreshing, false);
1773
+ }
973
1774
  }
974
- return ctx.renderer.read(value);
975
1775
  }
976
- /**
977
- * Registers a callback which fires when the component commits. Will only
978
- * fire once per callback and update.
979
- */
980
1776
  schedule(callback) {
981
- const ctx = this[_ContextImpl];
1777
+ if (!callback) {
1778
+ return new Promise((resolve) => this.schedule(resolve));
1779
+ }
1780
+ const ctx = this[_ContextState];
982
1781
  let callbacks = scheduleMap.get(ctx);
983
1782
  if (!callbacks) {
984
1783
  callbacks = new Set();
@@ -986,35 +1785,35 @@
986
1785
  }
987
1786
  callbacks.add(callback);
988
1787
  }
989
- /**
990
- * Registers a callback which fires when the component’s children are
991
- * rendered into the root. Will only fire once per callback and render.
992
- */
993
- flush(callback) {
994
- const ctx = this[_ContextImpl];
995
- if (typeof ctx.root !== "object" || ctx.root === null) {
996
- return;
1788
+ after(callback) {
1789
+ if (!callback) {
1790
+ return new Promise((resolve) => this.after(resolve));
997
1791
  }
998
- let flushMap = flushMaps.get(ctx.root);
999
- if (!flushMap) {
1000
- flushMap = new Map();
1001
- flushMaps.set(ctx.root, flushMap);
1792
+ const ctx = this[_ContextState];
1793
+ const root = ctx.root || ANONYMOUS_ROOT;
1794
+ let afterMap = afterMapByRoot.get(root);
1795
+ if (!afterMap) {
1796
+ afterMap = new Map();
1797
+ afterMapByRoot.set(root, afterMap);
1002
1798
  }
1003
- let callbacks = flushMap.get(ctx);
1799
+ let callbacks = afterMap.get(ctx);
1004
1800
  if (!callbacks) {
1005
1801
  callbacks = new Set();
1006
- flushMap.set(ctx, callbacks);
1802
+ afterMap.set(ctx, callbacks);
1007
1803
  }
1008
1804
  callbacks.add(callback);
1009
1805
  }
1010
- /**
1011
- * Registers a callback which fires when the component unmounts. Will only
1012
- * fire once per callback.
1013
- */
1806
+ flush(callback) {
1807
+ console.error("Context.flush() method has been renamed to after()");
1808
+ this.after(callback);
1809
+ }
1014
1810
  cleanup(callback) {
1015
- const ctx = this[_ContextImpl];
1016
- if (ctx.f & IsUnmounted) {
1017
- const value = ctx.renderer.read(getValue(ctx.ret));
1811
+ if (!callback) {
1812
+ return new Promise((resolve) => this.cleanup(resolve));
1813
+ }
1814
+ const ctx = this[_ContextState];
1815
+ if (getFlag(ctx.ret, IsUnmounted)) {
1816
+ const value = ctx.adapter.read(getValue(ctx.ret));
1018
1817
  callback(value);
1019
1818
  return;
1020
1819
  }
@@ -1026,7 +1825,7 @@
1026
1825
  callbacks.add(callback);
1027
1826
  }
1028
1827
  consume(key) {
1029
- for (let ctx = this[_ContextImpl].parent; ctx !== undefined; ctx = ctx.parent) {
1828
+ for (let ctx = this[_ContextState].parent; ctx !== undefined; ctx = ctx.parent) {
1030
1829
  const provisions = provisionMaps.get(ctx);
1031
1830
  if (provisions && provisions.has(key)) {
1032
1831
  return provisions.get(key);
@@ -1034,7 +1833,7 @@
1034
1833
  }
1035
1834
  }
1036
1835
  provide(key, value) {
1037
- const ctx = this[_ContextImpl];
1836
+ const ctx = this[_ContextState];
1038
1837
  let provisions = provisionMaps.get(ctx);
1039
1838
  if (!provisions) {
1040
1839
  provisions = new Map();
@@ -1042,1279 +1841,1283 @@
1042
1841
  }
1043
1842
  provisions.set(key, value);
1044
1843
  }
1045
- addEventListener(type, listener, options) {
1046
- const ctx = this[_ContextImpl];
1047
- let listeners;
1048
- if (!isListenerOrListenerObject(listener)) {
1049
- return;
1844
+ [CustomEventTarget.dispatchEventOnSelf](ev) {
1845
+ const ctx = this[_ContextState];
1846
+ // dispatchEvent calls the prop callback if it exists
1847
+ let propCallback = ctx.ret.el.props["on" + ev.type];
1848
+ if (typeof propCallback === "function") {
1849
+ propCallback(ev);
1050
1850
  }
1051
1851
  else {
1052
- const listeners1 = listenersMap.get(ctx);
1053
- if (listeners1) {
1054
- listeners = listeners1;
1055
- }
1056
- else {
1057
- listeners = [];
1058
- listenersMap.set(ctx, listeners);
1852
+ for (const propName in ctx.ret.el.props) {
1853
+ if (propName.toLowerCase() === "on" + ev.type.toLowerCase()) {
1854
+ propCallback = ctx.ret.el.props[propName];
1855
+ if (typeof propCallback === "function") {
1856
+ propCallback(ev);
1857
+ }
1858
+ }
1059
1859
  }
1060
1860
  }
1061
- options = normalizeListenerOptions(options);
1062
- let callback;
1063
- if (typeof listener === "object") {
1064
- callback = () => listener.handleEvent.apply(listener, arguments);
1861
+ }
1862
+ }
1863
+ function diffComponent(adapter, root, host, parent, scope, ret) {
1864
+ let ctx;
1865
+ if (ret.ctx) {
1866
+ ctx = ret.ctx;
1867
+ if (getFlag(ctx.ret, IsExecuting)) {
1868
+ console.error(`Component <${getTagName(ctx.ret.el.tag)}> is already executing`);
1869
+ return;
1065
1870
  }
1066
- else {
1067
- callback = listener;
1068
- }
1069
- const record = { type, listener, callback, options };
1070
- if (options.once) {
1071
- record.callback = function () {
1072
- const i = listeners.indexOf(record);
1073
- if (i !== -1) {
1074
- listeners.splice(i, 1);
1075
- }
1076
- return callback.apply(this, arguments);
1077
- };
1078
- }
1079
- if (listeners.some((record1) => record.type === record1.type &&
1080
- record.listener === record1.listener &&
1081
- !record.options.capture === !record1.options.capture)) {
1082
- return;
1083
- }
1084
- listeners.push(record);
1085
- // TODO: is it possible to separate out the EventTarget delegation logic
1086
- for (const value of getChildValues(ctx.ret)) {
1087
- if (isEventTarget(value)) {
1088
- value.addEventListener(record.type, record.callback, record.options);
1089
- }
1090
- }
1091
- }
1092
- removeEventListener(type, listener, options) {
1093
- const ctx = this[_ContextImpl];
1094
- const listeners = listenersMap.get(ctx);
1095
- if (listeners == null || !isListenerOrListenerObject(listener)) {
1096
- return;
1097
- }
1098
- const options1 = normalizeListenerOptions(options);
1099
- const i = listeners.findIndex((record) => record.type === type &&
1100
- record.listener === listener &&
1101
- !record.options.capture === !options1.capture);
1102
- if (i === -1) {
1103
- return;
1104
- }
1105
- const record = listeners[i];
1106
- listeners.splice(i, 1);
1107
- // TODO: is it possible to separate out the EventTarget delegation logic
1108
- for (const value of getChildValues(ctx.ret)) {
1109
- if (isEventTarget(value)) {
1110
- value.removeEventListener(record.type, record.callback, record.options);
1111
- }
1112
- }
1113
- }
1114
- dispatchEvent(ev) {
1115
- const ctx = this[_ContextImpl];
1116
- const path = [];
1117
- for (let parent = ctx.parent; parent !== undefined; parent = parent.parent) {
1118
- path.push(parent);
1119
- }
1120
- // We patch the stopImmediatePropagation method because ev.cancelBubble
1121
- // only informs us if stopPropagation was called and there are no
1122
- // properties which inform us if stopImmediatePropagation was called.
1123
- let immediateCancelBubble = false;
1124
- const stopImmediatePropagation = ev.stopImmediatePropagation;
1125
- setEventProperty(ev, "stopImmediatePropagation", () => {
1126
- immediateCancelBubble = true;
1127
- return stopImmediatePropagation.call(ev);
1128
- });
1129
- setEventProperty(ev, "target", ctx.owner);
1130
- // The only possible errors in this block are errors thrown by callbacks,
1131
- // and dispatchEvent will only log these errors rather than throwing
1132
- // them. Therefore, we place all code in a try block, log errors in the
1133
- // catch block, and use an unsafe return statement in the finally block.
1134
- //
1135
- // Each early return within the try block returns true because while the
1136
- // return value is overridden in the finally block, TypeScript
1137
- // (justifiably) does not recognize the unsafe return statement.
1138
- try {
1139
- setEventProperty(ev, "eventPhase", CAPTURING_PHASE);
1140
- for (let i = path.length - 1; i >= 0; i--) {
1141
- const target = path[i];
1142
- const listeners = listenersMap.get(target);
1143
- if (listeners) {
1144
- setEventProperty(ev, "currentTarget", target.owner);
1145
- for (const record of listeners) {
1146
- if (record.type === ev.type && record.options.capture) {
1147
- try {
1148
- record.callback.call(target.owner, ev);
1149
- }
1150
- catch (err) {
1151
- console.error(err);
1152
- }
1153
- if (immediateCancelBubble) {
1154
- return true;
1155
- }
1156
- }
1157
- }
1158
- }
1159
- if (ev.cancelBubble) {
1160
- return true;
1161
- }
1162
- }
1163
- {
1164
- setEventProperty(ev, "eventPhase", AT_TARGET);
1165
- setEventProperty(ev, "currentTarget", ctx.owner);
1166
- // dispatchEvent calls the prop callback if it exists
1167
- let propCallback = ctx.ret.el.props["on" + ev.type];
1168
- if (typeof propCallback === "function") {
1169
- propCallback(ev);
1170
- if (immediateCancelBubble || ev.cancelBubble) {
1171
- return true;
1172
- }
1173
- }
1174
- else {
1175
- // Checks for camel-cased event props
1176
- for (const propName in ctx.ret.el.props) {
1177
- if (propName.toLowerCase() === "on" + ev.type.toLowerCase()) {
1178
- propCallback = ctx.ret.el.props[propName];
1179
- if (typeof propCallback === "function") {
1180
- propCallback(ev);
1181
- if (immediateCancelBubble || ev.cancelBubble) {
1182
- return true;
1183
- }
1184
- }
1185
- }
1186
- }
1187
- }
1188
- const listeners = listenersMap.get(ctx);
1189
- if (listeners) {
1190
- for (const record of listeners) {
1191
- if (record.type === ev.type) {
1192
- try {
1193
- record.callback.call(ctx.owner, ev);
1194
- }
1195
- catch (err) {
1196
- console.error(err);
1197
- }
1198
- if (immediateCancelBubble) {
1199
- return true;
1200
- }
1201
- }
1202
- }
1203
- if (ev.cancelBubble) {
1204
- return true;
1205
- }
1206
- }
1207
- }
1208
- if (ev.bubbles) {
1209
- setEventProperty(ev, "eventPhase", BUBBLING_PHASE);
1210
- for (let i = 0; i < path.length; i++) {
1211
- const target = path[i];
1212
- const listeners = listenersMap.get(target);
1213
- if (listeners) {
1214
- setEventProperty(ev, "currentTarget", target.owner);
1215
- for (const record of listeners) {
1216
- if (record.type === ev.type && !record.options.capture) {
1217
- try {
1218
- record.callback.call(target.owner, ev);
1219
- }
1220
- catch (err) {
1221
- console.error(err);
1222
- }
1223
- if (immediateCancelBubble) {
1224
- return true;
1225
- }
1226
- }
1227
- }
1228
- }
1229
- if (ev.cancelBubble) {
1230
- return true;
1231
- }
1232
- }
1233
- }
1234
- }
1235
- finally {
1236
- setEventProperty(ev, "eventPhase", NONE);
1237
- setEventProperty(ev, "currentTarget", null);
1238
- // eslint-disable-next-line no-unsafe-finally
1239
- return !ev.defaultPrevented;
1240
- }
1241
- }
1242
- }
1243
- /*** PRIVATE CONTEXT FUNCTIONS ***/
1244
- function ctxContains(parent, child) {
1245
- for (let current = child; current !== undefined; current = current.parent) {
1246
- if (current === parent) {
1247
- return true;
1248
- }
1249
- }
1250
- return false;
1251
- }
1252
- function updateComponent(renderer, root, host, parent, scope, ret, oldProps, hydrationData) {
1253
- let ctx;
1254
- if (oldProps) {
1255
- ctx = ret.ctx;
1256
- if (ctx.f & IsSyncExecuting) {
1257
- console.error("Component is already executing");
1258
- return ret.cachedChildValues;
1871
+ else if (ctx.schedule) {
1872
+ return ctx.schedule.promise.then(() => {
1873
+ return diffComponent(adapter, root, host, parent, scope, ret);
1874
+ });
1259
1875
  }
1260
1876
  }
1261
1877
  else {
1262
- ctx = ret.ctx = new ContextImpl(renderer, root, host, parent, scope, ret);
1878
+ ctx = ret.ctx = new ContextState(adapter, root, host, parent, scope, ret);
1263
1879
  }
1264
- ctx.f |= IsUpdating;
1265
- return enqueueComponentRun(ctx, hydrationData);
1880
+ setFlag(ctx.ret, IsUpdating);
1881
+ return enqueueComponent(ctx);
1266
1882
  }
1267
- function updateComponentChildren(ctx, children, hydrationData) {
1268
- if (ctx.f & IsUnmounted) {
1269
- return;
1270
- }
1271
- else if (ctx.f & IsErrored) {
1272
- // This branch is necessary for some race conditions where this function is
1273
- // called after iterator.throw() in async generator components.
1883
+ function diffComponentChildren(ctx, children, isYield) {
1884
+ if (getFlag(ctx.ret, IsUnmounted) || getFlag(ctx.ret, IsErrored)) {
1274
1885
  return;
1275
1886
  }
1276
1887
  else if (children === undefined) {
1277
- console.error("A component has returned or yielded undefined. If this was intentional, return or yield null instead.");
1888
+ console.error(`Component <${getTagName(ctx.ret.el.tag)}> has ${isYield ? "yielded" : "returned"} undefined. If this was intentional, ${isYield ? "yield" : "return"} null instead.`);
1278
1889
  }
1279
- let childValues;
1890
+ let diff;
1280
1891
  try {
1281
- // TODO: WAT
1892
+ // TODO: Use a different flag here to indicate the component is
1893
+ // synchronously rendering children
1282
1894
  // We set the isExecuting flag in case a child component dispatches an event
1283
1895
  // which bubbles to this component and causes a synchronous refresh().
1284
- ctx.f |= IsSyncExecuting;
1285
- childValues = diffChildren(ctx.renderer, ctx.root, ctx.host, ctx, ctx.scope, ctx.ret, narrow(children), hydrationData);
1286
- }
1287
- finally {
1288
- ctx.f &= -3;
1289
- }
1290
- if (isPromiseLike(childValues)) {
1291
- ctx.ret.inflightValue = childValues.then((childValues) => commitComponent(ctx, childValues));
1292
- return ctx.ret.inflightValue;
1293
- }
1294
- return commitComponent(ctx, childValues);
1295
- }
1296
- function commitComponent(ctx, values) {
1297
- if (ctx.f & IsUnmounted) {
1298
- return;
1299
- }
1300
- const listeners = listenersMap.get(ctx);
1301
- if (listeners && listeners.length) {
1302
- for (let i = 0; i < values.length; i++) {
1303
- const value = values[i];
1304
- if (isEventTarget(value)) {
1305
- for (let j = 0; j < listeners.length; j++) {
1306
- const record = listeners[j];
1307
- value.addEventListener(record.type, record.callback, record.options);
1308
- }
1309
- }
1310
- }
1311
- }
1312
- const oldValues = wrap(ctx.ret.cachedChildValues);
1313
- let value = (ctx.ret.cachedChildValues = unwrap(values));
1314
- if (ctx.f & IsScheduling) {
1315
- ctx.f |= IsSchedulingRefresh;
1316
- }
1317
- else if (!(ctx.f & IsUpdating)) {
1318
- // If we’re not updating the component, which happens when components are
1319
- // refreshed, or when async generator components iterate, we have to do a
1320
- // little bit housekeeping when a component’s child values have changed.
1321
- if (!arrayEqual(oldValues, values)) {
1322
- const records = getListenerRecords(ctx.parent, ctx.host);
1323
- if (records.length) {
1324
- for (let i = 0; i < values.length; i++) {
1325
- const value = values[i];
1326
- if (isEventTarget(value)) {
1327
- for (let j = 0; j < records.length; j++) {
1328
- const record = records[j];
1329
- value.addEventListener(record.type, record.callback, record.options);
1330
- }
1331
- }
1332
- }
1333
- }
1334
- // rearranging the nearest ancestor host element
1335
- const host = ctx.host;
1336
- const oldHostValues = wrap(host.cachedChildValues);
1337
- invalidate(ctx, host);
1338
- const hostValues = getChildValues(host);
1339
- ctx.renderer.arrange(host.el.tag, host.value, host.el.props, hostValues,
1340
- // props and oldProps are the same because the host isn’t updated.
1341
- host.el.props, oldHostValues);
1342
- }
1343
- flush(ctx.renderer, ctx.root, ctx);
1344
- }
1345
- const callbacks = scheduleMap.get(ctx);
1346
- if (callbacks) {
1347
- scheduleMap.delete(ctx);
1348
- ctx.f |= IsScheduling;
1349
- const value1 = ctx.renderer.read(value);
1350
- for (const callback of callbacks) {
1351
- callback(value1);
1352
- }
1353
- ctx.f &= -1025;
1354
- // Handles an edge case where refresh() is called during a schedule().
1355
- if (ctx.f & IsSchedulingRefresh) {
1356
- ctx.f &= -2049;
1357
- value = getValue(ctx.ret);
1896
+ setFlag(ctx.ret, IsExecuting);
1897
+ diff = diffChildren(ctx.adapter, ctx.root, ctx.host, ctx, ctx.scope, ctx.ret, narrow(children));
1898
+ if (diff) {
1899
+ diff = diff.catch((err) => handleChildError(ctx, err));
1358
1900
  }
1359
1901
  }
1360
- ctx.f &= -2;
1361
- return value;
1362
- }
1363
- function invalidate(ctx, host) {
1364
- for (let parent = ctx.parent; parent !== undefined && parent.host === host; parent = parent.parent) {
1365
- parent.ret.cachedChildValues = undefined;
1366
- }
1367
- host.cachedChildValues = undefined;
1368
- }
1369
- function arrayEqual(arr1, arr2) {
1370
- if (arr1.length !== arr2.length) {
1371
- return false;
1902
+ catch (err) {
1903
+ diff = handleChildError(ctx, err);
1372
1904
  }
1373
- for (let i = 0; i < arr1.length; i++) {
1374
- const value1 = arr1[i];
1375
- const value2 = arr2[i];
1376
- if (value1 !== value2) {
1377
- return false;
1378
- }
1905
+ finally {
1906
+ setFlag(ctx.ret, IsExecuting, false);
1379
1907
  }
1380
- return true;
1908
+ return diff;
1381
1909
  }
1382
1910
  /** Enqueues and executes the component associated with the context. */
1383
- function enqueueComponentRun(ctx, hydrationData) {
1384
- if (ctx.f & IsAsyncGen && !(ctx.f & IsInForOfLoop)) {
1385
- if (hydrationData !== undefined) {
1386
- throw new Error("Hydration error");
1387
- }
1388
- // This branch will run for non-initial renders of async generator
1389
- // components when they are not in for...of loops. When in a for...of loop,
1390
- // async generator components will behave normally.
1391
- //
1392
- // Async gen componennts can be in one of three states:
1393
- //
1394
- // 1. propsAvailable flag is true: "available"
1395
- //
1396
- // The component is suspended somewhere in the loop. When the component
1397
- // reaches the bottom of the loop, it will run again with the next props.
1398
- //
1399
- // 2. onAvailable callback is defined: "suspended"
1400
- //
1401
- // The component has suspended at the bottom of the loop and is waiting
1402
- // for new props.
1403
- //
1404
- // 3. neither 1 or 2: "Running"
1405
- //
1406
- // The component is suspended somewhere in the loop. When the component
1407
- // reaches the bottom of the loop, it will suspend.
1408
- //
1409
- // Components will never be both available and suspended at
1410
- // the same time.
1411
- //
1412
- // If the component is at the loop bottom, this means that the next value
1413
- // produced by the component will have the most up to date props, so we can
1414
- // simply return the current inflight value. Otherwise, we have to wait for
1415
- // the bottom of the loop to be reached before returning the inflight
1416
- // value.
1417
- const isAtLoopbottom = ctx.f & IsInForAwaitOfLoop && !ctx.onProps;
1418
- resumePropsAsyncIterator(ctx);
1419
- if (isAtLoopbottom) {
1420
- if (ctx.inflightBlock == null) {
1421
- ctx.inflightBlock = new Promise((resolve) => (ctx.onPropsRequested = resolve));
1422
- }
1423
- return ctx.inflightBlock.then(() => {
1424
- ctx.inflightBlock = undefined;
1425
- return ctx.inflightValue;
1426
- });
1427
- }
1428
- return ctx.inflightValue;
1429
- }
1430
- else if (!ctx.inflightBlock) {
1431
- try {
1432
- const [block, value] = runComponent(ctx, hydrationData);
1433
- if (block) {
1434
- ctx.inflightBlock = block
1435
- // TODO: there is some fuckery going on here related to async
1436
- // generator components resuming when they’re meant to be returned.
1437
- .then((v) => v)
1438
- .finally(() => advanceComponent(ctx));
1439
- // stepComponent will only return a block if the value is asynchronous
1440
- ctx.inflightValue = value;
1441
- }
1442
- return value;
1443
- }
1444
- catch (err) {
1445
- if (!(ctx.f & IsUpdating)) {
1446
- if (!ctx.parent) {
1447
- throw err;
1448
- }
1449
- return propagateError(ctx.parent, err);
1450
- }
1451
- throw err;
1452
- }
1453
- }
1454
- else if (!ctx.enqueuedBlock) {
1455
- if (hydrationData !== undefined) {
1456
- throw new Error("Hydration error");
1457
- }
1458
- // We need to assign enqueuedBlock and enqueuedValue synchronously, hence
1459
- // the Promise constructor call here.
1460
- let resolveEnqueuedBlock;
1461
- ctx.enqueuedBlock = new Promise((resolve) => (resolveEnqueuedBlock = resolve));
1462
- ctx.enqueuedValue = ctx.inflightBlock.then(() => {
1463
- try {
1464
- const [block, value] = runComponent(ctx);
1465
- if (block) {
1466
- resolveEnqueuedBlock(block.finally(() => advanceComponent(ctx)));
1467
- }
1468
- return value;
1469
- }
1470
- catch (err) {
1471
- if (!(ctx.f & IsUpdating)) {
1472
- if (!ctx.parent) {
1473
- throw err;
1474
- }
1475
- return propagateError(ctx.parent, err);
1476
- }
1477
- throw err;
1478
- }
1479
- });
1480
- }
1481
- return ctx.enqueuedValue;
1911
+ function enqueueComponent(ctx) {
1912
+ if (!ctx.inflight) {
1913
+ const [block, diff] = runComponent(ctx);
1914
+ if (block) {
1915
+ // if block is a promise, diff is a promise
1916
+ ctx.inflight = [block.finally(() => advanceComponent(ctx)), diff];
1917
+ }
1918
+ return diff;
1919
+ }
1920
+ else if (!ctx.enqueued) {
1921
+ // The enqueuedBlock and enqueuedDiff properties must be set
1922
+ // simultaneously, hence the usage of the Promise constructor.
1923
+ let resolve;
1924
+ ctx.enqueued = [
1925
+ new Promise((resolve1) => (resolve = resolve1)).finally(() => advanceComponent(ctx)),
1926
+ ctx.inflight[0].finally(() => {
1927
+ const [block, diff] = runComponent(ctx);
1928
+ resolve(block);
1929
+ return diff;
1930
+ }),
1931
+ ];
1932
+ }
1933
+ return ctx.enqueued[1];
1482
1934
  }
1483
1935
  /** Called when the inflight block promise settles. */
1484
1936
  function advanceComponent(ctx) {
1485
- if (ctx.f & IsAsyncGen && !(ctx.f & IsInForOfLoop)) {
1486
- return;
1487
- }
1488
- ctx.inflightBlock = ctx.enqueuedBlock;
1489
- ctx.inflightValue = ctx.enqueuedValue;
1490
- ctx.enqueuedBlock = undefined;
1491
- ctx.enqueuedValue = undefined;
1937
+ ctx.inflight = ctx.enqueued;
1938
+ ctx.enqueued = undefined;
1492
1939
  }
1493
1940
  /**
1494
- * This function is responsible for executing the component and handling all
1495
- * the different component types. We cannot identify whether a component is a
1496
- * generator or async without calling it and inspecting the return value.
1941
+ * This function is responsible for executing components, and handling the
1942
+ * different component types.
1497
1943
  *
1498
- * @returns {[block, value]} A tuple where
1499
- * block - A possible promise which represents the duration during which the
1500
- * component is blocked from updating.
1501
- * value - A possible promise resolving to the rendered value of children.
1944
+ * @returns {[block, diff]} A tuple where:
1945
+ * - block is a promise or undefined which represents the duration during which
1946
+ * the component is blocked.
1947
+ * - diff is a promise or undefined which represents the duration for diffing
1948
+ * of children.
1502
1949
  *
1503
- * Each component type will block according to the type of the component.
1504
- * - Sync function components never block and will transparently pass updates
1505
- * to children.
1506
- * - Async function components and async generator components block while
1507
- * executing itself, but will not block for async children.
1508
- * - Sync generator components block while any children are executing, because
1509
- * they are expected to only resume when they’ve actually rendered.
1950
+ * While a component is blocked, further updates to the component are enqueued.
1951
+ *
1952
+ * Each component type blocks according to its implementation:
1953
+ * - Sync function components never block; when props or state change,
1954
+ * updates are immediately passed to children.
1955
+ * - Async function components block only while awaiting their own async work
1956
+ * (e.g., during an await), but do not block while their async children are rendering.
1957
+ * - Sync generator components block while their children are rendering;
1958
+ * they only resume once their children have finished.
1959
+ * - Async generator components can block in two different ways:
1960
+ * - By default, they behave like sync generator components, blocking while
1961
+ * the component or its children are rendering.
1962
+ * - Within a for await...of loop, they block only while waiting for new
1963
+ * props to be requested, and not while children are rendering.
1510
1964
  */
1511
- function runComponent(ctx, hydrationData) {
1965
+ function runComponent(ctx) {
1966
+ if (getFlag(ctx.ret, IsUnmounted)) {
1967
+ return [undefined, undefined];
1968
+ }
1512
1969
  const ret = ctx.ret;
1513
1970
  const initial = !ctx.iterator;
1514
1971
  if (initial) {
1515
- resumePropsAsyncIterator(ctx);
1516
- ctx.f |= IsSyncExecuting;
1517
- clearEventListeners(ctx);
1518
- let result;
1972
+ setFlag(ctx.ret, IsExecuting);
1973
+ clearEventListeners(ctx.ctx);
1974
+ let returned;
1519
1975
  try {
1520
- result = ret.el.tag.call(ctx.owner, ret.el.props, ctx.owner);
1976
+ returned = ret.el.tag.call(ctx.ctx, ret.el.props, ctx.ctx);
1521
1977
  }
1522
1978
  catch (err) {
1523
- ctx.f |= IsErrored;
1979
+ setFlag(ctx.ret, IsErrored);
1524
1980
  throw err;
1525
1981
  }
1526
1982
  finally {
1527
- ctx.f &= -3;
1528
- }
1529
- if (isIteratorLike(result)) {
1530
- ctx.iterator = result;
1983
+ setFlag(ctx.ret, IsExecuting, false);
1531
1984
  }
1532
- else if (isPromiseLike(result)) {
1533
- // async function component
1534
- const result1 = result instanceof Promise ? result : Promise.resolve(result);
1535
- const value = result1.then((result) => updateComponentChildren(ctx, result, hydrationData), (err) => {
1536
- ctx.f |= IsErrored;
1537
- throw err;
1538
- });
1539
- return [result1.catch(NOOP), value];
1985
+ if (isIteratorLike(returned)) {
1986
+ ctx.iterator = returned;
1540
1987
  }
1541
- else {
1988
+ else if (!isPromiseLike(returned)) {
1542
1989
  // sync function component
1543
1990
  return [
1544
1991
  undefined,
1545
- updateComponentChildren(ctx, result, hydrationData),
1992
+ diffComponentChildren(ctx, returned, false),
1993
+ ];
1994
+ }
1995
+ else {
1996
+ // async function component
1997
+ const returned1 = returned instanceof Promise ? returned : Promise.resolve(returned);
1998
+ return [
1999
+ returned1.catch(NOOP),
2000
+ returned1.then((returned) => diffComponentChildren(ctx, returned, false), (err) => {
2001
+ setFlag(ctx.ret, IsErrored);
2002
+ throw err;
2003
+ }),
1546
2004
  ];
1547
2005
  }
1548
- }
1549
- else if (hydrationData !== undefined) {
1550
- // hydration data should only be passed on the initial render
1551
- throw new Error("Hydration error");
1552
2006
  }
1553
2007
  let iteration;
1554
2008
  if (initial) {
1555
2009
  try {
1556
- ctx.f |= IsSyncExecuting;
2010
+ setFlag(ctx.ret, IsExecuting);
1557
2011
  iteration = ctx.iterator.next();
1558
2012
  }
1559
2013
  catch (err) {
1560
- ctx.f |= IsErrored;
2014
+ setFlag(ctx.ret, IsErrored);
1561
2015
  throw err;
1562
2016
  }
1563
2017
  finally {
1564
- ctx.f &= -3;
2018
+ setFlag(ctx.ret, IsExecuting, false);
1565
2019
  }
1566
2020
  if (isPromiseLike(iteration)) {
1567
- ctx.f |= IsAsyncGen;
2021
+ setFlag(ctx.ret, IsAsyncGen);
1568
2022
  }
1569
2023
  else {
1570
- ctx.f |= IsSyncGen;
2024
+ setFlag(ctx.ret, IsSyncGen);
1571
2025
  }
1572
2026
  }
1573
- if (ctx.f & IsSyncGen) {
2027
+ if (getFlag(ctx.ret, IsSyncGen)) {
1574
2028
  // sync generator component
1575
2029
  if (!initial) {
1576
2030
  try {
1577
- ctx.f |= IsSyncExecuting;
1578
- iteration = ctx.iterator.next(ctx.renderer.read(getValue(ret)));
2031
+ setFlag(ctx.ret, IsExecuting);
2032
+ const oldResult = ctx.adapter.read(getValue(ctx.ret));
2033
+ iteration = ctx.iterator.next(oldResult);
1579
2034
  }
1580
2035
  catch (err) {
1581
- ctx.f |= IsErrored;
2036
+ setFlag(ctx.ret, IsErrored);
1582
2037
  throw err;
1583
2038
  }
1584
2039
  finally {
1585
- ctx.f &= -3;
2040
+ setFlag(ctx.ret, IsExecuting, false);
1586
2041
  }
1587
2042
  }
1588
2043
  if (isPromiseLike(iteration)) {
1589
2044
  throw new Error("Mixed generator component");
1590
2045
  }
1591
- if (ctx.f & IsInForOfLoop &&
1592
- !(ctx.f & NeedsToYield) &&
1593
- !(ctx.f & IsUnmounted)) {
1594
- console.error("Component yielded more than once in for...of loop");
2046
+ if (getFlag(ctx.ret, IsInForOfLoop) &&
2047
+ !getFlag(ctx.ret, NeedsToYield) &&
2048
+ !getFlag(ctx.ret, IsUnmounted) &&
2049
+ !getFlag(ctx.ret, IsScheduling)) {
2050
+ console.error(`Component <${getTagName(ctx.ret.el.tag)}> yielded/returned more than once in for...of loop`);
1595
2051
  }
1596
- ctx.f &= -17;
2052
+ setFlag(ctx.ret, NeedsToYield, false);
1597
2053
  if (iteration.done) {
1598
- ctx.f &= -257;
2054
+ setFlag(ctx.ret, IsSyncGen, false);
1599
2055
  ctx.iterator = undefined;
1600
2056
  }
1601
- let value;
1602
- try {
1603
- value = updateComponentChildren(ctx,
1604
- // Children can be void so we eliminate that here
1605
- iteration.value, hydrationData);
1606
- if (isPromiseLike(value)) {
1607
- value = value.catch((err) => handleChildError(ctx, err));
1608
- }
1609
- }
1610
- catch (err) {
1611
- value = handleChildError(ctx, err);
1612
- }
1613
- const block = isPromiseLike(value) ? value.catch(NOOP) : undefined;
1614
- return [block, value];
2057
+ const diff = diffComponentChildren(ctx, iteration.value, !iteration.done);
2058
+ const block = isPromiseLike(diff) ? diff.catch(NOOP) : undefined;
2059
+ return [block, diff];
1615
2060
  }
1616
2061
  else {
1617
- if (ctx.f & IsInForOfLoop) {
1618
- // Async generator component using for...of loops behave similar to sync
1619
- // generator components. This allows for easier refactoring of sync to
1620
- // async generator components.
2062
+ if (getFlag(ctx.ret, IsInForAwaitOfLoop)) {
2063
+ // initializes the async generator loop
2064
+ pullComponent(ctx, iteration);
2065
+ const block = resumePropsAsyncIterator(ctx);
2066
+ return [block, ctx.pull && ctx.pull.diff];
2067
+ }
2068
+ else {
2069
+ // We call resumePropsAsyncIterator in case the component exits the
2070
+ // for...of loop
2071
+ resumePropsAsyncIterator(ctx);
1621
2072
  if (!initial) {
1622
2073
  try {
1623
- ctx.f |= IsSyncExecuting;
1624
- iteration = ctx.iterator.next(ctx.renderer.read(getValue(ret)));
2074
+ setFlag(ctx.ret, IsExecuting);
2075
+ const oldResult = ctx.adapter.read(getValue(ctx.ret));
2076
+ iteration = ctx.iterator.next(oldResult);
1625
2077
  }
1626
2078
  catch (err) {
1627
- ctx.f |= IsErrored;
2079
+ setFlag(ctx.ret, IsErrored);
1628
2080
  throw err;
1629
2081
  }
1630
2082
  finally {
1631
- ctx.f &= -3;
2083
+ setFlag(ctx.ret, IsExecuting, false);
1632
2084
  }
1633
2085
  }
1634
2086
  if (!isPromiseLike(iteration)) {
1635
2087
  throw new Error("Mixed generator component");
1636
2088
  }
1637
- const block = iteration.catch(NOOP);
1638
- const value = iteration.then((iteration) => {
1639
- let value;
1640
- if (!(ctx.f & IsInForOfLoop)) {
1641
- runAsyncGenComponent(ctx, Promise.resolve(iteration), hydrationData);
2089
+ const diff = iteration.then((iteration) => {
2090
+ if (getFlag(ctx.ret, IsInForAwaitOfLoop)) {
2091
+ // We have entered a for await...of loop, so we start pulling
2092
+ pullComponent(ctx, iteration);
1642
2093
  }
1643
2094
  else {
1644
- if (!(ctx.f & NeedsToYield) && !(ctx.f & IsUnmounted)) {
1645
- console.error("Component yielded more than once in for...of loop");
1646
- }
1647
- }
1648
- ctx.f &= -17;
1649
- try {
1650
- value = updateComponentChildren(ctx,
1651
- // Children can be void so we eliminate that here
1652
- iteration.value, hydrationData);
1653
- if (isPromiseLike(value)) {
1654
- value = value.catch((err) => handleChildError(ctx, err));
2095
+ if (getFlag(ctx.ret, IsInForOfLoop) &&
2096
+ !getFlag(ctx.ret, NeedsToYield) &&
2097
+ !getFlag(ctx.ret, IsUnmounted) &&
2098
+ !getFlag(ctx.ret, IsScheduling)) {
2099
+ console.error(`Component <${getTagName(ctx.ret.el.tag)}> yielded/returned more than once in for...of loop`);
1655
2100
  }
1656
2101
  }
1657
- catch (err) {
1658
- value = handleChildError(ctx, err);
2102
+ setFlag(ctx.ret, NeedsToYield, false);
2103
+ if (iteration.done) {
2104
+ setFlag(ctx.ret, IsAsyncGen, false);
2105
+ ctx.iterator = undefined;
1659
2106
  }
1660
- return value;
2107
+ return diffComponentChildren(ctx,
2108
+ // Children can be void so we eliminate that here
2109
+ iteration.value, !iteration.done);
1661
2110
  }, (err) => {
1662
- ctx.f |= IsErrored;
2111
+ setFlag(ctx.ret, IsErrored);
1663
2112
  throw err;
1664
2113
  });
1665
- return [block, value];
2114
+ return [diff.catch(NOOP), diff];
1666
2115
  }
1667
- else {
1668
- runAsyncGenComponent(ctx, iteration, hydrationData, initial);
1669
- return [ctx.inflightBlock, ctx.inflightValue];
2116
+ }
2117
+ }
2118
+ /**
2119
+ * Called to resume the props async iterator for async generator components.
2120
+ *
2121
+ * @returns {Promise<undefined> | undefined} A possible promise which
2122
+ * represents the duration during which the component is blocked.
2123
+ */
2124
+ function resumePropsAsyncIterator(ctx) {
2125
+ if (ctx.onPropsProvided) {
2126
+ ctx.onPropsProvided(ctx.ret.el.props);
2127
+ ctx.onPropsProvided = undefined;
2128
+ setFlag(ctx.ret, PropsAvailable, false);
2129
+ }
2130
+ else {
2131
+ setFlag(ctx.ret, PropsAvailable);
2132
+ if (getFlag(ctx.ret, IsInForAwaitOfLoop)) {
2133
+ return new Promise((resolve) => (ctx.onPropsRequested = resolve));
1670
2134
  }
1671
2135
  }
2136
+ return (ctx.pull && ctx.pull.iterationP && ctx.pull.iterationP.then(NOOP, NOOP));
1672
2137
  }
1673
- async function runAsyncGenComponent(ctx, iterationP, hydrationData, initial = false) {
2138
+ /**
2139
+ * The logic for pulling from async generator components when they are in a for
2140
+ * await...of loop is implemented here.
2141
+ *
2142
+ * It makes sense to group this logic in a single async loop to prevent race
2143
+ * conditions caused by calling next(), throw() and return() concurrently.
2144
+ */
2145
+ async function pullComponent(ctx, iterationP) {
2146
+ if (!iterationP || ctx.pull) {
2147
+ return;
2148
+ }
2149
+ ctx.pull = { iterationP: undefined, diff: undefined, onChildError: undefined };
2150
+ // TODO: replace done with iteration
2151
+ //let iteration: ChildrenIteratorResult | undefined;
1674
2152
  let done = false;
1675
2153
  try {
2154
+ let childError;
1676
2155
  while (!done) {
1677
- if (ctx.f & IsInForOfLoop) {
1678
- break;
1679
- }
1680
- // inflightValue must be set synchronously.
1681
- let onValue;
1682
- ctx.inflightValue = new Promise((resolve) => (onValue = resolve));
1683
- if (ctx.f & IsUpdating) {
1684
- // We should not swallow unhandled promise rejections if the component is
1685
- // updating independently.
1686
- // TODO: Does this handle this.refresh() calls?
1687
- ctx.inflightValue.catch(NOOP);
2156
+ if (isPromiseLike(iterationP)) {
2157
+ ctx.pull.iterationP = iterationP;
1688
2158
  }
2159
+ let onDiff;
2160
+ ctx.pull.diff = new Promise((resolve) => (onDiff = resolve)).then(() => {
2161
+ if (!(getFlag(ctx.ret, IsUpdating) || getFlag(ctx.ret, IsRefreshing))) {
2162
+ commitComponent(ctx, []);
2163
+ }
2164
+ }, (err) => {
2165
+ if (!(getFlag(ctx.ret, IsUpdating) || getFlag(ctx.ret, IsRefreshing)) ||
2166
+ // TODO: is this flag necessary?
2167
+ !getFlag(ctx.ret, NeedsToYield)) {
2168
+ return propagateError(ctx, err, []);
2169
+ }
2170
+ throw err;
2171
+ });
1689
2172
  let iteration;
1690
2173
  try {
1691
2174
  iteration = await iterationP;
1692
2175
  }
1693
2176
  catch (err) {
1694
2177
  done = true;
1695
- ctx.f |= IsErrored;
1696
- onValue(Promise.reject(err));
2178
+ setFlag(ctx.ret, IsErrored);
2179
+ setFlag(ctx.ret, NeedsToYield, false);
2180
+ onDiff(Promise.reject(err));
1697
2181
  break;
1698
2182
  }
1699
- if (!(ctx.f & IsInForAwaitOfLoop)) {
1700
- ctx.f &= ~PropsAvailable;
2183
+ // this must be set after iterationP is awaited
2184
+ let oldResult;
2185
+ {
2186
+ // The 'floating' flag tracks whether the promise passed to the generator
2187
+ // is handled (via await, then, or catch). If handled, we reject the
2188
+ // promise so the user can catch errors. If not, we inject the error back
2189
+ // into the generator using throw, like for sync generator components.
2190
+ let floating = true;
2191
+ const oldResult1 = new Promise((resolve, reject) => {
2192
+ ctx.ctx.schedule(resolve);
2193
+ ctx.pull.onChildError = (err) => {
2194
+ reject(err);
2195
+ if (floating) {
2196
+ childError = err;
2197
+ resumePropsAsyncIterator(ctx);
2198
+ return ctx.pull.diff;
2199
+ }
2200
+ };
2201
+ });
2202
+ oldResult1.catch(NOOP);
2203
+ // We use Object.create() to clone the promise for float detection
2204
+ // because modern JS engines skip calling .then() on promises awaited
2205
+ // with await.
2206
+ oldResult = Object.create(oldResult1);
2207
+ oldResult.then = function (onfulfilled, onrejected) {
2208
+ floating = false;
2209
+ return oldResult1.then(onfulfilled, onrejected);
2210
+ };
2211
+ oldResult.catch = function (onrejected) {
2212
+ floating = false;
2213
+ return oldResult1.catch(onrejected);
2214
+ };
2215
+ }
2216
+ if (childError != null) {
2217
+ try {
2218
+ setFlag(ctx.ret, IsExecuting);
2219
+ if (typeof ctx.iterator.throw !== "function") {
2220
+ throw childError;
2221
+ }
2222
+ iteration = await ctx.iterator.throw(childError);
2223
+ }
2224
+ catch (err) {
2225
+ done = true;
2226
+ setFlag(ctx.ret, IsErrored);
2227
+ setFlag(ctx.ret, NeedsToYield, false);
2228
+ onDiff(Promise.reject(err));
2229
+ break;
2230
+ }
2231
+ finally {
2232
+ childError = undefined;
2233
+ setFlag(ctx.ret, IsExecuting, false);
2234
+ }
2235
+ }
2236
+ // this makes sure we pause before entering a loop if we yield before it
2237
+ if (!getFlag(ctx.ret, IsInForAwaitOfLoop)) {
2238
+ setFlag(ctx.ret, PropsAvailable, false);
1701
2239
  }
1702
2240
  done = !!iteration.done;
1703
- let value;
2241
+ let diff;
1704
2242
  try {
1705
- if (!(ctx.f & NeedsToYield) &&
1706
- ctx.f & PropsAvailable &&
1707
- ctx.f & IsInForAwaitOfLoop &&
1708
- !initial &&
1709
- !done) {
1710
- // We skip stale iterations in for await...of loops.
1711
- value = ctx.ret.inflightValue || getValue(ctx.ret);
2243
+ if (!isPromiseLike(iterationP)) {
2244
+ // if iterationP is an iteration and not a promise, the component was
2245
+ // not in a for await...of loop when the iteration started, so we can
2246
+ // skip the diffing of children as it is handled elsewhere.
2247
+ diff = undefined;
2248
+ }
2249
+ else if (!getFlag(ctx.ret, NeedsToYield) &&
2250
+ getFlag(ctx.ret, PropsAvailable) &&
2251
+ getFlag(ctx.ret, IsInForAwaitOfLoop)) {
2252
+ // logic to skip yielded children in a stale for await of iteration.
2253
+ diff = undefined;
1712
2254
  }
1713
2255
  else {
1714
- value = updateComponentChildren(ctx, iteration.value, hydrationData);
1715
- hydrationData = undefined;
1716
- if (isPromiseLike(value)) {
1717
- value = value.catch((err) => handleChildError(ctx, err));
1718
- }
2256
+ diff = diffComponentChildren(ctx, iteration.value, !iteration.done);
1719
2257
  }
1720
- ctx.f &= ~NeedsToYield;
1721
2258
  }
1722
2259
  catch (err) {
1723
- // Do we need to catch potential errors here in the case of unhandled
1724
- // promise rejections?
1725
- value = handleChildError(ctx, err);
2260
+ onDiff(Promise.reject(err));
1726
2261
  }
1727
2262
  finally {
1728
- onValue(value);
1729
- }
1730
- let oldResult;
1731
- if (ctx.ret.inflightValue) {
1732
- // The value passed back into the generator as the argument to the next
1733
- // method is a promise if an async generator component has async
1734
- // children. Sync generator components only resume when their children
1735
- // have fulfilled so the element’s inflight child values will never be
1736
- // defined.
1737
- oldResult = ctx.ret.inflightValue.then((value) => ctx.renderer.read(value));
1738
- oldResult.catch((err) => {
1739
- if (ctx.f & IsUpdating) {
1740
- return;
2263
+ onDiff(diff);
2264
+ setFlag(ctx.ret, NeedsToYield, false);
2265
+ }
2266
+ if (getFlag(ctx.ret, IsUnmounted)) {
2267
+ // TODO: move this unmounted branch outside the loop
2268
+ while ((!iteration || !iteration.done) &&
2269
+ ctx.iterator &&
2270
+ getFlag(ctx.ret, IsInForAwaitOfLoop)) {
2271
+ try {
2272
+ setFlag(ctx.ret, IsExecuting);
2273
+ iteration = await ctx.iterator.next(oldResult);
1741
2274
  }
1742
- if (!ctx.parent) {
2275
+ catch (err) {
2276
+ setFlag(ctx.ret, IsErrored);
2277
+ // we throw the error here to cause an unhandled rejection because
2278
+ // the promise returned from pullComponent is never awaited
1743
2279
  throw err;
1744
2280
  }
1745
- return propagateError(ctx.parent, err);
1746
- });
1747
- }
1748
- else {
1749
- oldResult = ctx.renderer.read(getValue(ctx.ret));
1750
- }
1751
- if (ctx.f & IsUnmounted) {
1752
- if (ctx.f & IsInForAwaitOfLoop) {
2281
+ finally {
2282
+ setFlag(ctx.ret, IsExecuting, false);
2283
+ }
2284
+ }
2285
+ if ((!iteration || !iteration.done) &&
2286
+ ctx.iterator &&
2287
+ typeof ctx.iterator.return === "function") {
1753
2288
  try {
1754
- ctx.f |= IsSyncExecuting;
1755
- iterationP = ctx.iterator.next(oldResult);
2289
+ setFlag(ctx.ret, IsExecuting);
2290
+ await ctx.iterator.return();
2291
+ }
2292
+ catch (err) {
2293
+ setFlag(ctx.ret, IsErrored);
2294
+ throw err;
1756
2295
  }
1757
2296
  finally {
1758
- ctx.f &= ~IsSyncExecuting;
2297
+ setFlag(ctx.ret, IsExecuting, false);
1759
2298
  }
1760
2299
  }
1761
- else {
1762
- returnComponent(ctx);
1763
- break;
1764
- }
2300
+ break;
1765
2301
  }
1766
- else if (!done && !(ctx.f & IsInForOfLoop)) {
2302
+ else if (!getFlag(ctx.ret, IsInForAwaitOfLoop)) {
2303
+ // we have exited the for...await of, so updates will be handled by the
2304
+ // regular runComponent/enqueueComponent logic.
2305
+ break;
2306
+ }
2307
+ else if (!iteration.done) {
1767
2308
  try {
1768
- ctx.f |= IsSyncExecuting;
2309
+ setFlag(ctx.ret, IsExecuting);
1769
2310
  iterationP = ctx.iterator.next(oldResult);
1770
2311
  }
1771
2312
  finally {
1772
- ctx.f &= ~IsSyncExecuting;
2313
+ setFlag(ctx.ret, IsExecuting, false);
1773
2314
  }
1774
2315
  }
1775
- initial = false;
1776
2316
  }
1777
2317
  }
1778
2318
  finally {
1779
2319
  if (done) {
1780
- ctx.f &= -513;
2320
+ setFlag(ctx.ret, IsAsyncGen, false);
1781
2321
  ctx.iterator = undefined;
1782
2322
  }
2323
+ ctx.pull = undefined;
1783
2324
  }
1784
2325
  }
1785
- /**
1786
- * Called to resume the props async iterator for async generator components.
1787
- */
1788
- function resumePropsAsyncIterator(ctx) {
1789
- if (ctx.onProps) {
1790
- ctx.onProps(ctx.ret.el.props);
1791
- ctx.onProps = undefined;
1792
- ctx.f &= -33;
2326
+ function commitComponent(ctx, schedulePromises, hydrationNodes) {
2327
+ if (ctx.schedule) {
2328
+ ctx.schedule.promise.then(() => {
2329
+ commitComponent(ctx, []);
2330
+ propagateComponent(ctx);
2331
+ });
2332
+ return getValue(ctx.ret);
2333
+ }
2334
+ const wasScheduling = getFlag(ctx.ret, IsScheduling);
2335
+ const values = commitChildren(ctx.adapter, ctx.host, ctx, ctx.scope, ctx.ret, ctx.index, schedulePromises, hydrationNodes);
2336
+ if (getFlag(ctx.ret, IsUnmounted)) {
2337
+ return;
2338
+ }
2339
+ addEventTargetDelegates(ctx.ctx, values);
2340
+ // Execute schedule callbacks early to check for async deferral
2341
+ const callbacks = scheduleMap.get(ctx);
2342
+ let schedulePromises1;
2343
+ if (callbacks) {
2344
+ scheduleMap.delete(ctx);
2345
+ // TODO: think about error handling for schedule callbacks
2346
+ setFlag(ctx.ret, IsScheduling);
2347
+ const result = ctx.adapter.read(unwrap(values));
2348
+ for (const callback of callbacks) {
2349
+ const scheduleResult = callback(result);
2350
+ if (isPromiseLike(scheduleResult)) {
2351
+ (schedulePromises1 = schedulePromises1 || []).push(scheduleResult);
2352
+ }
2353
+ }
2354
+ if (schedulePromises1 && !getFlag(ctx.ret, DidCommit)) {
2355
+ const scheduleCallbacksP = Promise.all(schedulePromises1).then(() => {
2356
+ setFlag(ctx.ret, IsScheduling, false);
2357
+ propagateComponent(ctx);
2358
+ if (ctx.ret.fallback) {
2359
+ unmount(ctx.adapter, ctx.host, ctx.parent, ctx.ret.fallback, false);
2360
+ }
2361
+ ctx.ret.fallback = undefined;
2362
+ });
2363
+ let onAbort;
2364
+ const scheduleP = safeRace([
2365
+ scheduleCallbacksP,
2366
+ new Promise((resolve) => (onAbort = resolve)),
2367
+ ]).finally(() => {
2368
+ ctx.schedule = undefined;
2369
+ });
2370
+ ctx.schedule = { promise: scheduleP, onAbort };
2371
+ schedulePromises.push(scheduleP);
2372
+ }
2373
+ else {
2374
+ setFlag(ctx.ret, IsScheduling, wasScheduling);
2375
+ }
1793
2376
  }
1794
2377
  else {
1795
- ctx.f |= PropsAvailable;
2378
+ setFlag(ctx.ret, IsScheduling, wasScheduling);
2379
+ }
2380
+ if (!getFlag(ctx.ret, IsScheduling)) {
2381
+ if (!getFlag(ctx.ret, IsUpdating)) {
2382
+ propagateComponent(ctx);
2383
+ }
2384
+ if (ctx.ret.fallback) {
2385
+ unmount(ctx.adapter, ctx.host, ctx.parent, ctx.ret.fallback, false);
2386
+ }
2387
+ ctx.ret.fallback = undefined;
2388
+ setFlag(ctx.ret, IsUpdating, false);
1796
2389
  }
2390
+ setFlag(ctx.ret, DidCommit);
2391
+ // We always use getValue() instead of the unwrapping values because there
2392
+ // are various ways in which the values could have been updated, especially
2393
+ // if schedule callbacks call refresh() or async mounting is happening.
2394
+ return getValue(ctx.ret, true);
2395
+ }
2396
+ /**
2397
+ * Propagates component changes up to ancestors when rendering starts from a
2398
+ * component via refresh() or multiple for await...of renders. This handles
2399
+ * event listeners and DOM arrangement that would normally happen during
2400
+ * top-down rendering.
2401
+ */
2402
+ function propagateComponent(ctx) {
2403
+ const values = getChildValues(ctx.ret, ctx.index);
2404
+ addEventTargetDelegates(ctx.ctx, values, (ctx1) => ctx1[_ContextState].host === ctx.host);
2405
+ const host = ctx.host;
2406
+ const props = stripSpecialProps(host.el.props);
2407
+ ctx.adapter.arrange({
2408
+ tag: host.el.tag,
2409
+ tagName: getTagName(host.el.tag),
2410
+ node: host.value,
2411
+ props,
2412
+ oldProps: props,
2413
+ children: getChildValues(host, 0),
2414
+ });
2415
+ flush(ctx.adapter, ctx.root, ctx);
1797
2416
  }
1798
- // TODO: async unmounting
1799
- function unmountComponent(ctx) {
1800
- if (ctx.f & IsUnmounted) {
2417
+ async function unmountComponent(ctx, isNested) {
2418
+ if (getFlag(ctx.ret, IsUnmounted)) {
1801
2419
  return;
1802
2420
  }
1803
- clearEventListeners(ctx);
2421
+ let cleanupPromises;
2422
+ // TODO: think about errror handling for callbacks
1804
2423
  const callbacks = cleanupMap.get(ctx);
1805
2424
  if (callbacks) {
2425
+ const oldResult = ctx.adapter.read(getValue(ctx.ret));
1806
2426
  cleanupMap.delete(ctx);
1807
- const value = ctx.renderer.read(getValue(ctx.ret));
1808
2427
  for (const callback of callbacks) {
1809
- callback(value);
2428
+ const cleanup = callback(oldResult);
2429
+ if (isPromiseLike(cleanup)) {
2430
+ (cleanupPromises = cleanupPromises || []).push(cleanup);
2431
+ }
2432
+ }
2433
+ }
2434
+ let didLinger = false;
2435
+ if (!isNested && cleanupPromises && getChildValues(ctx.ret).length > 0) {
2436
+ didLinger = true;
2437
+ const index = ctx.index;
2438
+ const lingerers = ctx.host.lingerers || (ctx.host.lingerers = []);
2439
+ let set = lingerers[index];
2440
+ if (set == null) {
2441
+ set = new Set();
2442
+ lingerers[index] = set;
2443
+ }
2444
+ set.add(ctx.ret);
2445
+ await Promise.all(cleanupPromises);
2446
+ set.delete(ctx.ret);
2447
+ if (set.size === 0) {
2448
+ lingerers[index] = undefined;
2449
+ }
2450
+ if (!lingerers.some(Boolean)) {
2451
+ // If there are no lingerers remaining, we can remove the lingerers array
2452
+ ctx.host.lingerers = undefined;
2453
+ }
2454
+ }
2455
+ if (getFlag(ctx.ret, IsUnmounted)) {
2456
+ // If the component was unmounted while awaiting the cleanup callbacks,
2457
+ // we do not need to continue unmounting.
2458
+ return;
2459
+ }
2460
+ setFlag(ctx.ret, IsUnmounted);
2461
+ // If component has pending schedule promises, resolve them since component
2462
+ // is unmounting
2463
+ if (ctx.schedule) {
2464
+ ctx.schedule.onAbort();
2465
+ ctx.schedule = undefined;
2466
+ }
2467
+ clearEventListeners(ctx.ctx);
2468
+ unmountChildren(ctx.adapter, ctx.host, ctx, ctx.ret, isNested);
2469
+ if (didLinger) {
2470
+ // If we lingered, we call finalize to ensure rendering is finalized
2471
+ if (ctx.root != null) {
2472
+ ctx.adapter.finalize(ctx.root);
1810
2473
  }
1811
2474
  }
1812
- ctx.f |= IsUnmounted;
1813
2475
  if (ctx.iterator) {
1814
- if (ctx.f & IsSyncGen) {
1815
- let value;
1816
- if (ctx.f & IsInForOfLoop) {
1817
- value = enqueueComponentRun(ctx);
1818
- }
1819
- if (isPromiseLike(value)) {
1820
- value.then(() => {
1821
- if (ctx.f & IsInForOfLoop) {
1822
- unmountComponent(ctx);
1823
- }
1824
- else {
1825
- returnComponent(ctx);
1826
- }
1827
- }, (err) => {
1828
- if (!ctx.parent) {
1829
- throw err;
2476
+ if (ctx.pull) {
2477
+ // we let pullComponent handle unmounting
2478
+ resumePropsAsyncIterator(ctx);
2479
+ return;
2480
+ }
2481
+ // we wait for inflight value so yields resume with the most up to date
2482
+ // props
2483
+ if (ctx.inflight) {
2484
+ await ctx.inflight[1];
2485
+ }
2486
+ let iteration;
2487
+ if (getFlag(ctx.ret, IsInForOfLoop)) {
2488
+ try {
2489
+ setFlag(ctx.ret, IsExecuting);
2490
+ const oldResult = ctx.adapter.read(getValue(ctx.ret));
2491
+ const iterationP = ctx.iterator.next(oldResult);
2492
+ if (isPromiseLike(iterationP)) {
2493
+ if (!getFlag(ctx.ret, IsAsyncGen)) {
2494
+ throw new Error("Mixed generator component");
1830
2495
  }
1831
- return propagateError(ctx.parent, err);
1832
- });
1833
- }
1834
- else {
1835
- if (ctx.f & IsInForOfLoop) {
1836
- unmountComponent(ctx);
2496
+ iteration = await iterationP;
1837
2497
  }
1838
2498
  else {
1839
- returnComponent(ctx);
2499
+ if (!getFlag(ctx.ret, IsSyncGen)) {
2500
+ throw new Error("Mixed generator component");
2501
+ }
2502
+ iteration = iterationP;
1840
2503
  }
1841
2504
  }
1842
- }
1843
- else if (ctx.f & IsAsyncGen) {
1844
- if (ctx.f & IsInForOfLoop) {
1845
- const value = enqueueComponentRun(ctx);
1846
- value.then(() => {
1847
- if (ctx.f & IsInForOfLoop) {
1848
- unmountComponent(ctx);
1849
- }
1850
- else {
1851
- returnComponent(ctx);
1852
- }
1853
- }, (err) => {
1854
- if (!ctx.parent) {
1855
- throw err;
1856
- }
1857
- return propagateError(ctx.parent, err);
1858
- });
2505
+ catch (err) {
2506
+ setFlag(ctx.ret, IsErrored);
2507
+ throw err;
1859
2508
  }
1860
- else {
1861
- // The logic for unmounting async generator components is in the
1862
- // runAsyncGenComponent function.
1863
- resumePropsAsyncIterator(ctx);
2509
+ finally {
2510
+ setFlag(ctx.ret, IsExecuting, false);
1864
2511
  }
1865
2512
  }
1866
- }
1867
- }
1868
- function returnComponent(ctx) {
1869
- resumePropsAsyncIterator(ctx);
1870
- if (ctx.iterator && typeof ctx.iterator.return === "function") {
1871
- try {
1872
- ctx.f |= IsSyncExecuting;
1873
- const iteration = ctx.iterator.return();
1874
- if (isPromiseLike(iteration)) {
1875
- iteration.catch((err) => {
1876
- if (!ctx.parent) {
1877
- throw err;
2513
+ if ((!iteration || !iteration.done) &&
2514
+ ctx.iterator &&
2515
+ typeof ctx.iterator.return === "function") {
2516
+ try {
2517
+ setFlag(ctx.ret, IsExecuting);
2518
+ const iterationP = ctx.iterator.return();
2519
+ if (isPromiseLike(iterationP)) {
2520
+ if (!getFlag(ctx.ret, IsAsyncGen)) {
2521
+ throw new Error("Mixed generator component");
1878
2522
  }
1879
- return propagateError(ctx.parent, err);
1880
- });
1881
- }
1882
- }
1883
- finally {
1884
- ctx.f &= -3;
1885
- }
1886
- }
1887
- }
1888
- /*** EVENT TARGET UTILITIES ***/
1889
- // EVENT PHASE CONSTANTS
1890
- // https://developer.mozilla.org/en-US/docs/Web/API/Event/eventPhase
1891
- const NONE = 0;
1892
- const CAPTURING_PHASE = 1;
1893
- const AT_TARGET = 2;
1894
- const BUBBLING_PHASE = 3;
1895
- const listenersMap = new WeakMap();
1896
- function isListenerOrListenerObject(value) {
1897
- return (typeof value === "function" ||
1898
- (value !== null &&
1899
- typeof value === "object" &&
1900
- typeof value.handleEvent === "function"));
1901
- }
1902
- function normalizeListenerOptions(options) {
1903
- if (typeof options === "boolean") {
1904
- return { capture: options };
1905
- }
1906
- else if (options == null) {
1907
- return {};
1908
- }
1909
- return options;
1910
- }
1911
- function isEventTarget(value) {
1912
- return (value != null &&
1913
- typeof value.addEventListener === "function" &&
1914
- typeof value.removeEventListener === "function" &&
1915
- typeof value.dispatchEvent === "function");
1916
- }
1917
- function setEventProperty(ev, key, value) {
1918
- Object.defineProperty(ev, key, { value, writable: false, configurable: true });
1919
- }
1920
- // TODO: Maybe we can pass in the current context directly, rather than
1921
- // starting from the parent?
1922
- /**
1923
- * A function to reconstruct an array of every listener given a context and a
1924
- * host element.
1925
- *
1926
- * This function exploits the fact that contexts retain their nearest ancestor
1927
- * host element. We can determine all the contexts which are directly listening
1928
- * to an element by traversing up the context tree and checking that the host
1929
- * element passed in matches the parent context’s host element.
1930
- */
1931
- function getListenerRecords(ctx, ret) {
1932
- let listeners = [];
1933
- while (ctx !== undefined && ctx.host === ret) {
1934
- const listeners1 = listenersMap.get(ctx);
1935
- if (listeners1) {
1936
- listeners = listeners.concat(listeners1);
1937
- }
1938
- ctx = ctx.parent;
1939
- }
1940
- return listeners;
1941
- }
1942
- function clearEventListeners(ctx) {
1943
- const listeners = listenersMap.get(ctx);
1944
- if (listeners && listeners.length) {
1945
- for (const value of getChildValues(ctx.ret)) {
1946
- if (isEventTarget(value)) {
1947
- for (const record of listeners) {
1948
- value.removeEventListener(record.type, record.callback, record.options);
2523
+ iteration = await iterationP;
1949
2524
  }
2525
+ else {
2526
+ if (!getFlag(ctx.ret, IsSyncGen)) {
2527
+ throw new Error("Mixed generator component");
2528
+ }
2529
+ iteration = iterationP;
2530
+ }
2531
+ }
2532
+ catch (err) {
2533
+ setFlag(ctx.ret, IsErrored);
2534
+ throw err;
2535
+ }
2536
+ finally {
2537
+ setFlag(ctx.ret, IsExecuting, false);
1950
2538
  }
1951
2539
  }
1952
- listeners.length = 0;
1953
2540
  }
1954
2541
  }
1955
2542
  /*** ERROR HANDLING UTILITIES ***/
1956
2543
  function handleChildError(ctx, err) {
1957
- if (!ctx.iterator || typeof ctx.iterator.throw !== "function") {
2544
+ if (!ctx.iterator) {
2545
+ throw err;
2546
+ }
2547
+ if (ctx.pull) {
2548
+ // we let pullComponent handle child errors
2549
+ ctx.pull.onChildError(err);
2550
+ return ctx.pull.diff;
2551
+ }
2552
+ if (!ctx.iterator.throw) {
1958
2553
  throw err;
1959
2554
  }
1960
2555
  resumePropsAsyncIterator(ctx);
1961
2556
  let iteration;
1962
2557
  try {
1963
- ctx.f |= IsSyncExecuting;
2558
+ setFlag(ctx.ret, IsExecuting);
1964
2559
  iteration = ctx.iterator.throw(err);
1965
2560
  }
1966
2561
  catch (err) {
1967
- ctx.f |= IsErrored;
2562
+ setFlag(ctx.ret, IsErrored);
1968
2563
  throw err;
1969
2564
  }
1970
2565
  finally {
1971
- ctx.f &= -3;
2566
+ setFlag(ctx.ret, IsExecuting, false);
1972
2567
  }
1973
2568
  if (isPromiseLike(iteration)) {
1974
2569
  return iteration.then((iteration) => {
1975
2570
  if (iteration.done) {
1976
- ctx.f &= -513;
2571
+ setFlag(ctx.ret, IsSyncGen, false);
2572
+ setFlag(ctx.ret, IsAsyncGen, false);
1977
2573
  ctx.iterator = undefined;
1978
2574
  }
1979
- return updateComponentChildren(ctx, iteration.value);
2575
+ return diffComponentChildren(ctx, iteration.value, !iteration.done);
1980
2576
  }, (err) => {
1981
- ctx.f |= IsErrored;
2577
+ setFlag(ctx.ret, IsErrored);
1982
2578
  throw err;
1983
2579
  });
1984
2580
  }
1985
2581
  if (iteration.done) {
1986
- ctx.f &= -257;
1987
- ctx.f &= -513;
2582
+ setFlag(ctx.ret, IsSyncGen, false);
2583
+ setFlag(ctx.ret, IsAsyncGen, false);
1988
2584
  ctx.iterator = undefined;
1989
2585
  }
1990
- return updateComponentChildren(ctx, iteration.value);
2586
+ return diffComponentChildren(ctx, iteration.value, !iteration.done);
1991
2587
  }
1992
- function propagateError(ctx, err) {
1993
- let result;
2588
+ /**
2589
+ * Propagates an error up the context tree by calling handleChildError with
2590
+ * each parent.
2591
+ *
2592
+ * @returns A promise which resolves to undefined when the error has been
2593
+ * handled, or undefined if the error was handled synchronously.
2594
+ */
2595
+ function propagateError(ctx, err, schedulePromises) {
2596
+ const parent = ctx.parent;
2597
+ if (!parent) {
2598
+ throw err;
2599
+ }
2600
+ let diff;
1994
2601
  try {
1995
- result = handleChildError(ctx, err);
2602
+ diff = handleChildError(parent, err);
1996
2603
  }
1997
2604
  catch (err) {
1998
- if (!ctx.parent) {
1999
- throw err;
2000
- }
2001
- return propagateError(ctx.parent, err);
2605
+ return propagateError(parent, err, schedulePromises);
2002
2606
  }
2003
- if (isPromiseLike(result)) {
2004
- return result.catch((err) => {
2005
- if (!ctx.parent) {
2006
- throw err;
2007
- }
2008
- return propagateError(ctx.parent, err);
2009
- });
2607
+ if (isPromiseLike(diff)) {
2608
+ return diff.then(() => void commitComponent(parent, schedulePromises), (err) => propagateError(parent, err, schedulePromises));
2010
2609
  }
2011
- return result;
2610
+ commitComponent(parent, schedulePromises);
2611
+ }
2612
+
2613
+ /**
2614
+ * CSS utility functions for style property transformation.
2615
+ *
2616
+ * This module handles camelCase to kebab-case conversion and automatic
2617
+ * px unit conversion for numeric CSS values, making Crank more React-compatible.
2618
+ */
2619
+ /**
2620
+ * Converts camelCase CSS property names to kebab-case.
2621
+ * Handles vendor prefixes correctly (WebkitTransform -> -webkit-transform).
2622
+ */
2623
+ function camelToKebabCase(str) {
2624
+ // Handle vendor prefixes that start with capital letters (WebkitTransform -> -webkit-transform)
2625
+ if (/^[A-Z]/.test(str)) {
2626
+ return `-${str.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`).slice(1)}`;
2627
+ }
2628
+ // Handle normal camelCase (fontSize -> font-size)
2629
+ return str.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`);
2630
+ }
2631
+ /**
2632
+ * CSS properties that should remain unitless when given numeric values.
2633
+ * Based on React's list of unitless properties.
2634
+ */
2635
+ const UNITLESS_PROPERTIES = new Set([
2636
+ "animation-iteration-count",
2637
+ "aspect-ratio",
2638
+ "border-image-outset",
2639
+ "border-image-slice",
2640
+ "border-image-width",
2641
+ "box-flex",
2642
+ "box-flex-group",
2643
+ "box-ordinal-group",
2644
+ "column-count",
2645
+ "columns",
2646
+ "flex",
2647
+ "flex-grow",
2648
+ "flex-positive",
2649
+ "flex-shrink",
2650
+ "flex-negative",
2651
+ "flex-order",
2652
+ "font-weight",
2653
+ "grid-area",
2654
+ "grid-column",
2655
+ "grid-column-end",
2656
+ "grid-column-span",
2657
+ "grid-column-start",
2658
+ "grid-row",
2659
+ "grid-row-end",
2660
+ "grid-row-span",
2661
+ "grid-row-start",
2662
+ "line-height",
2663
+ "opacity",
2664
+ "order",
2665
+ "orphans",
2666
+ "tab-size",
2667
+ "widows",
2668
+ "z-index",
2669
+ "zoom",
2670
+ ]);
2671
+ /**
2672
+ * Formats CSS property values, automatically adding "px" to numeric values
2673
+ * for properties that are not unitless.
2674
+ */
2675
+ function formatStyleValue(name, value) {
2676
+ if (typeof value === "number") {
2677
+ // If the property should remain unitless, keep the number as-is
2678
+ if (UNITLESS_PROPERTIES.has(name)) {
2679
+ return String(value);
2680
+ }
2681
+ // Otherwise, append "px" for numeric values
2682
+ return `${value}px`;
2683
+ }
2684
+ return String(value);
2012
2685
  }
2013
2686
 
2014
2687
  const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
2015
- const impl$1 = {
2016
- scope(xmlns, tag, props) {
2688
+ const MATHML_NAMESPACE = "http://www.w3.org/1998/Math/MathML";
2689
+ function isWritableProperty(element, name) {
2690
+ // walk up the object's prototype chain to find the owner
2691
+ let propOwner = element;
2692
+ do {
2693
+ if (Object.prototype.hasOwnProperty.call(propOwner, name)) {
2694
+ break;
2695
+ }
2696
+ } while ((propOwner = Object.getPrototypeOf(propOwner)));
2697
+ if (propOwner === null) {
2698
+ return false;
2699
+ }
2700
+ // get the descriptor for the named property and check whether it implies
2701
+ // that the property is writable
2702
+ const descriptor = Object.getOwnPropertyDescriptor(propOwner, name);
2703
+ if (descriptor != null &&
2704
+ (descriptor.writable === true || descriptor.set !== undefined)) {
2705
+ return true;
2706
+ }
2707
+ return false;
2708
+ }
2709
+ function emitHydrationWarning(propName, quietProps, expectedValue, actualValue, element, displayName) {
2710
+ const checkName = propName;
2711
+ const showName = displayName || propName;
2712
+ if (!quietProps || !quietProps.has(checkName)) {
2713
+ if (expectedValue === null || expectedValue === false) {
2714
+ console.warn(`Expected "${showName}" to be missing but found ${String(actualValue)} while hydrating:`, element);
2715
+ }
2716
+ else if (expectedValue === true || expectedValue === "") {
2717
+ console.warn(`Expected "${showName}" to be ${expectedValue === true ? "present" : '""'} but found ${String(actualValue)} while hydrating:`, element);
2718
+ }
2719
+ else if (typeof window !== "undefined" &&
2720
+ window.location &&
2721
+ new URL(expectedValue, window.location.origin).href ===
2722
+ new URL(actualValue, window.location.origin).href) ;
2723
+ else {
2724
+ console.warn(`Expected "${showName}" to be "${String(expectedValue)}" but found ${String(actualValue)} while hydrating:`, element);
2725
+ }
2726
+ }
2727
+ }
2728
+ const adapter = {
2729
+ scope({ scope: xmlns, tag, props, }) {
2017
2730
  switch (tag) {
2018
2731
  case Portal:
2732
+ // TODO: read the namespace from the portal root element
2019
2733
  xmlns = undefined;
2020
2734
  break;
2021
2735
  case "svg":
2022
2736
  xmlns = SVG_NAMESPACE;
2023
2737
  break;
2738
+ case "math":
2739
+ xmlns = MATHML_NAMESPACE;
2740
+ break;
2024
2741
  }
2025
2742
  return props.xmlns || xmlns;
2026
2743
  },
2027
- create(tag, _props, xmlns) {
2744
+ create({ tag, tagName, scope: xmlns, }) {
2028
2745
  if (typeof tag !== "string") {
2029
- throw new Error(`Unknown tag: ${tag.toString()}`);
2746
+ throw new Error(`Unknown tag: ${tagName}`);
2030
2747
  }
2031
2748
  else if (tag.toLowerCase() === "svg") {
2032
2749
  xmlns = SVG_NAMESPACE;
2033
2750
  }
2751
+ else if (tag.toLowerCase() === "math") {
2752
+ xmlns = MATHML_NAMESPACE;
2753
+ }
2034
2754
  return xmlns
2035
2755
  ? document.createElementNS(xmlns, tag)
2036
2756
  : document.createElement(tag);
2037
2757
  },
2038
- hydrate(tag, node, props) {
2758
+ adopt({ tag, tagName, node, }) {
2039
2759
  if (typeof tag !== "string" && tag !== Portal) {
2040
- throw new Error(`Unknown tag: ${tag.toString()}`);
2760
+ throw new Error(`Unknown tag: ${tagName}`);
2761
+ }
2762
+ if (node === document.body ||
2763
+ node === document.head ||
2764
+ node === document.documentElement ||
2765
+ node === document) {
2766
+ console.warn(`Hydrating ${node.nodeName.toLowerCase()} is discouraged as it is destructive and may remove unknown nodes.`);
2767
+ }
2768
+ if (node == null ||
2769
+ (typeof tag === "string" &&
2770
+ (node.nodeType !== Node.ELEMENT_NODE ||
2771
+ tag.toLowerCase() !== node.tagName.toLowerCase()))) {
2772
+ console.warn(`Expected <${tagName}> while hydrating but found: `, node);
2773
+ return;
2041
2774
  }
2042
- if (typeof tag === "string" &&
2043
- tag.toUpperCase() !== node.tagName) {
2044
- // TODO: consider pros and cons of hydration warnings
2045
- //console.error(`Expected <${tag}> while hydrating but found:`, node);
2046
- return undefined;
2775
+ return Array.from(node.childNodes);
2776
+ },
2777
+ patch({ tagName, node, props, oldProps, scope: xmlns, copyProps, quietProps, isHydrating, }) {
2778
+ if (node.nodeType !== Node.ELEMENT_NODE) {
2779
+ throw new TypeError(`Cannot patch node: ${String(node)}`);
2047
2780
  }
2048
- const children = [];
2049
- for (let i = 0; i < node.childNodes.length; i++) {
2050
- const child = node.childNodes[i];
2051
- if (child.nodeType === Node.TEXT_NODE) {
2052
- children.push(child.data);
2053
- }
2054
- else if (child.nodeType === Node.ELEMENT_NODE) {
2055
- children.push(child);
2056
- }
2781
+ else if (props.class && props.className) {
2782
+ console.error(`Both "class" and "className" set in props for <${tagName}>. Use one or the other.`);
2057
2783
  }
2058
- // TODO: extract props from nodes
2059
- return { props, children };
2060
- },
2061
- patch(_tag,
2062
- // TODO: Why does this assignment work?
2063
- node, name,
2064
- // TODO: Stricter typings?
2065
- value, oldValue, xmlns) {
2784
+ const element = node;
2066
2785
  const isSVG = xmlns === SVG_NAMESPACE;
2067
- const colonIndex = name.indexOf(":");
2068
- if (colonIndex !== -1) {
2069
- const ns = name.slice(0, colonIndex);
2070
- const name1 = name.slice(colonIndex + 1);
2071
- switch (ns) {
2072
- case "prop":
2073
- node[name1] = value;
2074
- return;
2075
- case "attr":
2786
+ const isMathML = xmlns === MATHML_NAMESPACE;
2787
+ for (let name in { ...oldProps, ...props }) {
2788
+ let value = props[name];
2789
+ const oldValue = oldProps ? oldProps[name] : undefined;
2790
+ {
2791
+ if (copyProps != null && copyProps.has(name)) {
2792
+ continue;
2793
+ }
2794
+ // handle prop:name or attr:name properties
2795
+ const colonIndex = name.indexOf(":");
2796
+ if (colonIndex !== -1) {
2797
+ const [ns, name1] = [
2798
+ name.slice(0, colonIndex),
2799
+ name.slice(colonIndex + 1),
2800
+ ];
2801
+ switch (ns) {
2802
+ case "prop":
2803
+ node[name1] = value;
2804
+ continue;
2805
+ case "attr":
2806
+ if (value == null || value === false) {
2807
+ if (isHydrating && element.hasAttribute(name1)) {
2808
+ emitHydrationWarning(name, quietProps, value, element.getAttribute(name1), element);
2809
+ }
2810
+ element.removeAttribute(name1);
2811
+ }
2812
+ else if (value === true) {
2813
+ if (isHydrating && !element.hasAttribute(name1)) {
2814
+ emitHydrationWarning(name, quietProps, value, null, element);
2815
+ }
2816
+ element.setAttribute(name1, "");
2817
+ }
2818
+ else if (typeof value !== "string") {
2819
+ value = String(value);
2820
+ }
2821
+ if (isHydrating && element.getAttribute(name1) !== value) {
2822
+ emitHydrationWarning(name, quietProps, value, element.getAttribute(name1), element);
2823
+ }
2824
+ element.setAttribute(name1, String(value));
2825
+ continue;
2826
+ }
2827
+ }
2828
+ }
2829
+ switch (name) {
2830
+ // TODO: fix hydration warnings for the style prop
2831
+ case "style": {
2832
+ const style = element.style;
2076
2833
  if (value == null || value === false) {
2077
- node.removeAttribute(name1);
2834
+ if (isHydrating && style.cssText !== "") {
2835
+ emitHydrationWarning(name, quietProps, value, style.cssText, element);
2836
+ }
2837
+ element.removeAttribute("style");
2078
2838
  }
2079
2839
  else if (value === true) {
2080
- node.setAttribute(name1, "");
2840
+ if (isHydrating && style.cssText !== "") {
2841
+ emitHydrationWarning(name, quietProps, "", style.cssText, element);
2842
+ }
2843
+ element.setAttribute("style", "");
2081
2844
  }
2082
2845
  else if (typeof value === "string") {
2083
- node.setAttribute(name1, value);
2846
+ if (style.cssText !== value) {
2847
+ // TODO: Fix hydration warnings for styles
2848
+ //if (isHydrating) {
2849
+ // emitHydrationWarning(
2850
+ // name,
2851
+ // quietProps,
2852
+ // value,
2853
+ // style.cssText,
2854
+ // element,
2855
+ // );
2856
+ //}
2857
+ style.cssText = value;
2858
+ }
2084
2859
  }
2085
2860
  else {
2086
- node.setAttribute(name, String(value));
2861
+ if (typeof oldValue === "string") {
2862
+ // if the old value was a string, we need to clear the style
2863
+ // TODO: only clear the styles enumerated in the old value
2864
+ style.cssText = "";
2865
+ }
2866
+ for (const styleName in { ...oldValue, ...value }) {
2867
+ const cssName = camelToKebabCase(styleName);
2868
+ const styleValue = value && value[styleName];
2869
+ if (styleValue == null) {
2870
+ if (isHydrating && style.getPropertyValue(cssName) !== "") {
2871
+ emitHydrationWarning(name, quietProps, null, style.getPropertyValue(cssName), element, `style.${styleName}`);
2872
+ }
2873
+ style.removeProperty(cssName);
2874
+ }
2875
+ else {
2876
+ const formattedValue = formatStyleValue(cssName, styleValue);
2877
+ if (style.getPropertyValue(cssName) !== formattedValue) {
2878
+ // TODO: hydration warnings for style props
2879
+ //if (isHydrating) {
2880
+ // emitHydrationWarning(
2881
+ // name,
2882
+ // quietProps,
2883
+ // formattedValue,
2884
+ // style.getPropertyValue(cssName),
2885
+ // element,
2886
+ // `style.${styleName}`,
2887
+ // );
2888
+ //}
2889
+ style.setProperty(cssName, formattedValue);
2890
+ }
2891
+ }
2892
+ }
2087
2893
  }
2088
- return;
2089
- }
2090
- }
2091
- switch (name) {
2092
- case "style": {
2093
- const style = node.style;
2094
- if (style == null) {
2095
- node.setAttribute("style", value);
2096
- }
2097
- else if (value == null || value === false) {
2098
- node.removeAttribute("style");
2099
- }
2100
- else if (value === true) {
2101
- node.setAttribute("style", "");
2894
+ break;
2102
2895
  }
2103
- else if (typeof value === "string") {
2104
- if (style.cssText !== value) {
2105
- style.cssText = value;
2896
+ case "class":
2897
+ case "className":
2898
+ if (value === true) {
2899
+ if (isHydrating && element.getAttribute("class") !== "") {
2900
+ emitHydrationWarning(name, quietProps, "", element.getAttribute("class"), element);
2901
+ }
2902
+ element.setAttribute("class", "");
2106
2903
  }
2107
- }
2108
- else {
2109
- if (typeof oldValue === "string") {
2110
- style.cssText = "";
2904
+ else if (value == null) {
2905
+ if (isHydrating && element.hasAttribute("class")) {
2906
+ emitHydrationWarning(name, quietProps, value, element.getAttribute("class"), element);
2907
+ }
2908
+ element.removeAttribute("class");
2111
2909
  }
2112
- for (const styleName in { ...oldValue, ...value }) {
2113
- const styleValue = value && value[styleName];
2114
- if (styleValue == null) {
2115
- style.removeProperty(styleName);
2910
+ else if (typeof value === "object") {
2911
+ // class={{"included-class": true, "excluded-class": false}} syntax
2912
+ if (typeof oldValue === "string") {
2913
+ // if the old value was a string, we need to clear all classes
2914
+ element.setAttribute("class", "");
2915
+ }
2916
+ let shouldIssueWarning = false;
2917
+ const hydratingClasses = isHydrating
2918
+ ? new Set(Array.from(element.classList))
2919
+ : undefined;
2920
+ const hydratingClassName = isHydrating
2921
+ ? element.getAttribute("class")
2922
+ : undefined;
2923
+ for (const className in { ...oldValue, ...value }) {
2924
+ const classValue = value && value[className];
2925
+ if (classValue) {
2926
+ element.classList.add(className);
2927
+ if (hydratingClasses && hydratingClasses.has(className)) {
2928
+ hydratingClasses.delete(className);
2929
+ }
2930
+ else if (isHydrating) {
2931
+ shouldIssueWarning = true;
2932
+ }
2933
+ }
2934
+ else {
2935
+ element.classList.remove(className);
2936
+ }
2116
2937
  }
2117
- else if (style.getPropertyValue(styleName) !== styleValue) {
2118
- style.setProperty(styleName, styleValue);
2938
+ if (shouldIssueWarning ||
2939
+ (hydratingClasses && hydratingClasses.size > 0)) {
2940
+ emitHydrationWarning(name, quietProps, Object.keys(value)
2941
+ .filter((k) => value[k])
2942
+ .join(" "), hydratingClassName || "", element);
2119
2943
  }
2120
2944
  }
2121
- }
2122
- break;
2123
- }
2124
- case "class":
2125
- case "className":
2126
- if (value === true) {
2127
- node.setAttribute("class", "");
2128
- }
2129
- else if (value == null) {
2130
- node.removeAttribute("class");
2131
- }
2132
- else if (!isSVG) {
2133
- if (node.className !== value) {
2134
- node["className"] = value;
2945
+ else if (!isSVG && !isMathML) {
2946
+ if (element.className !== value) {
2947
+ if (isHydrating) {
2948
+ emitHydrationWarning(name, quietProps, value, element.className, element);
2949
+ }
2950
+ element.className = value;
2951
+ }
2135
2952
  }
2136
- }
2137
- else if (node.getAttribute("class") !== value) {
2138
- node.setAttribute("class", value);
2139
- }
2140
- break;
2141
- case "innerHTML":
2142
- if (value !== oldValue) {
2143
- node.innerHTML = value;
2144
- }
2145
- break;
2146
- default: {
2147
- if (name[0] === "o" &&
2148
- name[1] === "n" &&
2149
- name[2] === name[2].toUpperCase() &&
2150
- typeof value === "function") {
2151
- // Support React-style event names (onClick, onChange, etc.)
2152
- name = name.toLowerCase();
2153
- }
2154
- if (name in node &&
2155
- // boolean properties will coerce strings, but sometimes they map to
2156
- // enumerated attributes, where truthy strings ("false", "no") map to
2157
- // falsy properties, so we use attributes in this case.
2158
- !(typeof value === "string" &&
2159
- typeof node[name] === "boolean")) {
2160
- // walk up the object's prototype chain to find the owner of the
2161
- // named property
2162
- let obj = node;
2163
- do {
2164
- if (Object.prototype.hasOwnProperty.call(obj, name)) {
2165
- break;
2953
+ else if (element.getAttribute("class") !== value) {
2954
+ if (isHydrating) {
2955
+ emitHydrationWarning(name, quietProps, value, element.getAttribute("class"), element);
2166
2956
  }
2167
- } while ((obj = Object.getPrototypeOf(obj)));
2168
- // get the descriptor for the named property and check whether it
2169
- // implies that the property is writable
2170
- const descriptor = Object.getOwnPropertyDescriptor(obj, name);
2171
- if (descriptor != null &&
2172
- (descriptor.writable === true || descriptor.set !== undefined)) {
2173
- if (node[name] !== value || oldValue === undefined) {
2174
- node[name] = value;
2957
+ element.setAttribute("class", value);
2958
+ }
2959
+ break;
2960
+ case "innerHTML":
2961
+ if (value !== oldValue) {
2962
+ if (isHydrating) {
2963
+ emitHydrationWarning(name, quietProps, value, element.innerHTML, element);
2175
2964
  }
2176
- return;
2965
+ element.innerHTML = value;
2177
2966
  }
2178
- // if the property wasn't writable, fall through to the code below
2179
- // which uses setAttribute() instead of assigning directly.
2180
- }
2181
- if (value === true) {
2182
- value = "";
2183
- }
2184
- else if (value == null || value === false) {
2185
- node.removeAttribute(name);
2186
- return;
2187
- }
2188
- if (node.getAttribute(name) !== value) {
2189
- node.setAttribute(name, value);
2190
- }
2191
- }
2192
- }
2193
- },
2194
- arrange(tag, node, props, children, _oldProps, oldChildren) {
2195
- if (tag === Portal && (node == null || typeof node.nodeType !== "number")) {
2196
- throw new TypeError(`Portal root is not a node. Received: ${JSON.stringify(node && node.toString())}`);
2197
- }
2198
- if (!("innerHTML" in props) &&
2199
- // We don’t want to update elements without explicit children (<div/>),
2200
- // because these elements sometimes have child nodes added via raw
2201
- // DOM manipulations.
2202
- // However, if an element has previously rendered children, we clear the
2203
- // them because it would be surprising not to clear Crank managed
2204
- // children, even if the new element does not have explicit children.
2205
- ("children" in props || (oldChildren && oldChildren.length))) {
2206
- if (children.length === 0) {
2207
- node.textContent = "";
2208
- }
2209
- else {
2210
- let oldChild = node.firstChild;
2211
- let i = 0;
2212
- while (oldChild !== null && i < children.length) {
2213
- const newChild = children[i];
2214
- if (oldChild === newChild) {
2215
- oldChild = oldChild.nextSibling;
2216
- i++;
2967
+ break;
2968
+ default: {
2969
+ if (name[0] === "o" &&
2970
+ name[1] === "n" &&
2971
+ name[2] === name[2].toUpperCase() &&
2972
+ typeof value === "function") {
2973
+ // Support React-style event names (onClick, onChange, etc.)
2974
+ name = name.toLowerCase();
2217
2975
  }
2218
- else if (typeof newChild === "string") {
2219
- if (oldChild.nodeType === Node.TEXT_NODE) {
2220
- if (oldChild.data !== newChild) {
2221
- oldChild.data = newChild;
2976
+ // try to set the property directly
2977
+ if (name in element &&
2978
+ // boolean properties will coerce strings, but sometimes they map to
2979
+ // enumerated attributes, where truthy strings ("false", "no") map to
2980
+ // falsy properties, so we force using setAttribute.
2981
+ !(typeof value === "string" &&
2982
+ typeof element[name] === "boolean") &&
2983
+ isWritableProperty(element, name)) {
2984
+ if (element[name] !== value || oldValue === undefined) {
2985
+ if (isHydrating &&
2986
+ typeof element[name] === "string" &&
2987
+ element[name] !== value) {
2988
+ emitHydrationWarning(name, quietProps, value, element[name], element);
2222
2989
  }
2223
- oldChild = oldChild.nextSibling;
2990
+ // if the property is writable, assign it directly
2991
+ element[name] = value;
2224
2992
  }
2225
- else {
2226
- node.insertBefore(document.createTextNode(newChild), oldChild);
2993
+ continue;
2994
+ }
2995
+ if (value === true) {
2996
+ value = "";
2997
+ }
2998
+ else if (value == null || value === false) {
2999
+ if (isHydrating && element.hasAttribute(name)) {
3000
+ emitHydrationWarning(name, quietProps, value, element.getAttribute(name), element);
2227
3001
  }
2228
- i++;
3002
+ element.removeAttribute(name);
3003
+ continue;
2229
3004
  }
2230
- else if (oldChild.nodeType === Node.TEXT_NODE) {
2231
- const nextSibling = oldChild.nextSibling;
2232
- node.removeChild(oldChild);
2233
- oldChild = nextSibling;
3005
+ else if (typeof value !== "string") {
3006
+ value = String(value);
2234
3007
  }
2235
- else {
2236
- node.insertBefore(newChild, oldChild);
2237
- i++;
2238
- // TODO: This is an optimization but we need to think a little more about other cases like prepending.
2239
- if (oldChild !== children[i]) {
2240
- const nextSibling = oldChild.nextSibling;
2241
- node.removeChild(oldChild);
2242
- oldChild = nextSibling;
3008
+ if (element.getAttribute(name) !== value) {
3009
+ if (isHydrating) {
3010
+ emitHydrationWarning(name, quietProps, value, element.getAttribute(name), element);
2243
3011
  }
3012
+ element.setAttribute(name, value);
2244
3013
  }
2245
3014
  }
2246
- // remove excess DOM nodes
2247
- while (oldChild !== null) {
2248
- const nextSibling = oldChild.nextSibling;
2249
- node.removeChild(oldChild);
2250
- oldChild = nextSibling;
3015
+ }
3016
+ }
3017
+ },
3018
+ arrange({ tag, node, props, children, }) {
3019
+ if (tag === Portal && (node == null || typeof node.nodeType !== "number")) {
3020
+ throw new TypeError(`<Portal> root is not a node. Received: ${String(node)}`);
3021
+ }
3022
+ if (!("innerHTML" in props)) {
3023
+ let oldChild = node.firstChild;
3024
+ for (let i = 0; i < children.length; i++) {
3025
+ const newChild = children[i];
3026
+ if (oldChild === newChild) {
3027
+ // the child is already in the right place, so we can skip it
3028
+ oldChild = oldChild.nextSibling;
2251
3029
  }
2252
- // append excess children
2253
- for (; i < children.length; i++) {
2254
- const newChild = children[i];
2255
- node.appendChild(typeof newChild === "string"
2256
- ? document.createTextNode(newChild)
2257
- : newChild);
3030
+ else {
3031
+ node.insertBefore(newChild, oldChild);
3032
+ if (tag !== Portal &&
3033
+ oldChild &&
3034
+ i + 1 < children.length &&
3035
+ oldChild !== children[i + 1]) {
3036
+ oldChild = oldChild.nextSibling;
3037
+ }
2258
3038
  }
2259
3039
  }
2260
3040
  }
2261
3041
  },
2262
- text(text, _scope, hydrationData) {
2263
- if (hydrationData != null) {
2264
- let value = hydrationData.children.shift();
2265
- if (typeof value !== "string" || !value.startsWith(text)) ;
2266
- else if (text.length < value.length) {
2267
- value = value.slice(text.length);
2268
- hydrationData.children.unshift(value);
3042
+ remove({ node, parentNode, isNested, }) {
3043
+ if (!isNested && node.parentNode === parentNode) {
3044
+ parentNode.removeChild(node);
3045
+ }
3046
+ },
3047
+ text({ value, oldNode, hydrationNodes, }) {
3048
+ if (hydrationNodes != null) {
3049
+ let node = hydrationNodes.shift();
3050
+ if (!node || node.nodeType !== Node.TEXT_NODE) {
3051
+ console.warn(`Expected "${value}" while hydrating but found:`, node);
2269
3052
  }
3053
+ else {
3054
+ // value is a text node, check if it matches the expected text
3055
+ const textData = node.data;
3056
+ if (textData.length > value.length) {
3057
+ if (textData.startsWith(value)) {
3058
+ // the text node is longer than the expected text, so we
3059
+ // reuse the existing text node, but truncate it and unshift the rest
3060
+ node.data = value;
3061
+ hydrationNodes.unshift(document.createTextNode(textData.slice(value.length)));
3062
+ return node;
3063
+ }
3064
+ }
3065
+ else if (textData === value) {
3066
+ return node;
3067
+ }
3068
+ // We log textData and not node because node will be mutated
3069
+ console.warn(`Expected "${value}" while hydrating but found:`, textData);
3070
+ oldNode = node;
3071
+ }
3072
+ }
3073
+ if (oldNode != null) {
3074
+ if (oldNode.data !== value) {
3075
+ oldNode.data = value;
3076
+ }
3077
+ return oldNode;
2270
3078
  }
2271
- return text;
3079
+ return document.createTextNode(value);
2272
3080
  },
2273
- raw(value, xmlns, hydrationData) {
2274
- let result;
3081
+ raw({ value, scope: xmlns, hydrationNodes, }) {
3082
+ let nodes;
2275
3083
  if (typeof value === "string") {
2276
3084
  const el = xmlns == null
2277
3085
  ? document.createElement("div")
2278
- : document.createElementNS(xmlns, "svg");
3086
+ : xmlns === SVG_NAMESPACE
3087
+ ? document.createElementNS(xmlns, "svg")
3088
+ : document.createElementNS(xmlns, "math");
2279
3089
  el.innerHTML = value;
2280
- if (el.childNodes.length === 0) {
2281
- result = undefined;
2282
- }
2283
- else if (el.childNodes.length === 1) {
2284
- result = el.childNodes[0];
2285
- }
2286
- else {
2287
- result = Array.from(el.childNodes);
2288
- }
3090
+ nodes = Array.from(el.childNodes);
2289
3091
  }
2290
3092
  else {
2291
- result = value;
2292
- }
2293
- if (hydrationData != null) {
2294
- // TODO: maybe we should warn on incorrect values
2295
- if (Array.isArray(result)) {
2296
- for (let i = 0; i < result.length; i++) {
2297
- const node = result[i];
2298
- if (typeof node !== "string" &&
2299
- (node.nodeType === Node.ELEMENT_NODE ||
2300
- node.nodeType === Node.TEXT_NODE)) {
2301
- hydrationData.children.shift();
2302
- }
3093
+ nodes = value == null ? [] : Array.isArray(value) ? [...value] : [value];
3094
+ }
3095
+ if (hydrationNodes != null) {
3096
+ for (let i = 0; i < nodes.length; i++) {
3097
+ const node = nodes[i];
3098
+ // check if node is equal to the next node in the hydration array
3099
+ const hydrationNode = hydrationNodes.shift();
3100
+ if (hydrationNode &&
3101
+ typeof hydrationNode === "object" &&
3102
+ typeof hydrationNode.nodeType === "number" &&
3103
+ node.isEqualNode(hydrationNode)) {
3104
+ nodes[i] = hydrationNode;
2303
3105
  }
2304
- }
2305
- else if (result != null && typeof result !== "string") {
2306
- if (result.nodeType === Node.ELEMENT_NODE ||
2307
- result.nodeType === Node.TEXT_NODE) {
2308
- hydrationData.children.shift();
3106
+ else {
3107
+ console.warn(`Expected <Raw value="${String(value)}"> while hydrating but found:`, hydrationNode);
2309
3108
  }
2310
3109
  }
2311
3110
  }
2312
- return result;
3111
+ return nodes.length === 0
3112
+ ? undefined
3113
+ : nodes.length === 1
3114
+ ? nodes[0]
3115
+ : nodes;
2313
3116
  },
2314
3117
  };
2315
3118
  class DOMRenderer extends Renderer {
2316
3119
  constructor() {
2317
- super(impl$1);
3120
+ super(adapter);
2318
3121
  }
2319
3122
  render(children, root, ctx) {
2320
3123
  validateRoot(root);
@@ -2326,9 +3129,12 @@
2326
3129
  }
2327
3130
  }
2328
3131
  function validateRoot(root) {
2329
- if (root === null ||
3132
+ if (root == null ||
2330
3133
  (typeof root === "object" && typeof root.nodeType !== "number")) {
2331
- throw new TypeError(`Render root is not a node. Received: ${JSON.stringify(root && root.toString())}`);
3134
+ throw new TypeError(`Render root is not a node. Received: ${String(root)}`);
3135
+ }
3136
+ else if (root.nodeType !== Node.ELEMENT_NODE) {
3137
+ throw new TypeError(`Render root must be an element node. Received: ${String(root)}`);
2332
3138
  }
2333
3139
  }
2334
3140
  const renderer$1 = new DOMRenderer();
@@ -2336,7 +3142,7 @@
2336
3142
  var dom = /*#__PURE__*/Object.freeze({
2337
3143
  __proto__: null,
2338
3144
  DOMRenderer: DOMRenderer,
2339
- impl: impl$1,
3145
+ adapter: adapter,
2340
3146
  renderer: renderer$1
2341
3147
  });
2342
3148
 
@@ -2380,7 +3186,9 @@
2380
3186
  const cssStrings = [];
2381
3187
  for (const [name, value] of Object.entries(style)) {
2382
3188
  if (value != null) {
2383
- cssStrings.push(`${name}:${value};`);
3189
+ const cssName = camelToKebabCase(name);
3190
+ const cssValue = formatStyleValue(cssName, value);
3191
+ cssStrings.push(`${cssName}:${cssValue};`);
2384
3192
  }
2385
3193
  }
2386
3194
  return cssStrings.join("");
@@ -2388,31 +3196,14 @@
2388
3196
  function printAttrs(props) {
2389
3197
  const attrs = [];
2390
3198
  for (let [name, value] of Object.entries(props)) {
2391
- // TODO: Because printAttrs is called in arrange, special props are not filtered out.
2392
- // This should be handled by the core library.
2393
- if (name === "children" ||
2394
- name === "innerHTML" ||
2395
- name === "key" ||
2396
- name === "ref" ||
2397
- name === "copy" ||
2398
- name.startsWith("prop:") ||
2399
- // TODO: Remove deprecated special props
2400
- name === "crank-key" ||
2401
- name === "crank-ref" ||
2402
- name === "crank-static" ||
2403
- name === "c-key" ||
2404
- name === "c-ref" ||
2405
- name === "c-static" ||
2406
- name === "$key" ||
2407
- name === "$ref" ||
2408
- name === "$static") {
3199
+ if (name === "innerHTML" || name.startsWith("prop:")) {
2409
3200
  continue;
2410
3201
  }
2411
3202
  else if (name === "style") {
2412
3203
  if (typeof value === "string") {
2413
3204
  attrs.push(`style="${escape(value)}"`);
2414
3205
  }
2415
- else if (typeof value === "object") {
3206
+ else if (typeof value === "object" && value !== null) {
2416
3207
  attrs.push(`style="${escape(printStyleObject(value))}"`);
2417
3208
  }
2418
3209
  }
@@ -2451,8 +3242,8 @@
2451
3242
  create() {
2452
3243
  return { value: "" };
2453
3244
  },
2454
- text(text) {
2455
- return escape(text);
3245
+ text({ value }) {
3246
+ return { value: escape(value) };
2456
3247
  },
2457
3248
  read(value) {
2458
3249
  if (Array.isArray(value)) {
@@ -2465,15 +3256,15 @@
2465
3256
  return value;
2466
3257
  }
2467
3258
  else {
2468
- return value.value;
3259
+ return value.value || "";
2469
3260
  }
2470
3261
  },
2471
- arrange(tag, node, props, children) {
3262
+ arrange({ tag, tagName, node, props, children, }) {
2472
3263
  if (tag === Portal) {
2473
3264
  return;
2474
3265
  }
2475
3266
  else if (typeof tag !== "string") {
2476
- throw new Error(`Unknown tag: ${tag.toString()}`);
3267
+ throw new Error(`Unknown tag: ${tagName}`);
2477
3268
  }
2478
3269
  const attrs = printAttrs(props);
2479
3270
  const open = `<${tag}${attrs.length ? " " : ""}${attrs}>`;
@@ -2510,6 +3301,7 @@
2510
3301
  exports.Portal = Portal;
2511
3302
  exports.Raw = Raw;
2512
3303
  exports.Renderer = Renderer;
3304
+ exports.Text = Text;
2513
3305
  exports.cloneElement = cloneElement;
2514
3306
  exports.createElement = createElement;
2515
3307
  exports.dom = dom;