@b9g/crank 0.5.0-beta.2 → 0.5.0-beta.4

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.cjs CHANGED
@@ -161,22 +161,6 @@ function createElement(tag, props, ...children) {
161
161
  else if (children.length === 1) {
162
162
  props1.children = children[0];
163
163
  }
164
- // string aliases for the special tags
165
- // TODO: Does this logic belong here, or in the Element constructor
166
- switch (tag) {
167
- case "$FRAGMENT":
168
- tag = Fragment;
169
- break;
170
- case "$PORTAL":
171
- tag = Portal;
172
- break;
173
- case "$COPY":
174
- tag = Copy;
175
- break;
176
- case "$RAW":
177
- tag = Raw;
178
- break;
179
- }
180
164
  return new Element(tag, props1, key, ref, static_);
181
165
  }
182
166
  /** Clones a given element, shallowly copying the props object. */
@@ -255,13 +239,13 @@ function normalize(values) {
255
239
  class Retainer {
256
240
  constructor(el) {
257
241
  this.el = el;
258
- this.value = undefined;
259
242
  this.ctx = undefined;
260
243
  this.children = undefined;
261
- this.cached = undefined;
262
- this.fallback = undefined;
263
- this.inflight = undefined;
264
- this.onCommit = undefined;
244
+ this.value = undefined;
245
+ this.cachedChildValues = undefined;
246
+ this.fallbackValue = undefined;
247
+ this.inflightValue = undefined;
248
+ this.onNextValues = undefined;
265
249
  }
266
250
  }
267
251
  /**
@@ -270,10 +254,10 @@ class Retainer {
270
254
  * @returns The value of the element.
271
255
  */
272
256
  function getValue(ret) {
273
- if (typeof ret.fallback !== "undefined") {
274
- return typeof ret.fallback === "object"
275
- ? getValue(ret.fallback)
276
- : ret.fallback;
257
+ if (typeof ret.fallbackValue !== "undefined") {
258
+ return typeof ret.fallbackValue === "object"
259
+ ? getValue(ret.fallbackValue)
260
+ : ret.fallbackValue;
277
261
  }
278
262
  else if (ret.el.tag === Portal) {
279
263
  return;
@@ -289,8 +273,8 @@ function getValue(ret) {
289
273
  * @returns A normalized array of nodes and strings.
290
274
  */
291
275
  function getChildValues(ret) {
292
- if (ret.cached) {
293
- return wrap(ret.cached);
276
+ if (ret.cachedChildValues) {
277
+ return wrap(ret.cachedChildValues);
294
278
  }
295
279
  const values = [];
296
280
  const children = wrap(ret.children);
@@ -303,7 +287,7 @@ function getChildValues(ret) {
303
287
  const values1 = normalize(values);
304
288
  const tag = ret.el.tag;
305
289
  if (typeof tag === "function" || (tag !== Fragment && tag !== Raw)) {
306
- ret.cached = unwrap(values1);
290
+ ret.cachedChildValues = unwrap(values1);
307
291
  }
308
292
  return values1;
309
293
  }
@@ -320,7 +304,7 @@ const defaultRendererImpl = {
320
304
  dispose: NOOP,
321
305
  flush: NOOP,
322
306
  };
323
- const $RendererImpl = Symbol.for("crank.RendererImpl");
307
+ const _RendererImpl = Symbol.for("crank.RendererImpl");
324
308
  /**
325
309
  * An abstract class which is subclassed to render to different target
326
310
  * environments. This class is responsible for kicking off the rendering
@@ -334,7 +318,7 @@ const $RendererImpl = Symbol.for("crank.RendererImpl");
334
318
  class Renderer {
335
319
  constructor(impl) {
336
320
  this.cache = new WeakMap();
337
- this[$RendererImpl] = {
321
+ this[_RendererImpl] = {
338
322
  ...defaultRendererImpl,
339
323
  ...impl,
340
324
  };
@@ -346,7 +330,7 @@ class Renderer {
346
330
  * used root to delete the previously rendered element tree from the cache.
347
331
  * @param root - The node to be rendered into. The renderer will cache
348
332
  * element trees per root.
349
- * @param ctx - An optional context that will be the ancestor context of all
333
+ * @param bridge - An optional context that will be the ancestor context of all
350
334
  * elements in the tree. Useful for connecting different renderers so that
351
335
  * events/provisions properly propagate. The context for a given root must be
352
336
  * the same or an error will be thrown.
@@ -356,7 +340,7 @@ class Renderer {
356
340
  */
357
341
  render(children, root, bridge) {
358
342
  let ret;
359
- const ctx = bridge && bridge[$ContextImpl];
343
+ const ctx = bridge && bridge[_ContextImpl];
360
344
  if (typeof root === "object" && root !== null) {
361
345
  ret = this.cache.get(root);
362
346
  }
@@ -379,7 +363,7 @@ class Renderer {
379
363
  this.cache.delete(root);
380
364
  }
381
365
  }
382
- const impl = this[$RendererImpl];
366
+ const impl = this[_RendererImpl];
383
367
  const childValues = diffChildren(impl, root, ret, ctx, impl.scope(undefined, Portal, ret.el.props), ret, children);
384
368
  // We return the child values of the portal because portal elements
385
369
  // themselves have no readable value.
@@ -392,15 +376,15 @@ class Renderer {
392
376
  /*** PRIVATE RENDERER FUNCTIONS ***/
393
377
  function commitRootRender(renderer, root, ctx, ret, childValues, oldProps) {
394
378
  // element is a host or portal element
395
- if (root !== undefined) {
396
- renderer.arrange(Portal, root, ret.el.props, childValues, oldProps, wrap(ret.cached));
379
+ if (root != null) {
380
+ renderer.arrange(Portal, root, ret.el.props, childValues, oldProps, wrap(ret.cachedChildValues));
397
381
  flush(renderer, root);
398
382
  }
399
- ret.cached = unwrap(childValues);
383
+ ret.cachedChildValues = unwrap(childValues);
400
384
  if (root == null) {
401
385
  unmount(renderer, ret, ctx, ret);
402
386
  }
403
- return renderer.read(ret.cached);
387
+ return renderer.read(ret.cachedChildValues);
404
388
  }
405
389
  function diffChildren(renderer, root, host, ctx, scope, parent, children) {
406
390
  const oldRetained = wrap(parent.children);
@@ -472,7 +456,7 @@ function diffChildren(renderer, root, host, ctx, scope, parent, children) {
472
456
  }
473
457
  const fallback = ret;
474
458
  ret = new Retainer(child);
475
- ret.fallback = fallback;
459
+ ret.fallbackValue = fallback;
476
460
  }
477
461
  if (child.tag === Raw) {
478
462
  value = updateRaw(renderer, ret, scope, oldProps);
@@ -540,27 +524,29 @@ function diffChildren(renderer, root, host, ctx, scope, parent, children) {
540
524
  childValues1,
541
525
  new Promise((resolve) => (onChildValues = resolve)),
542
526
  ]);
543
- if (parent.onCommit) {
544
- parent.onCommit(childValues1);
527
+ if (parent.onNextValues) {
528
+ parent.onNextValues(childValues1);
545
529
  }
546
- parent.onCommit = onChildValues;
530
+ parent.onNextValues = onChildValues;
547
531
  return childValues1.then((childValues) => {
548
- parent.inflight = parent.fallback = undefined;
532
+ parent.inflightValue = parent.fallbackValue = undefined;
549
533
  return normalize(childValues);
550
534
  });
551
535
  }
552
- if (graveyard) {
553
- for (let i = 0; i < graveyard.length; i++) {
554
- unmount(renderer, host, ctx, graveyard[i]);
536
+ else {
537
+ if (graveyard) {
538
+ for (let i = 0; i < graveyard.length; i++) {
539
+ unmount(renderer, host, ctx, graveyard[i]);
540
+ }
555
541
  }
542
+ if (parent.onNextValues) {
543
+ parent.onNextValues(values);
544
+ parent.onNextValues = undefined;
545
+ }
546
+ parent.inflightValue = parent.fallbackValue = undefined;
547
+ // We can assert there are no promises in the array because isAsync is false
548
+ return normalize(values);
556
549
  }
557
- if (parent.onCommit) {
558
- parent.onCommit(values);
559
- parent.onCommit = undefined;
560
- }
561
- parent.inflight = parent.fallback = undefined;
562
- // We can assert there are no promises in the array because isAsync is false
563
- return normalize(values);
564
550
  }
565
551
  function createChildrenByKey(children, offset) {
566
552
  const childrenByKey = new Map();
@@ -580,8 +566,8 @@ function getInflightValue(child) {
580
566
  if (ctx && ctx.f & IsUpdating && ctx.inflightValue) {
581
567
  return ctx.inflightValue;
582
568
  }
583
- else if (child.inflight) {
584
- return child.inflight;
569
+ else if (child.inflightValue) {
570
+ return child.inflightValue;
585
571
  }
586
572
  return getValue(child);
587
573
  }
@@ -600,8 +586,8 @@ function updateRaw(renderer, ret, scope, oldProps) {
600
586
  function updateFragment(renderer, root, host, ctx, scope, ret) {
601
587
  const childValues = diffChildren(renderer, root, host, ctx, scope, ret, ret.el.props.children);
602
588
  if (isPromiseLike(childValues)) {
603
- ret.inflight = childValues.then((childValues) => unwrap(childValues));
604
- return ret.inflight;
589
+ ret.inflightValue = childValues.then((childValues) => unwrap(childValues));
590
+ return ret.inflightValue;
605
591
  }
606
592
  return unwrap(childValues);
607
593
  }
@@ -618,8 +604,8 @@ function updateHost(renderer, root, ctx, scope, ret, oldProps) {
618
604
  scope = renderer.scope(scope, tag, el.props);
619
605
  const childValues = diffChildren(renderer, root, ret, ctx, scope, ret, ret.el.props.children);
620
606
  if (isPromiseLike(childValues)) {
621
- ret.inflight = childValues.then((childValues) => commitHost(renderer, scope, ret, childValues, oldProps));
622
- return ret.inflight;
607
+ ret.inflightValue = childValues.then((childValues) => commitHost(renderer, scope, ret, childValues, oldProps));
608
+ return ret.inflightValue;
623
609
  }
624
610
  return commitHost(renderer, scope, ret, childValues, oldProps);
625
611
  }
@@ -646,8 +632,8 @@ function commitHost(renderer, scope, ret, childValues, oldProps) {
646
632
  }
647
633
  ret.el = new Element(tag, props, ret.el.key, ret.el.ref);
648
634
  }
649
- renderer.arrange(tag, value, props, childValues, oldProps, wrap(ret.cached));
650
- ret.cached = unwrap(childValues);
635
+ renderer.arrange(tag, value, props, childValues, oldProps, wrap(ret.cachedChildValues));
636
+ ret.cachedChildValues = unwrap(childValues);
651
637
  if (tag === Portal) {
652
638
  flush(renderer, ret.value);
653
639
  return;
@@ -694,7 +680,7 @@ function unmount(renderer, host, ctx, ret) {
694
680
  }
695
681
  else if (ret.el.tag === Portal) {
696
682
  host = ret;
697
- renderer.arrange(Portal, host.value, host.el.props, [], host.el.props, wrap(host.cached));
683
+ renderer.arrange(Portal, host.value, host.el.props, [], host.el.props, wrap(host.cachedChildValues));
698
684
  flush(renderer, host.value);
699
685
  }
700
686
  else if (ret.el.tag !== Fragment) {
@@ -718,38 +704,39 @@ function unmount(renderer, host, ctx, ret) {
718
704
  }
719
705
  /*** CONTEXT FLAGS ***/
720
706
  /**
721
- * A flag which is set when the component is being updated by the parent and
722
- * cleared when the component has committed. Used to determine things like
723
- * whether the nearest host ancestor needs to be rearranged.
707
+ * A flag which is true when the component is initialized or updated by an
708
+ * ancestor component or the root render call.
709
+ *
710
+ * Used to determine things like whether the nearest host ancestor needs to be
711
+ * rearranged.
724
712
  */
725
713
  const IsUpdating = 1 << 0;
726
714
  /**
727
- * A flag which is set when the component function or generator is
728
- * synchronously executing. This flags is used to ensure that a component which
729
- * triggers a second update in the course of rendering does not cause a stack
730
- * overflow or a generator error.
715
+ * A flag which is true when the component is synchronously executing.
716
+ *
717
+ * Used to guard against components triggering stack overflow or generator error.
731
718
  */
732
- const IsExecuting = 1 << 1;
719
+ const IsSyncExecuting = 1 << 1;
733
720
  /**
734
- * A flag used to make sure multiple values are not pulled from context prop
735
- * iterators without a yield.
721
+ * A flag which is true when the component is in the render loop.
736
722
  */
737
- const IsIterating = 1 << 2;
723
+ const IsInRenderLoop = 1 << 2;
738
724
  /**
739
- * A flag used by async generator components in conjunction with the
740
- * onavailable (_oa) callback to mark whether new props can be pulled via the
741
- * context async iterator. See the Symbol.asyncIterator method and the
742
- * resumeCtxIterator function.
725
+ * A flag which is true when the component starts the render loop but has not
726
+ * yielded yet.
727
+ *
728
+ * Used to make sure that components yield at least once per loop.
743
729
  */
744
- const IsAvailable = 1 << 3;
730
+ const NeedsToYield = 1 << 3;
745
731
  /**
746
- * A flag which is set when a generator components returns, i.e. the done
747
- * property on the iteration is set to true. Generator components will stick to
748
- * their last rendered value and ignore further updates.
732
+ * A flag used by async generator components in conjunction with the
733
+ * onAvailable callback to mark whether new props can be pulled via the context
734
+ * async iterator. See the Symbol.asyncIterator method and the
735
+ * resumeCtxIterator function.
749
736
  */
750
- const IsDone = 1 << 4;
737
+ const PropsAvailable = 1 << 4;
751
738
  /**
752
- * A flag which is set when a generator component errors.
739
+ * A flag which is set when a component errors.
753
740
  *
754
741
  * NOTE: This is mainly used to prevent some false positives in component
755
742
  * yields or returns undefined warnings. The reason we’re using this versus
@@ -757,28 +744,28 @@ const IsDone = 1 << 4;
757
744
  * sync generator child) where synchronous code causes a stack overflow error
758
745
  * in a non-deterministic way. Deeply disturbing stuff.
759
746
  */
760
- const IsErrored = 1 << 5;
747
+ const IsErrored = 1 << 6;
761
748
  /**
762
749
  * A flag which is set when the component is unmounted. Unmounted components
763
750
  * are no longer in the element tree and cannot refresh or rerender.
764
751
  */
765
- const IsUnmounted = 1 << 6;
752
+ const IsUnmounted = 1 << 7;
766
753
  /**
767
754
  * A flag which indicates that the component is a sync generator component.
768
755
  */
769
- const IsSyncGen = 1 << 7;
756
+ const IsSyncGen = 1 << 8;
770
757
  /**
771
758
  * A flag which indicates that the component is an async generator component.
772
759
  */
773
- const IsAsyncGen = 1 << 8;
760
+ const IsAsyncGen = 1 << 9;
774
761
  /**
775
762
  * A flag which is set while schedule callbacks are called.
776
763
  */
777
- const IsScheduling = 1 << 9;
764
+ const IsScheduling = 1 << 10;
778
765
  /**
779
766
  * A flag which is set when a schedule callback calls refresh.
780
767
  */
781
- const IsSchedulingRefresh = 1 << 10;
768
+ const IsSchedulingRefresh = 1 << 11;
782
769
  const provisionMaps = new WeakMap();
783
770
  const scheduleMap = new WeakMap();
784
771
  const cleanupMap = new WeakMap();
@@ -786,12 +773,12 @@ const cleanupMap = new WeakMap();
786
773
  const flushMaps = new WeakMap();
787
774
  /**
788
775
  * @internal
789
- * The internal class which holds all context data.
776
+ * The internal class which holds context data.
790
777
  */
791
778
  class ContextImpl {
792
779
  constructor(renderer, root, host, parent, scope, ret) {
793
780
  this.f = 0;
794
- this.ctx = new Context(this);
781
+ this.owner = new Context(this);
795
782
  this.renderer = renderer;
796
783
  this.root = root;
797
784
  this.host = host;
@@ -803,10 +790,11 @@ class ContextImpl {
803
790
  this.inflightValue = undefined;
804
791
  this.enqueuedBlock = undefined;
805
792
  this.enqueuedValue = undefined;
806
- this.onAvailable = undefined;
793
+ this.onProps = undefined;
794
+ this.onPropsRequested = undefined;
807
795
  }
808
796
  }
809
- const $ContextImpl = Symbol.for("crank.ContextImpl");
797
+ const _ContextImpl = Symbol.for("crank.ContextImpl");
810
798
  /**
811
799
  * A class which is instantiated and passed to every component as its this
812
800
  * value. Contexts form a tree just like elements and all components in the
@@ -820,8 +808,10 @@ const $ContextImpl = Symbol.for("crank.ContextImpl");
820
808
  * schedule and cleanup callbacks.
821
809
  */
822
810
  class Context {
811
+ // TODO: If we could make the constructor function take a nicer value, it
812
+ // would be useful for testing purposes.
823
813
  constructor(impl) {
824
- this[$ContextImpl] = impl;
814
+ this[_ContextImpl] = impl;
825
815
  }
826
816
  /**
827
817
  * The current props of the associated element.
@@ -831,7 +821,7 @@ class Context {
831
821
  * plugins or utilities which wrap contexts.
832
822
  */
833
823
  get props() {
834
- return this[$ContextImpl].ret.el.props;
824
+ return this[_ContextImpl].ret.el.props;
835
825
  }
836
826
  // TODO: Should we rename this???
837
827
  /**
@@ -842,45 +832,70 @@ class Context {
842
832
  * mainly for plugins or utilities which wrap contexts.
843
833
  */
844
834
  get value() {
845
- return this[$ContextImpl].renderer.read(getValue(this[$ContextImpl].ret));
835
+ return this[_ContextImpl].renderer.read(getValue(this[_ContextImpl].ret));
846
836
  }
847
837
  *[Symbol.iterator]() {
848
- const impl = this[$ContextImpl];
849
- while (!(impl.f & IsDone)) {
850
- if (impl.f & IsIterating) {
851
- throw new Error("Context iterated twice without a yield");
852
- }
853
- else if (impl.f & IsAsyncGen) {
854
- throw new Error("Use for await…of in async generator components");
838
+ const ctx = this[_ContextImpl];
839
+ if (ctx.f & IsAsyncGen) {
840
+ throw new Error("Use for await…of in async generator components");
841
+ }
842
+ try {
843
+ ctx.f |= IsInRenderLoop;
844
+ while (!(ctx.f & IsUnmounted)) {
845
+ if (ctx.f & NeedsToYield) {
846
+ throw new Error("Context iterated twice without a yield");
847
+ }
848
+ else {
849
+ ctx.f |= NeedsToYield;
850
+ }
851
+ yield ctx.ret.el.props;
855
852
  }
856
- impl.f |= IsIterating;
857
- yield impl.ret.el.props;
853
+ }
854
+ finally {
855
+ ctx.f &= ~IsInRenderLoop;
858
856
  }
859
857
  }
860
858
  async *[Symbol.asyncIterator]() {
861
- // We use a do while loop rather than a while loop to handle an edge case
862
- // where an async generator component is unmounted synchronously and
863
- // therefore “done” before it starts iterating over the context.
864
- const impl = this[$ContextImpl];
865
- do {
866
- if (impl.f & IsIterating) {
867
- throw new Error("Context iterated twice without a yield");
868
- }
869
- else if (impl.f & IsSyncGen) {
870
- throw new Error("Use for…of in sync generator components");
871
- }
872
- impl.f |= IsIterating;
873
- if (impl.f & IsAvailable) {
874
- impl.f &= ~IsAvailable;
875
- }
876
- else {
877
- await new Promise((resolve) => (impl.onAvailable = resolve));
878
- if (impl.f & IsDone) {
879
- break;
859
+ const ctx = this[_ContextImpl];
860
+ if (ctx.f & IsSyncGen) {
861
+ throw new Error("Use for…of in sync generator components");
862
+ }
863
+ try {
864
+ // await an empty promise to prevent the IsInRenderLoop flag from
865
+ // returning false positives in the case of async generator components
866
+ // which immediately enter the loop
867
+ ctx.f |= IsInRenderLoop;
868
+ while (!(ctx.f & IsUnmounted)) {
869
+ if (ctx.f & NeedsToYield) {
870
+ throw new Error("Context iterated twice without a yield");
871
+ }
872
+ else {
873
+ ctx.f |= NeedsToYield;
874
+ }
875
+ if (ctx.f & PropsAvailable) {
876
+ ctx.f &= ~PropsAvailable;
877
+ yield ctx.ret.el.props;
878
+ }
879
+ else {
880
+ const props = await new Promise((resolve) => (ctx.onProps = resolve));
881
+ if (ctx.f & IsUnmounted) {
882
+ break;
883
+ }
884
+ yield props;
885
+ }
886
+ if (ctx.onPropsRequested) {
887
+ ctx.onPropsRequested();
888
+ ctx.onPropsRequested = undefined;
880
889
  }
881
890
  }
882
- yield impl.ret.el.props;
883
- } while (!(impl.f & IsDone));
891
+ }
892
+ finally {
893
+ ctx.f &= ~IsInRenderLoop;
894
+ if (ctx.onPropsRequested) {
895
+ ctx.onPropsRequested();
896
+ ctx.onPropsRequested = undefined;
897
+ }
898
+ }
884
899
  }
885
900
  /**
886
901
  * Re-executes a component.
@@ -895,32 +910,31 @@ class Context {
895
910
  * async iterator to suspend.
896
911
  */
897
912
  refresh() {
898
- const impl = this[$ContextImpl];
899
- if (impl.f & IsUnmounted) {
913
+ const ctx = this[_ContextImpl];
914
+ if (ctx.f & IsUnmounted) {
900
915
  console.error("Component is unmounted");
901
- return impl.renderer.read(undefined);
916
+ return ctx.renderer.read(undefined);
902
917
  }
903
- else if (impl.f & IsExecuting) {
918
+ else if (ctx.f & IsSyncExecuting) {
904
919
  console.error("Component is already executing");
905
920
  return this.value;
906
921
  }
907
- resumeCtxIterator(impl);
908
- const value = runComponent(impl);
922
+ const value = enqueueComponentRun(ctx);
909
923
  if (isPromiseLike(value)) {
910
- return value.then((value) => impl.renderer.read(value));
924
+ return value.then((value) => ctx.renderer.read(value));
911
925
  }
912
- return impl.renderer.read(value);
926
+ return ctx.renderer.read(value);
913
927
  }
914
928
  /**
915
929
  * Registers a callback which fires when the component commits. Will only
916
930
  * fire once per callback and update.
917
931
  */
918
932
  schedule(callback) {
919
- const impl = this[$ContextImpl];
920
- let callbacks = scheduleMap.get(impl);
933
+ const ctx = this[_ContextImpl];
934
+ let callbacks = scheduleMap.get(ctx);
921
935
  if (!callbacks) {
922
936
  callbacks = new Set();
923
- scheduleMap.set(impl, callbacks);
937
+ scheduleMap.set(ctx, callbacks);
924
938
  }
925
939
  callbacks.add(callback);
926
940
  }
@@ -929,19 +943,19 @@ class Context {
929
943
  * rendered into the root. Will only fire once per callback and render.
930
944
  */
931
945
  flush(callback) {
932
- const impl = this[$ContextImpl];
933
- if (typeof impl.root !== "object" || impl.root === null) {
946
+ const ctx = this[_ContextImpl];
947
+ if (typeof ctx.root !== "object" || ctx.root === null) {
934
948
  return;
935
949
  }
936
- let flushMap = flushMaps.get(impl.root);
950
+ let flushMap = flushMaps.get(ctx.root);
937
951
  if (!flushMap) {
938
952
  flushMap = new Map();
939
- flushMaps.set(impl.root, flushMap);
953
+ flushMaps.set(ctx.root, flushMap);
940
954
  }
941
- let callbacks = flushMap.get(impl);
955
+ let callbacks = flushMap.get(ctx);
942
956
  if (!callbacks) {
943
957
  callbacks = new Set();
944
- flushMap.set(impl, callbacks);
958
+ flushMap.set(ctx, callbacks);
945
959
  }
946
960
  callbacks.add(callback);
947
961
  }
@@ -950,45 +964,45 @@ class Context {
950
964
  * fire once per callback.
951
965
  */
952
966
  cleanup(callback) {
953
- const impl = this[$ContextImpl];
954
- let callbacks = cleanupMap.get(impl);
967
+ const ctx = this[_ContextImpl];
968
+ let callbacks = cleanupMap.get(ctx);
955
969
  if (!callbacks) {
956
970
  callbacks = new Set();
957
- cleanupMap.set(impl, callbacks);
971
+ cleanupMap.set(ctx, callbacks);
958
972
  }
959
973
  callbacks.add(callback);
960
974
  }
961
975
  consume(key) {
962
- for (let parent = this[$ContextImpl].parent; parent !== undefined; parent = parent.parent) {
963
- const provisions = provisionMaps.get(parent);
976
+ for (let ctx = this[_ContextImpl].parent; ctx !== undefined; ctx = ctx.parent) {
977
+ const provisions = provisionMaps.get(ctx);
964
978
  if (provisions && provisions.has(key)) {
965
979
  return provisions.get(key);
966
980
  }
967
981
  }
968
982
  }
969
983
  provide(key, value) {
970
- const impl = this[$ContextImpl];
971
- let provisions = provisionMaps.get(impl);
984
+ const ctx = this[_ContextImpl];
985
+ let provisions = provisionMaps.get(ctx);
972
986
  if (!provisions) {
973
987
  provisions = new Map();
974
- provisionMaps.set(impl, provisions);
988
+ provisionMaps.set(ctx, provisions);
975
989
  }
976
990
  provisions.set(key, value);
977
991
  }
978
992
  addEventListener(type, listener, options) {
979
- const impl = this[$ContextImpl];
993
+ const ctx = this[_ContextImpl];
980
994
  let listeners;
981
995
  if (!isListenerOrListenerObject(listener)) {
982
996
  return;
983
997
  }
984
998
  else {
985
- const listeners1 = listenersMap.get(impl);
999
+ const listeners1 = listenersMap.get(ctx);
986
1000
  if (listeners1) {
987
1001
  listeners = listeners1;
988
1002
  }
989
1003
  else {
990
1004
  listeners = [];
991
- listenersMap.set(impl, listeners);
1005
+ listenersMap.set(ctx, listeners);
992
1006
  }
993
1007
  }
994
1008
  options = normalizeListenerOptions(options);
@@ -999,7 +1013,7 @@ class Context {
999
1013
  else {
1000
1014
  callback = listener;
1001
1015
  }
1002
- const record = { type, callback, listener, options };
1016
+ const record = { type, listener, callback, options };
1003
1017
  if (options.once) {
1004
1018
  record.callback = function () {
1005
1019
  const i = listeners.indexOf(record);
@@ -1016,15 +1030,15 @@ class Context {
1016
1030
  }
1017
1031
  listeners.push(record);
1018
1032
  // TODO: is it possible to separate out the EventTarget delegation logic
1019
- for (const value of getChildValues(impl.ret)) {
1033
+ for (const value of getChildValues(ctx.ret)) {
1020
1034
  if (isEventTarget(value)) {
1021
1035
  value.addEventListener(record.type, record.callback, record.options);
1022
1036
  }
1023
1037
  }
1024
1038
  }
1025
1039
  removeEventListener(type, listener, options) {
1026
- const impl = this[$ContextImpl];
1027
- const listeners = listenersMap.get(impl);
1040
+ const ctx = this[_ContextImpl];
1041
+ const listeners = listenersMap.get(ctx);
1028
1042
  if (listeners == null || !isListenerOrListenerObject(listener)) {
1029
1043
  return;
1030
1044
  }
@@ -1038,16 +1052,16 @@ class Context {
1038
1052
  const record = listeners[i];
1039
1053
  listeners.splice(i, 1);
1040
1054
  // TODO: is it possible to separate out the EventTarget delegation logic
1041
- for (const value of getChildValues(impl.ret)) {
1055
+ for (const value of getChildValues(ctx.ret)) {
1042
1056
  if (isEventTarget(value)) {
1043
1057
  value.removeEventListener(record.type, record.callback, record.options);
1044
1058
  }
1045
1059
  }
1046
1060
  }
1047
1061
  dispatchEvent(ev) {
1048
- const impl = this[$ContextImpl];
1062
+ const ctx = this[_ContextImpl];
1049
1063
  const path = [];
1050
- for (let parent = impl.parent; parent !== undefined; parent = parent.parent) {
1064
+ for (let parent = ctx.parent; parent !== undefined; parent = parent.parent) {
1051
1065
  path.push(parent);
1052
1066
  }
1053
1067
  // We patch the stopImmediatePropagation method because ev.cancelBubble
@@ -1059,7 +1073,7 @@ class Context {
1059
1073
  immediateCancelBubble = true;
1060
1074
  return stopImmediatePropagation.call(ev);
1061
1075
  });
1062
- setEventProperty(ev, "target", impl.ctx);
1076
+ setEventProperty(ev, "target", ctx.owner);
1063
1077
  // The only possible errors in this block are errors thrown by callbacks,
1064
1078
  // and dispatchEvent will only log these errors rather than throwing
1065
1079
  // them. Therefore, we place all code in a try block, log errors in the
@@ -1068,18 +1082,21 @@ class Context {
1068
1082
  // Each early return within the try block returns true because while the
1069
1083
  // return value is overridden in the finally block, TypeScript
1070
1084
  // (justifiably) does not recognize the unsafe return statement.
1071
- //
1072
- // TODO: Run all callbacks even if one of them errors
1073
1085
  try {
1074
1086
  setEventProperty(ev, "eventPhase", CAPTURING_PHASE);
1075
1087
  for (let i = path.length - 1; i >= 0; i--) {
1076
1088
  const target = path[i];
1077
1089
  const listeners = listenersMap.get(target);
1078
1090
  if (listeners) {
1079
- setEventProperty(ev, "currentTarget", target.ctx);
1091
+ setEventProperty(ev, "currentTarget", target.owner);
1080
1092
  for (const record of listeners) {
1081
1093
  if (record.type === ev.type && record.options.capture) {
1082
- record.callback.call(target.ctx, ev);
1094
+ try {
1095
+ record.callback.call(target.owner, ev);
1096
+ }
1097
+ catch (err) {
1098
+ console.error(err);
1099
+ }
1083
1100
  if (immediateCancelBubble) {
1084
1101
  return true;
1085
1102
  }
@@ -1091,13 +1108,25 @@ class Context {
1091
1108
  }
1092
1109
  }
1093
1110
  {
1094
- const listeners = listenersMap.get(impl);
1111
+ setEventProperty(ev, "eventPhase", AT_TARGET);
1112
+ setEventProperty(ev, "currentTarget", ctx.owner);
1113
+ const propCallback = ctx.ret.el.props["on" + ev.type];
1114
+ if (propCallback != null) {
1115
+ propCallback(ev);
1116
+ if (immediateCancelBubble || ev.cancelBubble) {
1117
+ return true;
1118
+ }
1119
+ }
1120
+ const listeners = listenersMap.get(ctx);
1095
1121
  if (listeners) {
1096
- setEventProperty(ev, "eventPhase", AT_TARGET);
1097
- setEventProperty(ev, "currentTarget", impl.ctx);
1098
1122
  for (const record of listeners) {
1099
1123
  if (record.type === ev.type) {
1100
- record.callback.call(impl.ctx, ev);
1124
+ try {
1125
+ record.callback.call(ctx.owner, ev);
1126
+ }
1127
+ catch (err) {
1128
+ console.error(err);
1129
+ }
1101
1130
  if (immediateCancelBubble) {
1102
1131
  return true;
1103
1132
  }
@@ -1114,10 +1143,15 @@ class Context {
1114
1143
  const target = path[i];
1115
1144
  const listeners = listenersMap.get(target);
1116
1145
  if (listeners) {
1117
- setEventProperty(ev, "currentTarget", target.ctx);
1146
+ setEventProperty(ev, "currentTarget", target.owner);
1118
1147
  for (const record of listeners) {
1119
1148
  if (record.type === ev.type && !record.options.capture) {
1120
- record.callback.call(target.ctx, ev);
1149
+ try {
1150
+ record.callback.call(target.owner, ev);
1151
+ }
1152
+ catch (err) {
1153
+ console.error(err);
1154
+ }
1121
1155
  if (immediateCancelBubble) {
1122
1156
  return true;
1123
1157
  }
@@ -1130,10 +1164,6 @@ class Context {
1130
1164
  }
1131
1165
  }
1132
1166
  }
1133
- catch (err) {
1134
- // TODO: Use setTimeout to rethrow the error.
1135
- console.error(err);
1136
- }
1137
1167
  finally {
1138
1168
  setEventProperty(ev, "eventPhase", NONE);
1139
1169
  setEventProperty(ev, "currentTarget", null);
@@ -1155,38 +1185,43 @@ function updateComponent(renderer, root, host, parent, scope, ret, oldProps) {
1155
1185
  let ctx;
1156
1186
  if (oldProps) {
1157
1187
  ctx = ret.ctx;
1158
- if (ctx.f & IsExecuting) {
1188
+ if (ctx.f & IsSyncExecuting) {
1159
1189
  console.error("Component is already executing");
1160
- return ret.cached;
1190
+ return ret.cachedChildValues;
1161
1191
  }
1162
1192
  }
1163
1193
  else {
1164
1194
  ctx = ret.ctx = new ContextImpl(renderer, root, host, parent, scope, ret);
1165
1195
  }
1166
1196
  ctx.f |= IsUpdating;
1167
- resumeCtxIterator(ctx);
1168
- return runComponent(ctx);
1197
+ return enqueueComponentRun(ctx);
1169
1198
  }
1170
1199
  function updateComponentChildren(ctx, children) {
1171
- if (ctx.f & IsUnmounted || ctx.f & IsErrored) {
1200
+ if (ctx.f & IsUnmounted) {
1201
+ return;
1202
+ }
1203
+ else if (ctx.f & IsErrored) {
1204
+ // This branch is necessary for some race conditions where this function is
1205
+ // called after iterator.throw() in async generator components.
1172
1206
  return;
1173
1207
  }
1174
1208
  else if (children === undefined) {
1175
1209
  console.error("A component has returned or yielded undefined. If this was intentional, return or yield null instead.");
1176
1210
  }
1177
1211
  let childValues;
1178
- // We set the isExecuting flag in case a child component dispatches an event
1179
- // which bubbles to this component and causes a synchronous refresh().
1180
- ctx.f |= IsExecuting;
1181
1212
  try {
1213
+ // TODO: WAT
1214
+ // We set the isExecuting flag in case a child component dispatches an event
1215
+ // which bubbles to this component and causes a synchronous refresh().
1216
+ ctx.f |= IsSyncExecuting;
1182
1217
  childValues = diffChildren(ctx.renderer, ctx.root, ctx.host, ctx, ctx.scope, ctx.ret, narrow(children));
1183
1218
  }
1184
1219
  finally {
1185
- ctx.f &= ~IsExecuting;
1220
+ ctx.f &= ~IsSyncExecuting;
1186
1221
  }
1187
1222
  if (isPromiseLike(childValues)) {
1188
- ctx.ret.inflight = childValues.then((childValues) => commitComponent(ctx, childValues));
1189
- return ctx.ret.inflight;
1223
+ ctx.ret.inflightValue = childValues.then((childValues) => commitComponent(ctx, childValues));
1224
+ return ctx.ret.inflightValue;
1190
1225
  }
1191
1226
  return commitComponent(ctx, childValues);
1192
1227
  }
@@ -1206,8 +1241,8 @@ function commitComponent(ctx, values) {
1206
1241
  }
1207
1242
  }
1208
1243
  }
1209
- const oldValues = wrap(ctx.ret.cached);
1210
- let value = (ctx.ret.cached = unwrap(values));
1244
+ const oldValues = wrap(ctx.ret.cachedChildValues);
1245
+ let value = (ctx.ret.cachedChildValues = unwrap(values));
1211
1246
  if (ctx.f & IsScheduling) {
1212
1247
  ctx.f |= IsSchedulingRefresh;
1213
1248
  }
@@ -1215,7 +1250,7 @@ function commitComponent(ctx, values) {
1215
1250
  // If we’re not updating the component, which happens when components are
1216
1251
  // refreshed, or when async generator components iterate, we have to do a
1217
1252
  // little bit housekeeping when a component’s child values have changed.
1218
- if (!valuesEqual(oldValues, values)) {
1253
+ if (!arrayEqual(oldValues, values)) {
1219
1254
  const records = getListenerRecords(ctx.parent, ctx.host);
1220
1255
  if (records.length) {
1221
1256
  for (let i = 0; i < values.length; i++) {
@@ -1230,7 +1265,7 @@ function commitComponent(ctx, values) {
1230
1265
  }
1231
1266
  // rearranging the nearest ancestor host element
1232
1267
  const host = ctx.host;
1233
- const oldHostValues = wrap(host.cached);
1268
+ const oldHostValues = wrap(host.cachedChildValues);
1234
1269
  invalidate(ctx, host);
1235
1270
  const hostValues = getChildValues(host);
1236
1271
  ctx.renderer.arrange(host.el.tag, host.value, host.el.props, hostValues,
@@ -1259,50 +1294,75 @@ function commitComponent(ctx, values) {
1259
1294
  }
1260
1295
  function invalidate(ctx, host) {
1261
1296
  for (let parent = ctx.parent; parent !== undefined && parent.host === host; parent = parent.parent) {
1262
- parent.ret.cached = undefined;
1297
+ parent.ret.cachedChildValues = undefined;
1263
1298
  }
1264
- host.cached = undefined;
1299
+ host.cachedChildValues = undefined;
1265
1300
  }
1266
- function valuesEqual(values1, values2) {
1267
- if (values1.length !== values2.length) {
1301
+ function arrayEqual(arr1, arr2) {
1302
+ if (arr1.length !== arr2.length) {
1268
1303
  return false;
1269
1304
  }
1270
- for (let i = 0; i < values1.length; i++) {
1271
- const value1 = values1[i];
1272
- const value2 = values2[i];
1305
+ for (let i = 0; i < arr1.length; i++) {
1306
+ const value1 = arr1[i];
1307
+ const value2 = arr2[i];
1273
1308
  if (value1 !== value2) {
1274
1309
  return false;
1275
1310
  }
1276
1311
  }
1277
1312
  return true;
1278
1313
  }
1279
- /**
1280
- * Enqueues and executes the component associated with the context.
1281
- *
1282
- * The functions stepComponent and runComponent work together
1283
- * to implement the async queueing behavior of components. The runComponent
1284
- * function calls the stepComponent function, which returns two results in a
1285
- * tuple. The first result, called the “block,” is a possible promise which
1286
- * represents the duration for which the component is blocked from accepting
1287
- * new updates. The second result, called the “value,” is the actual result of
1288
- * the update. The runComponent function caches block/value from the
1289
- * stepComponent function on the context, according to whether the component
1290
- * blocks. The “inflight” block/value properties are the currently executing
1291
- * update, and the “enqueued” block/value properties represent an enqueued next
1292
- * stepComponent. Enqueued steps are dequeued every time the current block
1293
- * promise settles.
1294
- */
1295
- function runComponent(ctx) {
1296
- if (!ctx.inflightBlock) {
1314
+ /** Enqueues and executes the component associated with the context. */
1315
+ function enqueueComponentRun(ctx) {
1316
+ if (ctx.f & IsAsyncGen) {
1317
+ // This branch will only run for async generator components after the
1318
+ // initial render.
1319
+ //
1320
+ // Async generator components which are in the props loop can be in one of
1321
+ // three states:
1322
+ //
1323
+ // 1. propsAvailable flag is true: "available"
1324
+ //
1325
+ // The component is paused somewhere in the loop. When the component
1326
+ // reaches the bottom of the loop, it will run again with the next props.
1327
+ //
1328
+ // 2. onAvailable callback is defined: "suspended"
1329
+ //
1330
+ // The component has reached the bottom of the loop and is waiting for
1331
+ // new props.
1332
+ //
1333
+ // 3. neither 1 or 2: "Running"
1334
+ //
1335
+ // The component is paused somewhere in the loop. When the component
1336
+ // reaches the bottom of the loop, it will suspend.
1337
+ //
1338
+ // By definition, components will never be both available and suspended at
1339
+ // the same time.
1340
+ //
1341
+ // If the component is at the loop bottom, this means that the next value
1342
+ // produced by the component will have the most up to date props, so we can
1343
+ // simply return the current inflight value. Otherwise, we have to wait for
1344
+ // the bottom of the loop before returning the inflight value.
1345
+ const isAtLoopbottom = ctx.f & IsInRenderLoop && !ctx.onProps;
1346
+ resumePropsIterator(ctx);
1347
+ if (isAtLoopbottom) {
1348
+ if (ctx.inflightBlock == null) {
1349
+ ctx.inflightBlock = new Promise((resolve) => (ctx.onPropsRequested = resolve));
1350
+ }
1351
+ return ctx.inflightBlock.then(() => {
1352
+ ctx.inflightBlock = undefined;
1353
+ return ctx.inflightValue;
1354
+ });
1355
+ }
1356
+ return ctx.inflightValue;
1357
+ }
1358
+ else if (!ctx.inflightBlock) {
1297
1359
  try {
1298
- const [block, value] = stepComponent(ctx);
1360
+ const [block, value] = runComponent(ctx);
1299
1361
  if (block) {
1300
1362
  ctx.inflightBlock = block
1301
- .catch((err) => {
1302
- if (!(ctx.f & IsUpdating)) {
1303
- return propagateError(ctx.parent, err);
1304
- }
1305
- })
1363
+ // TODO: there is some fuckery going on here related to async
1364
+ // generator components resuming when they’re meant to be returned.
1365
+ .then((v) => v)
1306
1366
  .finally(() => advanceComponent(ctx));
1307
1367
  // stepComponent will only return a block if the value is asynchronous
1308
1368
  ctx.inflightValue = value;
@@ -1316,38 +1376,43 @@ function runComponent(ctx) {
1316
1376
  throw err;
1317
1377
  }
1318
1378
  }
1319
- else if (ctx.f & IsAsyncGen) {
1320
- return ctx.inflightValue;
1321
- }
1322
1379
  else if (!ctx.enqueuedBlock) {
1323
- let resolve;
1324
- ctx.enqueuedBlock = ctx.inflightBlock
1325
- .then(() => {
1380
+ // We need to assign enqueuedBlock and enqueuedValue synchronously, hence
1381
+ // the Promise constructor call.
1382
+ let resolveEnqueuedBlock;
1383
+ ctx.enqueuedBlock = new Promise((resolve) => (resolveEnqueuedBlock = resolve));
1384
+ ctx.enqueuedValue = ctx.inflightBlock.then(() => {
1326
1385
  try {
1327
- const [block, value] = stepComponent(ctx);
1328
- resolve(value);
1386
+ const [block, value] = runComponent(ctx);
1329
1387
  if (block) {
1330
- return block.catch((err) => {
1331
- if (!(ctx.f & IsUpdating)) {
1332
- return propagateError(ctx.parent, err);
1333
- }
1334
- });
1388
+ resolveEnqueuedBlock(block.finally(() => advanceComponent(ctx)));
1335
1389
  }
1390
+ return value;
1336
1391
  }
1337
1392
  catch (err) {
1338
1393
  if (!(ctx.f & IsUpdating)) {
1339
1394
  return propagateError(ctx.parent, err);
1340
1395
  }
1396
+ throw err;
1341
1397
  }
1342
- })
1343
- .finally(() => advanceComponent(ctx));
1344
- ctx.enqueuedValue = new Promise((resolve1) => (resolve = resolve1));
1398
+ });
1345
1399
  }
1346
1400
  return ctx.enqueuedValue;
1347
1401
  }
1402
+ /** Called when the inflight block promise settles. */
1403
+ function advanceComponent(ctx) {
1404
+ if (ctx.f & IsAsyncGen) {
1405
+ return;
1406
+ }
1407
+ ctx.inflightBlock = ctx.enqueuedBlock;
1408
+ ctx.inflightValue = ctx.enqueuedValue;
1409
+ ctx.enqueuedBlock = undefined;
1410
+ ctx.enqueuedValue = undefined;
1411
+ }
1348
1412
  /**
1349
1413
  * This function is responsible for executing the component and handling all
1350
- * the different component types.
1414
+ * the different component types. We cannot identify whether a component is a
1415
+ * generator or async without calling it and inspecting the return value.
1351
1416
  *
1352
1417
  * @returns {[block, value]} A tuple where
1353
1418
  * block - A possible promise which represents the duration during which the
@@ -1362,25 +1427,23 @@ function runComponent(ctx) {
1362
1427
  * - Sync generator components block while any children are executing, because
1363
1428
  * they are expected to only resume when they’ve actually rendered.
1364
1429
  */
1365
- function stepComponent(ctx) {
1430
+ function runComponent(ctx) {
1366
1431
  const ret = ctx.ret;
1367
- if (ctx.f & IsDone) {
1368
- return [undefined, getValue(ret)];
1369
- }
1370
1432
  const initial = !ctx.iterator;
1371
1433
  if (initial) {
1372
- ctx.f |= IsExecuting;
1434
+ resumePropsIterator(ctx);
1435
+ ctx.f |= IsSyncExecuting;
1373
1436
  clearEventListeners(ctx);
1374
1437
  let result;
1375
1438
  try {
1376
- result = ret.el.tag.call(ctx.ctx, ret.el.props);
1439
+ result = ret.el.tag.call(ctx.owner, ret.el.props);
1377
1440
  }
1378
1441
  catch (err) {
1379
1442
  ctx.f |= IsErrored;
1380
1443
  throw err;
1381
1444
  }
1382
1445
  finally {
1383
- ctx.f &= ~IsExecuting;
1446
+ ctx.f &= ~IsSyncExecuting;
1384
1447
  }
1385
1448
  if (isIteratorLike(result)) {
1386
1449
  ctx.iterator = result;
@@ -1392,120 +1455,182 @@ function stepComponent(ctx) {
1392
1455
  ctx.f |= IsErrored;
1393
1456
  throw err;
1394
1457
  });
1395
- return [result1, value];
1458
+ return [result1.catch(NOOP), value];
1396
1459
  }
1397
1460
  else {
1398
1461
  // sync function component
1399
1462
  return [undefined, updateComponentChildren(ctx, result)];
1400
1463
  }
1401
1464
  }
1402
- let oldValue;
1465
+ let iteration;
1403
1466
  if (initial) {
1404
- // The argument passed to the first call to next is ignored.
1405
- oldValue = undefined;
1467
+ try {
1468
+ ctx.f |= IsSyncExecuting;
1469
+ iteration = ctx.iterator.next();
1470
+ }
1471
+ catch (err) {
1472
+ ctx.f |= IsErrored;
1473
+ throw err;
1474
+ }
1475
+ finally {
1476
+ ctx.f &= ~IsSyncExecuting;
1477
+ }
1478
+ if (isPromiseLike(iteration)) {
1479
+ ctx.f |= IsAsyncGen;
1480
+ runAsyncGenComponent(ctx, iteration);
1481
+ }
1482
+ else {
1483
+ ctx.f |= IsSyncGen;
1484
+ }
1406
1485
  }
1407
- else if (ctx.ret.inflight) {
1408
- // The value passed back into the generator as the argument to the next
1409
- // method is a promise if an async generator component has async children.
1410
- // Sync generator components only resume when their children have fulfilled
1411
- // so the element’s inflight child values will never be defined.
1412
- oldValue = ctx.ret.inflight.then((value) => ctx.renderer.read(value), () => ctx.renderer.read(undefined));
1486
+ if (ctx.f & IsSyncGen) {
1487
+ // sync generator component
1488
+ ctx.f &= ~NeedsToYield;
1489
+ if (!initial) {
1490
+ try {
1491
+ ctx.f |= IsSyncExecuting;
1492
+ iteration = ctx.iterator.next(ctx.renderer.read(getValue(ret)));
1493
+ }
1494
+ catch (err) {
1495
+ ctx.f |= IsErrored;
1496
+ throw err;
1497
+ }
1498
+ finally {
1499
+ ctx.f &= ~IsSyncExecuting;
1500
+ }
1501
+ }
1502
+ if (isPromiseLike(iteration)) {
1503
+ throw new Error("Sync generator component returned an async iteration");
1504
+ }
1505
+ if (iteration.done) {
1506
+ ctx.f &= ~IsSyncGen;
1507
+ ctx.iterator = undefined;
1508
+ }
1509
+ let value;
1510
+ try {
1511
+ value = updateComponentChildren(ctx,
1512
+ // Children can be void so we eliminate that here
1513
+ iteration.value);
1514
+ if (isPromiseLike(value)) {
1515
+ value = value.catch((err) => handleChildError(ctx, err));
1516
+ }
1517
+ }
1518
+ catch (err) {
1519
+ value = handleChildError(ctx, err);
1520
+ }
1521
+ const block = isPromiseLike(value) ? value.catch(NOOP) : undefined;
1522
+ return [block, value];
1413
1523
  }
1414
1524
  else {
1415
- oldValue = ctx.renderer.read(getValue(ret));
1525
+ // async generator component
1526
+ return [undefined, ctx.inflightValue];
1416
1527
  }
1417
- let iteration;
1418
- ctx.f |= IsExecuting;
1528
+ }
1529
+ async function runAsyncGenComponent(ctx, iterationP) {
1530
+ let done = false;
1419
1531
  try {
1420
- iteration = ctx.iterator.next(oldValue);
1421
- }
1422
- catch (err) {
1423
- ctx.f |= IsDone | IsErrored;
1424
- throw err;
1425
- }
1426
- finally {
1427
- ctx.f &= ~IsExecuting;
1428
- }
1429
- if (isPromiseLike(iteration)) {
1430
- // async generator component
1431
- if (initial) {
1432
- ctx.f |= IsAsyncGen;
1433
- }
1434
- const value = iteration.then((iteration) => {
1435
- if (!(ctx.f & IsIterating)) {
1436
- ctx.f &= ~IsAvailable;
1532
+ while (!done) {
1533
+ // inflightValue must be set synchronously.
1534
+ let onValue;
1535
+ ctx.inflightValue = new Promise((resolve) => (onValue = resolve));
1536
+ if (ctx.f & IsUpdating) {
1537
+ // We should not swallow unhandled promise rejections if the component is
1538
+ // updating independently.
1539
+ // TODO: Does this handle this.refresh() calls?
1540
+ ctx.inflightValue.catch(NOOP);
1437
1541
  }
1438
- ctx.f &= ~IsIterating;
1439
- if (iteration.done) {
1440
- ctx.f |= IsDone;
1542
+ let iteration;
1543
+ try {
1544
+ iteration = await iterationP;
1441
1545
  }
1546
+ catch (err) {
1547
+ done = true;
1548
+ ctx.f |= IsErrored;
1549
+ onValue(Promise.reject(err));
1550
+ break;
1551
+ }
1552
+ finally {
1553
+ ctx.f &= ~NeedsToYield;
1554
+ if (!(ctx.f & IsInRenderLoop)) {
1555
+ ctx.f &= ~PropsAvailable;
1556
+ }
1557
+ }
1558
+ done = !!iteration.done;
1559
+ let value;
1442
1560
  try {
1443
- const value = updateComponentChildren(ctx, iteration.value);
1561
+ value = updateComponentChildren(ctx, iteration.value);
1444
1562
  if (isPromiseLike(value)) {
1445
- return value.catch((err) => handleChildError(ctx, err));
1563
+ value = value.catch((err) => handleChildError(ctx, err));
1446
1564
  }
1447
- return value;
1448
1565
  }
1449
1566
  catch (err) {
1450
- return handleChildError(ctx, err);
1567
+ done = true;
1568
+ // Do we need to catch potential errors here in the case of unhandled
1569
+ // promise rejections?
1570
+ value = handleChildError(ctx, err);
1571
+ }
1572
+ finally {
1573
+ onValue(value);
1574
+ }
1575
+ // TODO: this can be done more elegantly
1576
+ let oldValue;
1577
+ if (ctx.ret.inflightValue) {
1578
+ // The value passed back into the generator as the argument to the next
1579
+ // method is a promise if an async generator component has async
1580
+ // children. Sync generator components only resume when their children
1581
+ // have fulfilled so the element’s inflight child values will never be
1582
+ // defined.
1583
+ oldValue = ctx.ret.inflightValue.then((value) => ctx.renderer.read(value), () => ctx.renderer.read(undefined));
1584
+ }
1585
+ else {
1586
+ oldValue = ctx.renderer.read(getValue(ctx.ret));
1587
+ }
1588
+ if (ctx.f & IsUnmounted) {
1589
+ if (ctx.f & IsInRenderLoop) {
1590
+ try {
1591
+ ctx.f |= IsSyncExecuting;
1592
+ iterationP = ctx.iterator.next(oldValue);
1593
+ }
1594
+ finally {
1595
+ ctx.f &= ~IsSyncExecuting;
1596
+ }
1597
+ }
1598
+ else {
1599
+ returnComponent(ctx);
1600
+ break;
1601
+ }
1602
+ }
1603
+ else if (!done) {
1604
+ try {
1605
+ ctx.f |= IsSyncExecuting;
1606
+ iterationP = ctx.iterator.next(oldValue);
1607
+ }
1608
+ finally {
1609
+ ctx.f &= ~IsSyncExecuting;
1610
+ }
1451
1611
  }
1452
- }, (err) => {
1453
- ctx.f |= IsDone | IsErrored;
1454
- throw err;
1455
- });
1456
- return [iteration, value];
1457
- }
1458
- // sync generator component
1459
- if (initial) {
1460
- ctx.f |= IsSyncGen;
1461
- }
1462
- ctx.f &= ~IsIterating;
1463
- if (iteration.done) {
1464
- ctx.f |= IsDone;
1465
- }
1466
- let value;
1467
- try {
1468
- value = updateComponentChildren(ctx, iteration.value);
1469
- if (isPromiseLike(value)) {
1470
- value = value.catch((err) => handleChildError(ctx, err));
1471
1612
  }
1472
1613
  }
1473
- catch (err) {
1474
- value = handleChildError(ctx, err);
1475
- }
1476
- if (isPromiseLike(value)) {
1477
- return [value.catch(NOOP), value];
1478
- }
1479
- return [undefined, value];
1480
- }
1481
- /**
1482
- * Called when the inflight block promise settles.
1483
- */
1484
- function advanceComponent(ctx) {
1485
- ctx.inflightBlock = ctx.enqueuedBlock;
1486
- ctx.inflightValue = ctx.enqueuedValue;
1487
- ctx.enqueuedBlock = undefined;
1488
- ctx.enqueuedValue = undefined;
1489
- if (ctx.f & IsAsyncGen && !(ctx.f & IsDone) && !(ctx.f & IsUnmounted)) {
1490
- runComponent(ctx);
1614
+ finally {
1615
+ ctx.f &= ~IsAsyncGen;
1616
+ ctx.iterator = undefined;
1491
1617
  }
1492
1618
  }
1493
1619
  /**
1494
- * Called to make props available to the props async iterator for async
1495
- * generator components.
1620
+ * Called to resume the props async iterator for async generator components.
1496
1621
  */
1497
- function resumeCtxIterator(ctx) {
1498
- if (ctx.onAvailable) {
1499
- ctx.onAvailable();
1500
- ctx.onAvailable = undefined;
1622
+ function resumePropsIterator(ctx) {
1623
+ if (ctx.onProps) {
1624
+ ctx.onProps(ctx.ret.el.props);
1625
+ ctx.onProps = undefined;
1626
+ ctx.f &= ~PropsAvailable;
1501
1627
  }
1502
1628
  else {
1503
- ctx.f |= IsAvailable;
1629
+ ctx.f |= PropsAvailable;
1504
1630
  }
1505
1631
  }
1506
1632
  // TODO: async unmounting
1507
1633
  function unmountComponent(ctx) {
1508
- ctx.f |= IsUnmounted;
1509
1634
  clearEventListeners(ctx);
1510
1635
  const callbacks = cleanupMap.get(ctx);
1511
1636
  if (callbacks) {
@@ -1515,21 +1640,54 @@ function unmountComponent(ctx) {
1515
1640
  callback(value);
1516
1641
  }
1517
1642
  }
1518
- if (!(ctx.f & IsDone)) {
1519
- ctx.f |= IsDone;
1520
- resumeCtxIterator(ctx);
1521
- if (ctx.iterator && typeof ctx.iterator.return === "function") {
1522
- ctx.f |= IsExecuting;
1523
- try {
1524
- const iteration = ctx.iterator.return();
1525
- if (isPromiseLike(iteration)) {
1526
- iteration.catch((err) => propagateError(ctx.parent, err));
1643
+ ctx.f |= IsUnmounted;
1644
+ if (ctx.iterator) {
1645
+ if (ctx.f & IsSyncGen) {
1646
+ let value;
1647
+ if (ctx.f & IsInRenderLoop) {
1648
+ value = enqueueComponentRun(ctx);
1649
+ }
1650
+ if (isPromiseLike(value)) {
1651
+ value.then(() => {
1652
+ if (ctx.f & IsInRenderLoop) {
1653
+ unmountComponent(ctx);
1654
+ }
1655
+ else {
1656
+ returnComponent(ctx);
1657
+ }
1658
+ }, (err) => {
1659
+ propagateError(ctx.parent, err);
1660
+ });
1661
+ }
1662
+ else {
1663
+ if (ctx.f & IsInRenderLoop) {
1664
+ unmountComponent(ctx);
1665
+ }
1666
+ else {
1667
+ returnComponent(ctx);
1527
1668
  }
1528
1669
  }
1529
- finally {
1530
- ctx.f &= ~IsExecuting;
1670
+ }
1671
+ else if (ctx.f & IsAsyncGen) {
1672
+ // The logic for unmounting async generator components is in the
1673
+ // runAsyncGenComponent function.
1674
+ resumePropsIterator(ctx);
1675
+ }
1676
+ }
1677
+ }
1678
+ function returnComponent(ctx) {
1679
+ resumePropsIterator(ctx);
1680
+ if (ctx.iterator && typeof ctx.iterator.return === "function") {
1681
+ try {
1682
+ ctx.f |= IsSyncExecuting;
1683
+ const iteration = ctx.iterator.return();
1684
+ if (isPromiseLike(iteration)) {
1685
+ iteration.catch((err) => propagateError(ctx.parent, err));
1531
1686
  }
1532
1687
  }
1688
+ finally {
1689
+ ctx.f &= ~IsSyncExecuting;
1690
+ }
1533
1691
  }
1534
1692
  }
1535
1693
  /*** EVENT TARGET UTILITIES ***/
@@ -1600,39 +1758,38 @@ function clearEventListeners(ctx) {
1600
1758
  }
1601
1759
  }
1602
1760
  /*** ERROR HANDLING UTILITIES ***/
1603
- // TODO: generator components which throw errors should be recoverable
1604
1761
  function handleChildError(ctx, err) {
1605
- if (ctx.f & IsDone ||
1606
- !ctx.iterator ||
1607
- typeof ctx.iterator.throw !== "function") {
1762
+ if (!ctx.iterator || typeof ctx.iterator.throw !== "function") {
1608
1763
  throw err;
1609
1764
  }
1610
- resumeCtxIterator(ctx);
1765
+ resumePropsIterator(ctx);
1611
1766
  let iteration;
1612
1767
  try {
1613
- ctx.f |= IsExecuting;
1768
+ ctx.f |= IsSyncExecuting;
1614
1769
  iteration = ctx.iterator.throw(err);
1615
1770
  }
1616
1771
  catch (err) {
1617
- ctx.f |= IsDone | IsErrored;
1772
+ ctx.f |= IsErrored;
1618
1773
  throw err;
1619
1774
  }
1620
1775
  finally {
1621
- ctx.f &= ~IsExecuting;
1776
+ ctx.f &= ~IsSyncExecuting;
1622
1777
  }
1623
1778
  if (isPromiseLike(iteration)) {
1624
1779
  return iteration.then((iteration) => {
1625
1780
  if (iteration.done) {
1626
- ctx.f |= IsDone;
1781
+ ctx.f &= ~IsAsyncGen;
1782
+ ctx.iterator = undefined;
1627
1783
  }
1628
1784
  return updateComponentChildren(ctx, iteration.value);
1629
1785
  }, (err) => {
1630
- ctx.f |= IsDone | IsErrored;
1786
+ ctx.f |= IsErrored;
1631
1787
  throw err;
1632
1788
  });
1633
1789
  }
1634
1790
  if (iteration.done) {
1635
- ctx.f |= IsDone;
1791
+ ctx.f &= ~IsSyncGen;
1792
+ ctx.iterator = undefined;
1636
1793
  }
1637
1794
  return updateComponentChildren(ctx, iteration.value);
1638
1795
  }