@b9g/crank 0.6.0 → 0.7.0

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