@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/crank.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /// <reference types="crank.d.ts" />
2
- const NOOP = () => { };
3
- const IDENTITY = (value) => value;
2
+ import { removeEventTargetDelegates, addEventTargetDelegates, clearEventListeners, CustomEventTarget } from './event-target.js';
3
+
4
4
  function wrap(value) {
5
5
  return value === undefined ? [] : Array.isArray(value) ? value : [value];
6
6
  }
@@ -22,8 +22,7 @@ function arrayify(value) {
22
22
  : typeof value === "string" ||
23
23
  typeof value[Symbol.iterator] !== "function"
24
24
  ? [value]
25
- : // TODO: inference broke in TypeScript 3.9.
26
- [...value];
25
+ : [...value];
27
26
  }
28
27
  function isIteratorLike(value) {
29
28
  return value != null && typeof value.next === "function";
@@ -31,11 +30,84 @@ function isIteratorLike(value) {
31
30
  function isPromiseLike(value) {
32
31
  return value != null && typeof value.then === "function";
33
32
  }
34
- /***
35
- * SPECIAL TAGS
36
- *
37
- * Crank provides a couple tags which have special meaning for the renderer.
38
- ***/
33
+ function createRaceRecord(contender) {
34
+ const deferreds = new Set();
35
+ const record = { deferreds, settled: false };
36
+ // This call to `then` happens once for the lifetime of the value.
37
+ Promise.resolve(contender).then((value) => {
38
+ for (const { resolve } of deferreds) {
39
+ resolve(value);
40
+ }
41
+ deferreds.clear();
42
+ record.settled = true;
43
+ }, (err) => {
44
+ for (const { reject } of deferreds) {
45
+ reject(err);
46
+ }
47
+ deferreds.clear();
48
+ record.settled = true;
49
+ });
50
+ return record;
51
+ }
52
+ // Promise.race is memory unsafe. This is alternative which is. See:
53
+ // https://github.com/nodejs/node/issues/17469#issuecomment-685235106
54
+ // Keys are the values passed to race.
55
+ // Values are a record of data containing a set of deferreds and whether the
56
+ // value has settled.
57
+ const wm = new WeakMap();
58
+ function safeRace(contenders) {
59
+ let deferred;
60
+ const result = new Promise((resolve, reject) => {
61
+ deferred = { resolve, reject };
62
+ for (const contender of contenders) {
63
+ if (!isPromiseLike(contender)) {
64
+ // If the contender is a not a then-able, attempting to use it as a key
65
+ // in the weakmap would throw an error. Luckily, it is safe to call
66
+ // `Promise.resolve(contender).then` on regular values multiple
67
+ // times because the promise fulfills immediately.
68
+ Promise.resolve(contender).then(resolve, reject);
69
+ continue;
70
+ }
71
+ let record = wm.get(contender);
72
+ if (record === undefined) {
73
+ record = createRaceRecord(contender);
74
+ record.deferreds.add(deferred);
75
+ wm.set(contender, record);
76
+ }
77
+ else if (record.settled) {
78
+ // If the value has settled, it is safe to call
79
+ // `Promise.resolve(contender).then` on it.
80
+ Promise.resolve(contender).then(resolve, reject);
81
+ }
82
+ else {
83
+ record.deferreds.add(deferred);
84
+ }
85
+ }
86
+ });
87
+ // The finally callback executes when any value settles, preventing any of
88
+ // the unresolved values from retaining a reference to the resolved value.
89
+ return result.finally(() => {
90
+ for (const contender of contenders) {
91
+ if (isPromiseLike(contender)) {
92
+ const record = wm.get(contender);
93
+ if (record) {
94
+ record.deferreds.delete(deferred);
95
+ }
96
+ }
97
+ }
98
+ });
99
+ }
100
+
101
+ const NOOP = () => { };
102
+ function getTagName(tag) {
103
+ return typeof tag === "function"
104
+ ? tag.name || "Anonymous"
105
+ : typeof tag === "string"
106
+ ? tag
107
+ : // tag is symbol, using else branch to avoid typeof tag === "symbol"
108
+ tag.description || "Anonymous";
109
+ }
110
+ /*** SPECIAL TAGS ***/
39
111
  /**
40
112
  * A special tag for grouping multiple children within the same parent.
41
113
  *
@@ -47,8 +119,8 @@ function isPromiseLike(value) {
47
119
  * reference this export.
48
120
  */
49
121
  const Fragment = "";
50
- // TODO: We assert the following symbol tags as any because TypeScript support
51
- // for symbol tags in JSX doesnt exist yet.
122
+ // TODO: We assert the following symbol tags as Components because TypeScript
123
+ // support for symbol tags in JSX doesn't exist yet.
52
124
  // https://github.com/microsoft/TypeScript/issues/38367
53
125
  /**
54
126
  * A special tag for rendering into a new root node via a root prop.
@@ -56,13 +128,13 @@ const Fragment = "";
56
128
  * This tag is useful for creating element trees with multiple roots, for
57
129
  * things like modals or tooltips.
58
130
  *
59
- * Renderer.prototype.render() will implicitly wrap top-level element trees in
60
- * a Portal element.
131
+ * Renderer.prototype.render() implicitly wraps top-level in a Portal element
132
+ * with the root set to the second argument passed in.
61
133
  */
62
134
  const Portal = Symbol.for("crank.Portal");
63
135
  /**
64
136
  * A special tag which preserves whatever was previously rendered in the
65
- * elements position.
137
+ * element's position.
66
138
  *
67
139
  * Copy elements are useful for when you want to prevent a subtree from
68
140
  * rerendering as a performance optimization. Copy elements can also be keyed,
@@ -70,10 +142,13 @@ const Portal = Symbol.for("crank.Portal");
70
142
  */
71
143
  const Copy = Symbol.for("crank.Copy");
72
144
  /**
73
- * A special tag for injecting raw nodes or strings via a value prop.
145
+ * A special tag for rendering text nodes.
74
146
  *
75
- * Renderer.prototype.raw() is called with the value prop.
147
+ * Strings in the element tree are implicitly wrapped in a Text element with
148
+ * value set to the string.
76
149
  */
150
+ const Text = Symbol.for("crank.Text");
151
+ /** A special tag for injecting raw nodes or strings via a value prop. */
77
152
  const Raw = Symbol.for("crank.Raw");
78
153
  const ElementSymbol = Symbol.for("crank.Element");
79
154
  /**
@@ -101,15 +176,6 @@ class Element {
101
176
  this.tag = tag;
102
177
  this.props = props;
103
178
  }
104
- get key() {
105
- return this.props.key;
106
- }
107
- get ref() {
108
- return this.props.ref;
109
- }
110
- get copy() {
111
- return !!this.props.copy;
112
- }
113
179
  }
114
180
  // See Element interface
115
181
  Element.prototype.$$typeof = ElementSymbol;
@@ -117,34 +183,34 @@ function isElement(value) {
117
183
  return value != null && value.$$typeof === ElementSymbol;
118
184
  }
119
185
  const DEPRECATED_PROP_PREFIXES = ["crank-", "c-", "$"];
120
- const DEPRECATED_SPECIAL_PROP_BASES = ["key", "ref", "static"];
121
- const SPECIAL_PROPS = new Set(["children", "key", "ref", "copy"]);
122
- for (const propPrefix of DEPRECATED_PROP_PREFIXES) {
123
- for (const propBase of DEPRECATED_SPECIAL_PROP_BASES) {
124
- SPECIAL_PROPS.add(propPrefix + propBase);
125
- }
126
- }
186
+ const DEPRECATED_SPECIAL_PROP_BASES = ["key", "ref", "static", "copy"];
127
187
  /**
128
188
  * Creates an element with the specified tag, props and children.
129
189
  *
130
190
  * This function is usually used as a transpilation target for JSX transpilers,
131
191
  * but it can also be called directly. It additionally extracts special props so
132
- * they arent accessible to renderer methods or components, and assigns the
192
+ * they aren't accessible to renderer methods or components, and assigns the
133
193
  * children prop according to any additional arguments passed to the function.
134
194
  */
135
195
  function createElement(tag, props, ...children) {
136
196
  if (props == null) {
137
197
  props = {};
138
198
  }
199
+ if ("static" in props) {
200
+ console.error(`The \`static\` prop is deprecated. Use \`copy\` instead.`);
201
+ props["copy"] = props["static"];
202
+ delete props["static"];
203
+ }
139
204
  for (let i = 0; i < DEPRECATED_PROP_PREFIXES.length; i++) {
140
205
  const propPrefix = DEPRECATED_PROP_PREFIXES[i];
141
206
  for (let j = 0; j < DEPRECATED_SPECIAL_PROP_BASES.length; j++) {
142
207
  const propBase = DEPRECATED_SPECIAL_PROP_BASES[j];
143
208
  const deprecatedPropName = propPrefix + propBase;
144
- const targetPropBase = propBase === "static" ? "copy" : propBase;
145
209
  if (deprecatedPropName in props) {
146
- console.warn(`The \`${deprecatedPropName}\` prop is deprecated. Use \`${targetPropBase}\` instead.`);
210
+ const targetPropBase = propBase === "static" ? "copy" : propBase;
211
+ console.error(`The \`${deprecatedPropName}\` prop is deprecated. Use \`${targetPropBase}\` instead.`);
147
212
  props[targetPropBase] = props[deprecatedPropName];
213
+ delete props[deprecatedPropName];
148
214
  }
149
215
  }
150
216
  }
@@ -159,13 +225,13 @@ function createElement(tag, props, ...children) {
159
225
  /** Clones a given element, shallowly copying the props object. */
160
226
  function cloneElement(el) {
161
227
  if (!isElement(el)) {
162
- throw new TypeError("Cannot clone non-element");
228
+ throw new TypeError(`Cannot clone non-element: ${String(el)}`);
163
229
  }
164
230
  return new Element(el.tag, { ...el.props });
165
231
  }
166
232
  function narrow(value) {
167
233
  if (typeof value === "boolean" || value == null) {
168
- return undefined;
234
+ return;
169
235
  }
170
236
  else if (typeof value === "string" || isElement(value)) {
171
237
  return value;
@@ -175,137 +241,203 @@ function narrow(value) {
175
241
  }
176
242
  return value.toString();
177
243
  }
178
- /**
179
- * Takes an array of element values and normalizes the output as an array of
180
- * nodes and strings.
181
- *
182
- * @returns Normalized array of nodes and/or strings.
183
- *
184
- * Normalize will flatten only one level of nested arrays, because it is
185
- * designed to be called once at each level of the tree. It will also
186
- * concatenate adjacent strings and remove all undefined values.
187
- */
188
- function normalize(values) {
189
- const result = [];
190
- let buffer;
191
- for (let i = 0; i < values.length; i++) {
192
- const value = values[i];
193
- if (!value) ;
194
- else if (typeof value === "string") {
195
- buffer = (buffer || "") + value;
196
- }
197
- else if (!Array.isArray(value)) {
198
- if (buffer) {
199
- result.push(buffer);
200
- buffer = undefined;
201
- }
202
- result.push(value);
203
- }
204
- else {
205
- // We could use recursion here but it’s just easier to do it inline.
206
- for (let j = 0; j < value.length; j++) {
207
- const value1 = value[j];
208
- if (!value1) ;
209
- else if (typeof value1 === "string") {
210
- buffer = (buffer || "") + value1;
211
- }
212
- else {
213
- if (buffer) {
214
- result.push(buffer);
215
- buffer = undefined;
216
- }
217
- result.push(value1);
218
- }
219
- }
220
- }
244
+ /*** RETAINER FLAGS ***/
245
+ const DidDiff = 1 << 0;
246
+ const DidCommit = 1 << 1;
247
+ const IsCopied = 1 << 2;
248
+ const IsUpdating = 1 << 3;
249
+ const IsExecuting = 1 << 4;
250
+ const IsRefreshing = 1 << 5;
251
+ const IsScheduling = 1 << 6;
252
+ const IsSchedulingFallback = 1 << 7;
253
+ const IsUnmounted = 1 << 8;
254
+ // TODO: Is this flag still necessary or can we use IsUnmounted?
255
+ const IsErrored = 1 << 9;
256
+ const IsResurrecting = 1 << 10;
257
+ // TODO: Maybe we can get rid of IsSyncGen and IsAsyncGen
258
+ const IsSyncGen = 1 << 11;
259
+ const IsAsyncGen = 1 << 12;
260
+ const IsInForOfLoop = 1 << 13;
261
+ const IsInForAwaitOfLoop = 1 << 14;
262
+ const NeedsToYield = 1 << 15;
263
+ const PropsAvailable = 1 << 16;
264
+ function getFlag(ret, flag) {
265
+ return !!(ret.f & flag);
266
+ }
267
+ function setFlag(ret, flag, value = true) {
268
+ if (value) {
269
+ ret.f |= flag;
221
270
  }
222
- if (buffer) {
223
- result.push(buffer);
271
+ else {
272
+ ret.f &= ~flag;
224
273
  }
225
- return result;
226
274
  }
227
275
  /**
228
276
  * @internal
229
- * The internal nodes which are cached and diffed against new elements when
230
- * rendering element trees.
277
+ * Retainers are objects which act as the internal representation of elements,
278
+ * mirroring the element tree.
231
279
  */
232
280
  class Retainer {
233
281
  constructor(el) {
282
+ this.f = 0;
234
283
  this.el = el;
235
284
  this.ctx = undefined;
236
285
  this.children = undefined;
286
+ this.fallback = undefined;
237
287
  this.value = undefined;
238
- this.cachedChildValues = undefined;
239
- this.fallbackValue = undefined;
240
- this.inflightValue = undefined;
241
- this.onNextValues = undefined;
288
+ this.oldProps = undefined;
289
+ this.pendingDiff = undefined;
290
+ this.onNextDiff = undefined;
291
+ this.graveyard = undefined;
292
+ this.lingerers = undefined;
242
293
  }
243
294
  }
295
+ function cloneRetainer(ret) {
296
+ const clone = new Retainer(ret.el);
297
+ clone.f = ret.f;
298
+ clone.ctx = ret.ctx;
299
+ clone.children = ret.children;
300
+ clone.fallback = ret.fallback;
301
+ clone.value = ret.value;
302
+ clone.scope = ret.scope;
303
+ clone.oldProps = ret.oldProps;
304
+ clone.pendingDiff = ret.pendingDiff;
305
+ clone.onNextDiff = ret.onNextDiff;
306
+ clone.graveyard = ret.graveyard;
307
+ clone.lingerers = ret.lingerers;
308
+ return clone;
309
+ }
244
310
  /**
245
311
  * Finds the value of the element according to its type.
246
312
  *
247
- * @returns The value of the element.
313
+ * @returns A node, an array of nodes or undefined.
248
314
  */
249
- function getValue(ret) {
250
- if (typeof ret.fallbackValue !== "undefined") {
251
- return typeof ret.fallbackValue === "object"
252
- ? getValue(ret.fallbackValue)
253
- : ret.fallbackValue;
315
+ function getValue(ret, isNested = false, index) {
316
+ if (getFlag(ret, IsScheduling) && isNested) {
317
+ return ret.fallback ? getValue(ret.fallback, isNested, index) : undefined;
318
+ }
319
+ else if (ret.fallback && !getFlag(ret, DidDiff)) {
320
+ return ret.fallback
321
+ ? getValue(ret.fallback, isNested, index)
322
+ : ret.fallback;
254
323
  }
255
324
  else if (ret.el.tag === Portal) {
256
325
  return;
257
326
  }
258
- else if (typeof ret.el.tag !== "function" && ret.el.tag !== Fragment) {
259
- return ret.value;
327
+ else if (ret.el.tag === Fragment || typeof ret.el.tag === "function") {
328
+ if (index != null && ret.ctx) {
329
+ ret.ctx.index = index;
330
+ }
331
+ return unwrap(getChildValues(ret, index));
260
332
  }
261
- return unwrap(getChildValues(ret));
333
+ return ret.value;
262
334
  }
263
335
  /**
264
- * Walks an elements children to find its child values.
336
+ * Walks an element's children to find its child values.
265
337
  *
266
- * @returns A normalized array of nodes and strings.
338
+ * @param ret - The retainer whose child values we are reading.
339
+ * @param startIndex - Starting index to thread through for context index updates.
340
+ *
341
+ * @returns An array of nodes.
267
342
  */
268
- function getChildValues(ret) {
269
- if (ret.cachedChildValues) {
270
- return wrap(ret.cachedChildValues);
271
- }
343
+ function getChildValues(ret, startIndex) {
272
344
  const values = [];
345
+ const lingerers = ret.lingerers;
273
346
  const children = wrap(ret.children);
347
+ let currentIndex = startIndex;
274
348
  for (let i = 0; i < children.length; i++) {
349
+ if (lingerers != null && lingerers[i] != null) {
350
+ const rets = lingerers[i];
351
+ for (const ret of rets) {
352
+ const value = getValue(ret, true, currentIndex);
353
+ if (Array.isArray(value)) {
354
+ for (let j = 0; j < value.length; j++) {
355
+ values.push(value[j]);
356
+ }
357
+ if (currentIndex != null) {
358
+ currentIndex += value.length;
359
+ }
360
+ }
361
+ else if (value) {
362
+ values.push(value);
363
+ if (currentIndex != null) {
364
+ currentIndex++;
365
+ }
366
+ }
367
+ }
368
+ }
275
369
  const child = children[i];
276
370
  if (child) {
277
- values.push(typeof child === "string" ? child : getValue(child));
371
+ const value = getValue(child, true, currentIndex);
372
+ if (Array.isArray(value)) {
373
+ for (let j = 0; j < value.length; j++) {
374
+ values.push(value[j]);
375
+ }
376
+ if (currentIndex != null) {
377
+ currentIndex += value.length;
378
+ }
379
+ }
380
+ else if (value) {
381
+ values.push(value);
382
+ if (currentIndex != null) {
383
+ currentIndex++;
384
+ }
385
+ }
278
386
  }
279
387
  }
280
- const values1 = normalize(values);
281
- const tag = ret.el.tag;
282
- if (typeof tag === "function" || (tag !== Fragment && tag !== Raw)) {
283
- ret.cachedChildValues = unwrap(values1);
388
+ if (lingerers != null && lingerers.length > children.length) {
389
+ for (let i = children.length; i < lingerers.length; i++) {
390
+ const rets = lingerers[i];
391
+ if (rets != null) {
392
+ for (const ret of rets) {
393
+ const value = getValue(ret, true, currentIndex);
394
+ if (Array.isArray(value)) {
395
+ for (let j = 0; j < value.length; j++) {
396
+ values.push(value[j]);
397
+ }
398
+ if (currentIndex != null) {
399
+ currentIndex += value.length;
400
+ }
401
+ }
402
+ else if (value) {
403
+ values.push(value);
404
+ if (currentIndex != null) {
405
+ currentIndex++;
406
+ }
407
+ }
408
+ }
409
+ }
410
+ }
284
411
  }
285
- return values1;
412
+ return values;
413
+ }
414
+ function stripSpecialProps(props) {
415
+ let _;
416
+ let result;
417
+ ({ key: _, ref: _, copy: _, hydrate: _, children: _, ...result } = props);
418
+ return result;
286
419
  }
287
- const defaultRendererImpl = {
420
+ const defaultAdapter = {
288
421
  create() {
289
- throw new Error("Not implemented");
422
+ throw new Error("adapter must implement create");
290
423
  },
291
- hydrate() {
292
- throw new Error("Not implemented");
424
+ adopt() {
425
+ throw new Error("adapter must implement adopt() for hydration");
293
426
  },
294
- scope: IDENTITY,
295
- read: IDENTITY,
296
- text: IDENTITY,
297
- raw: IDENTITY,
427
+ scope: ({ scope }) => scope,
428
+ read: (value) => value,
429
+ text: ({ value }) => value,
430
+ raw: ({ value }) => value,
298
431
  patch: NOOP,
299
432
  arrange: NOOP,
300
- dispose: NOOP,
301
- flush: NOOP,
433
+ remove: NOOP,
434
+ finalize: NOOP,
302
435
  };
303
- const _RendererImpl = Symbol.for("crank.RendererImpl");
304
436
  /**
305
437
  * An abstract class which is subclassed to render to different target
306
- * environments. Subclasses will typically call super() with a custom
307
- * RendererImpl. This class is responsible for kicking off the rendering
308
- * process and caching previous trees by root.
438
+ * environments. Subclasses call super() with a custom RenderAdapter object.
439
+ * This class is responsible for kicking off the rendering process and caching
440
+ * previous trees by root.
309
441
  *
310
442
  * @template TNode - The type of the node for a rendering environment.
311
443
  * @template TScope - Data which is passed down the tree.
@@ -313,125 +445,128 @@ const _RendererImpl = Symbol.for("crank.RendererImpl");
313
445
  * @template TResult - The type of exposed values.
314
446
  */
315
447
  class Renderer {
316
- constructor(impl) {
448
+ constructor(adapter) {
317
449
  this.cache = new WeakMap();
318
- this[_RendererImpl] = {
319
- ...defaultRendererImpl,
320
- ...impl,
321
- };
450
+ this.adapter = { ...defaultAdapter, ...adapter };
322
451
  }
323
452
  /**
324
453
  * Renders an element tree into a specific root.
325
454
  *
326
- * @param children - An element tree. You can render null with a previously
327
- * used root to delete the previously rendered element tree from the cache.
328
- * @param root - The node to be rendered into. The renderer will cache
329
- * element trees per root.
330
- * @param bridge - An optional context that will be the ancestor context of all
331
- * elements in the tree. Useful for connecting different renderers so that
332
- * events/provisions properly propagate. The context for a given root must be
333
- * the same or an error will be thrown.
455
+ * @param children - An element tree. Rendering null deletes cached renders.
456
+ * @param root - The root to be rendered into. The renderer caches renders
457
+ * per root.
458
+ * @param bridge - An optional context that will be the ancestor context of
459
+ * all elements in the tree. Useful for connecting different renderers so
460
+ * that events/provisions/errors properly propagate. The context for a given
461
+ * root must be the same between renders.
334
462
  *
335
463
  * @returns The result of rendering the children, or a possible promise of
336
464
  * the result if the element tree renders asynchronously.
337
465
  */
338
466
  render(children, root, bridge) {
339
- let ret;
340
- const ctx = bridge && bridge[_ContextImpl];
341
- if (typeof root === "object" && root !== null) {
342
- ret = this.cache.get(root);
343
- }
344
- let oldProps;
345
- if (ret === undefined) {
346
- ret = new Retainer(createElement(Portal, { children, root }));
347
- ret.value = root;
348
- ret.ctx = ctx;
349
- if (typeof root === "object" && root !== null && children != null) {
350
- this.cache.set(root, ret);
351
- }
352
- }
353
- else if (ret.ctx !== ctx) {
354
- throw new Error("Context mismatch");
355
- }
356
- else {
357
- oldProps = ret.el.props;
358
- ret.el = createElement(Portal, { children, root });
359
- if (typeof root === "object" && root !== null && children == null) {
360
- this.cache.delete(root);
361
- }
362
- }
363
- const impl = this[_RendererImpl];
364
- const childValues = diffChildren(impl, root, ret, ctx, impl.scope(undefined, Portal, ret.el.props), ret, children, undefined);
365
- // We return the child values of the portal because portal elements
366
- // themselves have no readable value.
367
- if (isPromiseLike(childValues)) {
368
- return childValues.then((childValues) => commitRootRender(impl, root, ctx, ret, childValues, oldProps));
369
- }
370
- return commitRootRender(impl, root, ctx, ret, childValues, oldProps);
467
+ const ret = getRootRetainer(this, bridge, { children, root });
468
+ return renderRoot(this.adapter, root, ret, children);
371
469
  }
372
470
  hydrate(children, root, bridge) {
373
- const impl = this[_RendererImpl];
374
- const ctx = bridge && bridge[_ContextImpl];
375
- let ret;
376
- ret = this.cache.get(root);
377
- if (ret !== undefined) {
378
- // If there is a retainer for the root, hydration is not necessary.
379
- return this.render(children, root, bridge);
380
- }
381
- let oldProps;
382
- ret = new Retainer(createElement(Portal, { children, root }));
471
+ const ret = getRootRetainer(this, bridge, {
472
+ children,
473
+ root,
474
+ hydrate: true,
475
+ });
476
+ return renderRoot(this.adapter, root, ret, children);
477
+ }
478
+ }
479
+ /*** PRIVATE RENDERER FUNCTIONS ***/
480
+ function getRootRetainer(renderer, bridge, { children, root, hydrate, }) {
481
+ let ret;
482
+ const bridgeCtx = bridge && bridge[_ContextState];
483
+ if (typeof root === "object" && root !== null) {
484
+ ret = renderer.cache.get(root);
485
+ }
486
+ const adapter = renderer.adapter;
487
+ if (ret === undefined) {
488
+ ret = new Retainer(createElement(Portal, { children, root, hydrate }));
383
489
  ret.value = root;
490
+ ret.ctx = bridgeCtx;
491
+ ret.scope = adapter.scope({
492
+ tag: Portal,
493
+ tagName: getTagName(Portal),
494
+ props: stripSpecialProps(ret.el.props),
495
+ scope: undefined,
496
+ });
497
+ // remember that typeof null === "object"
384
498
  if (typeof root === "object" && root !== null && children != null) {
385
- this.cache.set(root, ret);
499
+ renderer.cache.set(root, ret);
386
500
  }
387
- const hydrationData = impl.hydrate(Portal, root, {});
388
- const childValues = diffChildren(impl, root, ret, ctx, impl.scope(undefined, Portal, ret.el.props), ret, children, hydrationData);
389
- // We return the child values of the portal because portal elements
390
- // themselves have no readable value.
391
- if (isPromiseLike(childValues)) {
392
- return childValues.then((childValues) => commitRootRender(impl, root, ctx, ret, childValues, oldProps));
501
+ }
502
+ else if (ret.ctx !== bridgeCtx) {
503
+ throw new Error("A previous call to render() was passed a different context");
504
+ }
505
+ else {
506
+ ret.el = createElement(Portal, { children, root, hydrate });
507
+ if (typeof root === "object" && root !== null && children == null) {
508
+ renderer.cache.delete(root);
393
509
  }
394
- return commitRootRender(impl, root, ctx, ret, childValues, oldProps);
395
510
  }
511
+ return ret;
396
512
  }
397
- /*** PRIVATE RENDERER FUNCTIONS ***/
398
- function commitRootRender(renderer, root, ctx, ret, childValues, oldProps) {
399
- // element is a host or portal element
400
- if (root != null) {
401
- renderer.arrange(Portal, root, ret.el.props, childValues, oldProps, wrap(ret.cachedChildValues));
402
- flush(renderer, root);
513
+ function renderRoot(adapter, root, ret, children) {
514
+ const diff = diffChildren(adapter, root, ret, ret.ctx, ret.scope, ret, children);
515
+ const schedulePromises = [];
516
+ if (isPromiseLike(diff)) {
517
+ return diff.then(() => {
518
+ commit(adapter, ret, ret, ret.ctx, ret.scope, 0, schedulePromises, undefined);
519
+ if (schedulePromises.length > 0) {
520
+ return Promise.all(schedulePromises).then(() => {
521
+ if (typeof root !== "object" || root === null) {
522
+ unmount(adapter, ret, ret.ctx, ret, false);
523
+ }
524
+ return adapter.read(unwrap(getChildValues(ret)));
525
+ });
526
+ }
527
+ if (typeof root !== "object" || root === null) {
528
+ unmount(adapter, ret, ret.ctx, ret, false);
529
+ }
530
+ return adapter.read(unwrap(getChildValues(ret)));
531
+ });
532
+ }
533
+ commit(adapter, ret, ret, ret.ctx, ret.scope, 0, schedulePromises, undefined);
534
+ if (schedulePromises.length > 0) {
535
+ return Promise.all(schedulePromises).then(() => {
536
+ if (typeof root !== "object" || root === null) {
537
+ unmount(adapter, ret, ret.ctx, ret, false);
538
+ }
539
+ return adapter.read(unwrap(getChildValues(ret)));
540
+ });
403
541
  }
404
- ret.cachedChildValues = unwrap(childValues);
405
- if (root == null) {
406
- unmount(renderer, ret, ctx, ret);
542
+ if (typeof root !== "object" || root === null) {
543
+ unmount(adapter, ret, ret.ctx, ret, false);
407
544
  }
408
- return renderer.read(ret.cachedChildValues);
545
+ return adapter.read(unwrap(getChildValues(ret)));
409
546
  }
410
- function diffChildren(renderer, root, host, ctx, scope, parent, children, hydrationData) {
547
+ function diffChildren(adapter, root, host, ctx, scope, parent, newChildren) {
411
548
  const oldRetained = wrap(parent.children);
412
549
  const newRetained = [];
413
- const newChildren = arrayify(children);
414
- const values = [];
415
- let graveyard;
550
+ const newChildren1 = arrayify(newChildren);
551
+ const diffs = [];
416
552
  let childrenByKey;
417
553
  let seenKeys;
418
554
  let isAsync = false;
419
- // When hydrating, sibling element trees must be rendered in order, because
420
- // we do not know how many DOM nodes an element will render.
421
- let hydrationBlock;
422
555
  let oi = 0;
423
556
  let oldLength = oldRetained.length;
424
- for (let ni = 0, newLength = newChildren.length; ni < newLength; ni++) {
557
+ let graveyard;
558
+ for (let ni = 0, newLength = newChildren1.length; ni < newLength; ni++) {
425
559
  // length checks to prevent index out of bounds deoptimizations.
426
560
  let ret = oi >= oldLength ? undefined : oldRetained[oi];
427
- let child = narrow(newChildren[ni]);
561
+ let child = narrow(newChildren1[ni]);
428
562
  {
429
563
  // aligning new children with old retainers
430
- let oldKey = typeof ret === "object" ? ret.el.key : undefined;
431
- let newKey = typeof child === "object" ? child.key : undefined;
564
+ let oldKey = typeof ret === "object" ? ret.el.props.key : undefined;
565
+ let newKey = typeof child === "object" ? child.props.key : undefined;
432
566
  if (newKey !== undefined && seenKeys && seenKeys.has(newKey)) {
433
- console.error("Duplicate key", newKey);
434
- newKey = undefined;
567
+ console.error(`Duplicate key found in <${getTagName(parent.el.tag)}>`, newKey);
568
+ child = cloneElement(child);
569
+ newKey = child.props.key = undefined;
435
570
  }
436
571
  if (oldKey === newKey) {
437
572
  if (childrenByKey !== undefined && newKey !== undefined) {
@@ -445,7 +580,7 @@ function diffChildren(renderer, root, host, ctx, scope, parent, children, hydrat
445
580
  while (ret !== undefined && oldKey !== undefined) {
446
581
  oi++;
447
582
  ret = oldRetained[oi];
448
- oldKey = typeof ret === "object" ? ret.el.key : undefined;
583
+ oldKey = typeof ret === "object" ? ret.el.props.key : undefined;
449
584
  }
450
585
  oi++;
451
586
  }
@@ -458,405 +593,748 @@ function diffChildren(renderer, root, host, ctx, scope, parent, children, hydrat
458
593
  }
459
594
  }
460
595
  }
461
- // Updating
462
- let value;
596
+ let diff = undefined;
463
597
  if (typeof child === "object") {
464
- if (child.tag === Copy || (typeof ret === "object" && ret.el === child)) {
465
- value = getInflightValue(ret);
598
+ let childCopied = false;
599
+ if (child.tag === Copy) {
600
+ childCopied = true;
601
+ }
602
+ else if (typeof ret === "object" &&
603
+ ret.el === child &&
604
+ getFlag(ret, DidCommit)) {
605
+ // If the child is the same as the retained element, we skip
606
+ // re-rendering.
607
+ childCopied = true;
466
608
  }
467
609
  else {
468
- let oldProps;
469
- let copy = false;
470
- if (typeof ret === "object" && ret.el.tag === child.tag) {
471
- oldProps = ret.el.props;
610
+ if (ret && ret.el.tag === child.tag) {
472
611
  ret.el = child;
473
- if (child.copy) {
474
- value = getInflightValue(ret);
475
- copy = true;
612
+ if (child.props.copy && typeof child.props.copy !== "string") {
613
+ childCopied = true;
476
614
  }
477
615
  }
478
- else {
479
- if (typeof ret === "object") {
480
- (graveyard = graveyard || []).push(ret);
616
+ else if (ret) {
617
+ // we do not need to add the retainer to the graveyard if it is the
618
+ // fallback of another retainer
619
+ // search for the tag in fallback chain
620
+ let candidateFound = false;
621
+ for (let predecessor = ret, candidate = ret.fallback; candidate; predecessor = candidate, candidate = candidate.fallback) {
622
+ if (candidate.el.tag === child.tag) {
623
+ // If we find a retainer in the fallback chain with the same tag,
624
+ // we reuse it rather than creating a new retainer to preserve
625
+ // state. This behavior is useful for when a Suspense component
626
+ // re-renders and the children are re-rendered quickly.
627
+ const clone = cloneRetainer(candidate);
628
+ setFlag(clone, IsResurrecting);
629
+ predecessor.fallback = clone;
630
+ const fallback = ret;
631
+ ret = candidate;
632
+ ret.el = child;
633
+ ret.fallback = fallback;
634
+ setFlag(ret, DidDiff, false);
635
+ candidateFound = true;
636
+ break;
637
+ }
638
+ }
639
+ if (!candidateFound) {
640
+ const fallback = ret;
641
+ ret = new Retainer(child);
642
+ ret.fallback = fallback;
481
643
  }
482
- const fallback = ret;
483
- ret = new Retainer(child);
484
- ret.fallbackValue = fallback;
485
644
  }
486
- if (copy) ;
487
- else if (child.tag === Raw) {
488
- value = hydrationBlock
489
- ? hydrationBlock.then(() => updateRaw(renderer, ret, scope, oldProps, hydrationData))
490
- : updateRaw(renderer, ret, scope, oldProps, hydrationData);
645
+ else {
646
+ ret = new Retainer(child);
491
647
  }
648
+ if (childCopied && getFlag(ret, DidCommit)) ;
649
+ else if (child.tag === Raw || child.tag === Text) ;
492
650
  else if (child.tag === Fragment) {
493
- value = hydrationBlock
494
- ? hydrationBlock.then(() => updateFragment(renderer, root, host, ctx, scope, ret, hydrationData))
495
- : updateFragment(renderer, root, host, ctx, scope, ret, hydrationData);
651
+ diff = diffChildren(adapter, root, host, ctx, scope, ret, ret.el.props.children);
496
652
  }
497
653
  else if (typeof child.tag === "function") {
498
- value = hydrationBlock
499
- ? hydrationBlock.then(() => updateComponent(renderer, root, host, ctx, scope, ret, oldProps, hydrationData))
500
- : updateComponent(renderer, root, host, ctx, scope, ret, oldProps, hydrationData);
654
+ diff = diffComponent(adapter, root, host, ctx, scope, ret);
655
+ }
656
+ else {
657
+ diff = diffHost(adapter, root, ctx, scope, ret);
658
+ }
659
+ }
660
+ if (typeof ret === "object") {
661
+ if (childCopied) {
662
+ setFlag(ret, IsCopied);
663
+ diff = getInflightDiff(ret);
501
664
  }
502
665
  else {
503
- value = hydrationBlock
504
- ? hydrationBlock.then(() => updateHost(renderer, root, ctx, scope, ret, oldProps, hydrationData))
505
- : updateHost(renderer, root, ctx, scope, ret, oldProps, hydrationData);
666
+ setFlag(ret, IsCopied, false);
506
667
  }
507
668
  }
508
- if (isPromiseLike(value)) {
669
+ if (isPromiseLike(diff)) {
509
670
  isAsync = true;
510
- if (hydrationData !== undefined) {
511
- hydrationBlock = value;
671
+ }
672
+ }
673
+ else if (typeof child === "string") {
674
+ if (typeof ret === "object" && ret.el.tag === Text) {
675
+ ret.el.props.value = child;
676
+ }
677
+ else {
678
+ if (typeof ret === "object") {
679
+ (graveyard = graveyard || []).push(ret);
512
680
  }
681
+ ret = new Retainer(createElement(Text, { value: child }));
513
682
  }
514
683
  }
515
684
  else {
516
- // child is a string or undefined
517
685
  if (typeof ret === "object") {
518
686
  (graveyard = graveyard || []).push(ret);
519
687
  }
520
- if (typeof child === "string") {
521
- value = ret = renderer.text(child, scope, hydrationData);
522
- }
523
- else {
524
- ret = undefined;
525
- }
688
+ ret = undefined;
526
689
  }
527
- values[ni] = value;
690
+ diffs[ni] = diff;
528
691
  newRetained[ni] = ret;
529
692
  }
530
693
  // cleanup remaining retainers
531
694
  for (; oi < oldLength; oi++) {
532
695
  const ret = oldRetained[oi];
533
696
  if (typeof ret === "object" &&
534
- (typeof ret.el.key === "undefined" ||
697
+ (typeof ret.el.props.key === "undefined" ||
535
698
  !seenKeys ||
536
- !seenKeys.has(ret.el.key))) {
699
+ !seenKeys.has(ret.el.props.key))) {
537
700
  (graveyard = graveyard || []).push(ret);
538
701
  }
539
702
  }
540
703
  if (childrenByKey !== undefined && childrenByKey.size > 0) {
541
- (graveyard = graveyard || []).push(...childrenByKey.values());
704
+ graveyard = graveyard || [];
705
+ for (const ret of childrenByKey.values()) {
706
+ graveyard.push(ret);
707
+ }
542
708
  }
543
709
  parent.children = unwrap(newRetained);
544
710
  if (isAsync) {
545
- let childValues1 = Promise.all(values).finally(() => {
711
+ const diffs1 = Promise.all(diffs)
712
+ .then(() => undefined)
713
+ .finally(() => {
714
+ setFlag(parent, DidDiff);
546
715
  if (graveyard) {
547
- for (let i = 0; i < graveyard.length; i++) {
548
- unmount(renderer, host, ctx, graveyard[i]);
716
+ if (parent.graveyard) {
717
+ for (let i = 0; i < graveyard.length; i++) {
718
+ parent.graveyard.push(graveyard[i]);
719
+ }
720
+ }
721
+ else {
722
+ parent.graveyard = graveyard;
549
723
  }
550
724
  }
551
725
  });
552
- let onChildValues;
553
- childValues1 = Promise.race([
554
- childValues1,
555
- new Promise((resolve) => (onChildValues = resolve)),
556
- ]);
557
- if (parent.onNextValues) {
558
- parent.onNextValues(childValues1);
559
- }
560
- parent.onNextValues = onChildValues;
561
- return childValues1.then((childValues) => {
562
- parent.inflightValue = parent.fallbackValue = undefined;
563
- return normalize(childValues);
564
- });
726
+ let onNextDiffs;
727
+ const diffs2 = (parent.pendingDiff = safeRace([
728
+ diffs1,
729
+ new Promise((resolve) => (onNextDiffs = resolve)),
730
+ ]));
731
+ if (parent.onNextDiff) {
732
+ parent.onNextDiff(diffs2);
733
+ }
734
+ parent.onNextDiff = onNextDiffs;
735
+ return diffs2;
565
736
  }
566
737
  else {
738
+ setFlag(parent, DidDiff);
567
739
  if (graveyard) {
568
- for (let i = 0; i < graveyard.length; i++) {
569
- unmount(renderer, host, ctx, graveyard[i]);
740
+ if (parent.graveyard) {
741
+ for (let i = 0; i < graveyard.length; i++) {
742
+ parent.graveyard.push(graveyard[i]);
743
+ }
744
+ }
745
+ else {
746
+ parent.graveyard = graveyard;
570
747
  }
571
748
  }
572
- if (parent.onNextValues) {
573
- parent.onNextValues(values);
574
- parent.onNextValues = undefined;
749
+ if (parent.onNextDiff) {
750
+ parent.onNextDiff(diffs);
751
+ parent.onNextDiff = undefined;
575
752
  }
576
- parent.inflightValue = parent.fallbackValue = undefined;
577
- // We can assert there are no promises in the array because isAsync is false
578
- return normalize(values);
753
+ parent.pendingDiff = undefined;
754
+ }
755
+ }
756
+ function getInflightDiff(ret) {
757
+ // It is not enough to check pendingDiff because pendingDiff is the diff for
758
+ // children, but not the diff of an async component retainer's current run.
759
+ // For the latter we check ctx.inflight.
760
+ if (ret.ctx && ret.ctx.inflight) {
761
+ return ret.ctx.inflight[1];
762
+ }
763
+ else if (ret.pendingDiff) {
764
+ return ret.pendingDiff;
579
765
  }
580
766
  }
581
767
  function createChildrenByKey(children, offset) {
582
768
  const childrenByKey = new Map();
583
769
  for (let i = offset; i < children.length; i++) {
584
770
  const child = children[i];
585
- if (typeof child === "object" && typeof child.el.key !== "undefined") {
586
- childrenByKey.set(child.el.key, child);
771
+ if (typeof child === "object" &&
772
+ typeof child.el.props.key !== "undefined") {
773
+ childrenByKey.set(child.el.props.key, child);
587
774
  }
588
775
  }
589
776
  return childrenByKey;
590
777
  }
591
- function getInflightValue(child) {
592
- if (typeof child !== "object") {
593
- return child;
594
- }
595
- const ctx = typeof child.el.tag === "function" ? child.ctx : undefined;
596
- if (ctx && ctx.f & IsUpdating && ctx.inflightValue) {
597
- return ctx.inflightValue;
778
+ function diffHost(adapter, root, ctx, scope, ret) {
779
+ const el = ret.el;
780
+ const tag = el.tag;
781
+ if (el.tag === Portal) {
782
+ root = ret.value = el.props.root;
598
783
  }
599
- else if (child.inflightValue) {
600
- return child.inflightValue;
784
+ if (getFlag(ret, DidCommit)) {
785
+ scope = ret.scope;
601
786
  }
602
- return getValue(child);
603
- }
604
- function updateRaw(renderer, ret, scope, oldProps, hydrationData) {
605
- const props = ret.el.props;
606
- if (!oldProps || oldProps.value !== props.value) {
607
- ret.value = renderer.raw(props.value, scope, hydrationData);
608
- if (typeof ret.el.ref === "function") {
609
- ret.el.ref(ret.value);
610
- }
787
+ else {
788
+ scope = ret.scope = adapter.scope({
789
+ tag,
790
+ tagName: getTagName(tag),
791
+ props: el.props,
792
+ scope,
793
+ });
611
794
  }
612
- return ret.value;
795
+ return diffChildren(adapter, root, ret, ctx, scope, ret, ret.el.props.children);
613
796
  }
614
- function updateFragment(renderer, root, host, ctx, scope, ret, hydrationData) {
615
- const childValues = diffChildren(renderer, root, host, ctx, scope, ret, ret.el.props.children, hydrationData);
616
- if (isPromiseLike(childValues)) {
617
- ret.inflightValue = childValues.then((childValues) => unwrap(childValues));
618
- return ret.inflightValue;
797
+ function commit(adapter, host, ret, ctx, scope, index, schedulePromises, hydrationNodes) {
798
+ if (getFlag(ret, IsCopied) && getFlag(ret, DidCommit)) {
799
+ return getValue(ret);
619
800
  }
620
- return unwrap(childValues);
621
- }
622
- function updateHost(renderer, root, ctx, scope, ret, oldProps, hydrationData) {
623
801
  const el = ret.el;
624
802
  const tag = el.tag;
625
- let hydrationValue;
626
- if (el.tag === Portal) {
627
- root = ret.value = el.props.root;
803
+ if (typeof tag === "function" ||
804
+ tag === Fragment ||
805
+ tag === Portal ||
806
+ tag === Raw ||
807
+ tag === Text) {
808
+ if (typeof el.props.copy === "string") {
809
+ console.error(`String copy prop ignored for <${getTagName(tag)}>. Use booleans instead.`);
810
+ }
811
+ if (typeof el.props.hydrate === "string") {
812
+ console.error(`String hydrate prop ignored for <${getTagName(tag)}>. Use booleans instead.`);
813
+ }
814
+ }
815
+ let value;
816
+ let skippedHydrationNodes;
817
+ if (hydrationNodes &&
818
+ el.props.hydrate != null &&
819
+ !el.props.hydrate &&
820
+ typeof el.props.hydrate !== "string") {
821
+ skippedHydrationNodes = hydrationNodes;
822
+ hydrationNodes = undefined;
823
+ }
824
+ if (typeof tag === "function") {
825
+ ret.ctx.index = index;
826
+ value = commitComponent(ret.ctx, schedulePromises, hydrationNodes);
628
827
  }
629
828
  else {
630
- if (hydrationData !== undefined) {
631
- const value = hydrationData.children.shift();
632
- hydrationValue = value;
829
+ if (tag === Fragment) {
830
+ value = commitChildren(adapter, host, ctx, scope, ret, index, schedulePromises, hydrationNodes);
633
831
  }
634
- }
635
- scope = renderer.scope(scope, tag, el.props);
636
- let childHydrationData;
637
- if (hydrationValue != null && typeof hydrationValue !== "string") {
638
- childHydrationData = renderer.hydrate(tag, hydrationValue, el.props);
639
- if (childHydrationData === undefined) {
640
- hydrationValue = undefined;
832
+ else if (tag === Text) {
833
+ value = commitText(adapter, ret, el, scope, hydrationNodes);
834
+ }
835
+ else if (tag === Raw) {
836
+ value = commitRaw(adapter, host, ret, scope, hydrationNodes);
837
+ }
838
+ else {
839
+ value = commitHost(adapter, ret, ctx, schedulePromises, hydrationNodes);
840
+ }
841
+ if (ret.fallback) {
842
+ unmount(adapter, host, ctx, ret.fallback, false);
843
+ ret.fallback = undefined;
641
844
  }
642
845
  }
643
- const childValues = diffChildren(renderer, root, ret, ctx, scope, ret, ret.el.props.children, childHydrationData);
644
- if (isPromiseLike(childValues)) {
645
- ret.inflightValue = childValues.then((childValues) => commitHost(renderer, scope, ret, childValues, oldProps, hydrationValue));
646
- return ret.inflightValue;
846
+ if (skippedHydrationNodes) {
847
+ skippedHydrationNodes.splice(0, wrap(value).length);
647
848
  }
648
- return commitHost(renderer, scope, ret, childValues, oldProps, hydrationValue);
649
- }
650
- function commitHost(renderer, scope, ret, childValues, oldProps, hydrationValue) {
651
- const tag = ret.el.tag;
652
- let value = ret.value;
653
- if (hydrationValue != null) {
654
- value = ret.value = hydrationValue;
655
- if (typeof ret.el.ref === "function") {
656
- ret.el.ref(value);
849
+ if (!getFlag(ret, DidCommit)) {
850
+ setFlag(ret, DidCommit);
851
+ if (typeof tag !== "function" &&
852
+ tag !== Fragment &&
853
+ tag !== Portal &&
854
+ typeof el.props.ref === "function") {
855
+ el.props.ref(adapter.read(value));
657
856
  }
658
857
  }
659
- let props = ret.el.props;
660
- let copied;
661
- if (tag !== Portal) {
662
- if (value == null) {
663
- // This assumes that renderer.create does not return nullish values.
664
- value = ret.value = renderer.create(tag, props, scope);
665
- if (typeof ret.el.ref === "function") {
666
- ret.el.ref(value);
858
+ return value;
859
+ }
860
+ function commitChildren(adapter, host, ctx, scope, parent, index, schedulePromises, hydrationNodes) {
861
+ let values = [];
862
+ for (let i = 0, children = wrap(parent.children); i < children.length; i++) {
863
+ let child = children[i];
864
+ let schedulePromises1;
865
+ let isSchedulingFallback = false;
866
+ while (child &&
867
+ ((!getFlag(child, DidDiff) && child.fallback) ||
868
+ getFlag(child, IsScheduling))) {
869
+ // If the child is scheduling, it is a component retainer so ctx will be
870
+ // defined.
871
+ if (getFlag(child, IsScheduling) && child.ctx.schedule) {
872
+ (schedulePromises1 = schedulePromises1 || []).push(child.ctx.schedule.promise);
873
+ isSchedulingFallback = true;
874
+ }
875
+ if (!getFlag(child, DidDiff) && getFlag(child, DidCommit)) {
876
+ // If this child has not diffed but has committed, it means it is a
877
+ // fallback that is being resurrected.
878
+ for (const node of getChildValues(child)) {
879
+ adapter.remove({
880
+ node,
881
+ parentNode: host.value,
882
+ isNested: false,
883
+ });
884
+ }
885
+ }
886
+ child = child.fallback;
887
+ // When a scheduling component is mounting asynchronously but diffs
888
+ // immediately, it will cause previous async diffs to settle due to the
889
+ // chasing mechanism. This would cause earlier renders to resolve sooner
890
+ // than expected, because the render would be missing both its usual
891
+ // children and the children of the scheduling render. Therefore, we need
892
+ // to defer the settling of previous renders until either that render
893
+ // settles, or the scheduling component finally finishes scheduling.
894
+ //
895
+ // To do this, we take advantage of the fact that commits for aborted
896
+ // renders will still fire and walk the tree. During that commit walk,
897
+ // when we encounter a scheduling element, we push a race of the
898
+ // scheduling promise with the inflight diff of the async fallback
899
+ // fallback to schedulePromises to delay the initiator.
900
+ //
901
+ // However, we need to make sure we only use the inflight diffs for the
902
+ // fallback which we are trying to delay, in the case of multiple renders
903
+ // and fallbacks. To do this, we take advantage of the fact that when
904
+ // multiple renders race (e.g., render1->render2->render3->scheduling
905
+ // component), the chasing mechanism will call stale commits in reverse
906
+ // order.
907
+ //
908
+ // We can use this ordering to delay to find which fallbacks we need to
909
+ // add to the race. Each commit call progressively marks an additional
910
+ // fallback as a scheduling fallback, and does not contribute to the
911
+ // scheduling promises if it is further than the last seen level.
912
+ //
913
+ // This prevents promise contamination where newer renders settle early
914
+ // due to diffs from older renders.
915
+ if (schedulePromises1 && isSchedulingFallback && child) {
916
+ if (!getFlag(child, DidDiff)) {
917
+ const inflightDiff = getInflightDiff(child);
918
+ schedulePromises1.push(inflightDiff);
919
+ }
920
+ else {
921
+ // If a scheduling component's fallback has already diffed, we do not
922
+ // need delay the render.
923
+ schedulePromises1 = undefined;
924
+ }
925
+ if (getFlag(child, IsSchedulingFallback)) {
926
+ // This fallback was marked by a more recent commit - keep processing
927
+ // deeper levels
928
+ isSchedulingFallback = true;
929
+ }
930
+ else {
931
+ // First unmarked fallback we've encountered - mark it and stop
932
+ // contributing to schedulePromises1 for deeper levels.
933
+ setFlag(child, IsSchedulingFallback, true);
934
+ isSchedulingFallback = false;
935
+ }
667
936
  }
668
937
  }
669
- for (const propName in { ...oldProps, ...props }) {
670
- const propValue = props[propName];
671
- if (propValue === Copy) {
672
- // TODO: The Copy tag doubles as a way to skip the patching of a prop.
673
- // Not sure about this feature. Should probably be removed.
674
- (copied = copied || new Set()).add(propName);
938
+ if (schedulePromises1 && schedulePromises1.length > 1) {
939
+ schedulePromises.push(safeRace(schedulePromises1));
940
+ }
941
+ if (child) {
942
+ const value = commit(adapter, host, child, ctx, scope, index, schedulePromises, hydrationNodes);
943
+ if (Array.isArray(value)) {
944
+ for (let j = 0; j < value.length; j++) {
945
+ values.push(value[j]);
946
+ }
947
+ index += value.length;
675
948
  }
676
- else if (!SPECIAL_PROPS.has(propName)) {
677
- renderer.patch(tag, value, propName, propValue, oldProps && oldProps[propName], scope);
949
+ else if (value) {
950
+ values.push(value);
951
+ index++;
678
952
  }
679
953
  }
680
954
  }
681
- if (copied) {
682
- props = { ...ret.el.props };
683
- for (const name of copied) {
684
- props[name] = oldProps && oldProps[name];
955
+ if (parent.graveyard) {
956
+ for (let i = 0; i < parent.graveyard.length; i++) {
957
+ const child = parent.graveyard[i];
958
+ unmount(adapter, host, ctx, child, false);
685
959
  }
686
- ret.el = new Element(tag, props);
960
+ parent.graveyard = undefined;
687
961
  }
688
- renderer.arrange(tag, value, props, childValues, oldProps, wrap(ret.cachedChildValues));
689
- ret.cachedChildValues = unwrap(childValues);
690
- if (tag === Portal) {
691
- flush(renderer, ret.value);
692
- return;
962
+ if (parent.lingerers) {
963
+ // if parent.lingerers is set, a descendant component is unmounting
964
+ // asynchronously, so we overwrite values to include lingerering DOM nodes.
965
+ values = getChildValues(parent);
693
966
  }
967
+ return values;
968
+ }
969
+ function commitText(adapter, ret, el, scope, hydrationNodes) {
970
+ const value = adapter.text({
971
+ value: el.props.value,
972
+ scope,
973
+ oldNode: ret.value,
974
+ hydrationNodes,
975
+ });
976
+ ret.value = value;
694
977
  return value;
695
978
  }
696
- function flush(renderer, root, initiator) {
697
- renderer.flush(root);
698
- if (typeof root !== "object" || root === null) {
699
- return;
979
+ function commitRaw(adapter, host, ret, scope, hydrationNodes) {
980
+ if (!ret.oldProps || ret.oldProps.value !== ret.el.props.value) {
981
+ const oldNodes = wrap(ret.value);
982
+ for (let i = 0; i < oldNodes.length; i++) {
983
+ const oldNode = oldNodes[i];
984
+ adapter.remove({
985
+ node: oldNode,
986
+ parentNode: host.value,
987
+ isNested: false,
988
+ });
989
+ }
990
+ ret.value = adapter.raw({
991
+ value: ret.el.props.value,
992
+ scope,
993
+ hydrationNodes,
994
+ });
995
+ }
996
+ ret.oldProps = stripSpecialProps(ret.el.props);
997
+ return ret.value;
998
+ }
999
+ function commitHost(adapter, ret, ctx, schedulePromises, hydrationNodes) {
1000
+ if (getFlag(ret, IsCopied) && getFlag(ret, DidCommit)) {
1001
+ return getValue(ret);
700
1002
  }
701
- const flushMap = flushMaps.get(root);
702
- if (flushMap) {
703
- if (initiator) {
704
- const flushMap1 = new Map();
705
- for (let [ctx, callbacks] of flushMap) {
706
- if (!ctxContains(initiator, ctx)) {
707
- flushMap.delete(ctx);
708
- flushMap1.set(ctx, callbacks);
1003
+ const tag = ret.el.tag;
1004
+ const props = stripSpecialProps(ret.el.props);
1005
+ const oldProps = ret.oldProps;
1006
+ let node = ret.value;
1007
+ let copyProps;
1008
+ let copyChildren = false;
1009
+ if (oldProps) {
1010
+ for (const propName in props) {
1011
+ if (props[propName] === Copy) {
1012
+ // The Copy tag can be used to skip the patching of a prop.
1013
+ // <div class={shouldPatchClass ? "class-name" : Copy} />
1014
+ props[propName] = oldProps[propName];
1015
+ (copyProps = copyProps || new Set()).add(propName);
1016
+ }
1017
+ }
1018
+ if (typeof ret.el.props.copy === "string") {
1019
+ const copyMetaProp = new MetaProp("copy", ret.el.props.copy);
1020
+ if (copyMetaProp.include) {
1021
+ for (const propName of copyMetaProp.props) {
1022
+ if (propName in oldProps) {
1023
+ props[propName] = oldProps[propName];
1024
+ (copyProps = copyProps || new Set()).add(propName);
1025
+ }
1026
+ }
1027
+ }
1028
+ else {
1029
+ for (const propName in oldProps) {
1030
+ if (!copyMetaProp.props.has(propName)) {
1031
+ props[propName] = oldProps[propName];
1032
+ (copyProps = copyProps || new Set()).add(propName);
1033
+ }
1034
+ }
1035
+ }
1036
+ copyChildren = copyMetaProp.includes("children");
1037
+ }
1038
+ }
1039
+ const scope = ret.scope;
1040
+ let childHydrationNodes;
1041
+ let quietProps;
1042
+ let hydrationMetaProp;
1043
+ if (!getFlag(ret, DidCommit)) {
1044
+ if (tag === Portal) {
1045
+ if (ret.el.props.hydrate && typeof ret.el.props.hydrate !== "string") {
1046
+ childHydrationNodes = adapter.adopt({
1047
+ tag,
1048
+ tagName: getTagName(tag),
1049
+ node,
1050
+ props,
1051
+ scope,
1052
+ });
1053
+ if (childHydrationNodes) {
1054
+ for (let i = 0; i < childHydrationNodes.length; i++) {
1055
+ adapter.remove({
1056
+ node: childHydrationNodes[i],
1057
+ parentNode: node,
1058
+ isNested: false,
1059
+ });
1060
+ }
1061
+ }
1062
+ }
1063
+ }
1064
+ else {
1065
+ if (!node && hydrationNodes) {
1066
+ const nextChild = hydrationNodes.shift();
1067
+ if (typeof ret.el.props.hydrate === "string") {
1068
+ hydrationMetaProp = new MetaProp("hydration", ret.el.props.hydrate);
1069
+ if (hydrationMetaProp.include) {
1070
+ // if we're in inclusive mode, we add all props to quietProps and
1071
+ // remove props specified in the metaprop
1072
+ quietProps = new Set(Object.keys(props));
1073
+ for (const propName of hydrationMetaProp.props) {
1074
+ quietProps.delete(propName);
1075
+ }
1076
+ }
1077
+ else {
1078
+ quietProps = hydrationMetaProp.props;
1079
+ }
1080
+ }
1081
+ childHydrationNodes = adapter.adopt({
1082
+ tag,
1083
+ tagName: getTagName(tag),
1084
+ node: nextChild,
1085
+ props,
1086
+ scope,
1087
+ });
1088
+ if (childHydrationNodes) {
1089
+ node = nextChild;
1090
+ for (let i = 0; i < childHydrationNodes.length; i++) {
1091
+ adapter.remove({
1092
+ node: childHydrationNodes[i],
1093
+ parentNode: node,
1094
+ isNested: false,
1095
+ });
1096
+ }
709
1097
  }
710
1098
  }
711
- if (flushMap1.size) {
712
- flushMaps.set(root, flushMap1);
1099
+ // TODO: For some reason, there are cases where the node is already set
1100
+ // and the DidCommit flag is false. Not checking for node fails a test
1101
+ // where a child dispatches an event in a schedule callback, the parent
1102
+ // listens for this event and refreshes.
1103
+ if (!node) {
1104
+ node = adapter.create({
1105
+ tag,
1106
+ tagName: getTagName(tag),
1107
+ props,
1108
+ scope,
1109
+ });
1110
+ }
1111
+ ret.value = node;
1112
+ }
1113
+ }
1114
+ if (tag !== Portal) {
1115
+ adapter.patch({
1116
+ tag,
1117
+ tagName: getTagName(tag),
1118
+ node,
1119
+ props,
1120
+ oldProps,
1121
+ scope,
1122
+ copyProps,
1123
+ isHydrating: !!childHydrationNodes,
1124
+ quietProps,
1125
+ });
1126
+ }
1127
+ if (!copyChildren) {
1128
+ const children = commitChildren(adapter, ret, ctx, scope, ret, 0, schedulePromises, hydrationMetaProp && !hydrationMetaProp.includes("children")
1129
+ ? undefined
1130
+ : childHydrationNodes);
1131
+ adapter.arrange({
1132
+ tag,
1133
+ tagName: getTagName(tag),
1134
+ node: node,
1135
+ props,
1136
+ children,
1137
+ oldProps,
1138
+ });
1139
+ }
1140
+ ret.oldProps = props;
1141
+ if (tag === Portal) {
1142
+ flush(adapter, ret.value);
1143
+ // The root passed to Portal elements are opaque to parents so we return
1144
+ // undefined here.
1145
+ return;
1146
+ }
1147
+ return node;
1148
+ }
1149
+ class MetaProp {
1150
+ constructor(propName, propValue) {
1151
+ this.include = true;
1152
+ this.props = new Set();
1153
+ let noBangs = true;
1154
+ let allBangs = true;
1155
+ const tokens = propValue.split(/[,\s]+/);
1156
+ for (let i = 0; i < tokens.length; i++) {
1157
+ const token = tokens[i].trim();
1158
+ if (!token) {
1159
+ continue;
1160
+ }
1161
+ else if (token.startsWith("!")) {
1162
+ noBangs = false;
1163
+ this.props.add(token.slice(1));
713
1164
  }
714
1165
  else {
715
- flushMaps.delete(root);
1166
+ allBangs = false;
1167
+ this.props.add(token);
716
1168
  }
717
1169
  }
1170
+ if (!allBangs && !noBangs) {
1171
+ console.error(`Invalid ${propName} prop "${propValue}".\nUse prop or !prop but not both.`);
1172
+ this.include = true;
1173
+ this.props.clear();
1174
+ }
1175
+ else {
1176
+ this.include = noBangs;
1177
+ }
1178
+ }
1179
+ includes(propName) {
1180
+ if (this.include) {
1181
+ return this.props.has(propName);
1182
+ }
1183
+ else {
1184
+ return !this.props.has(propName);
1185
+ }
1186
+ }
1187
+ }
1188
+ function contextContains(parent, child) {
1189
+ for (let current = child; current !== undefined; current = current.parent) {
1190
+ if (current === parent) {
1191
+ return true;
1192
+ }
1193
+ }
1194
+ return false;
1195
+ }
1196
+ // When rendering is done without a root, we use this special anonymous root to
1197
+ // make sure after callbacks are still called.
1198
+ const ANONYMOUS_ROOT = {};
1199
+ function flush(adapter, root, initiator) {
1200
+ if (root != null) {
1201
+ adapter.finalize(root);
1202
+ }
1203
+ if (typeof root !== "object" || root === null) {
1204
+ root = ANONYMOUS_ROOT;
1205
+ }
1206
+ // The initiator is the context which initiated the rendering process. If
1207
+ // initiator is defined we call and clear all flush callbacks which are
1208
+ // registered with the initiator or with a child context of the initiator,
1209
+ // because they are fully rendered.
1210
+ //
1211
+ // If no initiator is provided, we can call and clear all flush callbacks
1212
+ // which are not scheduling.
1213
+ const afterMap = afterMapByRoot.get(root);
1214
+ if (afterMap) {
1215
+ const afterMap1 = new Map();
1216
+ for (const [ctx, callbacks] of afterMap) {
1217
+ if (getFlag(ctx.ret, IsScheduling) ||
1218
+ (initiator && !contextContains(initiator, ctx))) {
1219
+ // copy over callbacks to the new map (defer them)
1220
+ afterMap.delete(ctx);
1221
+ afterMap1.set(ctx, callbacks);
1222
+ }
1223
+ }
1224
+ if (afterMap1.size) {
1225
+ afterMapByRoot.set(root, afterMap1);
1226
+ }
718
1227
  else {
719
- flushMaps.delete(root);
1228
+ afterMapByRoot.delete(root);
720
1229
  }
721
- for (const [ctx, callbacks] of flushMap) {
722
- const value = renderer.read(getValue(ctx.ret));
1230
+ for (const [ctx, callbacks] of afterMap) {
1231
+ const value = adapter.read(getValue(ctx.ret));
723
1232
  for (const callback of callbacks) {
724
1233
  callback(value);
725
1234
  }
726
1235
  }
727
1236
  }
728
1237
  }
729
- function unmount(renderer, host, ctx, ret) {
1238
+ function unmount(adapter, host, ctx, ret, isNested) {
1239
+ // TODO: set the IsUnmounted flag consistently for all retainers
1240
+ if (ret.fallback) {
1241
+ unmount(adapter, host, ctx, ret.fallback, isNested);
1242
+ ret.fallback = undefined;
1243
+ }
1244
+ if (getFlag(ret, IsResurrecting)) {
1245
+ return;
1246
+ }
1247
+ if (ret.lingerers) {
1248
+ for (let i = 0; i < ret.lingerers.length; i++) {
1249
+ const lingerers = ret.lingerers[i];
1250
+ if (lingerers) {
1251
+ for (const lingerer of lingerers) {
1252
+ unmount(adapter, host, ctx, lingerer, isNested);
1253
+ }
1254
+ }
1255
+ }
1256
+ ret.lingerers = undefined;
1257
+ }
730
1258
  if (typeof ret.el.tag === "function") {
731
- ctx = ret.ctx;
732
- unmountComponent(ctx);
1259
+ unmountComponent(ret.ctx, isNested);
1260
+ }
1261
+ else if (ret.el.tag === Fragment) {
1262
+ unmountChildren(adapter, host, ctx, ret, isNested);
733
1263
  }
734
1264
  else if (ret.el.tag === Portal) {
735
- host = ret;
736
- renderer.arrange(Portal, host.value, host.el.props, [], host.el.props, wrap(host.cachedChildValues));
737
- flush(renderer, host.value);
1265
+ unmountChildren(adapter, ret, ctx, ret, false);
1266
+ if (ret.value != null) {
1267
+ adapter.finalize(ret.value);
1268
+ }
738
1269
  }
739
- else if (ret.el.tag !== Fragment) {
740
- if (isEventTarget(ret.value)) {
741
- const records = getListenerRecords(ctx, host);
742
- for (let i = 0; i < records.length; i++) {
743
- const record = records[i];
744
- ret.value.removeEventListener(record.type, record.callback, record.options);
745
- }
1270
+ else {
1271
+ unmountChildren(adapter, ret, ctx, ret, true);
1272
+ if (getFlag(ret, DidCommit)) {
1273
+ if (ctx) {
1274
+ // Remove the value from every context which shares the same host.
1275
+ removeEventTargetDelegates(ctx.ctx, [ret.value], (ctx1) => ctx1[_ContextState].host === host);
1276
+ }
1277
+ adapter.remove({
1278
+ node: ret.value,
1279
+ parentNode: host.value,
1280
+ isNested,
1281
+ });
746
1282
  }
747
- renderer.dispose(ret.el.tag, ret.value, ret.el.props);
748
- host = ret;
749
1283
  }
750
- const children = wrap(ret.children);
751
- for (let i = 0; i < children.length; i++) {
1284
+ }
1285
+ function unmountChildren(adapter, host, ctx, ret, isNested) {
1286
+ if (ret.graveyard) {
1287
+ for (let i = 0; i < ret.graveyard.length; i++) {
1288
+ const child = ret.graveyard[i];
1289
+ unmount(adapter, host, ctx, child, isNested);
1290
+ }
1291
+ ret.graveyard = undefined;
1292
+ }
1293
+ for (let i = 0, children = wrap(ret.children); i < children.length; i++) {
752
1294
  const child = children[i];
753
1295
  if (typeof child === "object") {
754
- unmount(renderer, host, ctx, child);
1296
+ unmount(adapter, host, ctx, child, isNested);
755
1297
  }
756
1298
  }
757
1299
  }
758
- /*** CONTEXT FLAGS ***/
1300
+ const provisionMaps = new WeakMap();
1301
+ const scheduleMap = new WeakMap();
1302
+ const cleanupMap = new WeakMap();
1303
+ // keys are roots
1304
+ const afterMapByRoot = new WeakMap();
1305
+ // TODO: allow ContextState to be initialized for testing purposes
759
1306
  /**
760
- * A flag which is true when the component is initialized or updated by an
761
- * ancestor component or the root render call.
762
- *
763
- * Used to determine things like whether the nearest host ancestor needs to be
764
- * rearranged.
1307
+ * @internal
1308
+ * The internal class which holds context data.
765
1309
  */
766
- const IsUpdating = 1 << 0;
767
- /**
768
- * A flag which is true when the component is synchronously executing.
769
- *
770
- * Used to guard against components triggering stack overflow or generator error.
771
- */
772
- const IsSyncExecuting = 1 << 1;
773
- /**
774
- * A flag which is true when the component is in a for...of loop.
775
- */
776
- const IsInForOfLoop = 1 << 2;
777
- /**
778
- * A flag which is true when the component is in a for await...of loop.
779
- */
780
- const IsInForAwaitOfLoop = 1 << 3;
781
- /**
782
- * A flag which is true when the component starts the render loop but has not
783
- * yielded yet.
784
- *
785
- * Used to make sure that components yield at least once per loop.
786
- */
787
- const NeedsToYield = 1 << 4;
788
- /**
789
- * A flag used by async generator components in conjunction with the
790
- * onAvailable callback to mark whether new props can be pulled via the context
791
- * async iterator. See the Symbol.asyncIterator method and the
792
- * resumeCtxIterator function.
793
- */
794
- const PropsAvailable = 1 << 5;
795
- /**
796
- * A flag which is set when a component errors.
797
- *
798
- * This is mainly used to prevent some false positives in "component yields or
799
- * returns undefined" warnings. The reason we’re using this versus IsUnmounted
800
- * is a very troubling test (cascades sync generator parent and sync generator
801
- * child) where synchronous code causes a stack overflow error in a
802
- * non-deterministic way. Deeply disturbing stuff.
803
- */
804
- const IsErrored = 1 << 6;
805
- /**
806
- * A flag which is set when the component is unmounted. Unmounted components
807
- * are no longer in the element tree and cannot refresh or rerender.
808
- */
809
- const IsUnmounted = 1 << 7;
810
- /**
811
- * A flag which indicates that the component is a sync generator component.
812
- */
813
- const IsSyncGen = 1 << 8;
814
- /**
815
- * A flag which indicates that the component is an async generator component.
816
- */
817
- const IsAsyncGen = 1 << 9;
818
- /**
819
- * A flag which is set while schedule callbacks are called.
820
- */
821
- const IsScheduling = 1 << 10;
822
- /**
823
- * A flag which is set when a schedule callback calls refresh.
824
- */
825
- const IsSchedulingRefresh = 1 << 11;
826
- const provisionMaps = new WeakMap();
827
- const scheduleMap = new WeakMap();
828
- const cleanupMap = new WeakMap();
829
- // keys are roots
830
- const flushMaps = new WeakMap();
831
- /**
832
- * @internal
833
- * The internal class which holds context data.
834
- */
835
- class ContextImpl {
836
- constructor(renderer, root, host, parent, scope, ret) {
837
- this.f = 0;
838
- this.owner = new Context(this);
839
- this.renderer = renderer;
1310
+ class ContextState {
1311
+ constructor(adapter, root, host, parent, scope, ret) {
1312
+ this.adapter = adapter;
840
1313
  this.root = root;
841
1314
  this.host = host;
842
1315
  this.parent = parent;
1316
+ // This property must be set after this.parent is set because the Context
1317
+ // constructor reads this.parent.
1318
+ this.ctx = new Context(this);
843
1319
  this.scope = scope;
844
1320
  this.ret = ret;
845
1321
  this.iterator = undefined;
846
- this.inflightBlock = undefined;
847
- this.inflightValue = undefined;
848
- this.enqueuedBlock = undefined;
849
- this.enqueuedValue = undefined;
850
- this.onProps = undefined;
1322
+ this.inflight = undefined;
1323
+ this.enqueued = undefined;
1324
+ this.onPropsProvided = undefined;
851
1325
  this.onPropsRequested = undefined;
1326
+ this.pull = undefined;
1327
+ this.index = 0;
1328
+ this.schedule = undefined;
852
1329
  }
853
1330
  }
854
- const _ContextImpl = Symbol.for("crank.ContextImpl");
1331
+ const _ContextState = Symbol.for("crank.ContextState");
855
1332
  /**
856
1333
  * A class which is instantiated and passed to every component as its this
857
- * value. Contexts form a tree just like elements and all components in the
858
- * element tree are connected via contexts. Components can use this tree to
859
- * communicate data upwards via events and downwards via provisions.
1334
+ * value/second parameter. Contexts form a tree just like elements and all
1335
+ * components in the element tree are connected via contexts. Components can
1336
+ * use this tree to communicate data upwards via events and downwards via
1337
+ * provisions.
860
1338
  *
861
1339
  * @template [T=*] - The expected shape of the props passed to the component,
862
1340
  * or a component function. Used to strongly type the Context iterator methods.
@@ -864,17 +1342,18 @@ const _ContextImpl = Symbol.for("crank.ContextImpl");
864
1342
  * places such as the return value of refresh and the argument passed to
865
1343
  * schedule and cleanup callbacks.
866
1344
  */
867
- class Context {
1345
+ class Context extends CustomEventTarget {
868
1346
  // TODO: If we could make the constructor function take a nicer value, it
869
1347
  // would be useful for testing purposes.
870
- constructor(impl) {
871
- this[_ContextImpl] = impl;
1348
+ constructor(state) {
1349
+ super(state.parent ? state.parent.ctx : null);
1350
+ this[_ContextState] = state;
872
1351
  }
873
1352
  /**
874
1353
  * The current props of the associated element.
875
1354
  */
876
1355
  get props() {
877
- return this[_ContextImpl].ret.el.props;
1356
+ return this[_ContextState].ret.el.props;
878
1357
  }
879
1358
  /**
880
1359
  * The current value of the associated element.
@@ -882,47 +1361,51 @@ class Context {
882
1361
  * @deprecated
883
1362
  */
884
1363
  get value() {
885
- return this[_ContextImpl].renderer.read(getValue(this[_ContextImpl].ret));
1364
+ console.warn("Context.value is deprecated.");
1365
+ return this[_ContextState].adapter.read(getValue(this[_ContextState].ret));
1366
+ }
1367
+ get isExecuting() {
1368
+ return getFlag(this[_ContextState].ret, IsExecuting);
1369
+ }
1370
+ get isUnmounted() {
1371
+ return getFlag(this[_ContextState].ret, IsUnmounted);
886
1372
  }
887
1373
  *[Symbol.iterator]() {
888
- const ctx = this[_ContextImpl];
1374
+ const ctx = this[_ContextState];
1375
+ setFlag(ctx.ret, IsInForOfLoop);
889
1376
  try {
890
- ctx.f |= IsInForOfLoop;
891
- while (!(ctx.f & IsUnmounted)) {
892
- if (ctx.f & NeedsToYield) {
893
- throw new Error("Context iterated twice without a yield");
1377
+ while (!getFlag(ctx.ret, IsUnmounted) && !getFlag(ctx.ret, IsErrored)) {
1378
+ if (getFlag(ctx.ret, NeedsToYield)) {
1379
+ throw new Error(`<${getTagName(ctx.ret.el.tag)}> context iterated twice without a yield`);
894
1380
  }
895
1381
  else {
896
- ctx.f |= NeedsToYield;
1382
+ setFlag(ctx.ret, NeedsToYield);
897
1383
  }
898
1384
  yield ctx.ret.el.props;
899
1385
  }
900
1386
  }
901
1387
  finally {
902
- ctx.f &= ~IsInForOfLoop;
1388
+ setFlag(ctx.ret, IsInForOfLoop, false);
903
1389
  }
904
1390
  }
905
1391
  async *[Symbol.asyncIterator]() {
906
- const ctx = this[_ContextImpl];
907
- if (ctx.f & IsSyncGen) {
908
- throw new Error("Use for...of in sync generator components");
909
- }
1392
+ const ctx = this[_ContextState];
1393
+ setFlag(ctx.ret, IsInForAwaitOfLoop);
910
1394
  try {
911
- ctx.f |= IsInForAwaitOfLoop;
912
- while (!(ctx.f & IsUnmounted)) {
913
- if (ctx.f & NeedsToYield) {
914
- throw new Error("Context iterated twice without a yield");
1395
+ while (!getFlag(ctx.ret, IsUnmounted) && !getFlag(ctx.ret, IsErrored)) {
1396
+ if (getFlag(ctx.ret, NeedsToYield)) {
1397
+ throw new Error(`<${getTagName(ctx.ret.el.tag)}> context iterated twice without a yield`);
915
1398
  }
916
1399
  else {
917
- ctx.f |= NeedsToYield;
1400
+ setFlag(ctx.ret, NeedsToYield);
918
1401
  }
919
- if (ctx.f & PropsAvailable) {
920
- ctx.f &= ~PropsAvailable;
1402
+ if (getFlag(ctx.ret, PropsAvailable)) {
1403
+ setFlag(ctx.ret, PropsAvailable, false);
921
1404
  yield ctx.ret.el.props;
922
1405
  }
923
1406
  else {
924
- const props = await new Promise((resolve) => (ctx.onProps = resolve));
925
- if (ctx.f & IsUnmounted) {
1407
+ const props = await new Promise((resolve) => (ctx.onPropsProvided = resolve));
1408
+ if (getFlag(ctx.ret, IsUnmounted) || getFlag(ctx.ret, IsErrored)) {
926
1409
  break;
927
1410
  }
928
1411
  yield props;
@@ -934,7 +1417,7 @@ class Context {
934
1417
  }
935
1418
  }
936
1419
  finally {
937
- ctx.f &= ~IsInForAwaitOfLoop;
1420
+ setFlag(ctx.ret, IsInForAwaitOfLoop, false);
938
1421
  if (ctx.onPropsRequested) {
939
1422
  ctx.onPropsRequested();
940
1423
  ctx.onPropsRequested = undefined;
@@ -944,37 +1427,108 @@ class Context {
944
1427
  /**
945
1428
  * Re-executes a component.
946
1429
  *
947
- * @returns The rendered value of the component or a promise thereof if the
1430
+ * @param callback - Optional callback to execute before refresh
1431
+ * @returns The rendered result of the component or a promise thereof if the
948
1432
  * component or its children execute asynchronously.
949
- *
950
- * The refresh method works a little differently for async generator
951
- * components, in that it will resume the Context’s props async iterator
952
- * rather than resuming execution. This is because async generator components
953
- * are perpetually resumed independent of updates, and rely on the props
954
- * async iterator to suspend.
955
1433
  */
956
- refresh() {
957
- const ctx = this[_ContextImpl];
958
- if (ctx.f & IsUnmounted) {
959
- console.error("Component is unmounted");
960
- return ctx.renderer.read(undefined);
1434
+ refresh(callback) {
1435
+ const ctx = this[_ContextState];
1436
+ if (getFlag(ctx.ret, IsUnmounted)) {
1437
+ console.error(`Component <${getTagName(ctx.ret.el.tag)}> is unmounted. Check the isUnmounted property if necessary.`);
1438
+ return ctx.adapter.read(getValue(ctx.ret));
1439
+ }
1440
+ else if (getFlag(ctx.ret, IsExecuting)) {
1441
+ console.error(`Component <${getTagName(ctx.ret.el.tag)}> is already executing Check the isExecuting property if necessary.`);
1442
+ return ctx.adapter.read(getValue(ctx.ret));
1443
+ }
1444
+ if (callback) {
1445
+ const result = callback();
1446
+ if (isPromiseLike(result)) {
1447
+ return Promise.resolve(result).then(() => {
1448
+ if (!getFlag(ctx.ret, IsUnmounted)) {
1449
+ return this.refresh();
1450
+ }
1451
+ return ctx.adapter.read(getValue(ctx.ret));
1452
+ });
1453
+ }
961
1454
  }
962
- else if (ctx.f & IsSyncExecuting) {
963
- console.error("Component is already executing");
964
- return ctx.renderer.read(getValue(ctx.ret));
1455
+ let diff;
1456
+ const schedulePromises = [];
1457
+ try {
1458
+ setFlag(ctx.ret, IsRefreshing);
1459
+ diff = enqueueComponent(ctx);
1460
+ if (isPromiseLike(diff)) {
1461
+ return diff
1462
+ .then(() => ctx.adapter.read(commitComponent(ctx, schedulePromises)))
1463
+ .then((result) => {
1464
+ if (schedulePromises.length) {
1465
+ return Promise.all(schedulePromises).then(() => {
1466
+ return ctx.adapter.read(getValue(ctx.ret));
1467
+ });
1468
+ }
1469
+ return result;
1470
+ })
1471
+ .catch((err) => {
1472
+ const diff = propagateError(ctx, err, schedulePromises);
1473
+ if (diff) {
1474
+ return diff.then(() => {
1475
+ if (schedulePromises.length) {
1476
+ return Promise.all(schedulePromises).then(() => {
1477
+ return ctx.adapter.read(getValue(ctx.ret));
1478
+ });
1479
+ }
1480
+ return ctx.adapter.read(getValue(ctx.ret));
1481
+ });
1482
+ }
1483
+ if (schedulePromises.length) {
1484
+ return Promise.all(schedulePromises).then(() => {
1485
+ return ctx.adapter.read(getValue(ctx.ret));
1486
+ });
1487
+ }
1488
+ return ctx.adapter.read(getValue(ctx.ret));
1489
+ })
1490
+ .finally(() => setFlag(ctx.ret, IsRefreshing, false));
1491
+ }
1492
+ const result = ctx.adapter.read(commitComponent(ctx, schedulePromises));
1493
+ if (schedulePromises.length) {
1494
+ return Promise.all(schedulePromises).then(() => {
1495
+ return ctx.adapter.read(getValue(ctx.ret));
1496
+ });
1497
+ }
1498
+ return result;
965
1499
  }
966
- const value = enqueueComponentRun(ctx);
967
- if (isPromiseLike(value)) {
968
- return value.then((value) => ctx.renderer.read(value));
1500
+ catch (err) {
1501
+ // TODO: await schedulePromises
1502
+ const diff = propagateError(ctx, err, schedulePromises);
1503
+ if (diff) {
1504
+ return diff
1505
+ .then(() => {
1506
+ if (schedulePromises.length) {
1507
+ return Promise.all(schedulePromises).then(() => {
1508
+ return ctx.adapter.read(getValue(ctx.ret));
1509
+ });
1510
+ }
1511
+ })
1512
+ .then(() => ctx.adapter.read(getValue(ctx.ret)));
1513
+ }
1514
+ if (schedulePromises.length) {
1515
+ return Promise.all(schedulePromises).then(() => {
1516
+ return ctx.adapter.read(getValue(ctx.ret));
1517
+ });
1518
+ }
1519
+ return ctx.adapter.read(getValue(ctx.ret));
1520
+ }
1521
+ finally {
1522
+ if (!isPromiseLike(diff)) {
1523
+ setFlag(ctx.ret, IsRefreshing, false);
1524
+ }
969
1525
  }
970
- return ctx.renderer.read(value);
971
1526
  }
972
- /**
973
- * Registers a callback which fires when the component commits. Will only
974
- * fire once per callback and update.
975
- */
976
1527
  schedule(callback) {
977
- const ctx = this[_ContextImpl];
1528
+ if (!callback) {
1529
+ return new Promise((resolve) => this.schedule(resolve));
1530
+ }
1531
+ const ctx = this[_ContextState];
978
1532
  let callbacks = scheduleMap.get(ctx);
979
1533
  if (!callbacks) {
980
1534
  callbacks = new Set();
@@ -982,35 +1536,35 @@ class Context {
982
1536
  }
983
1537
  callbacks.add(callback);
984
1538
  }
985
- /**
986
- * Registers a callback which fires when the component’s children are
987
- * rendered into the root. Will only fire once per callback and render.
988
- */
989
- flush(callback) {
990
- const ctx = this[_ContextImpl];
991
- if (typeof ctx.root !== "object" || ctx.root === null) {
992
- return;
1539
+ after(callback) {
1540
+ if (!callback) {
1541
+ return new Promise((resolve) => this.after(resolve));
993
1542
  }
994
- let flushMap = flushMaps.get(ctx.root);
995
- if (!flushMap) {
996
- flushMap = new Map();
997
- flushMaps.set(ctx.root, flushMap);
1543
+ const ctx = this[_ContextState];
1544
+ const root = ctx.root || ANONYMOUS_ROOT;
1545
+ let afterMap = afterMapByRoot.get(root);
1546
+ if (!afterMap) {
1547
+ afterMap = new Map();
1548
+ afterMapByRoot.set(root, afterMap);
998
1549
  }
999
- let callbacks = flushMap.get(ctx);
1550
+ let callbacks = afterMap.get(ctx);
1000
1551
  if (!callbacks) {
1001
1552
  callbacks = new Set();
1002
- flushMap.set(ctx, callbacks);
1553
+ afterMap.set(ctx, callbacks);
1003
1554
  }
1004
1555
  callbacks.add(callback);
1005
1556
  }
1006
- /**
1007
- * Registers a callback which fires when the component unmounts. Will only
1008
- * fire once per callback.
1009
- */
1557
+ flush(callback) {
1558
+ console.error("Context.flush() method has been renamed to after()");
1559
+ this.after(callback);
1560
+ }
1010
1561
  cleanup(callback) {
1011
- const ctx = this[_ContextImpl];
1012
- if (ctx.f & IsUnmounted) {
1013
- const value = ctx.renderer.read(getValue(ctx.ret));
1562
+ if (!callback) {
1563
+ return new Promise((resolve) => this.cleanup(resolve));
1564
+ }
1565
+ const ctx = this[_ContextState];
1566
+ if (getFlag(ctx.ret, IsUnmounted)) {
1567
+ const value = ctx.adapter.read(getValue(ctx.ret));
1014
1568
  callback(value);
1015
1569
  return;
1016
1570
  }
@@ -1022,7 +1576,7 @@ class Context {
1022
1576
  callbacks.add(callback);
1023
1577
  }
1024
1578
  consume(key) {
1025
- for (let ctx = this[_ContextImpl].parent; ctx !== undefined; ctx = ctx.parent) {
1579
+ for (let ctx = this[_ContextState].parent; ctx !== undefined; ctx = ctx.parent) {
1026
1580
  const provisions = provisionMaps.get(ctx);
1027
1581
  if (provisions && provisions.has(key)) {
1028
1582
  return provisions.get(key);
@@ -1030,7 +1584,7 @@ class Context {
1030
1584
  }
1031
1585
  }
1032
1586
  provide(key, value) {
1033
- const ctx = this[_ContextImpl];
1587
+ const ctx = this[_ContextState];
1034
1588
  let provisions = provisionMaps.get(ctx);
1035
1589
  if (!provisions) {
1036
1590
  provisions = new Map();
@@ -1038,977 +1592,781 @@ class Context {
1038
1592
  }
1039
1593
  provisions.set(key, value);
1040
1594
  }
1041
- addEventListener(type, listener, options) {
1042
- const ctx = this[_ContextImpl];
1043
- let listeners;
1044
- if (!isListenerOrListenerObject(listener)) {
1045
- return;
1046
- }
1047
- else {
1048
- const listeners1 = listenersMap.get(ctx);
1049
- if (listeners1) {
1050
- listeners = listeners1;
1051
- }
1052
- else {
1053
- listeners = [];
1054
- listenersMap.set(ctx, listeners);
1055
- }
1056
- }
1057
- options = normalizeListenerOptions(options);
1058
- let callback;
1059
- if (typeof listener === "object") {
1060
- callback = () => listener.handleEvent.apply(listener, arguments);
1595
+ [CustomEventTarget.dispatchEventOnSelf](ev) {
1596
+ const ctx = this[_ContextState];
1597
+ // dispatchEvent calls the prop callback if it exists
1598
+ let propCallback = ctx.ret.el.props["on" + ev.type];
1599
+ if (typeof propCallback === "function") {
1600
+ propCallback(ev);
1061
1601
  }
1062
1602
  else {
1063
- callback = listener;
1064
- }
1065
- const record = { type, listener, callback, options };
1066
- if (options.once) {
1067
- record.callback = function () {
1068
- const i = listeners.indexOf(record);
1069
- if (i !== -1) {
1070
- listeners.splice(i, 1);
1071
- }
1072
- return callback.apply(this, arguments);
1073
- };
1074
- }
1075
- if (listeners.some((record1) => record.type === record1.type &&
1076
- record.listener === record1.listener &&
1077
- !record.options.capture === !record1.options.capture)) {
1078
- return;
1079
- }
1080
- listeners.push(record);
1081
- // TODO: is it possible to separate out the EventTarget delegation logic
1082
- for (const value of getChildValues(ctx.ret)) {
1083
- if (isEventTarget(value)) {
1084
- value.addEventListener(record.type, record.callback, record.options);
1085
- }
1086
- }
1087
- }
1088
- removeEventListener(type, listener, options) {
1089
- const ctx = this[_ContextImpl];
1090
- const listeners = listenersMap.get(ctx);
1091
- if (listeners == null || !isListenerOrListenerObject(listener)) {
1092
- return;
1093
- }
1094
- const options1 = normalizeListenerOptions(options);
1095
- const i = listeners.findIndex((record) => record.type === type &&
1096
- record.listener === listener &&
1097
- !record.options.capture === !options1.capture);
1098
- if (i === -1) {
1099
- return;
1100
- }
1101
- const record = listeners[i];
1102
- listeners.splice(i, 1);
1103
- // TODO: is it possible to separate out the EventTarget delegation logic
1104
- for (const value of getChildValues(ctx.ret)) {
1105
- if (isEventTarget(value)) {
1106
- value.removeEventListener(record.type, record.callback, record.options);
1107
- }
1108
- }
1109
- }
1110
- dispatchEvent(ev) {
1111
- const ctx = this[_ContextImpl];
1112
- const path = [];
1113
- for (let parent = ctx.parent; parent !== undefined; parent = parent.parent) {
1114
- path.push(parent);
1115
- }
1116
- // We patch the stopImmediatePropagation method because ev.cancelBubble
1117
- // only informs us if stopPropagation was called and there are no
1118
- // properties which inform us if stopImmediatePropagation was called.
1119
- let immediateCancelBubble = false;
1120
- const stopImmediatePropagation = ev.stopImmediatePropagation;
1121
- setEventProperty(ev, "stopImmediatePropagation", () => {
1122
- immediateCancelBubble = true;
1123
- return stopImmediatePropagation.call(ev);
1124
- });
1125
- setEventProperty(ev, "target", ctx.owner);
1126
- // The only possible errors in this block are errors thrown by callbacks,
1127
- // and dispatchEvent will only log these errors rather than throwing
1128
- // them. Therefore, we place all code in a try block, log errors in the
1129
- // catch block, and use an unsafe return statement in the finally block.
1130
- //
1131
- // Each early return within the try block returns true because while the
1132
- // return value is overridden in the finally block, TypeScript
1133
- // (justifiably) does not recognize the unsafe return statement.
1134
- try {
1135
- setEventProperty(ev, "eventPhase", CAPTURING_PHASE);
1136
- for (let i = path.length - 1; i >= 0; i--) {
1137
- const target = path[i];
1138
- const listeners = listenersMap.get(target);
1139
- if (listeners) {
1140
- setEventProperty(ev, "currentTarget", target.owner);
1141
- for (const record of listeners) {
1142
- if (record.type === ev.type && record.options.capture) {
1143
- try {
1144
- record.callback.call(target.owner, ev);
1145
- }
1146
- catch (err) {
1147
- console.error(err);
1148
- }
1149
- if (immediateCancelBubble) {
1150
- return true;
1151
- }
1152
- }
1153
- }
1154
- }
1155
- if (ev.cancelBubble) {
1156
- return true;
1157
- }
1158
- }
1159
- {
1160
- setEventProperty(ev, "eventPhase", AT_TARGET);
1161
- setEventProperty(ev, "currentTarget", ctx.owner);
1162
- // dispatchEvent calls the prop callback if it exists
1163
- let propCallback = ctx.ret.el.props["on" + ev.type];
1164
- if (typeof propCallback === "function") {
1165
- propCallback(ev);
1166
- if (immediateCancelBubble || ev.cancelBubble) {
1167
- return true;
1168
- }
1169
- }
1170
- else {
1171
- // Checks for camel-cased event props
1172
- for (const propName in ctx.ret.el.props) {
1173
- if (propName.toLowerCase() === "on" + ev.type.toLowerCase()) {
1174
- propCallback = ctx.ret.el.props[propName];
1175
- if (typeof propCallback === "function") {
1176
- propCallback(ev);
1177
- if (immediateCancelBubble || ev.cancelBubble) {
1178
- return true;
1179
- }
1180
- }
1181
- }
1182
- }
1183
- }
1184
- const listeners = listenersMap.get(ctx);
1185
- if (listeners) {
1186
- for (const record of listeners) {
1187
- if (record.type === ev.type) {
1188
- try {
1189
- record.callback.call(ctx.owner, ev);
1190
- }
1191
- catch (err) {
1192
- console.error(err);
1193
- }
1194
- if (immediateCancelBubble) {
1195
- return true;
1196
- }
1197
- }
1198
- }
1199
- if (ev.cancelBubble) {
1200
- return true;
1201
- }
1202
- }
1203
- }
1204
- if (ev.bubbles) {
1205
- setEventProperty(ev, "eventPhase", BUBBLING_PHASE);
1206
- for (let i = 0; i < path.length; i++) {
1207
- const target = path[i];
1208
- const listeners = listenersMap.get(target);
1209
- if (listeners) {
1210
- setEventProperty(ev, "currentTarget", target.owner);
1211
- for (const record of listeners) {
1212
- if (record.type === ev.type && !record.options.capture) {
1213
- try {
1214
- record.callback.call(target.owner, ev);
1215
- }
1216
- catch (err) {
1217
- console.error(err);
1218
- }
1219
- if (immediateCancelBubble) {
1220
- return true;
1221
- }
1222
- }
1223
- }
1224
- }
1225
- if (ev.cancelBubble) {
1226
- return true;
1603
+ for (const propName in ctx.ret.el.props) {
1604
+ if (propName.toLowerCase() === "on" + ev.type.toLowerCase()) {
1605
+ propCallback = ctx.ret.el.props[propName];
1606
+ if (typeof propCallback === "function") {
1607
+ propCallback(ev);
1227
1608
  }
1228
1609
  }
1229
1610
  }
1230
1611
  }
1231
- finally {
1232
- setEventProperty(ev, "eventPhase", NONE);
1233
- setEventProperty(ev, "currentTarget", null);
1234
- // eslint-disable-next-line no-unsafe-finally
1235
- return !ev.defaultPrevented;
1236
- }
1237
1612
  }
1238
1613
  }
1239
- /*** PRIVATE CONTEXT FUNCTIONS ***/
1240
- function ctxContains(parent, child) {
1241
- for (let current = child; current !== undefined; current = current.parent) {
1242
- if (current === parent) {
1243
- return true;
1244
- }
1245
- }
1246
- return false;
1247
- }
1248
- function updateComponent(renderer, root, host, parent, scope, ret, oldProps, hydrationData) {
1614
+ function diffComponent(adapter, root, host, parent, scope, ret) {
1249
1615
  let ctx;
1250
- if (oldProps) {
1616
+ if (ret.ctx) {
1251
1617
  ctx = ret.ctx;
1252
- if (ctx.f & IsSyncExecuting) {
1253
- console.error("Component is already executing");
1254
- return ret.cachedChildValues;
1618
+ if (getFlag(ctx.ret, IsExecuting)) {
1619
+ console.error(`Component <${getTagName(ctx.ret.el.tag)}> is already executing`);
1620
+ return;
1621
+ }
1622
+ else if (ctx.schedule) {
1623
+ return ctx.schedule.promise.then(() => {
1624
+ return diffComponent(adapter, root, host, parent, scope, ret);
1625
+ });
1255
1626
  }
1256
1627
  }
1257
1628
  else {
1258
- ctx = ret.ctx = new ContextImpl(renderer, root, host, parent, scope, ret);
1629
+ ctx = ret.ctx = new ContextState(adapter, root, host, parent, scope, ret);
1259
1630
  }
1260
- ctx.f |= IsUpdating;
1261
- return enqueueComponentRun(ctx, hydrationData);
1631
+ setFlag(ctx.ret, IsUpdating);
1632
+ return enqueueComponent(ctx);
1262
1633
  }
1263
- function updateComponentChildren(ctx, children, hydrationData) {
1264
- if (ctx.f & IsUnmounted) {
1265
- return;
1266
- }
1267
- else if (ctx.f & IsErrored) {
1268
- // This branch is necessary for some race conditions where this function is
1269
- // called after iterator.throw() in async generator components.
1634
+ function diffComponentChildren(ctx, children, isYield) {
1635
+ if (getFlag(ctx.ret, IsUnmounted) || getFlag(ctx.ret, IsErrored)) {
1270
1636
  return;
1271
1637
  }
1272
1638
  else if (children === undefined) {
1273
- console.error("A component has returned or yielded undefined. If this was intentional, return or yield null instead.");
1639
+ console.error(`Component <${getTagName(ctx.ret.el.tag)}> has ${isYield ? "yielded" : "returned"} undefined. If this was intentional, ${isYield ? "yield" : "return"} null instead.`);
1274
1640
  }
1275
- let childValues;
1641
+ let diff;
1276
1642
  try {
1277
- // TODO: WAT
1643
+ // TODO: Use a different flag here to indicate the component is
1644
+ // synchronously rendering children
1278
1645
  // We set the isExecuting flag in case a child component dispatches an event
1279
1646
  // which bubbles to this component and causes a synchronous refresh().
1280
- ctx.f |= IsSyncExecuting;
1281
- childValues = diffChildren(ctx.renderer, ctx.root, ctx.host, ctx, ctx.scope, ctx.ret, narrow(children), hydrationData);
1282
- }
1283
- finally {
1284
- ctx.f &= ~IsSyncExecuting;
1285
- }
1286
- if (isPromiseLike(childValues)) {
1287
- ctx.ret.inflightValue = childValues.then((childValues) => commitComponent(ctx, childValues));
1288
- return ctx.ret.inflightValue;
1289
- }
1290
- return commitComponent(ctx, childValues);
1291
- }
1292
- function commitComponent(ctx, values) {
1293
- if (ctx.f & IsUnmounted) {
1294
- return;
1295
- }
1296
- const listeners = listenersMap.get(ctx);
1297
- if (listeners && listeners.length) {
1298
- for (let i = 0; i < values.length; i++) {
1299
- const value = values[i];
1300
- if (isEventTarget(value)) {
1301
- for (let j = 0; j < listeners.length; j++) {
1302
- const record = listeners[j];
1303
- value.addEventListener(record.type, record.callback, record.options);
1304
- }
1305
- }
1306
- }
1307
- }
1308
- const oldValues = wrap(ctx.ret.cachedChildValues);
1309
- let value = (ctx.ret.cachedChildValues = unwrap(values));
1310
- if (ctx.f & IsScheduling) {
1311
- ctx.f |= IsSchedulingRefresh;
1312
- }
1313
- else if (!(ctx.f & IsUpdating)) {
1314
- // If we’re not updating the component, which happens when components are
1315
- // refreshed, or when async generator components iterate, we have to do a
1316
- // little bit housekeeping when a component’s child values have changed.
1317
- if (!arrayEqual(oldValues, values)) {
1318
- const records = getListenerRecords(ctx.parent, ctx.host);
1319
- if (records.length) {
1320
- for (let i = 0; i < values.length; i++) {
1321
- const value = values[i];
1322
- if (isEventTarget(value)) {
1323
- for (let j = 0; j < records.length; j++) {
1324
- const record = records[j];
1325
- value.addEventListener(record.type, record.callback, record.options);
1326
- }
1327
- }
1328
- }
1329
- }
1330
- // rearranging the nearest ancestor host element
1331
- const host = ctx.host;
1332
- const oldHostValues = wrap(host.cachedChildValues);
1333
- invalidate(ctx, host);
1334
- const hostValues = getChildValues(host);
1335
- ctx.renderer.arrange(host.el.tag, host.value, host.el.props, hostValues,
1336
- // props and oldProps are the same because the host isn’t updated.
1337
- host.el.props, oldHostValues);
1647
+ setFlag(ctx.ret, IsExecuting);
1648
+ diff = diffChildren(ctx.adapter, ctx.root, ctx.host, ctx, ctx.scope, ctx.ret, narrow(children));
1649
+ if (diff) {
1650
+ diff = diff.catch((err) => handleChildError(ctx, err));
1338
1651
  }
1339
- flush(ctx.renderer, ctx.root, ctx);
1340
1652
  }
1341
- const callbacks = scheduleMap.get(ctx);
1342
- if (callbacks) {
1343
- scheduleMap.delete(ctx);
1344
- ctx.f |= IsScheduling;
1345
- const value1 = ctx.renderer.read(value);
1346
- for (const callback of callbacks) {
1347
- callback(value1);
1348
- }
1349
- ctx.f &= ~IsScheduling;
1350
- // Handles an edge case where refresh() is called during a schedule().
1351
- if (ctx.f & IsSchedulingRefresh) {
1352
- ctx.f &= ~IsSchedulingRefresh;
1353
- value = getValue(ctx.ret);
1354
- }
1355
- }
1356
- ctx.f &= ~IsUpdating;
1357
- return value;
1358
- }
1359
- function invalidate(ctx, host) {
1360
- for (let parent = ctx.parent; parent !== undefined && parent.host === host; parent = parent.parent) {
1361
- parent.ret.cachedChildValues = undefined;
1362
- }
1363
- host.cachedChildValues = undefined;
1364
- }
1365
- function arrayEqual(arr1, arr2) {
1366
- if (arr1.length !== arr2.length) {
1367
- return false;
1653
+ catch (err) {
1654
+ diff = handleChildError(ctx, err);
1368
1655
  }
1369
- for (let i = 0; i < arr1.length; i++) {
1370
- const value1 = arr1[i];
1371
- const value2 = arr2[i];
1372
- if (value1 !== value2) {
1373
- return false;
1374
- }
1656
+ finally {
1657
+ setFlag(ctx.ret, IsExecuting, false);
1375
1658
  }
1376
- return true;
1659
+ return diff;
1377
1660
  }
1378
1661
  /** Enqueues and executes the component associated with the context. */
1379
- function enqueueComponentRun(ctx, hydrationData) {
1380
- if (ctx.f & IsAsyncGen && !(ctx.f & IsInForOfLoop)) {
1381
- if (hydrationData !== undefined) {
1382
- throw new Error("Hydration error");
1383
- }
1384
- // This branch will run for non-initial renders of async generator
1385
- // components when they are not in for...of loops. When in a for...of loop,
1386
- // async generator components will behave normally.
1387
- //
1388
- // Async gen componennts can be in one of three states:
1389
- //
1390
- // 1. propsAvailable flag is true: "available"
1391
- //
1392
- // The component is suspended somewhere in the loop. When the component
1393
- // reaches the bottom of the loop, it will run again with the next props.
1394
- //
1395
- // 2. onAvailable callback is defined: "suspended"
1396
- //
1397
- // The component has suspended at the bottom of the loop and is waiting
1398
- // for new props.
1399
- //
1400
- // 3. neither 1 or 2: "Running"
1401
- //
1402
- // The component is suspended somewhere in the loop. When the component
1403
- // reaches the bottom of the loop, it will suspend.
1404
- //
1405
- // Components will never be both available and suspended at
1406
- // the same time.
1407
- //
1408
- // If the component is at the loop bottom, this means that the next value
1409
- // produced by the component will have the most up to date props, so we can
1410
- // simply return the current inflight value. Otherwise, we have to wait for
1411
- // the bottom of the loop to be reached before returning the inflight
1412
- // value.
1413
- const isAtLoopbottom = ctx.f & IsInForAwaitOfLoop && !ctx.onProps;
1414
- resumePropsAsyncIterator(ctx);
1415
- if (isAtLoopbottom) {
1416
- if (ctx.inflightBlock == null) {
1417
- ctx.inflightBlock = new Promise((resolve) => (ctx.onPropsRequested = resolve));
1418
- }
1419
- return ctx.inflightBlock.then(() => {
1420
- ctx.inflightBlock = undefined;
1421
- return ctx.inflightValue;
1422
- });
1423
- }
1424
- return ctx.inflightValue;
1425
- }
1426
- else if (!ctx.inflightBlock) {
1427
- try {
1428
- const [block, value] = runComponent(ctx, hydrationData);
1429
- if (block) {
1430
- ctx.inflightBlock = block
1431
- // TODO: there is some fuckery going on here related to async
1432
- // generator components resuming when they’re meant to be returned.
1433
- .then((v) => v)
1434
- .finally(() => advanceComponent(ctx));
1435
- // stepComponent will only return a block if the value is asynchronous
1436
- ctx.inflightValue = value;
1437
- }
1438
- return value;
1439
- }
1440
- catch (err) {
1441
- if (!(ctx.f & IsUpdating)) {
1442
- if (!ctx.parent) {
1443
- throw err;
1444
- }
1445
- return propagateError(ctx.parent, err);
1446
- }
1447
- throw err;
1448
- }
1449
- }
1450
- else if (!ctx.enqueuedBlock) {
1451
- if (hydrationData !== undefined) {
1452
- throw new Error("Hydration error");
1453
- }
1454
- // We need to assign enqueuedBlock and enqueuedValue synchronously, hence
1455
- // the Promise constructor call here.
1456
- let resolveEnqueuedBlock;
1457
- ctx.enqueuedBlock = new Promise((resolve) => (resolveEnqueuedBlock = resolve));
1458
- ctx.enqueuedValue = ctx.inflightBlock.then(() => {
1459
- try {
1460
- const [block, value] = runComponent(ctx);
1461
- if (block) {
1462
- resolveEnqueuedBlock(block.finally(() => advanceComponent(ctx)));
1463
- }
1464
- return value;
1465
- }
1466
- catch (err) {
1467
- if (!(ctx.f & IsUpdating)) {
1468
- if (!ctx.parent) {
1469
- throw err;
1470
- }
1471
- return propagateError(ctx.parent, err);
1472
- }
1473
- throw err;
1474
- }
1475
- });
1476
- }
1477
- return ctx.enqueuedValue;
1662
+ function enqueueComponent(ctx) {
1663
+ if (!ctx.inflight) {
1664
+ const [block, diff] = runComponent(ctx);
1665
+ if (block) {
1666
+ // if block is a promise, diff is a promise
1667
+ ctx.inflight = [block.finally(() => advanceComponent(ctx)), diff];
1668
+ }
1669
+ return diff;
1670
+ }
1671
+ else if (!ctx.enqueued) {
1672
+ // The enqueuedBlock and enqueuedDiff properties must be set
1673
+ // simultaneously, hence the usage of the Promise constructor.
1674
+ let resolve;
1675
+ ctx.enqueued = [
1676
+ new Promise((resolve1) => (resolve = resolve1)).finally(() => advanceComponent(ctx)),
1677
+ ctx.inflight[0].finally(() => {
1678
+ const [block, diff] = runComponent(ctx);
1679
+ resolve(block);
1680
+ return diff;
1681
+ }),
1682
+ ];
1683
+ }
1684
+ return ctx.enqueued[1];
1478
1685
  }
1479
1686
  /** Called when the inflight block promise settles. */
1480
1687
  function advanceComponent(ctx) {
1481
- if (ctx.f & IsAsyncGen && !(ctx.f & IsInForOfLoop)) {
1482
- return;
1483
- }
1484
- ctx.inflightBlock = ctx.enqueuedBlock;
1485
- ctx.inflightValue = ctx.enqueuedValue;
1486
- ctx.enqueuedBlock = undefined;
1487
- ctx.enqueuedValue = undefined;
1688
+ ctx.inflight = ctx.enqueued;
1689
+ ctx.enqueued = undefined;
1488
1690
  }
1489
1691
  /**
1490
- * This function is responsible for executing the component and handling all
1491
- * the different component types. We cannot identify whether a component is a
1492
- * generator or async without calling it and inspecting the return value.
1692
+ * This function is responsible for executing components, and handling the
1693
+ * different component types.
1694
+ *
1695
+ * @returns {[block, diff]} A tuple where:
1696
+ * - block is a promise or undefined which represents the duration during which
1697
+ * the component is blocked.
1698
+ * - diff is a promise or undefined which represents the duration for diffing
1699
+ * of children.
1493
1700
  *
1494
- * @returns {[block, value]} A tuple where
1495
- * block - A possible promise which represents the duration during which the
1496
- * component is blocked from updating.
1497
- * value - A possible promise resolving to the rendered value of children.
1701
+ * While a component is blocked, further updates to the component are enqueued.
1498
1702
  *
1499
- * Each component type will block according to the type of the component.
1500
- * - Sync function components never block and will transparently pass updates
1501
- * to children.
1502
- * - Async function components and async generator components block while
1503
- * executing itself, but will not block for async children.
1504
- * - Sync generator components block while any children are executing, because
1505
- * they are expected to only resume when they’ve actually rendered.
1703
+ * Each component type blocks according to its implementation:
1704
+ * - Sync function components never block; when props or state change,
1705
+ * updates are immediately passed to children.
1706
+ * - Async function components block only while awaiting their own async work
1707
+ * (e.g., during an await), but do not block while their async children are rendering.
1708
+ * - Sync generator components block while their children are rendering;
1709
+ * they only resume once their children have finished.
1710
+ * - Async generator components can block in two different ways:
1711
+ * - By default, they behave like sync generator components, blocking while
1712
+ * the component or its children are rendering.
1713
+ * - Within a for await...of loop, they block only while waiting for new
1714
+ * props to be requested, and not while children are rendering.
1506
1715
  */
1507
- function runComponent(ctx, hydrationData) {
1716
+ function runComponent(ctx) {
1717
+ if (getFlag(ctx.ret, IsUnmounted)) {
1718
+ return [undefined, undefined];
1719
+ }
1508
1720
  const ret = ctx.ret;
1509
1721
  const initial = !ctx.iterator;
1510
1722
  if (initial) {
1511
- resumePropsAsyncIterator(ctx);
1512
- ctx.f |= IsSyncExecuting;
1513
- clearEventListeners(ctx);
1514
- let result;
1723
+ setFlag(ctx.ret, IsExecuting);
1724
+ clearEventListeners(ctx.ctx);
1725
+ let returned;
1515
1726
  try {
1516
- result = ret.el.tag.call(ctx.owner, ret.el.props, ctx.owner);
1727
+ returned = ret.el.tag.call(ctx.ctx, ret.el.props, ctx.ctx);
1517
1728
  }
1518
1729
  catch (err) {
1519
- ctx.f |= IsErrored;
1730
+ setFlag(ctx.ret, IsErrored);
1520
1731
  throw err;
1521
1732
  }
1522
1733
  finally {
1523
- ctx.f &= ~IsSyncExecuting;
1524
- }
1525
- if (isIteratorLike(result)) {
1526
- ctx.iterator = result;
1734
+ setFlag(ctx.ret, IsExecuting, false);
1527
1735
  }
1528
- else if (isPromiseLike(result)) {
1529
- // async function component
1530
- const result1 = result instanceof Promise ? result : Promise.resolve(result);
1531
- const value = result1.then((result) => updateComponentChildren(ctx, result, hydrationData), (err) => {
1532
- ctx.f |= IsErrored;
1533
- throw err;
1534
- });
1535
- return [result1.catch(NOOP), value];
1736
+ if (isIteratorLike(returned)) {
1737
+ ctx.iterator = returned;
1536
1738
  }
1537
- else {
1739
+ else if (!isPromiseLike(returned)) {
1538
1740
  // sync function component
1539
1741
  return [
1540
1742
  undefined,
1541
- updateComponentChildren(ctx, result, hydrationData),
1743
+ diffComponentChildren(ctx, returned, false),
1744
+ ];
1745
+ }
1746
+ else {
1747
+ // async function component
1748
+ const returned1 = returned instanceof Promise ? returned : Promise.resolve(returned);
1749
+ return [
1750
+ returned1.catch(NOOP),
1751
+ returned1.then((returned) => diffComponentChildren(ctx, returned, false), (err) => {
1752
+ setFlag(ctx.ret, IsErrored);
1753
+ throw err;
1754
+ }),
1542
1755
  ];
1543
1756
  }
1544
- }
1545
- else if (hydrationData !== undefined) {
1546
- // hydration data should only be passed on the initial render
1547
- throw new Error("Hydration error");
1548
1757
  }
1549
1758
  let iteration;
1550
1759
  if (initial) {
1551
1760
  try {
1552
- ctx.f |= IsSyncExecuting;
1761
+ setFlag(ctx.ret, IsExecuting);
1553
1762
  iteration = ctx.iterator.next();
1554
1763
  }
1555
1764
  catch (err) {
1556
- ctx.f |= IsErrored;
1765
+ setFlag(ctx.ret, IsErrored);
1557
1766
  throw err;
1558
1767
  }
1559
1768
  finally {
1560
- ctx.f &= ~IsSyncExecuting;
1769
+ setFlag(ctx.ret, IsExecuting, false);
1561
1770
  }
1562
1771
  if (isPromiseLike(iteration)) {
1563
- ctx.f |= IsAsyncGen;
1772
+ setFlag(ctx.ret, IsAsyncGen);
1564
1773
  }
1565
1774
  else {
1566
- ctx.f |= IsSyncGen;
1775
+ setFlag(ctx.ret, IsSyncGen);
1567
1776
  }
1568
1777
  }
1569
- if (ctx.f & IsSyncGen) {
1778
+ if (getFlag(ctx.ret, IsSyncGen)) {
1570
1779
  // sync generator component
1571
1780
  if (!initial) {
1572
1781
  try {
1573
- ctx.f |= IsSyncExecuting;
1574
- iteration = ctx.iterator.next(ctx.renderer.read(getValue(ret)));
1782
+ setFlag(ctx.ret, IsExecuting);
1783
+ const oldResult = ctx.adapter.read(getValue(ctx.ret));
1784
+ iteration = ctx.iterator.next(oldResult);
1575
1785
  }
1576
1786
  catch (err) {
1577
- ctx.f |= IsErrored;
1787
+ setFlag(ctx.ret, IsErrored);
1578
1788
  throw err;
1579
1789
  }
1580
1790
  finally {
1581
- ctx.f &= ~IsSyncExecuting;
1791
+ setFlag(ctx.ret, IsExecuting, false);
1582
1792
  }
1583
1793
  }
1584
1794
  if (isPromiseLike(iteration)) {
1585
1795
  throw new Error("Mixed generator component");
1586
1796
  }
1587
- if (ctx.f & IsInForOfLoop &&
1588
- !(ctx.f & NeedsToYield) &&
1589
- !(ctx.f & IsUnmounted)) {
1590
- console.error("Component yielded more than once in for...of loop");
1797
+ if (getFlag(ctx.ret, IsInForOfLoop) &&
1798
+ !getFlag(ctx.ret, NeedsToYield) &&
1799
+ !getFlag(ctx.ret, IsUnmounted) &&
1800
+ !getFlag(ctx.ret, IsScheduling)) {
1801
+ console.error(`Component <${getTagName(ctx.ret.el.tag)}> yielded/returned more than once in for...of loop`);
1591
1802
  }
1592
- ctx.f &= ~NeedsToYield;
1803
+ setFlag(ctx.ret, NeedsToYield, false);
1593
1804
  if (iteration.done) {
1594
- ctx.f &= ~IsSyncGen;
1805
+ setFlag(ctx.ret, IsSyncGen, false);
1595
1806
  ctx.iterator = undefined;
1596
1807
  }
1597
- let value;
1598
- try {
1599
- value = updateComponentChildren(ctx,
1600
- // Children can be void so we eliminate that here
1601
- iteration.value, hydrationData);
1602
- if (isPromiseLike(value)) {
1603
- value = value.catch((err) => handleChildError(ctx, err));
1604
- }
1605
- }
1606
- catch (err) {
1607
- value = handleChildError(ctx, err);
1608
- }
1609
- const block = isPromiseLike(value) ? value.catch(NOOP) : undefined;
1610
- return [block, value];
1808
+ const diff = diffComponentChildren(ctx, iteration.value, !iteration.done);
1809
+ const block = isPromiseLike(diff) ? diff.catch(NOOP) : undefined;
1810
+ return [block, diff];
1611
1811
  }
1612
1812
  else {
1613
- if (ctx.f & IsInForOfLoop) {
1614
- // Async generator component using for...of loops behave similar to sync
1615
- // generator components. This allows for easier refactoring of sync to
1616
- // async generator components.
1813
+ if (getFlag(ctx.ret, IsInForAwaitOfLoop)) {
1814
+ // initializes the async generator loop
1815
+ pullComponent(ctx, iteration);
1816
+ const block = resumePropsAsyncIterator(ctx);
1817
+ return [block, ctx.pull && ctx.pull.diff];
1818
+ }
1819
+ else {
1820
+ // We call resumePropsAsyncIterator in case the component exits the
1821
+ // for...of loop
1822
+ resumePropsAsyncIterator(ctx);
1617
1823
  if (!initial) {
1618
1824
  try {
1619
- ctx.f |= IsSyncExecuting;
1620
- iteration = ctx.iterator.next(ctx.renderer.read(getValue(ret)));
1825
+ setFlag(ctx.ret, IsExecuting);
1826
+ const oldResult = ctx.adapter.read(getValue(ctx.ret));
1827
+ iteration = ctx.iterator.next(oldResult);
1621
1828
  }
1622
1829
  catch (err) {
1623
- ctx.f |= IsErrored;
1830
+ setFlag(ctx.ret, IsErrored);
1624
1831
  throw err;
1625
1832
  }
1626
1833
  finally {
1627
- ctx.f &= ~IsSyncExecuting;
1834
+ setFlag(ctx.ret, IsExecuting, false);
1628
1835
  }
1629
1836
  }
1630
1837
  if (!isPromiseLike(iteration)) {
1631
1838
  throw new Error("Mixed generator component");
1632
1839
  }
1633
- const block = iteration.catch(NOOP);
1634
- const value = iteration.then((iteration) => {
1635
- let value;
1636
- if (!(ctx.f & IsInForOfLoop)) {
1637
- runAsyncGenComponent(ctx, Promise.resolve(iteration), hydrationData);
1840
+ const diff = iteration.then((iteration) => {
1841
+ if (getFlag(ctx.ret, IsInForAwaitOfLoop)) {
1842
+ // We have entered a for await...of loop, so we start pulling
1843
+ pullComponent(ctx, iteration);
1638
1844
  }
1639
1845
  else {
1640
- if (!(ctx.f & NeedsToYield) && !(ctx.f & IsUnmounted)) {
1641
- console.error("Component yielded more than once in for...of loop");
1642
- }
1643
- }
1644
- ctx.f &= ~NeedsToYield;
1645
- try {
1646
- value = updateComponentChildren(ctx,
1647
- // Children can be void so we eliminate that here
1648
- iteration.value, hydrationData);
1649
- if (isPromiseLike(value)) {
1650
- value = value.catch((err) => handleChildError(ctx, err));
1846
+ if (getFlag(ctx.ret, IsInForOfLoop) &&
1847
+ !getFlag(ctx.ret, NeedsToYield) &&
1848
+ !getFlag(ctx.ret, IsUnmounted) &&
1849
+ !getFlag(ctx.ret, IsScheduling)) {
1850
+ console.error(`Component <${getTagName(ctx.ret.el.tag)}> yielded/returned more than once in for...of loop`);
1651
1851
  }
1652
1852
  }
1653
- catch (err) {
1654
- value = handleChildError(ctx, err);
1853
+ setFlag(ctx.ret, NeedsToYield, false);
1854
+ if (iteration.done) {
1855
+ setFlag(ctx.ret, IsAsyncGen, false);
1856
+ ctx.iterator = undefined;
1655
1857
  }
1656
- return value;
1858
+ return diffComponentChildren(ctx,
1859
+ // Children can be void so we eliminate that here
1860
+ iteration.value, !iteration.done);
1657
1861
  }, (err) => {
1658
- ctx.f |= IsErrored;
1862
+ setFlag(ctx.ret, IsErrored);
1659
1863
  throw err;
1660
1864
  });
1661
- return [block, value];
1865
+ return [diff.catch(NOOP), diff];
1662
1866
  }
1663
- else {
1664
- runAsyncGenComponent(ctx, iteration, hydrationData, initial);
1665
- return [ctx.inflightBlock, ctx.inflightValue];
1867
+ }
1868
+ }
1869
+ /**
1870
+ * Called to resume the props async iterator for async generator components.
1871
+ *
1872
+ * @returns {Promise<undefined> | undefined} A possible promise which
1873
+ * represents the duration during which the component is blocked.
1874
+ */
1875
+ function resumePropsAsyncIterator(ctx) {
1876
+ if (ctx.onPropsProvided) {
1877
+ ctx.onPropsProvided(ctx.ret.el.props);
1878
+ ctx.onPropsProvided = undefined;
1879
+ setFlag(ctx.ret, PropsAvailable, false);
1880
+ }
1881
+ else {
1882
+ setFlag(ctx.ret, PropsAvailable);
1883
+ if (getFlag(ctx.ret, IsInForAwaitOfLoop)) {
1884
+ return new Promise((resolve) => (ctx.onPropsRequested = resolve));
1666
1885
  }
1667
1886
  }
1887
+ return (ctx.pull && ctx.pull.iterationP && ctx.pull.iterationP.then(NOOP, NOOP));
1668
1888
  }
1669
- async function runAsyncGenComponent(ctx, iterationP, hydrationData, initial = false) {
1889
+ /**
1890
+ * The logic for pulling from async generator components when they are in a for
1891
+ * await...of loop is implemented here.
1892
+ *
1893
+ * It makes sense to group this logic in a single async loop to prevent race
1894
+ * conditions caused by calling next(), throw() and return() concurrently.
1895
+ */
1896
+ async function pullComponent(ctx, iterationP) {
1897
+ if (!iterationP || ctx.pull) {
1898
+ return;
1899
+ }
1900
+ ctx.pull = { iterationP: undefined, diff: undefined, onChildError: undefined };
1901
+ // TODO: replace done with iteration
1902
+ //let iteration: ChildrenIteratorResult | undefined;
1670
1903
  let done = false;
1671
1904
  try {
1905
+ let childError;
1672
1906
  while (!done) {
1673
- if (ctx.f & IsInForOfLoop) {
1674
- break;
1675
- }
1676
- // inflightValue must be set synchronously.
1677
- let onValue;
1678
- ctx.inflightValue = new Promise((resolve) => (onValue = resolve));
1679
- if (ctx.f & IsUpdating) {
1680
- // We should not swallow unhandled promise rejections if the component is
1681
- // updating independently.
1682
- // TODO: Does this handle this.refresh() calls?
1683
- ctx.inflightValue.catch(NOOP);
1907
+ if (isPromiseLike(iterationP)) {
1908
+ ctx.pull.iterationP = iterationP;
1684
1909
  }
1910
+ let onDiff;
1911
+ ctx.pull.diff = new Promise((resolve) => (onDiff = resolve)).then(() => {
1912
+ if (!(getFlag(ctx.ret, IsUpdating) || getFlag(ctx.ret, IsRefreshing))) {
1913
+ commitComponent(ctx, []);
1914
+ }
1915
+ }, (err) => {
1916
+ if (!(getFlag(ctx.ret, IsUpdating) || getFlag(ctx.ret, IsRefreshing)) ||
1917
+ // TODO: is this flag necessary?
1918
+ !getFlag(ctx.ret, NeedsToYield)) {
1919
+ return propagateError(ctx, err, []);
1920
+ }
1921
+ throw err;
1922
+ });
1685
1923
  let iteration;
1686
1924
  try {
1687
1925
  iteration = await iterationP;
1688
1926
  }
1689
1927
  catch (err) {
1690
1928
  done = true;
1691
- ctx.f |= IsErrored;
1692
- onValue(Promise.reject(err));
1929
+ setFlag(ctx.ret, IsErrored);
1930
+ setFlag(ctx.ret, NeedsToYield, false);
1931
+ onDiff(Promise.reject(err));
1693
1932
  break;
1694
1933
  }
1695
- if (!(ctx.f & IsInForAwaitOfLoop)) {
1696
- ctx.f &= ~PropsAvailable;
1934
+ // this must be set after iterationP is awaited
1935
+ let oldResult;
1936
+ {
1937
+ // The 'floating' flag tracks whether the promise passed to the generator
1938
+ // is handled (via await, then, or catch). If handled, we reject the
1939
+ // promise so the user can catch errors. If not, we inject the error back
1940
+ // into the generator using throw, like for sync generator components.
1941
+ let floating = true;
1942
+ const oldResult1 = new Promise((resolve, reject) => {
1943
+ ctx.ctx.schedule(resolve);
1944
+ ctx.pull.onChildError = (err) => {
1945
+ reject(err);
1946
+ if (floating) {
1947
+ childError = err;
1948
+ resumePropsAsyncIterator(ctx);
1949
+ return ctx.pull.diff;
1950
+ }
1951
+ };
1952
+ });
1953
+ oldResult1.catch(NOOP);
1954
+ // We use Object.create() to clone the promise for float detection
1955
+ // because modern JS engines skip calling .then() on promises awaited
1956
+ // with await.
1957
+ oldResult = Object.create(oldResult1);
1958
+ oldResult.then = function (onfulfilled, onrejected) {
1959
+ floating = false;
1960
+ return oldResult1.then(onfulfilled, onrejected);
1961
+ };
1962
+ oldResult.catch = function (onrejected) {
1963
+ floating = false;
1964
+ return oldResult1.catch(onrejected);
1965
+ };
1966
+ }
1967
+ if (childError != null) {
1968
+ try {
1969
+ setFlag(ctx.ret, IsExecuting);
1970
+ if (typeof ctx.iterator.throw !== "function") {
1971
+ throw childError;
1972
+ }
1973
+ iteration = await ctx.iterator.throw(childError);
1974
+ }
1975
+ catch (err) {
1976
+ done = true;
1977
+ setFlag(ctx.ret, IsErrored);
1978
+ setFlag(ctx.ret, NeedsToYield, false);
1979
+ onDiff(Promise.reject(err));
1980
+ break;
1981
+ }
1982
+ finally {
1983
+ childError = undefined;
1984
+ setFlag(ctx.ret, IsExecuting, false);
1985
+ }
1986
+ }
1987
+ // this makes sure we pause before entering a loop if we yield before it
1988
+ if (!getFlag(ctx.ret, IsInForAwaitOfLoop)) {
1989
+ setFlag(ctx.ret, PropsAvailable, false);
1697
1990
  }
1698
1991
  done = !!iteration.done;
1699
- let value;
1992
+ let diff;
1700
1993
  try {
1701
- if (!(ctx.f & NeedsToYield) &&
1702
- ctx.f & PropsAvailable &&
1703
- ctx.f & IsInForAwaitOfLoop &&
1704
- !initial &&
1705
- !done) {
1706
- // We skip stale iterations in for await...of loops.
1707
- value = ctx.ret.inflightValue || getValue(ctx.ret);
1994
+ if (!isPromiseLike(iterationP)) {
1995
+ // if iterationP is an iteration and not a promise, the component was
1996
+ // not in a for await...of loop when the iteration started, so we can
1997
+ // skip the diffing of children as it is handled elsewhere.
1998
+ diff = undefined;
1999
+ }
2000
+ else if (!getFlag(ctx.ret, NeedsToYield) &&
2001
+ getFlag(ctx.ret, PropsAvailable) &&
2002
+ getFlag(ctx.ret, IsInForAwaitOfLoop)) {
2003
+ // logic to skip yielded children in a stale for await of iteration.
2004
+ diff = undefined;
1708
2005
  }
1709
2006
  else {
1710
- value = updateComponentChildren(ctx, iteration.value, hydrationData);
1711
- hydrationData = undefined;
1712
- if (isPromiseLike(value)) {
1713
- value = value.catch((err) => handleChildError(ctx, err));
1714
- }
2007
+ diff = diffComponentChildren(ctx, iteration.value, !iteration.done);
1715
2008
  }
1716
- ctx.f &= ~NeedsToYield;
1717
2009
  }
1718
2010
  catch (err) {
1719
- // Do we need to catch potential errors here in the case of unhandled
1720
- // promise rejections?
1721
- value = handleChildError(ctx, err);
2011
+ onDiff(Promise.reject(err));
1722
2012
  }
1723
2013
  finally {
1724
- onValue(value);
1725
- }
1726
- let oldResult;
1727
- if (ctx.ret.inflightValue) {
1728
- // The value passed back into the generator as the argument to the next
1729
- // method is a promise if an async generator component has async
1730
- // children. Sync generator components only resume when their children
1731
- // have fulfilled so the element’s inflight child values will never be
1732
- // defined.
1733
- oldResult = ctx.ret.inflightValue.then((value) => ctx.renderer.read(value));
1734
- oldResult.catch((err) => {
1735
- if (ctx.f & IsUpdating) {
1736
- return;
2014
+ onDiff(diff);
2015
+ setFlag(ctx.ret, NeedsToYield, false);
2016
+ }
2017
+ if (getFlag(ctx.ret, IsUnmounted)) {
2018
+ // TODO: move this unmounted branch outside the loop
2019
+ while ((!iteration || !iteration.done) &&
2020
+ ctx.iterator &&
2021
+ getFlag(ctx.ret, IsInForAwaitOfLoop)) {
2022
+ try {
2023
+ setFlag(ctx.ret, IsExecuting);
2024
+ iteration = await ctx.iterator.next(oldResult);
1737
2025
  }
1738
- if (!ctx.parent) {
2026
+ catch (err) {
2027
+ setFlag(ctx.ret, IsErrored);
2028
+ // we throw the error here to cause an unhandled rejection because
2029
+ // the promise returned from pullComponent is never awaited
1739
2030
  throw err;
1740
2031
  }
1741
- return propagateError(ctx.parent, err);
1742
- });
1743
- }
1744
- else {
1745
- oldResult = ctx.renderer.read(getValue(ctx.ret));
1746
- }
1747
- if (ctx.f & IsUnmounted) {
1748
- if (ctx.f & IsInForAwaitOfLoop) {
2032
+ finally {
2033
+ setFlag(ctx.ret, IsExecuting, false);
2034
+ }
2035
+ }
2036
+ if ((!iteration || !iteration.done) &&
2037
+ ctx.iterator &&
2038
+ typeof ctx.iterator.return === "function") {
1749
2039
  try {
1750
- ctx.f |= IsSyncExecuting;
1751
- iterationP = ctx.iterator.next(oldResult);
2040
+ setFlag(ctx.ret, IsExecuting);
2041
+ await ctx.iterator.return();
2042
+ }
2043
+ catch (err) {
2044
+ setFlag(ctx.ret, IsErrored);
2045
+ throw err;
1752
2046
  }
1753
2047
  finally {
1754
- ctx.f &= ~IsSyncExecuting;
2048
+ setFlag(ctx.ret, IsExecuting, false);
1755
2049
  }
1756
2050
  }
1757
- else {
1758
- returnComponent(ctx);
1759
- break;
1760
- }
2051
+ break;
2052
+ }
2053
+ else if (!getFlag(ctx.ret, IsInForAwaitOfLoop)) {
2054
+ // we have exited the for...await of, so updates will be handled by the
2055
+ // regular runComponent/enqueueComponent logic.
2056
+ break;
1761
2057
  }
1762
- else if (!done && !(ctx.f & IsInForOfLoop)) {
2058
+ else if (!iteration.done) {
1763
2059
  try {
1764
- ctx.f |= IsSyncExecuting;
2060
+ setFlag(ctx.ret, IsExecuting);
1765
2061
  iterationP = ctx.iterator.next(oldResult);
1766
2062
  }
1767
2063
  finally {
1768
- ctx.f &= ~IsSyncExecuting;
2064
+ setFlag(ctx.ret, IsExecuting, false);
1769
2065
  }
1770
2066
  }
1771
- initial = false;
1772
2067
  }
1773
2068
  }
1774
2069
  finally {
1775
2070
  if (done) {
1776
- ctx.f &= ~IsAsyncGen;
2071
+ setFlag(ctx.ret, IsAsyncGen, false);
1777
2072
  ctx.iterator = undefined;
1778
2073
  }
2074
+ ctx.pull = undefined;
1779
2075
  }
1780
2076
  }
1781
- /**
1782
- * Called to resume the props async iterator for async generator components.
1783
- */
1784
- function resumePropsAsyncIterator(ctx) {
1785
- if (ctx.onProps) {
1786
- ctx.onProps(ctx.ret.el.props);
1787
- ctx.onProps = undefined;
1788
- ctx.f &= ~PropsAvailable;
2077
+ function commitComponent(ctx, schedulePromises, hydrationNodes) {
2078
+ if (ctx.schedule) {
2079
+ ctx.schedule.promise.then(() => {
2080
+ commitComponent(ctx, []);
2081
+ propagateComponent(ctx);
2082
+ });
2083
+ return getValue(ctx.ret);
2084
+ }
2085
+ const wasScheduling = getFlag(ctx.ret, IsScheduling);
2086
+ const values = commitChildren(ctx.adapter, ctx.host, ctx, ctx.scope, ctx.ret, ctx.index, schedulePromises, hydrationNodes);
2087
+ if (getFlag(ctx.ret, IsUnmounted)) {
2088
+ return;
2089
+ }
2090
+ addEventTargetDelegates(ctx.ctx, values);
2091
+ // Execute schedule callbacks early to check for async deferral
2092
+ const callbacks = scheduleMap.get(ctx);
2093
+ let schedulePromises1;
2094
+ if (callbacks) {
2095
+ scheduleMap.delete(ctx);
2096
+ // TODO: think about error handling for schedule callbacks
2097
+ setFlag(ctx.ret, IsScheduling);
2098
+ const result = ctx.adapter.read(unwrap(values));
2099
+ for (const callback of callbacks) {
2100
+ const scheduleResult = callback(result);
2101
+ if (isPromiseLike(scheduleResult)) {
2102
+ (schedulePromises1 = schedulePromises1 || []).push(scheduleResult);
2103
+ }
2104
+ }
2105
+ if (schedulePromises1 && !getFlag(ctx.ret, DidCommit)) {
2106
+ const scheduleCallbacksP = Promise.all(schedulePromises1).then(() => {
2107
+ setFlag(ctx.ret, IsScheduling, false);
2108
+ propagateComponent(ctx);
2109
+ if (ctx.ret.fallback) {
2110
+ unmount(ctx.adapter, ctx.host, ctx.parent, ctx.ret.fallback, false);
2111
+ }
2112
+ ctx.ret.fallback = undefined;
2113
+ });
2114
+ let onAbort;
2115
+ const scheduleP = safeRace([
2116
+ scheduleCallbacksP,
2117
+ new Promise((resolve) => (onAbort = resolve)),
2118
+ ]).finally(() => {
2119
+ ctx.schedule = undefined;
2120
+ });
2121
+ ctx.schedule = { promise: scheduleP, onAbort };
2122
+ schedulePromises.push(scheduleP);
2123
+ }
2124
+ else {
2125
+ setFlag(ctx.ret, IsScheduling, wasScheduling);
2126
+ }
1789
2127
  }
1790
2128
  else {
1791
- ctx.f |= PropsAvailable;
2129
+ setFlag(ctx.ret, IsScheduling, wasScheduling);
2130
+ }
2131
+ if (!getFlag(ctx.ret, IsScheduling)) {
2132
+ if (!getFlag(ctx.ret, IsUpdating)) {
2133
+ propagateComponent(ctx);
2134
+ }
2135
+ if (ctx.ret.fallback) {
2136
+ unmount(ctx.adapter, ctx.host, ctx.parent, ctx.ret.fallback, false);
2137
+ }
2138
+ ctx.ret.fallback = undefined;
2139
+ setFlag(ctx.ret, IsUpdating, false);
1792
2140
  }
2141
+ setFlag(ctx.ret, DidCommit);
2142
+ // We always use getValue() instead of the unwrapping values because there
2143
+ // are various ways in which the values could have been updated, especially
2144
+ // if schedule callbacks call refresh() or async mounting is happening.
2145
+ return getValue(ctx.ret, true);
2146
+ }
2147
+ /**
2148
+ * Propagates component changes up to ancestors when rendering starts from a
2149
+ * component via refresh() or multiple for await...of renders. This handles
2150
+ * event listeners and DOM arrangement that would normally happen during
2151
+ * top-down rendering.
2152
+ */
2153
+ function propagateComponent(ctx) {
2154
+ const values = getChildValues(ctx.ret, ctx.index);
2155
+ addEventTargetDelegates(ctx.ctx, values, (ctx1) => ctx1[_ContextState].host === ctx.host);
2156
+ const host = ctx.host;
2157
+ const props = stripSpecialProps(host.el.props);
2158
+ ctx.adapter.arrange({
2159
+ tag: host.el.tag,
2160
+ tagName: getTagName(host.el.tag),
2161
+ node: host.value,
2162
+ props,
2163
+ oldProps: props,
2164
+ children: getChildValues(host, 0),
2165
+ });
2166
+ flush(ctx.adapter, ctx.root, ctx);
1793
2167
  }
1794
- // TODO: async unmounting
1795
- function unmountComponent(ctx) {
1796
- if (ctx.f & IsUnmounted) {
2168
+ async function unmountComponent(ctx, isNested) {
2169
+ if (getFlag(ctx.ret, IsUnmounted)) {
1797
2170
  return;
1798
2171
  }
1799
- clearEventListeners(ctx);
2172
+ let cleanupPromises;
2173
+ // TODO: think about errror handling for callbacks
1800
2174
  const callbacks = cleanupMap.get(ctx);
1801
2175
  if (callbacks) {
2176
+ const oldResult = ctx.adapter.read(getValue(ctx.ret));
1802
2177
  cleanupMap.delete(ctx);
1803
- const value = ctx.renderer.read(getValue(ctx.ret));
1804
2178
  for (const callback of callbacks) {
1805
- callback(value);
2179
+ const cleanup = callback(oldResult);
2180
+ if (isPromiseLike(cleanup)) {
2181
+ (cleanupPromises = cleanupPromises || []).push(cleanup);
2182
+ }
2183
+ }
2184
+ }
2185
+ let didLinger = false;
2186
+ if (!isNested && cleanupPromises && getChildValues(ctx.ret).length > 0) {
2187
+ didLinger = true;
2188
+ const index = ctx.index;
2189
+ const lingerers = ctx.host.lingerers || (ctx.host.lingerers = []);
2190
+ let set = lingerers[index];
2191
+ if (set == null) {
2192
+ set = new Set();
2193
+ lingerers[index] = set;
2194
+ }
2195
+ set.add(ctx.ret);
2196
+ await Promise.all(cleanupPromises);
2197
+ set.delete(ctx.ret);
2198
+ if (set.size === 0) {
2199
+ lingerers[index] = undefined;
2200
+ }
2201
+ if (!lingerers.some(Boolean)) {
2202
+ // If there are no lingerers remaining, we can remove the lingerers array
2203
+ ctx.host.lingerers = undefined;
2204
+ }
2205
+ }
2206
+ if (getFlag(ctx.ret, IsUnmounted)) {
2207
+ // If the component was unmounted while awaiting the cleanup callbacks,
2208
+ // we do not need to continue unmounting.
2209
+ return;
2210
+ }
2211
+ setFlag(ctx.ret, IsUnmounted);
2212
+ // If component has pending schedule promises, resolve them since component
2213
+ // is unmounting
2214
+ if (ctx.schedule) {
2215
+ ctx.schedule.onAbort();
2216
+ ctx.schedule = undefined;
2217
+ }
2218
+ clearEventListeners(ctx.ctx);
2219
+ unmountChildren(ctx.adapter, ctx.host, ctx, ctx.ret, isNested);
2220
+ if (didLinger) {
2221
+ // If we lingered, we call finalize to ensure rendering is finalized
2222
+ if (ctx.root != null) {
2223
+ ctx.adapter.finalize(ctx.root);
1806
2224
  }
1807
2225
  }
1808
- ctx.f |= IsUnmounted;
1809
2226
  if (ctx.iterator) {
1810
- if (ctx.f & IsSyncGen) {
1811
- let value;
1812
- if (ctx.f & IsInForOfLoop) {
1813
- value = enqueueComponentRun(ctx);
1814
- }
1815
- if (isPromiseLike(value)) {
1816
- value.then(() => {
1817
- if (ctx.f & IsInForOfLoop) {
1818
- unmountComponent(ctx);
1819
- }
1820
- else {
1821
- returnComponent(ctx);
1822
- }
1823
- }, (err) => {
1824
- if (!ctx.parent) {
1825
- throw err;
2227
+ if (ctx.pull) {
2228
+ // we let pullComponent handle unmounting
2229
+ resumePropsAsyncIterator(ctx);
2230
+ return;
2231
+ }
2232
+ // we wait for inflight value so yields resume with the most up to date
2233
+ // props
2234
+ if (ctx.inflight) {
2235
+ await ctx.inflight[1];
2236
+ }
2237
+ let iteration;
2238
+ if (getFlag(ctx.ret, IsInForOfLoop)) {
2239
+ try {
2240
+ setFlag(ctx.ret, IsExecuting);
2241
+ const oldResult = ctx.adapter.read(getValue(ctx.ret));
2242
+ const iterationP = ctx.iterator.next(oldResult);
2243
+ if (isPromiseLike(iterationP)) {
2244
+ if (!getFlag(ctx.ret, IsAsyncGen)) {
2245
+ throw new Error("Mixed generator component");
1826
2246
  }
1827
- return propagateError(ctx.parent, err);
1828
- });
1829
- }
1830
- else {
1831
- if (ctx.f & IsInForOfLoop) {
1832
- unmountComponent(ctx);
2247
+ iteration = await iterationP;
1833
2248
  }
1834
2249
  else {
1835
- returnComponent(ctx);
2250
+ if (!getFlag(ctx.ret, IsSyncGen)) {
2251
+ throw new Error("Mixed generator component");
2252
+ }
2253
+ iteration = iterationP;
1836
2254
  }
1837
2255
  }
1838
- }
1839
- else if (ctx.f & IsAsyncGen) {
1840
- if (ctx.f & IsInForOfLoop) {
1841
- const value = enqueueComponentRun(ctx);
1842
- value.then(() => {
1843
- if (ctx.f & IsInForOfLoop) {
1844
- unmountComponent(ctx);
1845
- }
1846
- else {
1847
- returnComponent(ctx);
1848
- }
1849
- }, (err) => {
1850
- if (!ctx.parent) {
1851
- throw err;
1852
- }
1853
- return propagateError(ctx.parent, err);
1854
- });
2256
+ catch (err) {
2257
+ setFlag(ctx.ret, IsErrored);
2258
+ throw err;
1855
2259
  }
1856
- else {
1857
- // The logic for unmounting async generator components is in the
1858
- // runAsyncGenComponent function.
1859
- resumePropsAsyncIterator(ctx);
2260
+ finally {
2261
+ setFlag(ctx.ret, IsExecuting, false);
1860
2262
  }
1861
2263
  }
1862
- }
1863
- }
1864
- function returnComponent(ctx) {
1865
- resumePropsAsyncIterator(ctx);
1866
- if (ctx.iterator && typeof ctx.iterator.return === "function") {
1867
- try {
1868
- ctx.f |= IsSyncExecuting;
1869
- const iteration = ctx.iterator.return();
1870
- if (isPromiseLike(iteration)) {
1871
- iteration.catch((err) => {
1872
- if (!ctx.parent) {
1873
- throw err;
2264
+ if ((!iteration || !iteration.done) &&
2265
+ ctx.iterator &&
2266
+ typeof ctx.iterator.return === "function") {
2267
+ try {
2268
+ setFlag(ctx.ret, IsExecuting);
2269
+ const iterationP = ctx.iterator.return();
2270
+ if (isPromiseLike(iterationP)) {
2271
+ if (!getFlag(ctx.ret, IsAsyncGen)) {
2272
+ throw new Error("Mixed generator component");
1874
2273
  }
1875
- return propagateError(ctx.parent, err);
1876
- });
1877
- }
1878
- }
1879
- finally {
1880
- ctx.f &= ~IsSyncExecuting;
1881
- }
1882
- }
1883
- }
1884
- /*** EVENT TARGET UTILITIES ***/
1885
- // EVENT PHASE CONSTANTS
1886
- // https://developer.mozilla.org/en-US/docs/Web/API/Event/eventPhase
1887
- const NONE = 0;
1888
- const CAPTURING_PHASE = 1;
1889
- const AT_TARGET = 2;
1890
- const BUBBLING_PHASE = 3;
1891
- const listenersMap = new WeakMap();
1892
- function isListenerOrListenerObject(value) {
1893
- return (typeof value === "function" ||
1894
- (value !== null &&
1895
- typeof value === "object" &&
1896
- typeof value.handleEvent === "function"));
1897
- }
1898
- function normalizeListenerOptions(options) {
1899
- if (typeof options === "boolean") {
1900
- return { capture: options };
1901
- }
1902
- else if (options == null) {
1903
- return {};
1904
- }
1905
- return options;
1906
- }
1907
- function isEventTarget(value) {
1908
- return (value != null &&
1909
- typeof value.addEventListener === "function" &&
1910
- typeof value.removeEventListener === "function" &&
1911
- typeof value.dispatchEvent === "function");
1912
- }
1913
- function setEventProperty(ev, key, value) {
1914
- Object.defineProperty(ev, key, { value, writable: false, configurable: true });
1915
- }
1916
- // TODO: Maybe we can pass in the current context directly, rather than
1917
- // starting from the parent?
1918
- /**
1919
- * A function to reconstruct an array of every listener given a context and a
1920
- * host element.
1921
- *
1922
- * This function exploits the fact that contexts retain their nearest ancestor
1923
- * host element. We can determine all the contexts which are directly listening
1924
- * to an element by traversing up the context tree and checking that the host
1925
- * element passed in matches the parent context’s host element.
1926
- */
1927
- function getListenerRecords(ctx, ret) {
1928
- let listeners = [];
1929
- while (ctx !== undefined && ctx.host === ret) {
1930
- const listeners1 = listenersMap.get(ctx);
1931
- if (listeners1) {
1932
- listeners = listeners.concat(listeners1);
1933
- }
1934
- ctx = ctx.parent;
1935
- }
1936
- return listeners;
1937
- }
1938
- function clearEventListeners(ctx) {
1939
- const listeners = listenersMap.get(ctx);
1940
- if (listeners && listeners.length) {
1941
- for (const value of getChildValues(ctx.ret)) {
1942
- if (isEventTarget(value)) {
1943
- for (const record of listeners) {
1944
- value.removeEventListener(record.type, record.callback, record.options);
2274
+ iteration = await iterationP;
2275
+ }
2276
+ else {
2277
+ if (!getFlag(ctx.ret, IsSyncGen)) {
2278
+ throw new Error("Mixed generator component");
2279
+ }
2280
+ iteration = iterationP;
1945
2281
  }
1946
2282
  }
2283
+ catch (err) {
2284
+ setFlag(ctx.ret, IsErrored);
2285
+ throw err;
2286
+ }
2287
+ finally {
2288
+ setFlag(ctx.ret, IsExecuting, false);
2289
+ }
1947
2290
  }
1948
- listeners.length = 0;
1949
2291
  }
1950
2292
  }
1951
2293
  /*** ERROR HANDLING UTILITIES ***/
1952
2294
  function handleChildError(ctx, err) {
1953
- if (!ctx.iterator || typeof ctx.iterator.throw !== "function") {
2295
+ if (!ctx.iterator) {
2296
+ throw err;
2297
+ }
2298
+ if (ctx.pull) {
2299
+ // we let pullComponent handle child errors
2300
+ ctx.pull.onChildError(err);
2301
+ return ctx.pull.diff;
2302
+ }
2303
+ if (!ctx.iterator.throw) {
1954
2304
  throw err;
1955
2305
  }
1956
2306
  resumePropsAsyncIterator(ctx);
1957
2307
  let iteration;
1958
2308
  try {
1959
- ctx.f |= IsSyncExecuting;
2309
+ setFlag(ctx.ret, IsExecuting);
1960
2310
  iteration = ctx.iterator.throw(err);
1961
2311
  }
1962
2312
  catch (err) {
1963
- ctx.f |= IsErrored;
2313
+ setFlag(ctx.ret, IsErrored);
1964
2314
  throw err;
1965
2315
  }
1966
2316
  finally {
1967
- ctx.f &= ~IsSyncExecuting;
2317
+ setFlag(ctx.ret, IsExecuting, false);
1968
2318
  }
1969
2319
  if (isPromiseLike(iteration)) {
1970
2320
  return iteration.then((iteration) => {
1971
2321
  if (iteration.done) {
1972
- ctx.f &= ~IsAsyncGen;
2322
+ setFlag(ctx.ret, IsSyncGen, false);
2323
+ setFlag(ctx.ret, IsAsyncGen, false);
1973
2324
  ctx.iterator = undefined;
1974
2325
  }
1975
- return updateComponentChildren(ctx, iteration.value);
2326
+ return diffComponentChildren(ctx, iteration.value, !iteration.done);
1976
2327
  }, (err) => {
1977
- ctx.f |= IsErrored;
2328
+ setFlag(ctx.ret, IsErrored);
1978
2329
  throw err;
1979
2330
  });
1980
2331
  }
1981
2332
  if (iteration.done) {
1982
- ctx.f &= ~IsSyncGen;
1983
- ctx.f &= ~IsAsyncGen;
2333
+ setFlag(ctx.ret, IsSyncGen, false);
2334
+ setFlag(ctx.ret, IsAsyncGen, false);
1984
2335
  ctx.iterator = undefined;
1985
2336
  }
1986
- return updateComponentChildren(ctx, iteration.value);
2337
+ return diffComponentChildren(ctx, iteration.value, !iteration.done);
1987
2338
  }
1988
- function propagateError(ctx, err) {
1989
- let result;
2339
+ /**
2340
+ * Propagates an error up the context tree by calling handleChildError with
2341
+ * each parent.
2342
+ *
2343
+ * @returns A promise which resolves to undefined when the error has been
2344
+ * handled, or undefined if the error was handled synchronously.
2345
+ */
2346
+ function propagateError(ctx, err, schedulePromises) {
2347
+ const parent = ctx.parent;
2348
+ if (!parent) {
2349
+ throw err;
2350
+ }
2351
+ let diff;
1990
2352
  try {
1991
- result = handleChildError(ctx, err);
2353
+ diff = handleChildError(parent, err);
1992
2354
  }
1993
2355
  catch (err) {
1994
- if (!ctx.parent) {
1995
- throw err;
1996
- }
1997
- return propagateError(ctx.parent, err);
2356
+ return propagateError(parent, err, schedulePromises);
1998
2357
  }
1999
- if (isPromiseLike(result)) {
2000
- return result.catch((err) => {
2001
- if (!ctx.parent) {
2002
- throw err;
2003
- }
2004
- return propagateError(ctx.parent, err);
2005
- });
2358
+ if (isPromiseLike(diff)) {
2359
+ return diff.then(() => void commitComponent(parent, schedulePromises), (err) => propagateError(parent, err, schedulePromises));
2006
2360
  }
2007
- return result;
2361
+ commitComponent(parent, schedulePromises);
2008
2362
  }
2009
- // Some JSX transpilation tools expect these functions to be defined on the
2010
- // default export. Prefer named exports when importing directly.
2363
+ /**
2364
+ * A re-export of some Crank exports as the default export.
2365
+ *
2366
+ * Some JSX tools expect things like createElement/Fragment to be defined on
2367
+ * the default export. Prefer using the named exports directly.
2368
+ */
2011
2369
  var crank = { createElement, Fragment };
2012
2370
 
2013
- export { Context, Copy, Element, Fragment, Portal, Raw, Renderer, cloneElement, createElement, crank as default, isElement };
2371
+ export { Context, Copy, Element, Fragment, Portal, Raw, Renderer, Text, cloneElement, createElement, crank as default, isElement };
2014
2372
  //# sourceMappingURL=crank.js.map