@wix/himalaya-cli 0.1618.0 → 0.1620.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -652,6 +652,20 @@ const register = withIapPresentation<S>({
652
652
  },
653
653
  });
654
654
 
655
+ /** A read that answers at once (a cache, a local fixture, a headless run whose transport never
656
+ * suspends) is over before the action that started it returns. Its `readChanged` is a dispatch
657
+ * of this same service from a detached job, which a headless run never services after the active
658
+ * action returns, and which on a device would commit under the skeleton the action is about to
659
+ * return. Wait a bounded number of turns and fold the settled status into the action's own
660
+ * result instead; a read that is still in flight keeps the skeleton and lands through
661
+ * `readChanged` as before. `undefined` means a newer action took over while this one waited. */
662
+ async function settledWithin(next: S, category: string, gen: number): Promise<S | undefined> {
663
+ const controller = reader(category);
664
+ for (let i = 0; i < 500 && controller.getStatus().refreshing; i++) await Promise.resolve();
665
+ if (gen !== generation) return undefined;
666
+ return controller.getStatus().refreshing ? next : currentReadState(next);
667
+ }
668
+
655
669
  type A = (s: S, arg?: unknown) => S | undefined | Promise<S | undefined>;
656
670
  const svc = register<S, Record<string, A>>("himiBrandedShop", {
657
671
  state: BOOT,
@@ -659,8 +673,12 @@ const svc = register<S, Record<string, A>>("himiBrandedShop", {
659
673
  actions: {
660
674
  load: (s, arg) => load(s, arg),
661
675
  // Pull to refresh: the categories and the scope again, from Wix.
662
- refresh: (s, arg) => load(s, arg, true),
663
- retry: (s) => showScope(s, s.categorySelected, true),
676
+ refresh: async (s, arg) => {
677
+ const next = await load(s, arg, true);
678
+ // The result owns its scope; another action may have changed the globals during load.
679
+ return next ? settledWithin(next, next.categorySelected, next.scopeGen) : undefined;
680
+ },
681
+ retry: (s) => settledWithin(showScope(s, s.categorySelected, true), s.categorySelected, generation),
664
682
  recover: async () => { await fetchScope(selectedCategory, generation, true); return undefined; },
665
683
  readChanged: (s, arg) => {
666
684
  if (!arg || typeof arg !== 'object') return undefined;
@@ -683,7 +701,7 @@ const svc = register<S, Record<string, A>>("himiBrandedShop", {
683
701
  // A background page landed for the current scope.
684
702
  scopeGrew: (s) => derive(s),
685
703
  // Single-select: one chip is always chosen.
686
- selectCategory: (s, arg) => {
704
+ selectCategory: async (s, arg) => {
687
705
  // The chip row sends { selectedIds }; a deselected chip falls back to All.
688
706
  const selection = arg as { selectedIds?: unknown; refresh?: boolean } | undefined;
689
707
  const id = selection?.selectedIds !== undefined ? selected(arg)[0] ?? ALL : idOf(arg);
@@ -691,12 +709,15 @@ const svc = register<S, Record<string, A>>("himiBrandedShop", {
691
709
  const cancelsPendingRoute = !!pendingLink || selectedCategory !== id;
692
710
  categoryIntentVersion++;
693
711
  pendingLink = undefined;
694
- if (id !== ALL && !categories.some((c) => c.id === id)) return s;
712
+ // undefined, not s: s is the snapshot from before any in-flight load, loading flag included.
713
+ if (id !== ALL && !categories.some((c) => c.id === id)) return undefined;
695
714
  // showScope compares the prior selection before committing the requested category.
696
715
  // categorySelected is committed with the tap (showScope), so a tap back to the chip
697
716
  // on screen while another category loads is a no-op on the right scope.
698
- if (id === s.categorySelected) return cancelsPendingRoute || refresh ? showScope(s, id, refresh) : s;
699
- const next = showScope(s, id, refresh);
717
+ if (id === s.categorySelected) return cancelsPendingRoute || refresh ? settledWithin(showScope(s, id, refresh), id, generation) : undefined;
718
+ const shown = showScope(s, id, refresh);
719
+ const next = await settledWithin(shown, id, generation);
720
+ if (!next) return undefined;
700
721
  const pending = pendingAuxiliary;
701
722
  if (pending && pending.version === auxiliaryVersion && pending.cartCount >= 0) {
702
723
  pendingAuxiliary = undefined;
@@ -347,10 +347,11 @@ export default {
347
347
  config,
348
348
  homeScreenId: "site_home",
349
349
  screens: {
350
- // A fresh copy per build: descriptors are pure functions of the site block.
350
+ // A fresh copy per build: descriptors are pure functions of the site block. A JSON round
351
+ // trip, not structuredClone: `himi test` builds screens in the contained realm, which withholds it.
351
352
  exact: {
352
- ...Object.fromEntries(Object.keys(SCREENS).map((id) => [id, () => structuredClone(SCREENS[id])])),
353
- himi_notification_settings: () => structuredClone(NOTIFICATIONS),
353
+ ...Object.fromEntries(Object.keys(SCREENS).map((id) => [id, () => JSON.parse(JSON.stringify(SCREENS[id]))])),
354
+ himi_notification_settings: () => JSON.parse(JSON.stringify(NOTIFICATIONS)),
354
355
  },
355
356
  templated: [],
356
357
  },
@@ -652,6 +652,20 @@ const register = withIapPresentation<S>({
652
652
  },
653
653
  });
654
654
 
655
+ /** A read that answers at once (a cache, a local fixture, a headless run whose transport never
656
+ * suspends) is over before the action that started it returns. Its `readChanged` is a dispatch
657
+ * of this same service from a detached job, which a headless run never services after the active
658
+ * action returns, and which on a device would commit under the skeleton the action is about to
659
+ * return. Wait a bounded number of turns and fold the settled status into the action's own
660
+ * result instead; a read that is still in flight keeps the skeleton and lands through
661
+ * `readChanged` as before. `undefined` means a newer action took over while this one waited. */
662
+ async function settledWithin(next: S, category: string, gen: number): Promise<S | undefined> {
663
+ const controller = reader(category);
664
+ for (let i = 0; i < 500 && controller.getStatus().refreshing; i++) await Promise.resolve();
665
+ if (gen !== generation) return undefined;
666
+ return controller.getStatus().refreshing ? next : currentReadState(next);
667
+ }
668
+
655
669
  type A = (s: S, arg?: unknown) => S | undefined | Promise<S | undefined>;
656
670
  const svc = register<S, Record<string, A>>("himiBrandedShop", {
657
671
  state: BOOT,
@@ -659,8 +673,12 @@ const svc = register<S, Record<string, A>>("himiBrandedShop", {
659
673
  actions: {
660
674
  load: (s, arg) => load(s, arg),
661
675
  // Pull to refresh: the categories and the scope again, from Wix.
662
- refresh: (s, arg) => load(s, arg, true),
663
- retry: (s) => showScope(s, s.categorySelected, true),
676
+ refresh: async (s, arg) => {
677
+ const next = await load(s, arg, true);
678
+ // The result owns its scope; another action may have changed the globals during load.
679
+ return next ? settledWithin(next, next.categorySelected, next.scopeGen) : undefined;
680
+ },
681
+ retry: (s) => settledWithin(showScope(s, s.categorySelected, true), s.categorySelected, generation),
664
682
  recover: async () => { await fetchScope(selectedCategory, generation, true); return undefined; },
665
683
  readChanged: (s, arg) => {
666
684
  if (!arg || typeof arg !== 'object') return undefined;
@@ -683,7 +701,7 @@ const svc = register<S, Record<string, A>>("himiBrandedShop", {
683
701
  // A background page landed for the current scope.
684
702
  scopeGrew: (s) => derive(s),
685
703
  // Single-select: one chip is always chosen.
686
- selectCategory: (s, arg) => {
704
+ selectCategory: async (s, arg) => {
687
705
  // The chip row sends { selectedIds }; a deselected chip falls back to All.
688
706
  const selection = arg as { selectedIds?: unknown; refresh?: boolean } | undefined;
689
707
  const id = selection?.selectedIds !== undefined ? selected(arg)[0] ?? ALL : idOf(arg);
@@ -691,12 +709,15 @@ const svc = register<S, Record<string, A>>("himiBrandedShop", {
691
709
  const cancelsPendingRoute = !!pendingLink || selectedCategory !== id;
692
710
  categoryIntentVersion++;
693
711
  pendingLink = undefined;
694
- if (id !== ALL && !categories.some((c) => c.id === id)) return s;
712
+ // undefined, not s: s is the snapshot from before any in-flight load, loading flag included.
713
+ if (id !== ALL && !categories.some((c) => c.id === id)) return undefined;
695
714
  // showScope compares the prior selection before committing the requested category.
696
715
  // categorySelected is committed with the tap (showScope), so a tap back to the chip
697
716
  // on screen while another category loads is a no-op on the right scope.
698
- if (id === s.categorySelected) return cancelsPendingRoute || refresh ? showScope(s, id, refresh) : s;
699
- const next = showScope(s, id, refresh);
717
+ if (id === s.categorySelected) return cancelsPendingRoute || refresh ? settledWithin(showScope(s, id, refresh), id, generation) : undefined;
718
+ const shown = showScope(s, id, refresh);
719
+ const next = await settledWithin(shown, id, generation);
720
+ if (!next) return undefined;
700
721
  const pending = pendingAuxiliary;
701
722
  if (pending && pending.version === auxiliaryVersion && pending.cartCount >= 0) {
702
723
  pendingAuxiliary = undefined;