@b9g/crank 0.5.0-debug.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/umd.js CHANGED
@@ -27,7 +27,8 @@
27
27
  : typeof value === "string" ||
28
28
  typeof value[Symbol.iterator] !== "function"
29
29
  ? [value]
30
- : [...value];
30
+ : // TODO: inference broke in TypeScript 3.9.
31
+ [...value];
31
32
  }
32
33
  function isIteratorLike(value) {
33
34
  return value != null && typeof value.next === "function";
@@ -76,8 +77,7 @@
76
77
  /**
77
78
  * A special tag for injecting raw nodes or strings via a value prop.
78
79
  *
79
- * If the value prop is a string, Renderer.prototype.parse() will be called on
80
- * the string and the result will be set as the element’s value.
80
+ * Renderer.prototype.raw() is called with the value prop.
81
81
  */
82
82
  const Raw = Symbol.for("crank.Raw");
83
83
  const ElementSymbol = Symbol.for("crank.Element");
@@ -163,22 +163,6 @@
163
163
  else if (children.length === 1) {
164
164
  props1.children = children[0];
165
165
  }
166
- // string aliases for the special tags
167
- // TODO: Does this logic belong here, or in the Element constructor
168
- switch (tag) {
169
- case "$FRAGMENT":
170
- tag = Fragment;
171
- break;
172
- case "$PORTAL":
173
- tag = Portal;
174
- break;
175
- case "$COPY":
176
- tag = Copy;
177
- break;
178
- case "$RAW":
179
- tag = Raw;
180
- break;
181
- }
182
166
  return new Element(tag, props1, key, ref, static_);
183
167
  }
184
168
  /** Clones a given element, shallowly copying the props object. */
@@ -257,13 +241,13 @@
257
241
  class Retainer {
258
242
  constructor(el) {
259
243
  this.el = el;
260
- this.value = undefined;
261
244
  this.ctx = undefined;
262
245
  this.children = undefined;
263
- this.cached = undefined;
264
- this.fallback = undefined;
265
- this.inflight = undefined;
266
- this.onCommit = undefined;
246
+ this.value = undefined;
247
+ this.cachedChildValues = undefined;
248
+ this.fallbackValue = undefined;
249
+ this.inflightValue = undefined;
250
+ this.onNextValues = undefined;
267
251
  }
268
252
  }
269
253
  /**
@@ -272,10 +256,10 @@
272
256
  * @returns The value of the element.
273
257
  */
274
258
  function getValue(ret) {
275
- if (typeof ret.fallback !== "undefined") {
276
- return typeof ret.fallback === "object"
277
- ? getValue(ret.fallback)
278
- : ret.fallback;
259
+ if (typeof ret.fallbackValue !== "undefined") {
260
+ return typeof ret.fallbackValue === "object"
261
+ ? getValue(ret.fallbackValue)
262
+ : ret.fallbackValue;
279
263
  }
280
264
  else if (ret.el.tag === Portal) {
281
265
  return;
@@ -291,8 +275,8 @@
291
275
  * @returns A normalized array of nodes and strings.
292
276
  */
293
277
  function getChildValues(ret) {
294
- if (ret.cached) {
295
- return wrap(ret.cached);
278
+ if (ret.cachedChildValues) {
279
+ return wrap(ret.cachedChildValues);
296
280
  }
297
281
  const values = [];
298
282
  const children = wrap(ret.children);
@@ -305,7 +289,7 @@
305
289
  const values1 = normalize(values);
306
290
  const tag = ret.el.tag;
307
291
  if (typeof tag === "function" || (tag !== Fragment && tag !== Raw)) {
308
- ret.cached = unwrap(values1);
292
+ ret.cachedChildValues = unwrap(values1);
309
293
  }
310
294
  return values1;
311
295
  }
@@ -313,16 +297,19 @@
313
297
  create() {
314
298
  throw new Error("Not implemented");
315
299
  },
300
+ hydrate() {
301
+ throw new Error("Not implemented");
302
+ },
316
303
  scope: IDENTITY,
317
304
  read: IDENTITY,
318
- escape: IDENTITY,
319
- parse: IDENTITY,
305
+ text: IDENTITY,
306
+ raw: IDENTITY,
320
307
  patch: NOOP,
321
308
  arrange: NOOP,
322
309
  dispose: NOOP,
323
310
  flush: NOOP,
324
311
  };
325
- const $RendererImpl = Symbol.for("crank.RendererImpl");
312
+ const _RendererImpl = Symbol.for("crank.RendererImpl");
326
313
  /**
327
314
  * An abstract class which is subclassed to render to different target
328
315
  * environments. This class is responsible for kicking off the rendering
@@ -336,7 +323,7 @@
336
323
  class Renderer {
337
324
  constructor(impl) {
338
325
  this.cache = new WeakMap();
339
- this[$RendererImpl] = {
326
+ this[_RendererImpl] = {
340
327
  ...defaultRendererImpl,
341
328
  ...impl,
342
329
  };
@@ -348,7 +335,7 @@
348
335
  * used root to delete the previously rendered element tree from the cache.
349
336
  * @param root - The node to be rendered into. The renderer will cache
350
337
  * element trees per root.
351
- * @param ctx - An optional context that will be the ancestor context of all
338
+ * @param bridge - An optional context that will be the ancestor context of all
352
339
  * elements in the tree. Useful for connecting different renderers so that
353
340
  * events/provisions properly propagate. The context for a given root must be
354
341
  * the same or an error will be thrown.
@@ -358,7 +345,7 @@
358
345
  */
359
346
  render(children, root, bridge) {
360
347
  let ret;
361
- const ctx = bridge && bridge[$ContextImpl];
348
+ const ctx = bridge && bridge[_ContextImpl];
362
349
  if (typeof root === "object" && root !== null) {
363
350
  ret = this.cache.get(root);
364
351
  }
@@ -381,8 +368,32 @@
381
368
  this.cache.delete(root);
382
369
  }
383
370
  }
384
- const impl = this[$RendererImpl];
385
- const childValues = diffChildren(impl, root, ret, ctx, impl.scope(undefined, Portal, ret.el.props), ret, children);
371
+ const impl = this[_RendererImpl];
372
+ const childValues = diffChildren(impl, root, ret, ctx, impl.scope(undefined, Portal, ret.el.props), ret, children, undefined);
373
+ // We return the child values of the portal because portal elements
374
+ // themselves have no readable value.
375
+ if (isPromiseLike(childValues)) {
376
+ return childValues.then((childValues) => commitRootRender(impl, root, ctx, ret, childValues, oldProps));
377
+ }
378
+ return commitRootRender(impl, root, ctx, ret, childValues, oldProps);
379
+ }
380
+ hydrate(children, root, bridge) {
381
+ const impl = this[_RendererImpl];
382
+ const ctx = bridge && bridge[_ContextImpl];
383
+ let ret;
384
+ ret = this.cache.get(root);
385
+ if (ret !== undefined) {
386
+ // If there is a retainer for the root, hydration is not necessary.
387
+ return this.render(children, root, bridge);
388
+ }
389
+ let oldProps;
390
+ ret = new Retainer(createElement(Portal, { children, root }));
391
+ ret.value = root;
392
+ if (typeof root === "object" && root !== null && children != null) {
393
+ this.cache.set(root, ret);
394
+ }
395
+ const hydrationData = impl.hydrate(Portal, root, {});
396
+ const childValues = diffChildren(impl, root, ret, ctx, impl.scope(undefined, Portal, ret.el.props), ret, children, hydrationData);
386
397
  // We return the child values of the portal because portal elements
387
398
  // themselves have no readable value.
388
399
  if (isPromiseLike(childValues)) {
@@ -394,17 +405,17 @@
394
405
  /*** PRIVATE RENDERER FUNCTIONS ***/
395
406
  function commitRootRender(renderer, root, ctx, ret, childValues, oldProps) {
396
407
  // element is a host or portal element
397
- if (root !== undefined) {
398
- renderer.arrange(Portal, root, ret.el.props, childValues, oldProps, wrap(ret.cached));
408
+ if (root != null) {
409
+ renderer.arrange(Portal, root, ret.el.props, childValues, oldProps, wrap(ret.cachedChildValues));
399
410
  flush(renderer, root);
400
411
  }
401
- ret.cached = unwrap(childValues);
412
+ ret.cachedChildValues = unwrap(childValues);
402
413
  if (root == null) {
403
414
  unmount(renderer, ret, ctx, ret);
404
415
  }
405
- return renderer.read(ret.cached);
416
+ return renderer.read(ret.cachedChildValues);
406
417
  }
407
- function diffChildren(renderer, root, host, ctx, scope, parent, children) {
418
+ function diffChildren(renderer, root, host, ctx, scope, parent, children, hydrationData) {
408
419
  const oldRetained = wrap(parent.children);
409
420
  const newRetained = [];
410
421
  const newChildren = arrayify(children);
@@ -413,14 +424,15 @@
413
424
  let childrenByKey;
414
425
  let seenKeys;
415
426
  let isAsync = false;
416
- let oi = 0, oldLength = oldRetained.length;
427
+ let hydrationBlock;
428
+ let oi = 0;
429
+ let oldLength = oldRetained.length;
417
430
  for (let ni = 0, newLength = newChildren.length; ni < newLength; ni++) {
418
- // We make sure we don’t access indices out of bounds to prevent
419
- // deoptimizations.
431
+ // length checks to prevent index out of bounds deoptimizations.
420
432
  let ret = oi >= oldLength ? undefined : oldRetained[oi];
421
433
  let child = narrow(newChildren[ni]);
422
434
  {
423
- // Aligning new children with old retainers
435
+ // aligning new children with old retainers
424
436
  let oldKey = typeof ret === "object" ? ret.el.key : undefined;
425
437
  let newKey = typeof child === "object" ? child.key : undefined;
426
438
  if (newKey !== undefined && seenKeys && seenKeys.has(newKey)) {
@@ -455,18 +467,19 @@
455
467
  // Updating
456
468
  let value;
457
469
  if (typeof child === "object") {
458
- if (typeof ret === "object" && child.static_) {
459
- ret.el = child;
460
- value = getInflightValue(ret);
461
- }
462
- else if (child.tag === Copy) {
470
+ if (child.tag === Copy) {
463
471
  value = getInflightValue(ret);
464
472
  }
465
473
  else {
466
474
  let oldProps;
475
+ let static_ = false;
467
476
  if (typeof ret === "object" && ret.el.tag === child.tag) {
468
477
  oldProps = ret.el.props;
469
478
  ret.el = child;
479
+ if (child.static_) {
480
+ value = getInflightValue(ret);
481
+ static_ = true;
482
+ }
470
483
  }
471
484
  else {
472
485
  if (typeof ret === "object") {
@@ -474,19 +487,28 @@
474
487
  }
475
488
  const fallback = ret;
476
489
  ret = new Retainer(child);
477
- ret.fallback = fallback;
490
+ ret.fallbackValue = fallback;
478
491
  }
479
- if (child.tag === Raw) {
480
- value = updateRaw(renderer, ret, scope, oldProps);
492
+ if (static_) ;
493
+ else if (child.tag === Raw) {
494
+ value = hydrationBlock
495
+ ? hydrationBlock.then(() => updateRaw(renderer, ret, scope, oldProps, hydrationData))
496
+ : updateRaw(renderer, ret, scope, oldProps, hydrationData);
481
497
  }
482
498
  else if (child.tag === Fragment) {
483
- value = updateFragment(renderer, root, host, ctx, scope, ret);
499
+ value = hydrationBlock
500
+ ? hydrationBlock.then(() => updateFragment(renderer, root, host, ctx, scope, ret, hydrationData))
501
+ : updateFragment(renderer, root, host, ctx, scope, ret, hydrationData);
484
502
  }
485
503
  else if (typeof child.tag === "function") {
486
- value = updateComponent(renderer, root, host, ctx, scope, ret, oldProps);
504
+ value = hydrationBlock
505
+ ? hydrationBlock.then(() => updateComponent(renderer, root, host, ctx, scope, ret, oldProps, hydrationData))
506
+ : updateComponent(renderer, root, host, ctx, scope, ret, oldProps, hydrationData);
487
507
  }
488
508
  else {
489
- value = updateHost(renderer, root, ctx, scope, ret, oldProps);
509
+ value = hydrationBlock
510
+ ? hydrationBlock.then(() => updateHost(renderer, root, ctx, scope, ret, oldProps, hydrationData))
511
+ : updateHost(renderer, root, ctx, scope, ret, oldProps, hydrationData);
490
512
  }
491
513
  }
492
514
  const ref = child.ref;
@@ -498,9 +520,14 @@
498
520
  return value;
499
521
  });
500
522
  }
523
+ if (hydrationData !== undefined) {
524
+ hydrationBlock = value;
525
+ }
501
526
  }
502
- else if (typeof ref === "function") {
503
- ref(renderer.read(value));
527
+ else {
528
+ if (typeof ref === "function") {
529
+ ref(renderer.read(value));
530
+ }
504
531
  }
505
532
  }
506
533
  else {
@@ -509,7 +536,7 @@
509
536
  (graveyard = graveyard || []).push(ret);
510
537
  }
511
538
  if (typeof child === "string") {
512
- value = ret = renderer.escape(child, scope);
539
+ value = ret = renderer.text(child, scope, hydrationData);
513
540
  }
514
541
  else {
515
542
  ret = undefined;
@@ -542,27 +569,29 @@
542
569
  childValues1,
543
570
  new Promise((resolve) => (onChildValues = resolve)),
544
571
  ]);
545
- if (parent.onCommit) {
546
- parent.onCommit(childValues1);
572
+ if (parent.onNextValues) {
573
+ parent.onNextValues(childValues1);
547
574
  }
548
- parent.onCommit = onChildValues;
575
+ parent.onNextValues = onChildValues;
549
576
  return childValues1.then((childValues) => {
550
- parent.inflight = parent.fallback = undefined;
577
+ parent.inflightValue = parent.fallbackValue = undefined;
551
578
  return normalize(childValues);
552
579
  });
553
580
  }
554
- if (graveyard) {
555
- for (let i = 0; i < graveyard.length; i++) {
556
- unmount(renderer, host, ctx, graveyard[i]);
581
+ else {
582
+ if (graveyard) {
583
+ for (let i = 0; i < graveyard.length; i++) {
584
+ unmount(renderer, host, ctx, graveyard[i]);
585
+ }
557
586
  }
587
+ if (parent.onNextValues) {
588
+ parent.onNextValues(values);
589
+ parent.onNextValues = undefined;
590
+ }
591
+ parent.inflightValue = parent.fallbackValue = undefined;
592
+ // We can assert there are no promises in the array because isAsync is false
593
+ return normalize(values);
558
594
  }
559
- if (parent.onCommit) {
560
- parent.onCommit(values);
561
- parent.onCommit = undefined;
562
- }
563
- parent.inflight = parent.fallback = undefined;
564
- // We can assert there are no promises in the array because isAsync is false
565
- return normalize(values);
566
595
  }
567
596
  function createChildrenByKey(children, offset) {
568
597
  const childrenByKey = new Map();
@@ -582,58 +611,72 @@
582
611
  if (ctx && ctx.f & IsUpdating && ctx.inflightValue) {
583
612
  return ctx.inflightValue;
584
613
  }
585
- else if (child.inflight) {
586
- return child.inflight;
614
+ else if (child.inflightValue) {
615
+ return child.inflightValue;
587
616
  }
588
617
  return getValue(child);
589
618
  }
590
- function updateRaw(renderer, ret, scope, oldProps) {
619
+ function updateRaw(renderer, ret, scope, oldProps, hydrationData) {
591
620
  const props = ret.el.props;
592
- if (typeof props.value === "string") {
593
- if (!oldProps || oldProps.value !== props.value) {
594
- ret.value = renderer.parse(props.value, scope);
595
- }
596
- }
597
- else {
598
- ret.value = props.value;
621
+ if (!oldProps || oldProps.value !== props.value) {
622
+ ret.value = renderer.raw(props.value, scope, hydrationData);
599
623
  }
600
624
  return ret.value;
601
625
  }
602
- function updateFragment(renderer, root, host, ctx, scope, ret) {
603
- const childValues = diffChildren(renderer, root, host, ctx, scope, ret, ret.el.props.children);
626
+ function updateFragment(renderer, root, host, ctx, scope, ret, hydrationData) {
627
+ const childValues = diffChildren(renderer, root, host, ctx, scope, ret, ret.el.props.children, hydrationData);
604
628
  if (isPromiseLike(childValues)) {
605
- ret.inflight = childValues.then((childValues) => unwrap(childValues));
606
- return ret.inflight;
629
+ ret.inflightValue = childValues.then((childValues) => unwrap(childValues));
630
+ return ret.inflightValue;
607
631
  }
608
632
  return unwrap(childValues);
609
633
  }
610
- function updateHost(renderer, root, ctx, scope, ret, oldProps) {
634
+ function updateHost(renderer, root, ctx, scope, ret, oldProps, hydrationData) {
611
635
  const el = ret.el;
612
636
  const tag = el.tag;
637
+ let hydrationValue;
613
638
  if (el.tag === Portal) {
614
639
  root = ret.value = el.props.root;
615
640
  }
616
- else if (!oldProps) {
617
- // We use the truthiness of oldProps to determine if this the first render.
618
- ret.value = renderer.create(tag, el.props, scope);
641
+ else {
642
+ if (hydrationData !== undefined) {
643
+ const value = hydrationData.children.shift();
644
+ hydrationValue = value;
645
+ }
619
646
  }
620
647
  scope = renderer.scope(scope, tag, el.props);
621
- const childValues = diffChildren(renderer, root, ret, ctx, scope, ret, ret.el.props.children);
648
+ let childHydrationData;
649
+ if (hydrationValue != null && typeof hydrationValue !== "string") {
650
+ childHydrationData = renderer.hydrate(tag, hydrationValue, el.props);
651
+ if (childHydrationData === undefined) {
652
+ hydrationValue = undefined;
653
+ }
654
+ }
655
+ const childValues = diffChildren(renderer, root, ret, ctx, scope, ret, ret.el.props.children, childHydrationData);
622
656
  if (isPromiseLike(childValues)) {
623
- ret.inflight = childValues.then((childValues) => commitHost(renderer, scope, ret, childValues, oldProps));
624
- return ret.inflight;
657
+ ret.inflightValue = childValues.then((childValues) => commitHost(renderer, scope, ret, childValues, oldProps, hydrationValue));
658
+ return ret.inflightValue;
625
659
  }
626
- return commitHost(renderer, scope, ret, childValues, oldProps);
660
+ return commitHost(renderer, scope, ret, childValues, oldProps, hydrationValue);
627
661
  }
628
- function commitHost(renderer, scope, ret, childValues, oldProps) {
662
+ function commitHost(renderer, scope, ret, childValues, oldProps, hydrationValue) {
629
663
  const tag = ret.el.tag;
630
- const value = ret.value;
664
+ let value = ret.value;
665
+ if (hydrationValue != null) {
666
+ value = ret.value = hydrationValue;
667
+ }
631
668
  let props = ret.el.props;
632
669
  let copied;
633
670
  if (tag !== Portal) {
671
+ if (value == null) {
672
+ // This assumes that renderer.create does not return nullish values.
673
+ value = ret.value = renderer.create(tag, props, scope);
674
+ }
634
675
  for (const propName in { ...oldProps, ...props }) {
635
676
  const propValue = props[propName];
636
677
  if (propValue === Copy) {
678
+ // TODO: The Copy tag doubles as a way to skip the patching of a prop.
679
+ // Not sure about this feature. Should probably be removed.
637
680
  (copied = copied || new Set()).add(propName);
638
681
  }
639
682
  else if (propName !== "children") {
@@ -648,8 +691,8 @@
648
691
  }
649
692
  ret.el = new Element(tag, props, ret.el.key, ret.el.ref);
650
693
  }
651
- renderer.arrange(tag, value, props, childValues, oldProps, wrap(ret.cached));
652
- ret.cached = unwrap(childValues);
694
+ renderer.arrange(tag, value, props, childValues, oldProps, wrap(ret.cachedChildValues));
695
+ ret.cachedChildValues = unwrap(childValues);
653
696
  if (tag === Portal) {
654
697
  flush(renderer, ret.value);
655
698
  return;
@@ -696,7 +739,7 @@
696
739
  }
697
740
  else if (ret.el.tag === Portal) {
698
741
  host = ret;
699
- renderer.arrange(Portal, host.value, host.el.props, [], host.el.props, wrap(host.cached));
742
+ renderer.arrange(Portal, host.value, host.el.props, [], host.el.props, wrap(host.cachedChildValues));
700
743
  flush(renderer, host.value);
701
744
  }
702
745
  else if (ret.el.tag !== Fragment) {
@@ -720,38 +763,43 @@
720
763
  }
721
764
  /*** CONTEXT FLAGS ***/
722
765
  /**
723
- * A flag which is set when the component is being updated by the parent and
724
- * cleared when the component has committed. Used to determine things like
725
- * whether the nearest host ancestor needs to be rearranged.
766
+ * A flag which is true when the component is initialized or updated by an
767
+ * ancestor component or the root render call.
768
+ *
769
+ * Used to determine things like whether the nearest host ancestor needs to be
770
+ * rearranged.
726
771
  */
727
772
  const IsUpdating = 1 << 0;
728
773
  /**
729
- * A flag which is set when the component function or generator is
730
- * synchronously executing. This flags is used to ensure that a component which
731
- * triggers a second update in the course of rendering does not cause a stack
732
- * overflow or a generator error.
774
+ * A flag which is true when the component is synchronously executing.
775
+ *
776
+ * Used to guard against components triggering stack overflow or generator error.
733
777
  */
734
- const IsExecuting = 1 << 1;
778
+ const IsSyncExecuting = 1 << 1;
735
779
  /**
736
- * A flag used to make sure multiple values are not pulled from context prop
737
- * iterators without a yield.
780
+ * A flag which is true when the component is in a for...of loop.
738
781
  */
739
- const IsIterating = 1 << 2;
782
+ const IsInForOfLoop = 1 << 2;
740
783
  /**
741
- * A flag used by async generator components in conjunction with the
742
- * onavailable (_oa) callback to mark whether new props can be pulled via the
743
- * context async iterator. See the Symbol.asyncIterator method and the
744
- * resumeCtxIterator function.
784
+ * A flag which is true when the component is in a for await...of loop.
785
+ */
786
+ const IsInForAwaitOfLoop = 1 << 3;
787
+ /**
788
+ * A flag which is true when the component starts the render loop but has not
789
+ * yielded yet.
790
+ *
791
+ * Used to make sure that components yield at least once per loop.
745
792
  */
746
- const IsAvailable = 1 << 3;
793
+ const NeedsToYield = 1 << 4;
747
794
  /**
748
- * A flag which is set when a generator components returns, i.e. the done
749
- * property on the iteration is set to true. Generator components will stick to
750
- * their last rendered value and ignore further updates.
795
+ * A flag used by async generator components in conjunction with the
796
+ * onAvailable callback to mark whether new props can be pulled via the context
797
+ * async iterator. See the Symbol.asyncIterator method and the
798
+ * resumeCtxIterator function.
751
799
  */
752
- const IsDone = 1 << 4;
800
+ const PropsAvailable = 1 << 5;
753
801
  /**
754
- * A flag which is set when a generator component errors.
802
+ * A flag which is set when a component errors.
755
803
  *
756
804
  * NOTE: This is mainly used to prevent some false positives in component
757
805
  * yields or returns undefined warnings. The reason we’re using this versus
@@ -759,28 +807,28 @@
759
807
  * sync generator child) where synchronous code causes a stack overflow error
760
808
  * in a non-deterministic way. Deeply disturbing stuff.
761
809
  */
762
- const IsErrored = 1 << 5;
810
+ const IsErrored = 1 << 6;
763
811
  /**
764
812
  * A flag which is set when the component is unmounted. Unmounted components
765
813
  * are no longer in the element tree and cannot refresh or rerender.
766
814
  */
767
- const IsUnmounted = 1 << 6;
815
+ const IsUnmounted = 1 << 7;
768
816
  /**
769
817
  * A flag which indicates that the component is a sync generator component.
770
818
  */
771
- const IsSyncGen = 1 << 7;
819
+ const IsSyncGen = 1 << 8;
772
820
  /**
773
821
  * A flag which indicates that the component is an async generator component.
774
822
  */
775
- const IsAsyncGen = 1 << 8;
823
+ const IsAsyncGen = 1 << 9;
776
824
  /**
777
825
  * A flag which is set while schedule callbacks are called.
778
826
  */
779
- const IsScheduling = 1 << 9;
827
+ const IsScheduling = 1 << 10;
780
828
  /**
781
829
  * A flag which is set when a schedule callback calls refresh.
782
830
  */
783
- const IsSchedulingRefresh = 1 << 10;
831
+ const IsSchedulingRefresh = 1 << 11;
784
832
  const provisionMaps = new WeakMap();
785
833
  const scheduleMap = new WeakMap();
786
834
  const cleanupMap = new WeakMap();
@@ -788,12 +836,12 @@
788
836
  const flushMaps = new WeakMap();
789
837
  /**
790
838
  * @internal
791
- * The internal class which holds all context data.
839
+ * The internal class which holds context data.
792
840
  */
793
841
  class ContextImpl {
794
842
  constructor(renderer, root, host, parent, scope, ret) {
795
843
  this.f = 0;
796
- this.ctx = new Context(this);
844
+ this.owner = new Context(this);
797
845
  this.renderer = renderer;
798
846
  this.root = root;
799
847
  this.host = host;
@@ -805,10 +853,11 @@
805
853
  this.inflightValue = undefined;
806
854
  this.enqueuedBlock = undefined;
807
855
  this.enqueuedValue = undefined;
808
- this.onAvailable = undefined;
856
+ this.onProps = undefined;
857
+ this.onPropsRequested = undefined;
809
858
  }
810
859
  }
811
- const $ContextImpl = Symbol.for("crank.ContextImpl");
860
+ const _ContextImpl = Symbol.for("crank.ContextImpl");
812
861
  /**
813
862
  * A class which is instantiated and passed to every component as its this
814
863
  * value. Contexts form a tree just like elements and all components in the
@@ -822,8 +871,10 @@
822
871
  * schedule and cleanup callbacks.
823
872
  */
824
873
  class Context {
874
+ // TODO: If we could make the constructor function take a nicer value, it
875
+ // would be useful for testing purposes.
825
876
  constructor(impl) {
826
- this[$ContextImpl] = impl;
877
+ this[_ContextImpl] = impl;
827
878
  }
828
879
  /**
829
880
  * The current props of the associated element.
@@ -833,7 +884,7 @@
833
884
  * plugins or utilities which wrap contexts.
834
885
  */
835
886
  get props() {
836
- return this[$ContextImpl].ret.el.props;
887
+ return this[_ContextImpl].ret.el.props;
837
888
  }
838
889
  // TODO: Should we rename this???
839
890
  /**
@@ -844,45 +895,64 @@
844
895
  * mainly for plugins or utilities which wrap contexts.
845
896
  */
846
897
  get value() {
847
- return this[$ContextImpl].renderer.read(getValue(this[$ContextImpl].ret));
898
+ return this[_ContextImpl].renderer.read(getValue(this[_ContextImpl].ret));
848
899
  }
849
900
  *[Symbol.iterator]() {
850
- const impl = this[$ContextImpl];
851
- while (!(impl.f & IsDone)) {
852
- if (impl.f & IsIterating) {
853
- throw new Error("Context iterated twice without a yield");
854
- }
855
- else if (impl.f & IsAsyncGen) {
856
- throw new Error("Use for await…of in async generator components");
901
+ const ctx = this[_ContextImpl];
902
+ try {
903
+ ctx.f |= IsInForOfLoop;
904
+ while (!(ctx.f & IsUnmounted)) {
905
+ if (ctx.f & NeedsToYield) {
906
+ throw new Error("Context iterated twice without a yield");
907
+ }
908
+ else {
909
+ ctx.f |= NeedsToYield;
910
+ }
911
+ yield ctx.ret.el.props;
857
912
  }
858
- impl.f |= IsIterating;
859
- yield impl.ret.el.props;
913
+ }
914
+ finally {
915
+ ctx.f &= ~IsInForOfLoop;
860
916
  }
861
917
  }
862
918
  async *[Symbol.asyncIterator]() {
863
- // We use a do while loop rather than a while loop to handle an edge case
864
- // where an async generator component is unmounted synchronously and
865
- // therefore “done” before it starts iterating over the context.
866
- const impl = this[$ContextImpl];
867
- do {
868
- if (impl.f & IsIterating) {
869
- throw new Error("Context iterated twice without a yield");
870
- }
871
- else if (impl.f & IsSyncGen) {
872
- throw new Error("Use for…of in sync generator components");
873
- }
874
- impl.f |= IsIterating;
875
- if (impl.f & IsAvailable) {
876
- impl.f &= ~IsAvailable;
877
- }
878
- else {
879
- await new Promise((resolve) => (impl.onAvailable = resolve));
880
- if (impl.f & IsDone) {
881
- break;
919
+ const ctx = this[_ContextImpl];
920
+ if (ctx.f & IsSyncGen) {
921
+ throw new Error("Use for...of in sync generator components");
922
+ }
923
+ try {
924
+ ctx.f |= IsInForAwaitOfLoop;
925
+ while (!(ctx.f & IsUnmounted)) {
926
+ if (ctx.f & NeedsToYield) {
927
+ throw new Error("Context iterated twice without a yield");
928
+ }
929
+ else {
930
+ ctx.f |= NeedsToYield;
931
+ }
932
+ if (ctx.f & PropsAvailable) {
933
+ ctx.f &= ~PropsAvailable;
934
+ yield ctx.ret.el.props;
935
+ }
936
+ else {
937
+ const props = await new Promise((resolve) => (ctx.onProps = resolve));
938
+ if (ctx.f & IsUnmounted) {
939
+ break;
940
+ }
941
+ yield props;
942
+ }
943
+ if (ctx.onPropsRequested) {
944
+ ctx.onPropsRequested();
945
+ ctx.onPropsRequested = undefined;
882
946
  }
883
947
  }
884
- yield impl.ret.el.props;
885
- } while (!(impl.f & IsDone));
948
+ }
949
+ finally {
950
+ ctx.f &= ~IsInForAwaitOfLoop;
951
+ if (ctx.onPropsRequested) {
952
+ ctx.onPropsRequested();
953
+ ctx.onPropsRequested = undefined;
954
+ }
955
+ }
886
956
  }
887
957
  /**
888
958
  * Re-executes a component.
@@ -897,32 +967,31 @@
897
967
  * async iterator to suspend.
898
968
  */
899
969
  refresh() {
900
- const impl = this[$ContextImpl];
901
- if (impl.f & IsUnmounted) {
970
+ const ctx = this[_ContextImpl];
971
+ if (ctx.f & IsUnmounted) {
902
972
  console.error("Component is unmounted");
903
- return impl.renderer.read(undefined);
973
+ return ctx.renderer.read(undefined);
904
974
  }
905
- else if (impl.f & IsExecuting) {
975
+ else if (ctx.f & IsSyncExecuting) {
906
976
  console.error("Component is already executing");
907
977
  return this.value;
908
978
  }
909
- resumeCtxIterator(impl);
910
- const value = runComponent(impl);
979
+ const value = enqueueComponentRun(ctx);
911
980
  if (isPromiseLike(value)) {
912
- return value.then((value) => impl.renderer.read(value));
981
+ return value.then((value) => ctx.renderer.read(value));
913
982
  }
914
- return impl.renderer.read(value);
983
+ return ctx.renderer.read(value);
915
984
  }
916
985
  /**
917
986
  * Registers a callback which fires when the component commits. Will only
918
987
  * fire once per callback and update.
919
988
  */
920
989
  schedule(callback) {
921
- const impl = this[$ContextImpl];
922
- let callbacks = scheduleMap.get(impl);
990
+ const ctx = this[_ContextImpl];
991
+ let callbacks = scheduleMap.get(ctx);
923
992
  if (!callbacks) {
924
993
  callbacks = new Set();
925
- scheduleMap.set(impl, callbacks);
994
+ scheduleMap.set(ctx, callbacks);
926
995
  }
927
996
  callbacks.add(callback);
928
997
  }
@@ -931,19 +1000,19 @@
931
1000
  * rendered into the root. Will only fire once per callback and render.
932
1001
  */
933
1002
  flush(callback) {
934
- const impl = this[$ContextImpl];
935
- if (typeof impl.root !== "object" || impl.root === null) {
1003
+ const ctx = this[_ContextImpl];
1004
+ if (typeof ctx.root !== "object" || ctx.root === null) {
936
1005
  return;
937
1006
  }
938
- let flushMap = flushMaps.get(impl.root);
1007
+ let flushMap = flushMaps.get(ctx.root);
939
1008
  if (!flushMap) {
940
1009
  flushMap = new Map();
941
- flushMaps.set(impl.root, flushMap);
1010
+ flushMaps.set(ctx.root, flushMap);
942
1011
  }
943
- let callbacks = flushMap.get(impl);
1012
+ let callbacks = flushMap.get(ctx);
944
1013
  if (!callbacks) {
945
1014
  callbacks = new Set();
946
- flushMap.set(impl, callbacks);
1015
+ flushMap.set(ctx, callbacks);
947
1016
  }
948
1017
  callbacks.add(callback);
949
1018
  }
@@ -952,45 +1021,45 @@
952
1021
  * fire once per callback.
953
1022
  */
954
1023
  cleanup(callback) {
955
- const impl = this[$ContextImpl];
956
- let callbacks = cleanupMap.get(impl);
1024
+ const ctx = this[_ContextImpl];
1025
+ let callbacks = cleanupMap.get(ctx);
957
1026
  if (!callbacks) {
958
1027
  callbacks = new Set();
959
- cleanupMap.set(impl, callbacks);
1028
+ cleanupMap.set(ctx, callbacks);
960
1029
  }
961
1030
  callbacks.add(callback);
962
1031
  }
963
1032
  consume(key) {
964
- for (let parent = this[$ContextImpl].parent; parent !== undefined; parent = parent.parent) {
965
- const provisions = provisionMaps.get(parent);
1033
+ for (let ctx = this[_ContextImpl].parent; ctx !== undefined; ctx = ctx.parent) {
1034
+ const provisions = provisionMaps.get(ctx);
966
1035
  if (provisions && provisions.has(key)) {
967
1036
  return provisions.get(key);
968
1037
  }
969
1038
  }
970
1039
  }
971
1040
  provide(key, value) {
972
- const impl = this[$ContextImpl];
973
- let provisions = provisionMaps.get(impl);
1041
+ const ctx = this[_ContextImpl];
1042
+ let provisions = provisionMaps.get(ctx);
974
1043
  if (!provisions) {
975
1044
  provisions = new Map();
976
- provisionMaps.set(impl, provisions);
1045
+ provisionMaps.set(ctx, provisions);
977
1046
  }
978
1047
  provisions.set(key, value);
979
1048
  }
980
1049
  addEventListener(type, listener, options) {
981
- const impl = this[$ContextImpl];
1050
+ const ctx = this[_ContextImpl];
982
1051
  let listeners;
983
1052
  if (!isListenerOrListenerObject(listener)) {
984
1053
  return;
985
1054
  }
986
1055
  else {
987
- const listeners1 = listenersMap.get(impl);
1056
+ const listeners1 = listenersMap.get(ctx);
988
1057
  if (listeners1) {
989
1058
  listeners = listeners1;
990
1059
  }
991
1060
  else {
992
1061
  listeners = [];
993
- listenersMap.set(impl, listeners);
1062
+ listenersMap.set(ctx, listeners);
994
1063
  }
995
1064
  }
996
1065
  options = normalizeListenerOptions(options);
@@ -1001,7 +1070,7 @@
1001
1070
  else {
1002
1071
  callback = listener;
1003
1072
  }
1004
- const record = { type, callback, listener, options };
1073
+ const record = { type, listener, callback, options };
1005
1074
  if (options.once) {
1006
1075
  record.callback = function () {
1007
1076
  const i = listeners.indexOf(record);
@@ -1018,15 +1087,15 @@
1018
1087
  }
1019
1088
  listeners.push(record);
1020
1089
  // TODO: is it possible to separate out the EventTarget delegation logic
1021
- for (const value of getChildValues(impl.ret)) {
1090
+ for (const value of getChildValues(ctx.ret)) {
1022
1091
  if (isEventTarget(value)) {
1023
1092
  value.addEventListener(record.type, record.callback, record.options);
1024
1093
  }
1025
1094
  }
1026
1095
  }
1027
1096
  removeEventListener(type, listener, options) {
1028
- const impl = this[$ContextImpl];
1029
- const listeners = listenersMap.get(impl);
1097
+ const ctx = this[_ContextImpl];
1098
+ const listeners = listenersMap.get(ctx);
1030
1099
  if (listeners == null || !isListenerOrListenerObject(listener)) {
1031
1100
  return;
1032
1101
  }
@@ -1040,16 +1109,16 @@
1040
1109
  const record = listeners[i];
1041
1110
  listeners.splice(i, 1);
1042
1111
  // TODO: is it possible to separate out the EventTarget delegation logic
1043
- for (const value of getChildValues(impl.ret)) {
1112
+ for (const value of getChildValues(ctx.ret)) {
1044
1113
  if (isEventTarget(value)) {
1045
1114
  value.removeEventListener(record.type, record.callback, record.options);
1046
1115
  }
1047
1116
  }
1048
1117
  }
1049
1118
  dispatchEvent(ev) {
1050
- const impl = this[$ContextImpl];
1119
+ const ctx = this[_ContextImpl];
1051
1120
  const path = [];
1052
- for (let parent = impl.parent; parent !== undefined; parent = parent.parent) {
1121
+ for (let parent = ctx.parent; parent !== undefined; parent = parent.parent) {
1053
1122
  path.push(parent);
1054
1123
  }
1055
1124
  // We patch the stopImmediatePropagation method because ev.cancelBubble
@@ -1061,7 +1130,7 @@
1061
1130
  immediateCancelBubble = true;
1062
1131
  return stopImmediatePropagation.call(ev);
1063
1132
  });
1064
- setEventProperty(ev, "target", impl.ctx);
1133
+ setEventProperty(ev, "target", ctx.owner);
1065
1134
  // The only possible errors in this block are errors thrown by callbacks,
1066
1135
  // and dispatchEvent will only log these errors rather than throwing
1067
1136
  // them. Therefore, we place all code in a try block, log errors in the
@@ -1070,18 +1139,21 @@
1070
1139
  // Each early return within the try block returns true because while the
1071
1140
  // return value is overridden in the finally block, TypeScript
1072
1141
  // (justifiably) does not recognize the unsafe return statement.
1073
- //
1074
- // TODO: Run all callbacks even if one of them errors
1075
1142
  try {
1076
1143
  setEventProperty(ev, "eventPhase", CAPTURING_PHASE);
1077
1144
  for (let i = path.length - 1; i >= 0; i--) {
1078
1145
  const target = path[i];
1079
1146
  const listeners = listenersMap.get(target);
1080
1147
  if (listeners) {
1081
- setEventProperty(ev, "currentTarget", target.ctx);
1148
+ setEventProperty(ev, "currentTarget", target.owner);
1082
1149
  for (const record of listeners) {
1083
1150
  if (record.type === ev.type && record.options.capture) {
1084
- record.callback.call(target.ctx, ev);
1151
+ try {
1152
+ record.callback.call(target.owner, ev);
1153
+ }
1154
+ catch (err) {
1155
+ console.error(err);
1156
+ }
1085
1157
  if (immediateCancelBubble) {
1086
1158
  return true;
1087
1159
  }
@@ -1093,13 +1165,25 @@
1093
1165
  }
1094
1166
  }
1095
1167
  {
1096
- const listeners = listenersMap.get(impl);
1168
+ setEventProperty(ev, "eventPhase", AT_TARGET);
1169
+ setEventProperty(ev, "currentTarget", ctx.owner);
1170
+ const propCallback = ctx.ret.el.props["on" + ev.type];
1171
+ if (propCallback != null) {
1172
+ propCallback(ev);
1173
+ if (immediateCancelBubble || ev.cancelBubble) {
1174
+ return true;
1175
+ }
1176
+ }
1177
+ const listeners = listenersMap.get(ctx);
1097
1178
  if (listeners) {
1098
- setEventProperty(ev, "eventPhase", AT_TARGET);
1099
- setEventProperty(ev, "currentTarget", impl.ctx);
1100
1179
  for (const record of listeners) {
1101
1180
  if (record.type === ev.type) {
1102
- record.callback.call(impl.ctx, ev);
1181
+ try {
1182
+ record.callback.call(ctx.owner, ev);
1183
+ }
1184
+ catch (err) {
1185
+ console.error(err);
1186
+ }
1103
1187
  if (immediateCancelBubble) {
1104
1188
  return true;
1105
1189
  }
@@ -1116,10 +1200,15 @@
1116
1200
  const target = path[i];
1117
1201
  const listeners = listenersMap.get(target);
1118
1202
  if (listeners) {
1119
- setEventProperty(ev, "currentTarget", target.ctx);
1203
+ setEventProperty(ev, "currentTarget", target.owner);
1120
1204
  for (const record of listeners) {
1121
1205
  if (record.type === ev.type && !record.options.capture) {
1122
- record.callback.call(target.ctx, ev);
1206
+ try {
1207
+ record.callback.call(target.owner, ev);
1208
+ }
1209
+ catch (err) {
1210
+ console.error(err);
1211
+ }
1123
1212
  if (immediateCancelBubble) {
1124
1213
  return true;
1125
1214
  }
@@ -1132,10 +1221,6 @@
1132
1221
  }
1133
1222
  }
1134
1223
  }
1135
- catch (err) {
1136
- // TODO: Use setTimeout to rethrow the error.
1137
- console.error(err);
1138
- }
1139
1224
  finally {
1140
1225
  setEventProperty(ev, "eventPhase", NONE);
1141
1226
  setEventProperty(ev, "currentTarget", null);
@@ -1153,42 +1238,47 @@
1153
1238
  }
1154
1239
  return false;
1155
1240
  }
1156
- function updateComponent(renderer, root, host, parent, scope, ret, oldProps) {
1241
+ function updateComponent(renderer, root, host, parent, scope, ret, oldProps, hydrationData) {
1157
1242
  let ctx;
1158
1243
  if (oldProps) {
1159
1244
  ctx = ret.ctx;
1160
- if (ctx.f & IsExecuting) {
1245
+ if (ctx.f & IsSyncExecuting) {
1161
1246
  console.error("Component is already executing");
1162
- return ret.cached;
1247
+ return ret.cachedChildValues;
1163
1248
  }
1164
1249
  }
1165
1250
  else {
1166
1251
  ctx = ret.ctx = new ContextImpl(renderer, root, host, parent, scope, ret);
1167
1252
  }
1168
1253
  ctx.f |= IsUpdating;
1169
- resumeCtxIterator(ctx);
1170
- return runComponent(ctx);
1254
+ return enqueueComponentRun(ctx, hydrationData);
1171
1255
  }
1172
- function updateComponentChildren(ctx, children) {
1173
- if (ctx.f & IsUnmounted || ctx.f & IsErrored) {
1256
+ function updateComponentChildren(ctx, children, hydrationData) {
1257
+ if (ctx.f & IsUnmounted) {
1258
+ return;
1259
+ }
1260
+ else if (ctx.f & IsErrored) {
1261
+ // This branch is necessary for some race conditions where this function is
1262
+ // called after iterator.throw() in async generator components.
1174
1263
  return;
1175
1264
  }
1176
1265
  else if (children === undefined) {
1177
1266
  console.error("A component has returned or yielded undefined. If this was intentional, return or yield null instead.");
1178
1267
  }
1179
1268
  let childValues;
1180
- // We set the isExecuting flag in case a child component dispatches an event
1181
- // which bubbles to this component and causes a synchronous refresh().
1182
- ctx.f |= IsExecuting;
1183
1269
  try {
1184
- childValues = diffChildren(ctx.renderer, ctx.root, ctx.host, ctx, ctx.scope, ctx.ret, narrow(children));
1270
+ // TODO: WAT
1271
+ // We set the isExecuting flag in case a child component dispatches an event
1272
+ // which bubbles to this component and causes a synchronous refresh().
1273
+ ctx.f |= IsSyncExecuting;
1274
+ childValues = diffChildren(ctx.renderer, ctx.root, ctx.host, ctx, ctx.scope, ctx.ret, narrow(children), hydrationData);
1185
1275
  }
1186
1276
  finally {
1187
- ctx.f &= ~IsExecuting;
1277
+ ctx.f &= ~IsSyncExecuting;
1188
1278
  }
1189
1279
  if (isPromiseLike(childValues)) {
1190
- ctx.ret.inflight = childValues.then((childValues) => commitComponent(ctx, childValues));
1191
- return ctx.ret.inflight;
1280
+ ctx.ret.inflightValue = childValues.then((childValues) => commitComponent(ctx, childValues));
1281
+ return ctx.ret.inflightValue;
1192
1282
  }
1193
1283
  return commitComponent(ctx, childValues);
1194
1284
  }
@@ -1208,8 +1298,8 @@
1208
1298
  }
1209
1299
  }
1210
1300
  }
1211
- const oldValues = wrap(ctx.ret.cached);
1212
- let value = (ctx.ret.cached = unwrap(values));
1301
+ const oldValues = wrap(ctx.ret.cachedChildValues);
1302
+ let value = (ctx.ret.cachedChildValues = unwrap(values));
1213
1303
  if (ctx.f & IsScheduling) {
1214
1304
  ctx.f |= IsSchedulingRefresh;
1215
1305
  }
@@ -1217,7 +1307,7 @@
1217
1307
  // If we’re not updating the component, which happens when components are
1218
1308
  // refreshed, or when async generator components iterate, we have to do a
1219
1309
  // little bit housekeeping when a component’s child values have changed.
1220
- if (!valuesEqual(oldValues, values)) {
1310
+ if (!arrayEqual(oldValues, values)) {
1221
1311
  const records = getListenerRecords(ctx.parent, ctx.host);
1222
1312
  if (records.length) {
1223
1313
  for (let i = 0; i < values.length; i++) {
@@ -1232,7 +1322,7 @@
1232
1322
  }
1233
1323
  // rearranging the nearest ancestor host element
1234
1324
  const host = ctx.host;
1235
- const oldHostValues = wrap(host.cached);
1325
+ const oldHostValues = wrap(host.cachedChildValues);
1236
1326
  invalidate(ctx, host);
1237
1327
  const hostValues = getChildValues(host);
1238
1328
  ctx.renderer.arrange(host.el.tag, host.value, host.el.props, hostValues,
@@ -1261,50 +1351,79 @@
1261
1351
  }
1262
1352
  function invalidate(ctx, host) {
1263
1353
  for (let parent = ctx.parent; parent !== undefined && parent.host === host; parent = parent.parent) {
1264
- parent.ret.cached = undefined;
1354
+ parent.ret.cachedChildValues = undefined;
1265
1355
  }
1266
- host.cached = undefined;
1356
+ host.cachedChildValues = undefined;
1267
1357
  }
1268
- function valuesEqual(values1, values2) {
1269
- if (values1.length !== values2.length) {
1358
+ function arrayEqual(arr1, arr2) {
1359
+ if (arr1.length !== arr2.length) {
1270
1360
  return false;
1271
1361
  }
1272
- for (let i = 0; i < values1.length; i++) {
1273
- const value1 = values1[i];
1274
- const value2 = values2[i];
1362
+ for (let i = 0; i < arr1.length; i++) {
1363
+ const value1 = arr1[i];
1364
+ const value2 = arr2[i];
1275
1365
  if (value1 !== value2) {
1276
1366
  return false;
1277
1367
  }
1278
1368
  }
1279
1369
  return true;
1280
1370
  }
1281
- /**
1282
- * Enqueues and executes the component associated with the context.
1283
- *
1284
- * The functions stepComponent and runComponent work together
1285
- * to implement the async queueing behavior of components. The runComponent
1286
- * function calls the stepComponent function, which returns two results in a
1287
- * tuple. The first result, called the “block,” is a possible promise which
1288
- * represents the duration for which the component is blocked from accepting
1289
- * new updates. The second result, called the “value,” is the actual result of
1290
- * the update. The runComponent function caches block/value from the
1291
- * stepComponent function on the context, according to whether the component
1292
- * blocks. The “inflight” block/value properties are the currently executing
1293
- * update, and the “enqueued” block/value properties represent an enqueued next
1294
- * stepComponent. Enqueued steps are dequeued every time the current block
1295
- * promise settles.
1296
- */
1297
- function runComponent(ctx) {
1298
- if (!ctx.inflightBlock) {
1371
+ /** Enqueues and executes the component associated with the context. */
1372
+ function enqueueComponentRun(ctx, hydrationData) {
1373
+ if (ctx.f & IsAsyncGen && !(ctx.f & IsInForOfLoop)) {
1374
+ if (hydrationData !== undefined) {
1375
+ throw new Error("Hydration error");
1376
+ }
1377
+ // This branch will run for non-initial renders of async generator
1378
+ // components when they are not in for...of loops. When in a for...of loop,
1379
+ // async generator components will behave normally.
1380
+ //
1381
+ // Async gen componennts can be in one of three states:
1382
+ //
1383
+ // 1. propsAvailable flag is true: "available"
1384
+ //
1385
+ // The component is suspended somewhere in the loop. When the component
1386
+ // reaches the bottom of the loop, it will run again with the next props.
1387
+ //
1388
+ // 2. onAvailable callback is defined: "suspended"
1389
+ //
1390
+ // The component has suspended at the bottom of the loop and is waiting
1391
+ // for new props.
1392
+ //
1393
+ // 3. neither 1 or 2: "Running"
1394
+ //
1395
+ // The component is suspended somewhere in the loop. When the component
1396
+ // reaches the bottom of the loop, it will suspend.
1397
+ //
1398
+ // Components will never be both available and suspended at
1399
+ // the same time.
1400
+ //
1401
+ // If the component is at the loop bottom, this means that the next value
1402
+ // produced by the component will have the most up to date props, so we can
1403
+ // simply return the current inflight value. Otherwise, we have to wait for
1404
+ // the bottom of the loop to be reached before returning the inflight
1405
+ // value.
1406
+ const isAtLoopbottom = ctx.f & IsInForAwaitOfLoop && !ctx.onProps;
1407
+ resumePropsIterator(ctx);
1408
+ if (isAtLoopbottom) {
1409
+ if (ctx.inflightBlock == null) {
1410
+ ctx.inflightBlock = new Promise((resolve) => (ctx.onPropsRequested = resolve));
1411
+ }
1412
+ return ctx.inflightBlock.then(() => {
1413
+ ctx.inflightBlock = undefined;
1414
+ return ctx.inflightValue;
1415
+ });
1416
+ }
1417
+ return ctx.inflightValue;
1418
+ }
1419
+ else if (!ctx.inflightBlock) {
1299
1420
  try {
1300
- const [block, value] = stepComponent(ctx);
1421
+ const [block, value] = runComponent(ctx, hydrationData);
1301
1422
  if (block) {
1302
1423
  ctx.inflightBlock = block
1303
- .catch((err) => {
1304
- if (!(ctx.f & IsUpdating)) {
1305
- return propagateError(ctx.parent, err);
1306
- }
1307
- })
1424
+ // TODO: there is some fuckery going on here related to async
1425
+ // generator components resuming when they’re meant to be returned.
1426
+ .then((v) => v)
1308
1427
  .finally(() => advanceComponent(ctx));
1309
1428
  // stepComponent will only return a block if the value is asynchronous
1310
1429
  ctx.inflightValue = value;
@@ -1318,38 +1437,46 @@
1318
1437
  throw err;
1319
1438
  }
1320
1439
  }
1321
- else if (ctx.f & IsAsyncGen) {
1322
- return ctx.inflightValue;
1323
- }
1324
1440
  else if (!ctx.enqueuedBlock) {
1325
- let resolve;
1326
- ctx.enqueuedBlock = ctx.inflightBlock
1327
- .then(() => {
1441
+ if (hydrationData !== undefined) {
1442
+ throw new Error("Hydration error");
1443
+ }
1444
+ // We need to assign enqueuedBlock and enqueuedValue synchronously, hence
1445
+ // the Promise constructor call here.
1446
+ let resolveEnqueuedBlock;
1447
+ ctx.enqueuedBlock = new Promise((resolve) => (resolveEnqueuedBlock = resolve));
1448
+ ctx.enqueuedValue = ctx.inflightBlock.then(() => {
1328
1449
  try {
1329
- const [block, value] = stepComponent(ctx);
1330
- resolve(value);
1450
+ const [block, value] = runComponent(ctx);
1331
1451
  if (block) {
1332
- return block.catch((err) => {
1333
- if (!(ctx.f & IsUpdating)) {
1334
- return propagateError(ctx.parent, err);
1335
- }
1336
- });
1452
+ resolveEnqueuedBlock(block.finally(() => advanceComponent(ctx)));
1337
1453
  }
1454
+ return value;
1338
1455
  }
1339
1456
  catch (err) {
1340
1457
  if (!(ctx.f & IsUpdating)) {
1341
1458
  return propagateError(ctx.parent, err);
1342
1459
  }
1460
+ throw err;
1343
1461
  }
1344
- })
1345
- .finally(() => advanceComponent(ctx));
1346
- ctx.enqueuedValue = new Promise((resolve1) => (resolve = resolve1));
1462
+ });
1347
1463
  }
1348
1464
  return ctx.enqueuedValue;
1349
1465
  }
1466
+ /** Called when the inflight block promise settles. */
1467
+ function advanceComponent(ctx) {
1468
+ if (ctx.f & IsAsyncGen && !(ctx.f & IsInForOfLoop)) {
1469
+ return;
1470
+ }
1471
+ ctx.inflightBlock = ctx.enqueuedBlock;
1472
+ ctx.inflightValue = ctx.enqueuedValue;
1473
+ ctx.enqueuedBlock = undefined;
1474
+ ctx.enqueuedValue = undefined;
1475
+ }
1350
1476
  /**
1351
1477
  * This function is responsible for executing the component and handling all
1352
- * the different component types.
1478
+ * the different component types. We cannot identify whether a component is a
1479
+ * generator or async without calling it and inspecting the return value.
1353
1480
  *
1354
1481
  * @returns {[block, value]} A tuple where
1355
1482
  * block - A possible promise which represents the duration during which the
@@ -1364,25 +1491,23 @@
1364
1491
  * - Sync generator components block while any children are executing, because
1365
1492
  * they are expected to only resume when they’ve actually rendered.
1366
1493
  */
1367
- function stepComponent(ctx) {
1494
+ function runComponent(ctx, hydrationData) {
1368
1495
  const ret = ctx.ret;
1369
- if (ctx.f & IsDone) {
1370
- return [undefined, getValue(ret)];
1371
- }
1372
1496
  const initial = !ctx.iterator;
1373
1497
  if (initial) {
1374
- ctx.f |= IsExecuting;
1498
+ resumePropsIterator(ctx);
1499
+ ctx.f |= IsSyncExecuting;
1375
1500
  clearEventListeners(ctx);
1376
1501
  let result;
1377
1502
  try {
1378
- result = ret.el.tag.call(ctx.ctx, ret.el.props);
1503
+ result = ret.el.tag.call(ctx.owner, ret.el.props);
1379
1504
  }
1380
1505
  catch (err) {
1381
1506
  ctx.f |= IsErrored;
1382
1507
  throw err;
1383
1508
  }
1384
1509
  finally {
1385
- ctx.f &= ~IsExecuting;
1510
+ ctx.f &= ~IsSyncExecuting;
1386
1511
  }
1387
1512
  if (isIteratorLike(result)) {
1388
1513
  ctx.iterator = result;
@@ -1390,124 +1515,241 @@
1390
1515
  else if (isPromiseLike(result)) {
1391
1516
  // async function component
1392
1517
  const result1 = result instanceof Promise ? result : Promise.resolve(result);
1393
- const value = result1.then((result) => updateComponentChildren(ctx, result), (err) => {
1518
+ const value = result1.then((result) => updateComponentChildren(ctx, result, hydrationData), (err) => {
1394
1519
  ctx.f |= IsErrored;
1395
1520
  throw err;
1396
1521
  });
1397
- return [result1, value];
1522
+ return [result1.catch(NOOP), value];
1398
1523
  }
1399
1524
  else {
1400
1525
  // sync function component
1401
- return [undefined, updateComponentChildren(ctx, result)];
1526
+ return [
1527
+ undefined,
1528
+ updateComponentChildren(ctx, result, hydrationData),
1529
+ ];
1402
1530
  }
1403
1531
  }
1404
- let oldValue;
1405
- if (initial) {
1406
- // The argument passed to the first call to next is ignored.
1407
- oldValue = undefined;
1408
- }
1409
- else if (ctx.ret.inflight) {
1410
- // The value passed back into the generator as the argument to the next
1411
- // method is a promise if an async generator component has async children.
1412
- // Sync generator components only resume when their children have fulfilled
1413
- // so the element’s inflight child values will never be defined.
1414
- oldValue = ctx.ret.inflight.then((value) => ctx.renderer.read(value), () => ctx.renderer.read(undefined));
1415
- }
1416
- else {
1417
- oldValue = ctx.renderer.read(getValue(ret));
1532
+ else if (hydrationData !== undefined) {
1533
+ throw new Error("Hydration error");
1418
1534
  }
1419
1535
  let iteration;
1420
- ctx.f |= IsExecuting;
1421
- try {
1422
- iteration = ctx.iterator.next(oldValue);
1423
- }
1424
- catch (err) {
1425
- ctx.f |= IsDone | IsErrored;
1426
- throw err;
1536
+ if (initial) {
1537
+ try {
1538
+ ctx.f |= IsSyncExecuting;
1539
+ iteration = ctx.iterator.next();
1540
+ }
1541
+ catch (err) {
1542
+ ctx.f |= IsErrored;
1543
+ throw err;
1544
+ }
1545
+ finally {
1546
+ ctx.f &= ~IsSyncExecuting;
1547
+ }
1548
+ if (isPromiseLike(iteration)) {
1549
+ ctx.f |= IsAsyncGen;
1550
+ }
1551
+ else {
1552
+ ctx.f |= IsSyncGen;
1553
+ }
1427
1554
  }
1428
- finally {
1429
- ctx.f &= ~IsExecuting;
1555
+ if (ctx.f & IsSyncGen) {
1556
+ ctx.f &= ~NeedsToYield;
1557
+ // sync generator component
1558
+ if (!initial) {
1559
+ try {
1560
+ ctx.f |= IsSyncExecuting;
1561
+ iteration = ctx.iterator.next(ctx.renderer.read(getValue(ret)));
1562
+ }
1563
+ catch (err) {
1564
+ ctx.f |= IsErrored;
1565
+ throw err;
1566
+ }
1567
+ finally {
1568
+ ctx.f &= ~IsSyncExecuting;
1569
+ }
1570
+ }
1571
+ if (isPromiseLike(iteration)) {
1572
+ throw new Error("Mixed generator component");
1573
+ }
1574
+ if (iteration.done) {
1575
+ ctx.f &= ~IsSyncGen;
1576
+ ctx.iterator = undefined;
1577
+ }
1578
+ let value;
1579
+ try {
1580
+ value = updateComponentChildren(ctx,
1581
+ // Children can be void so we eliminate that here
1582
+ iteration.value, hydrationData);
1583
+ if (isPromiseLike(value)) {
1584
+ value = value.catch((err) => handleChildError(ctx, err));
1585
+ }
1586
+ }
1587
+ catch (err) {
1588
+ value = handleChildError(ctx, err);
1589
+ }
1590
+ const block = isPromiseLike(value) ? value.catch(NOOP) : undefined;
1591
+ return [block, value];
1430
1592
  }
1431
- if (isPromiseLike(iteration)) {
1432
- // async generator component
1433
- if (initial) {
1434
- ctx.f |= IsAsyncGen;
1593
+ else if (ctx.f & IsInForOfLoop) {
1594
+ // TODO: does this need to be done async?
1595
+ ctx.f &= ~NeedsToYield;
1596
+ // we are in a for...of loop for async generator
1597
+ if (!initial) {
1598
+ try {
1599
+ ctx.f |= IsSyncExecuting;
1600
+ iteration = ctx.iterator.next(ctx.renderer.read(getValue(ret)));
1601
+ }
1602
+ catch (err) {
1603
+ ctx.f |= IsErrored;
1604
+ throw err;
1605
+ }
1606
+ finally {
1607
+ ctx.f &= ~IsSyncExecuting;
1608
+ }
1435
1609
  }
1610
+ if (!isPromiseLike(iteration)) {
1611
+ throw new Error("Mixed generator component");
1612
+ }
1613
+ const block = iteration.catch(NOOP);
1436
1614
  const value = iteration.then((iteration) => {
1437
- if (!(ctx.f & IsIterating)) {
1438
- ctx.f &= ~IsAvailable;
1439
- }
1440
- ctx.f &= ~IsIterating;
1441
- if (iteration.done) {
1442
- ctx.f |= IsDone;
1615
+ let value;
1616
+ if (!(ctx.f & IsInForOfLoop)) {
1617
+ runAsyncGenComponent(ctx, Promise.resolve(iteration), hydrationData);
1443
1618
  }
1444
1619
  try {
1445
- const value = updateComponentChildren(ctx, iteration.value);
1620
+ value = updateComponentChildren(ctx,
1621
+ // Children can be void so we eliminate that here
1622
+ iteration.value, hydrationData);
1446
1623
  if (isPromiseLike(value)) {
1447
- return value.catch((err) => handleChildError(ctx, err));
1624
+ value = value.catch((err) => handleChildError(ctx, err));
1448
1625
  }
1449
- return value;
1450
1626
  }
1451
1627
  catch (err) {
1452
- return handleChildError(ctx, err);
1628
+ value = handleChildError(ctx, err);
1453
1629
  }
1630
+ return value;
1454
1631
  }, (err) => {
1455
- ctx.f |= IsDone | IsErrored;
1632
+ ctx.f |= IsErrored;
1456
1633
  throw err;
1457
1634
  });
1458
- return [iteration, value];
1635
+ return [block, value];
1459
1636
  }
1460
- // sync generator component
1461
- if (initial) {
1462
- ctx.f |= IsSyncGen;
1463
- }
1464
- ctx.f &= ~IsIterating;
1465
- if (iteration.done) {
1466
- ctx.f |= IsDone;
1637
+ else {
1638
+ runAsyncGenComponent(ctx, iteration, hydrationData);
1639
+ // async generator component
1640
+ return [ctx.inflightBlock, ctx.inflightValue];
1467
1641
  }
1468
- let value;
1642
+ }
1643
+ async function runAsyncGenComponent(ctx, iterationP, hydrationData) {
1644
+ let done = false;
1469
1645
  try {
1470
- value = updateComponentChildren(ctx, iteration.value);
1471
- if (isPromiseLike(value)) {
1472
- value = value.catch((err) => handleChildError(ctx, err));
1646
+ while (!done) {
1647
+ if (ctx.f & IsInForOfLoop) {
1648
+ break;
1649
+ }
1650
+ // inflightValue must be set synchronously.
1651
+ let onValue;
1652
+ ctx.inflightValue = new Promise((resolve) => (onValue = resolve));
1653
+ if (ctx.f & IsUpdating) {
1654
+ // We should not swallow unhandled promise rejections if the component is
1655
+ // updating independently.
1656
+ // TODO: Does this handle this.refresh() calls?
1657
+ ctx.inflightValue.catch(NOOP);
1658
+ }
1659
+ let iteration;
1660
+ try {
1661
+ iteration = await iterationP;
1662
+ }
1663
+ catch (err) {
1664
+ done = true;
1665
+ ctx.f |= IsErrored;
1666
+ onValue(Promise.reject(err));
1667
+ break;
1668
+ }
1669
+ finally {
1670
+ ctx.f &= ~NeedsToYield;
1671
+ if (!(ctx.f & IsInForAwaitOfLoop)) {
1672
+ ctx.f &= ~PropsAvailable;
1673
+ }
1674
+ }
1675
+ done = !!iteration.done;
1676
+ let value;
1677
+ try {
1678
+ value = updateComponentChildren(ctx, iteration.value, hydrationData);
1679
+ hydrationData = undefined;
1680
+ if (isPromiseLike(value)) {
1681
+ value = value.catch((err) => handleChildError(ctx, err));
1682
+ }
1683
+ }
1684
+ catch (err) {
1685
+ // Do we need to catch potential errors here in the case of unhandled
1686
+ // promise rejections?
1687
+ value = handleChildError(ctx, err);
1688
+ }
1689
+ finally {
1690
+ onValue(value);
1691
+ }
1692
+ // TODO: this can be done more elegantly
1693
+ let oldValue;
1694
+ if (ctx.ret.inflightValue) {
1695
+ // The value passed back into the generator as the argument to the next
1696
+ // method is a promise if an async generator component has async
1697
+ // children. Sync generator components only resume when their children
1698
+ // have fulfilled so the element’s inflight child values will never be
1699
+ // defined.
1700
+ oldValue = ctx.ret.inflightValue.then((value) => ctx.renderer.read(value), () => ctx.renderer.read(undefined));
1701
+ }
1702
+ else {
1703
+ oldValue = ctx.renderer.read(getValue(ctx.ret));
1704
+ }
1705
+ if (ctx.f & IsUnmounted) {
1706
+ if (ctx.f & IsInForAwaitOfLoop) {
1707
+ try {
1708
+ ctx.f |= IsSyncExecuting;
1709
+ iterationP = ctx.iterator.next(oldValue);
1710
+ }
1711
+ finally {
1712
+ ctx.f &= ~IsSyncExecuting;
1713
+ }
1714
+ }
1715
+ else {
1716
+ returnComponent(ctx);
1717
+ break;
1718
+ }
1719
+ }
1720
+ else if (!done && !(ctx.f & IsInForOfLoop)) {
1721
+ try {
1722
+ ctx.f |= IsSyncExecuting;
1723
+ iterationP = ctx.iterator.next(oldValue);
1724
+ }
1725
+ finally {
1726
+ ctx.f &= ~IsSyncExecuting;
1727
+ }
1728
+ }
1473
1729
  }
1474
1730
  }
1475
- catch (err) {
1476
- value = handleChildError(ctx, err);
1477
- }
1478
- if (isPromiseLike(value)) {
1479
- return [value.catch(NOOP), value];
1480
- }
1481
- return [undefined, value];
1482
- }
1483
- /**
1484
- * Called when the inflight block promise settles.
1485
- */
1486
- function advanceComponent(ctx) {
1487
- ctx.inflightBlock = ctx.enqueuedBlock;
1488
- ctx.inflightValue = ctx.enqueuedValue;
1489
- ctx.enqueuedBlock = undefined;
1490
- ctx.enqueuedValue = undefined;
1491
- if (ctx.f & IsAsyncGen && !(ctx.f & IsDone) && !(ctx.f & IsUnmounted)) {
1492
- runComponent(ctx);
1731
+ finally {
1732
+ if (done) {
1733
+ ctx.f &= ~IsAsyncGen;
1734
+ ctx.iterator = undefined;
1735
+ }
1493
1736
  }
1494
1737
  }
1495
1738
  /**
1496
- * Called to make props available to the props async iterator for async
1497
- * generator components.
1739
+ * Called to resume the props async iterator for async generator components.
1498
1740
  */
1499
- function resumeCtxIterator(ctx) {
1500
- if (ctx.onAvailable) {
1501
- ctx.onAvailable();
1502
- ctx.onAvailable = undefined;
1741
+ function resumePropsIterator(ctx) {
1742
+ if (ctx.onProps) {
1743
+ ctx.onProps(ctx.ret.el.props);
1744
+ ctx.onProps = undefined;
1745
+ ctx.f &= ~PropsAvailable;
1503
1746
  }
1504
1747
  else {
1505
- ctx.f |= IsAvailable;
1748
+ ctx.f |= PropsAvailable;
1506
1749
  }
1507
1750
  }
1508
1751
  // TODO: async unmounting
1509
1752
  function unmountComponent(ctx) {
1510
- ctx.f |= IsUnmounted;
1511
1753
  clearEventListeners(ctx);
1512
1754
  const callbacks = cleanupMap.get(ctx);
1513
1755
  if (callbacks) {
@@ -1517,23 +1759,71 @@
1517
1759
  callback(value);
1518
1760
  }
1519
1761
  }
1520
- if (!(ctx.f & IsDone)) {
1521
- ctx.f |= IsDone;
1522
- resumeCtxIterator(ctx);
1523
- if (ctx.iterator && typeof ctx.iterator.return === "function") {
1524
- ctx.f |= IsExecuting;
1525
- try {
1526
- const iteration = ctx.iterator.return();
1527
- if (isPromiseLike(iteration)) {
1528
- iteration.catch((err) => propagateError(ctx.parent, err));
1762
+ ctx.f |= IsUnmounted;
1763
+ if (ctx.iterator) {
1764
+ if (ctx.f & IsSyncGen) {
1765
+ let value;
1766
+ if (ctx.f & IsInForOfLoop) {
1767
+ value = enqueueComponentRun(ctx);
1768
+ }
1769
+ if (isPromiseLike(value)) {
1770
+ value.then(() => {
1771
+ if (ctx.f & IsInForOfLoop) {
1772
+ unmountComponent(ctx);
1773
+ }
1774
+ else {
1775
+ returnComponent(ctx);
1776
+ }
1777
+ }, (err) => {
1778
+ propagateError(ctx.parent, err);
1779
+ });
1780
+ }
1781
+ else {
1782
+ if (ctx.f & IsInForOfLoop) {
1783
+ unmountComponent(ctx);
1784
+ }
1785
+ else {
1786
+ returnComponent(ctx);
1529
1787
  }
1530
1788
  }
1531
- finally {
1532
- ctx.f &= ~IsExecuting;
1789
+ }
1790
+ else if (ctx.f & IsAsyncGen) {
1791
+ if (ctx.f & IsInForOfLoop) {
1792
+ const value = enqueueComponentRun(ctx);
1793
+ value.then(() => {
1794
+ if (ctx.f & IsInForOfLoop) {
1795
+ unmountComponent(ctx);
1796
+ }
1797
+ else {
1798
+ returnComponent(ctx);
1799
+ }
1800
+ }, (err) => {
1801
+ propagateError(ctx.parent, err);
1802
+ });
1803
+ }
1804
+ else {
1805
+ // The logic for unmounting async generator components is in the
1806
+ // runAsyncGenComponent function.
1807
+ resumePropsIterator(ctx);
1533
1808
  }
1534
1809
  }
1535
1810
  }
1536
1811
  }
1812
+ function returnComponent(ctx) {
1813
+ resumePropsIterator(ctx);
1814
+ if (ctx.iterator && typeof ctx.iterator.return === "function") {
1815
+ try {
1816
+ ctx.f |= IsSyncExecuting;
1817
+ const iteration = ctx.iterator.return();
1818
+ if (isPromiseLike(iteration)) {
1819
+ iteration.catch((err) => propagateError(ctx.parent, err));
1820
+ }
1821
+ }
1822
+ finally {
1823
+ ctx.f &= ~IsSyncExecuting;
1824
+ }
1825
+ }
1826
+ }
1537
1827
  /*** EVENT TARGET UTILITIES ***/
1538
1828
  // EVENT PHASE CONSTANTS
1539
1829
  // https://developer.mozilla.org/en-US/docs/Web/API/Event/eventPhase
@@ -1602,39 +1892,39 @@
1602
1892
  }
1603
1893
  }
1604
1894
  /*** ERROR HANDLING UTILITIES ***/
1605
- // TODO: generator components which throw errors should be recoverable
1606
1895
  function handleChildError(ctx, err) {
1607
- if (ctx.f & IsDone ||
1608
- !ctx.iterator ||
1609
- typeof ctx.iterator.throw !== "function") {
1896
+ if (!ctx.iterator || typeof ctx.iterator.throw !== "function") {
1610
1897
  throw err;
1611
1898
  }
1612
- resumeCtxIterator(ctx);
1899
+ resumePropsIterator(ctx);
1613
1900
  let iteration;
1614
1901
  try {
1615
- ctx.f |= IsExecuting;
1902
+ ctx.f |= IsSyncExecuting;
1616
1903
  iteration = ctx.iterator.throw(err);
1617
1904
  }
1618
1905
  catch (err) {
1619
- ctx.f |= IsDone | IsErrored;
1906
+ ctx.f |= IsErrored;
1620
1907
  throw err;
1621
1908
  }
1622
1909
  finally {
1623
- ctx.f &= ~IsExecuting;
1910
+ ctx.f &= ~IsSyncExecuting;
1624
1911
  }
1625
1912
  if (isPromiseLike(iteration)) {
1626
1913
  return iteration.then((iteration) => {
1627
1914
  if (iteration.done) {
1628
- ctx.f |= IsDone;
1915
+ ctx.f &= ~IsAsyncGen;
1916
+ ctx.iterator = undefined;
1629
1917
  }
1630
1918
  return updateComponentChildren(ctx, iteration.value);
1631
1919
  }, (err) => {
1632
- ctx.f |= IsDone | IsErrored;
1920
+ ctx.f |= IsErrored;
1633
1921
  throw err;
1634
1922
  });
1635
1923
  }
1636
1924
  if (iteration.done) {
1637
- ctx.f |= IsDone;
1925
+ ctx.f &= ~IsSyncGen;
1926
+ ctx.f &= ~IsAsyncGen;
1927
+ ctx.iterator = undefined;
1638
1928
  }
1639
1929
  return updateComponentChildren(ctx, iteration.value);
1640
1930
  }
@@ -1657,44 +1947,58 @@
1657
1947
 
1658
1948
  const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
1659
1949
  const impl$1 = {
1660
- parse(text) {
1661
- if (typeof document.createRange === "function") {
1662
- const fragment = document.createRange().createContextualFragment(text);
1663
- return Array.from(fragment.childNodes);
1664
- }
1665
- else {
1666
- const childNodes = new DOMParser().parseFromString(text, "text/html").body
1667
- .childNodes;
1668
- return Array.from(childNodes);
1669
- }
1670
- },
1671
- scope(scope, tag) {
1950
+ scope(xmlns, tag) {
1672
1951
  // TODO: Should we handle xmlns???
1673
1952
  switch (tag) {
1674
1953
  case Portal:
1675
1954
  case "foreignObject":
1676
- return undefined;
1955
+ xmlns = undefined;
1956
+ break;
1677
1957
  case "svg":
1678
- return SVG_NAMESPACE;
1679
- default:
1680
- return scope;
1958
+ xmlns = SVG_NAMESPACE;
1959
+ break;
1681
1960
  }
1961
+ return xmlns;
1682
1962
  },
1683
- create(tag, _props, ns) {
1963
+ create(tag, _props, xmlns) {
1684
1964
  if (typeof tag !== "string") {
1685
1965
  throw new Error(`Unknown tag: ${tag.toString()}`);
1686
1966
  }
1687
1967
  else if (tag.toLowerCase() === "svg") {
1688
- ns = SVG_NAMESPACE;
1968
+ xmlns = SVG_NAMESPACE;
1969
+ }
1970
+ return xmlns
1971
+ ? document.createElementNS(xmlns, tag)
1972
+ : document.createElement(tag);
1973
+ },
1974
+ hydrate(tag, node, props) {
1975
+ if (typeof tag !== "string" && tag !== Portal) {
1976
+ throw new Error(`Unknown tag: ${tag.toString()}`);
1689
1977
  }
1690
- return ns ? document.createElementNS(ns, tag) : document.createElement(tag);
1978
+ if (typeof tag === "string" &&
1979
+ tag.toUpperCase() !== node.tagName) {
1980
+ console.error(`Expected <${tag}> while hydrating but found:`, node);
1981
+ return undefined;
1982
+ }
1983
+ const children = [];
1984
+ for (let i = 0; i < node.childNodes.length; i++) {
1985
+ const child = node.childNodes[i];
1986
+ if (child.nodeType === Node.TEXT_NODE) {
1987
+ children.push(child.data);
1988
+ }
1989
+ else if (child.nodeType === Node.ELEMENT_NODE) {
1990
+ children.push(child);
1991
+ }
1992
+ }
1993
+ // TODO: extract props from nodes
1994
+ return { props, children };
1691
1995
  },
1692
1996
  patch(_tag,
1693
1997
  // TODO: Why does this assignment work?
1694
1998
  node, name,
1695
1999
  // TODO: Stricter typings?
1696
- value, oldValue, scope) {
1697
- const isSVG = scope === SVG_NAMESPACE;
2000
+ value, oldValue, xmlns) {
2001
+ const isSVG = xmlns === SVG_NAMESPACE;
1698
2002
  switch (name) {
1699
2003
  case "style": {
1700
2004
  const style = node.style;
@@ -1770,7 +2074,9 @@
1770
2074
  const descriptor = Object.getOwnPropertyDescriptor(obj, name);
1771
2075
  if (descriptor != null &&
1772
2076
  (descriptor.writable === true || descriptor.set !== undefined)) {
1773
- node[name] = value;
2077
+ if (node[name] !== value) {
2078
+ node[name] = value;
2079
+ }
1774
2080
  return;
1775
2081
  }
1776
2082
  // if the property wasn't writable, fall through to the code below
@@ -1857,24 +2163,86 @@
1857
2163
  }
1858
2164
  }
1859
2165
  },
2166
+ text(text, _scope, hydrationData) {
2167
+ if (hydrationData != null) {
2168
+ let value = hydrationData.children.shift();
2169
+ if (typeof value !== "string" || !value.startsWith(text)) {
2170
+ console.error(`Expected "${text}" while hydrating but found:`, value);
2171
+ }
2172
+ else if (text.length < value.length) {
2173
+ value = value.slice(text.length);
2174
+ hydrationData.children.unshift(value);
2175
+ }
2176
+ }
2177
+ return text;
2178
+ },
2179
+ raw(value, xmlns, hydrationData) {
2180
+ let result;
2181
+ if (typeof value === "string") {
2182
+ const el = xmlns == null
2183
+ ? document.createElement("div")
2184
+ : document.createElementNS(xmlns, "svg");
2185
+ el.innerHTML = value;
2186
+ if (el.childNodes.length === 0) {
2187
+ result = undefined;
2188
+ }
2189
+ else if (el.childNodes.length === 1) {
2190
+ result = el.childNodes[0];
2191
+ }
2192
+ else {
2193
+ result = Array.from(el.childNodes);
2194
+ }
2195
+ }
2196
+ else {
2197
+ result = value;
2198
+ }
2199
+ if (hydrationData != null) {
2200
+ // TODO: maybe we should warn on incorrect values
2201
+ if (Array.isArray(result)) {
2202
+ for (let i = 0; i < result.length; i++) {
2203
+ const node = result[i];
2204
+ if (typeof node !== "string" &&
2205
+ (node.nodeType === Node.ELEMENT_NODE ||
2206
+ node.nodeType === Node.TEXT_NODE)) {
2207
+ hydrationData.children.shift();
2208
+ }
2209
+ }
2210
+ }
2211
+ else if (result != null && typeof result !== "string") {
2212
+ if (result.nodeType === Node.ELEMENT_NODE ||
2213
+ result.nodeType === Node.TEXT_NODE) {
2214
+ hydrationData.children.shift();
2215
+ }
2216
+ }
2217
+ }
2218
+ return result;
2219
+ },
1860
2220
  };
1861
2221
  class DOMRenderer extends Renderer {
1862
2222
  constructor() {
1863
2223
  super(impl$1);
1864
2224
  }
1865
2225
  render(children, root, ctx) {
1866
- if (root == null || typeof root.nodeType !== "number") {
1867
- throw new TypeError(`Render root is not a node. Received: ${JSON.stringify(root && root.toString())}`);
1868
- }
2226
+ validateRoot(root);
1869
2227
  return super.render(children, root, ctx);
1870
2228
  }
2229
+ hydrate(children, root, ctx) {
2230
+ validateRoot(root);
2231
+ return super.hydrate(children, root, ctx);
2232
+ }
2233
+ }
2234
+ function validateRoot(root) {
2235
+ if (root === null ||
2236
+ (typeof root === "object" && typeof root.nodeType !== "number")) {
2237
+ throw new TypeError(`Render root is not a node. Received: ${JSON.stringify(root && root.toString())}`);
2238
+ }
1871
2239
  }
1872
2240
  const renderer$1 = new DOMRenderer();
1873
2241
 
1874
2242
  var dom = /*#__PURE__*/Object.freeze({
1875
2243
  __proto__: null,
1876
- impl: impl$1,
1877
2244
  DOMRenderer: DOMRenderer,
2245
+ impl: impl$1,
1878
2246
  renderer: renderer$1
1879
2247
  });
1880
2248
 
@@ -1971,7 +2339,7 @@
1971
2339
  create() {
1972
2340
  return { value: "" };
1973
2341
  },
1974
- escape(text) {
2342
+ text(text) {
1975
2343
  return escape(text);
1976
2344
  },
1977
2345
  read(value) {
@@ -2018,8 +2386,8 @@
2018
2386
 
2019
2387
  var html = /*#__PURE__*/Object.freeze({
2020
2388
  __proto__: null,
2021
- impl: impl,
2022
2389
  HTMLRenderer: HTMLRenderer,
2390
+ impl: impl,
2023
2391
  renderer: renderer
2024
2392
  });
2025
2393
 
@@ -2036,7 +2404,5 @@
2036
2404
  exports.html = html;
2037
2405
  exports.isElement = isElement;
2038
2406
 
2039
- Object.defineProperty(exports, '__esModule', { value: true });
2040
-
2041
2407
  }));
2042
2408
  //# sourceMappingURL=umd.js.map