@b9g/crank 0.6.1 → 0.7.1

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