@wix/himalaya-cli 0.1446.0 → 0.1447.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.
Files changed (93) hide show
  1. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/state/src/index.d.ts +2 -1
  2. package/dist/cli.mjs +38 -27
  3. package/dist/preview-assets.generated.bin +0 -0
  4. package/guides/sdk-reference.json +1 -1
  5. package/package.json +2 -2
  6. package/templates/beauty-salon/tier5-src/_shared/collection-query.ts +43 -9
  7. package/templates/beauty-salon/tier5-src/_shared/collection-service.ts +15 -3
  8. package/templates/beauty-salon/tier5-src/_shared/iap-presentation.ts +3 -1
  9. package/templates/beauty-salon-owner/dev/index.ts +3 -14
  10. package/templates/beauty-salon-owner/tier5-src/_shared/collection-query.ts +43 -9
  11. package/templates/beauty-salon-owner/tier5-src/_shared/collection-service.ts +15 -3
  12. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-manage/index.ts +17 -12
  13. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-manage/logic.ts +18 -5
  14. package/templates/branded-lite/tier5-src/_shared/collection-query.ts +43 -9
  15. package/templates/branded-lite/tier5-src/_shared/collection-service.ts +15 -3
  16. package/templates/branded-lite/tier5-src/_shared/iap-presentation.ts +3 -1
  17. package/templates/community-club/tier5-src/_shared/collection-query.ts +43 -9
  18. package/templates/community-club/tier5-src/_shared/collection-service.ts +15 -3
  19. package/templates/community-club/tier5-src/_shared/iap-presentation.ts +3 -1
  20. package/templates/community-club-owner/dev/index.ts +3 -14
  21. package/templates/community-club-owner/tier5-src/_shared/collection-query.ts +43 -9
  22. package/templates/community-club-owner/tier5-src/_shared/collection-service.ts +15 -3
  23. package/templates/community-club-owner/tier5-src/himi-owner-lite-manage/index.ts +17 -12
  24. package/templates/community-club-owner/tier5-src/himi-owner-lite-manage/logic.ts +18 -5
  25. package/templates/events-meetups/tier5-src/_shared/collection-query.ts +43 -9
  26. package/templates/events-meetups/tier5-src/_shared/collection-service.ts +15 -3
  27. package/templates/events-meetups/tier5-src/_shared/iap-presentation.ts +3 -1
  28. package/templates/events-meetups-owner/dev/index.ts +3 -14
  29. package/templates/events-meetups-owner/tier5-src/_shared/collection-query.ts +43 -9
  30. package/templates/events-meetups-owner/tier5-src/_shared/collection-service.ts +15 -3
  31. package/templates/events-meetups-owner/tier5-src/himi-owner-lite-manage/index.ts +17 -12
  32. package/templates/events-meetups-owner/tier5-src/himi-owner-lite-manage/logic.ts +18 -5
  33. package/templates/gym-fitness/tier5-src/_shared/collection-query.ts +43 -9
  34. package/templates/gym-fitness/tier5-src/_shared/collection-service.ts +15 -3
  35. package/templates/gym-fitness/tier5-src/_shared/iap-presentation.ts +3 -1
  36. package/templates/gym-fitness-owner/dev/index.ts +3 -14
  37. package/templates/gym-fitness-owner/tier5-src/_shared/collection-query.ts +43 -9
  38. package/templates/gym-fitness-owner/tier5-src/_shared/collection-service.ts +15 -3
  39. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-manage/index.ts +17 -12
  40. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-manage/logic.ts +18 -5
  41. package/templates/nonprofit-cause/tier5-src/_shared/collection-query.ts +43 -9
  42. package/templates/nonprofit-cause/tier5-src/_shared/collection-service.ts +15 -3
  43. package/templates/nonprofit-cause/tier5-src/_shared/iap-presentation.ts +3 -1
  44. package/templates/nonprofit-cause-owner/dev/index.ts +3 -14
  45. package/templates/nonprofit-cause-owner/tier5-src/_shared/collection-query.ts +43 -9
  46. package/templates/nonprofit-cause-owner/tier5-src/_shared/collection-service.ts +15 -3
  47. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-manage/index.ts +17 -12
  48. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-manage/logic.ts +18 -5
  49. package/templates/owner-lite/dev/index.ts +3 -14
  50. package/templates/owner-lite/tier5-src/_shared/collection-query.ts +43 -9
  51. package/templates/owner-lite/tier5-src/_shared/collection-service.ts +15 -3
  52. package/templates/owner-lite/tier5-src/himi-owner-lite-manage/index.ts +17 -12
  53. package/templates/owner-lite/tier5-src/himi-owner-lite-manage/logic.ts +18 -5
  54. package/templates/podcast-creator/tier5-src/_shared/collection-query.ts +43 -9
  55. package/templates/podcast-creator/tier5-src/_shared/collection-service.ts +15 -3
  56. package/templates/podcast-creator/tier5-src/_shared/iap-presentation.ts +3 -1
  57. package/templates/podcast-creator-owner/dev/index.ts +3 -14
  58. package/templates/podcast-creator-owner/tier5-src/_shared/collection-query.ts +43 -9
  59. package/templates/podcast-creator-owner/tier5-src/_shared/collection-service.ts +15 -3
  60. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-manage/index.ts +17 -12
  61. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-manage/logic.ts +18 -5
  62. package/templates/portfolio-creative/tier5-src/_shared/collection-query.ts +43 -9
  63. package/templates/portfolio-creative/tier5-src/_shared/collection-service.ts +15 -3
  64. package/templates/portfolio-creative/tier5-src/_shared/iap-presentation.ts +3 -1
  65. package/templates/portfolio-creative-owner/dev/index.ts +3 -14
  66. package/templates/portfolio-creative-owner/tier5-src/_shared/collection-query.ts +43 -9
  67. package/templates/portfolio-creative-owner/tier5-src/_shared/collection-service.ts +15 -3
  68. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-manage/index.ts +17 -12
  69. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-manage/logic.ts +18 -5
  70. package/templates/restaurant/tier5-src/_shared/collection-query.ts +43 -9
  71. package/templates/restaurant/tier5-src/_shared/collection-service.ts +15 -3
  72. package/templates/restaurant/tier5-src/_shared/iap-presentation.ts +3 -1
  73. package/templates/restaurant-owner/dev/index.ts +3 -14
  74. package/templates/restaurant-owner/tier5-src/_shared/collection-query.ts +43 -9
  75. package/templates/restaurant-owner/tier5-src/_shared/collection-service.ts +15 -3
  76. package/templates/restaurant-owner/tier5-src/himi-owner-lite-manage/index.ts +17 -12
  77. package/templates/restaurant-owner/tier5-src/himi-owner-lite-manage/logic.ts +18 -5
  78. package/templates/retail-store/tier5-src/_shared/collection-query.ts +43 -9
  79. package/templates/retail-store/tier5-src/_shared/collection-service.ts +15 -3
  80. package/templates/retail-store/tier5-src/_shared/iap-presentation.ts +3 -1
  81. package/templates/retail-store-owner/dev/index.ts +3 -14
  82. package/templates/retail-store-owner/tier5-src/_shared/collection-query.ts +43 -9
  83. package/templates/retail-store-owner/tier5-src/_shared/collection-service.ts +15 -3
  84. package/templates/retail-store-owner/tier5-src/himi-owner-lite-manage/index.ts +17 -12
  85. package/templates/retail-store-owner/tier5-src/himi-owner-lite-manage/logic.ts +18 -5
  86. package/templates/yoga-studio/tier5-src/_shared/collection-query.ts +43 -9
  87. package/templates/yoga-studio/tier5-src/_shared/collection-service.ts +15 -3
  88. package/templates/yoga-studio/tier5-src/_shared/iap-presentation.ts +3 -1
  89. package/templates/yoga-studio-owner/dev/index.ts +3 -14
  90. package/templates/yoga-studio-owner/tier5-src/_shared/collection-query.ts +43 -9
  91. package/templates/yoga-studio-owner/tier5-src/_shared/collection-service.ts +15 -3
  92. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-manage/index.ts +17 -12
  93. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-manage/logic.ts +18 -5
@@ -19,7 +19,8 @@ export interface ServiceDefinition<S> {
19
19
  * Keys that start each VISIT from `state`: when a cached embed shows the screen again (its warm worker
20
20
  * reclaimed), these go back to their declared values before the screen reloads. For the same kind of
21
21
  * per-run UI state as `notHydrated` (an editor's one-shot seed, an open sheet); never data a returning
22
- * screen should keep showing. A full app's own screens and an uncached embed ignore it.
22
+ * screen should keep showing. A cold boot never restores them either. A full app's own screens and an
23
+ * uncached embed ignore the reset.
23
24
  */
24
25
  perVisit?: readonly (keyof S & string)[];
25
26
  }
package/dist/cli.mjs CHANGED
@@ -8115,6 +8115,7 @@ function resolveTranslation(value, catalog3, sourceCatalog) {
8115
8115
  var BINDING_SIGILS, BINDING_SIGIL_RE, TRANSLATION_PREFIX, TRANSLATION_KEY_RE;
8116
8116
  var init_bindings = __esm({
8117
8117
  "../../core/ts/src/bindings.ts"() {
8118
+ "use strict";
8118
8119
  BINDING_SIGILS = ["service", "flag", "item", "t"];
8119
8120
  BINDING_SIGIL_RE = new RegExp(
8120
8121
  `^\\$(?:${BINDING_SIGILS.filter((s2) => s2 !== "t").join("|")})\\b|^\\$t:`
@@ -11110,6 +11111,16 @@ function responseBytes(value) {
11110
11111
  return 0;
11111
11112
  }
11112
11113
  }
11114
+ function restoredState(def, snapshot2) {
11115
+ const declared = def?.state ?? {};
11116
+ if (!snapshot2 || typeof snapshot2 !== "object" || Array.isArray(snapshot2)) return declared;
11117
+ const fresh = [def?.notHydrated, def?.perVisit].flatMap((keys) => Array.isArray(keys) ? keys : []);
11118
+ const merged = { ...declared };
11119
+ for (const [key3, value] of Object.entries(snapshot2)) {
11120
+ if (key3 !== "_error" && !fresh.includes(key3)) merged[key3] = value;
11121
+ }
11122
+ return merged;
11123
+ }
11113
11124
  async function bootBundleWorker(bundleJs, opts) {
11114
11125
  const budgetMs = opts.budgetMs && opts.budgetMs > 0 ? opts.budgetMs : 5e3;
11115
11126
  const calls = [];
@@ -11169,7 +11180,7 @@ async function bootBundleWorker(bundleJs, opts) {
11169
11180
  dispatch: dispatchHost,
11170
11181
  defineService: (name, def) => {
11171
11182
  const entry = {
11172
- state: def?.state ?? {},
11183
+ state: restoredState(def, opts.persistedState?.[String(name)]),
11173
11184
  actions: def?.actions ?? {}
11174
11185
  };
11175
11186
  captured.push({ name: String(name), def: entry });
@@ -161512,32 +161523,32 @@ var init_preview_assets_generated = __esm({
161512
161523
  "../../packages/serve/src/preview-assets.generated.ts"() {
161513
161524
  init_preview_assets_format();
161514
161525
  PREVIEW_ASSET_MANIFEST = [
161515
- { key: "assets/DashboardHost-Hbp9cYDQ.js", contentType: "application/javascript; charset=utf-8", offset: 8, length: 2916, sha256: "67f18e346a0472d13699e34a121ad159ef72ff7070f022de0158ced1d8de8c9e" },
161516
- { key: "assets/DevDrawer-BCf5pKn3.js", contentType: "application/javascript; charset=utf-8", offset: 2924, length: 16744, sha256: "4e3f8faeef44171c9991857b7a3954ad3ac35d7ba087c1714cca89b613ef575c" },
161517
- { key: "assets/IdiomCompare-Du8fy2bA.js", contentType: "application/javascript; charset=utf-8", offset: 19668, length: 1892, sha256: "bf3251f0779f986ec17d132e2edee65187af52058ca152363672e8fa65c44c8a" },
161518
- { key: "assets/LightboxViewer-Bio4Uaf0.js", contentType: "application/javascript; charset=utf-8", offset: 21560, length: 8132, sha256: "5c3563c1bf16c102bf176fadac3e1b3bfafd03f0791c7d5f8065182830ba6388" },
161519
- { key: "assets/PhonePreviewControl-rUJWm5D9.js", contentType: "application/javascript; charset=utf-8", offset: 29692, length: 37729, sha256: "3ee2027d40341425a3fcf21e6b2222269e7e5be04ddd39581bfd66cd61fafa39" },
161520
- { key: "assets/SchemeCompare-cs9HGLIH.js", contentType: "application/javascript; charset=utf-8", offset: 67421, length: 1824, sha256: "fece2c0e802fd153f570fd5ac97985ee9060bb2856f009953f1c476c07d1df85" },
161521
- { key: "assets/index-B4otl_lr.js", contentType: "application/javascript; charset=utf-8", offset: 69245, length: 1268353, sha256: "168c393f88d0fb0df94eae9b1c14958c5ba0d707df7db7fd11de1d4c12a4db84" },
161522
- { key: "assets/pdf-BZPM9uB7.js", contentType: "application/javascript; charset=utf-8", offset: 1337598, length: 488199, sha256: "0593e16766ca0d3ebf849836a109bb58c1c869cadd005436d9258f6e4027a7ef" },
161523
- { key: "assets/pdf.worker.min-BQa_JDET.js", contentType: "application/javascript; charset=utf-8", offset: 1825797, length: 73, sha256: "41bcfda4f3c8c0a81c785ea87c69afc8e7d0ed8c333324aee94c273d881bb6aa" },
161524
- { key: "assets/pdf.worker.min-BmVo14Nb.mjs", contentType: "application/javascript; charset=utf-8", offset: 1825870, length: 1317034, sha256: "a33cfe728c584fdba4fcc1fd54bcdc2f9f2f13889ddbb5b2bd1d0f8cbe49b84e" },
161525
- { key: "assets/pdfDocument-6awpDNiQ.js", contentType: "application/javascript; charset=utf-8", offset: 3142904, length: 2868, sha256: "ce4700b049bccf122eac6666b8c83d6a2527cecc7ee6e358e96995554a9d3566" },
161526
- { key: "fonts/Roboto-LICENSE.txt", contentType: "application/octet-stream", offset: 3145772, length: 4610, sha256: "994062dd7af4186a739de55dd9e151c63b6301f8e2cbafc608d35c2e87da0cd2" },
161527
- { key: "fonts/Roboto-Variable.ttf", contentType: "font/ttf", offset: 3150382, length: 488584, sha256: "d7598e12c5dbef095ff8272cfc55da0250bd07fbdecbac8a530b9b277872a134" },
161528
- { key: "fonts/WixMadeforDisplay-Bold.ttf", contentType: "font/ttf", offset: 3638966, length: 90924, sha256: "ec4c7d63772d980d6e147b2df6215fd2737e4b04cebe0cf7e11dc695fb63cdcf" },
161529
- { key: "fonts/WixMadeforDisplay-ExtraBold.ttf", contentType: "font/ttf", offset: 3729890, length: 90984, sha256: "bf4c16a2774edd38a921edf3099469866cd74c094fd596f3fa4e81f54a04dfaa" },
161530
- { key: "fonts/WixMadeforDisplay-Medium.ttf", contentType: "font/ttf", offset: 3820874, length: 90980, sha256: "368faca44fb9f7207e4af784e94750b1ee59c1098b8689e0f4a918926f244fb5" },
161531
- { key: "fonts/WixMadeforDisplay-Regular.ttf", contentType: "font/ttf", offset: 3911854, length: 90872, sha256: "97ed8f1a6772abe26e3d8af5cecfc4b02faa94497d585b0398b5053b739a70c1" },
161532
- { key: "fonts/WixMadeforDisplay-SemiBold.ttf", contentType: "font/ttf", offset: 4002726, length: 90980, sha256: "642feab8a48ddf6c7a89d7c2a7c0edfa0e6e773f671287e460e222703563191e" },
161533
- { key: "fonts/WixMadeforText-Bold.ttf", contentType: "font/ttf", offset: 4093706, length: 108636, sha256: "37c179b4fefb63ea9637322b84583a2df53aa226c150294eea1675c14b1b8e29" },
161534
- { key: "fonts/WixMadeforText-ExtraBold.ttf", contentType: "font/ttf", offset: 4202342, length: 109084, sha256: "62e2fe4718129f36ecdb0ae4de385bd6b81ecc876146447167c6c140fad699a9" },
161535
- { key: "fonts/WixMadeforText-Medium.ttf", contentType: "font/ttf", offset: 4311426, length: 109048, sha256: "4ecf270136af928612c76fe4ebade91d8156975e5ea4c0cf9225501dd1788e02" },
161536
- { key: "fonts/WixMadeforText-Regular.ttf", contentType: "font/ttf", offset: 4420474, length: 107700, sha256: "dee56f09fd3efc127c1ea95557edc74110c1460a46287da36e0af25efd341ef5" },
161537
- { key: "fonts/WixMadeforText-SemiBold.ttf", contentType: "font/ttf", offset: 4528174, length: 88108, sha256: "dced97a77624bc5f8b1ae65850360908297f4588e09feb6f22792d29dc7e765b" },
161538
- { key: "himi-sw.js", contentType: "application/javascript; charset=utf-8", offset: 4616282, length: 6748, sha256: "4a92459977d7cefa1a6a7e020c3a63509737f083b3f2773557c1a5771665811a" },
161539
- { key: "index.html", contentType: "text/html; charset=utf-8", offset: 4623030, length: 4947, sha256: "9c692b83159ab0d8305f407bc0bc0dbdefa8db9e479d76ab86366b10282196c1" },
161540
- { key: "t5-frame.html", contentType: "text/html; charset=utf-8", offset: 4627977, length: 17786, sha256: "084107ca997628c248f1b0ba3a7494744850df39fad813546ab8bdb82a6058da" }
161526
+ { key: "assets/DashboardHost-C0zY6LXc.js", contentType: "application/javascript; charset=utf-8", offset: 8, length: 2916, sha256: "2ae0514356f33d624fbdea4f37763d9485585ae8c47efda81e9fd4976134aa66" },
161527
+ { key: "assets/DevDrawer--HNffqm4.js", contentType: "application/javascript; charset=utf-8", offset: 2924, length: 16744, sha256: "bc1aa20ec7d0244b30ee071da75acf380ebc7087013247c78a6a4f813ee42658" },
161528
+ { key: "assets/IdiomCompare-BcPm59UD.js", contentType: "application/javascript; charset=utf-8", offset: 19668, length: 1892, sha256: "e47795d60a59d872dc9d1a31cf166940f4eeb1683e2bc858b62da24f1d7835b2" },
161529
+ { key: "assets/LightboxViewer-B3iMQPin.js", contentType: "application/javascript; charset=utf-8", offset: 21560, length: 8132, sha256: "7a035923df5986b7775d3398725f9234d1d7ddf46e0a5af8e9f6bd74d771590d" },
161530
+ { key: "assets/PhonePreviewControl-CRKLF95b.js", contentType: "application/javascript; charset=utf-8", offset: 29692, length: 37729, sha256: "e65e0be6f821b3fd13c847c5ae7fff2880a39bf603a3bd426878af78e3d814a7" },
161531
+ { key: "assets/SchemeCompare-Bn99KkCH.js", contentType: "application/javascript; charset=utf-8", offset: 67421, length: 1824, sha256: "7f2f13e931a686d80e03ce3673dc16e93619c93326fb447d216ee2e5bffc4807" },
161532
+ { key: "assets/index-DFazEEqa.js", contentType: "application/javascript; charset=utf-8", offset: 69245, length: 1268538, sha256: "3718cafcaf0cdc962f5d1c5d6bc3cf5e9ee2b1f11e918fb7e9c629e5b0a49576" },
161533
+ { key: "assets/pdf-NnG-NcU-.js", contentType: "application/javascript; charset=utf-8", offset: 1337783, length: 488199, sha256: "f60ce93a839789588474a844c60da2ebc210efbd3f50f04cc4e44eb8e127cddc" },
161534
+ { key: "assets/pdf.worker.min-BQa_JDET.js", contentType: "application/javascript; charset=utf-8", offset: 1825982, length: 73, sha256: "41bcfda4f3c8c0a81c785ea87c69afc8e7d0ed8c333324aee94c273d881bb6aa" },
161535
+ { key: "assets/pdf.worker.min-BmVo14Nb.mjs", contentType: "application/javascript; charset=utf-8", offset: 1826055, length: 1317034, sha256: "a33cfe728c584fdba4fcc1fd54bcdc2f9f2f13889ddbb5b2bd1d0f8cbe49b84e" },
161536
+ { key: "assets/pdfDocument-MaeR1HVD.js", contentType: "application/javascript; charset=utf-8", offset: 3143089, length: 2868, sha256: "2752019f5f58a8818f6471de600ccfb647cfab7577501d19d0eface21e7820dc" },
161537
+ { key: "fonts/Roboto-LICENSE.txt", contentType: "application/octet-stream", offset: 3145957, length: 4610, sha256: "994062dd7af4186a739de55dd9e151c63b6301f8e2cbafc608d35c2e87da0cd2" },
161538
+ { key: "fonts/Roboto-Variable.ttf", contentType: "font/ttf", offset: 3150567, length: 488584, sha256: "d7598e12c5dbef095ff8272cfc55da0250bd07fbdecbac8a530b9b277872a134" },
161539
+ { key: "fonts/WixMadeforDisplay-Bold.ttf", contentType: "font/ttf", offset: 3639151, length: 90924, sha256: "ec4c7d63772d980d6e147b2df6215fd2737e4b04cebe0cf7e11dc695fb63cdcf" },
161540
+ { key: "fonts/WixMadeforDisplay-ExtraBold.ttf", contentType: "font/ttf", offset: 3730075, length: 90984, sha256: "bf4c16a2774edd38a921edf3099469866cd74c094fd596f3fa4e81f54a04dfaa" },
161541
+ { key: "fonts/WixMadeforDisplay-Medium.ttf", contentType: "font/ttf", offset: 3821059, length: 90980, sha256: "368faca44fb9f7207e4af784e94750b1ee59c1098b8689e0f4a918926f244fb5" },
161542
+ { key: "fonts/WixMadeforDisplay-Regular.ttf", contentType: "font/ttf", offset: 3912039, length: 90872, sha256: "97ed8f1a6772abe26e3d8af5cecfc4b02faa94497d585b0398b5053b739a70c1" },
161543
+ { key: "fonts/WixMadeforDisplay-SemiBold.ttf", contentType: "font/ttf", offset: 4002911, length: 90980, sha256: "642feab8a48ddf6c7a89d7c2a7c0edfa0e6e773f671287e460e222703563191e" },
161544
+ { key: "fonts/WixMadeforText-Bold.ttf", contentType: "font/ttf", offset: 4093891, length: 108636, sha256: "37c179b4fefb63ea9637322b84583a2df53aa226c150294eea1675c14b1b8e29" },
161545
+ { key: "fonts/WixMadeforText-ExtraBold.ttf", contentType: "font/ttf", offset: 4202527, length: 109084, sha256: "62e2fe4718129f36ecdb0ae4de385bd6b81ecc876146447167c6c140fad699a9" },
161546
+ { key: "fonts/WixMadeforText-Medium.ttf", contentType: "font/ttf", offset: 4311611, length: 109048, sha256: "4ecf270136af928612c76fe4ebade91d8156975e5ea4c0cf9225501dd1788e02" },
161547
+ { key: "fonts/WixMadeforText-Regular.ttf", contentType: "font/ttf", offset: 4420659, length: 107700, sha256: "dee56f09fd3efc127c1ea95557edc74110c1460a46287da36e0af25efd341ef5" },
161548
+ { key: "fonts/WixMadeforText-SemiBold.ttf", contentType: "font/ttf", offset: 4528359, length: 88108, sha256: "dced97a77624bc5f8b1ae65850360908297f4588e09feb6f22792d29dc7e765b" },
161549
+ { key: "himi-sw.js", contentType: "application/javascript; charset=utf-8", offset: 4616467, length: 6748, sha256: "4a92459977d7cefa1a6a7e020c3a63509737f083b3f2773557c1a5771665811a" },
161550
+ { key: "index.html", contentType: "text/html; charset=utf-8", offset: 4623215, length: 4947, sha256: "31e294c8a2eff7f23f4b24e1696713854eb517ad048ef9cc6967c90b9abca5ba" },
161551
+ { key: "t5-frame.html", contentType: "text/html; charset=utf-8", offset: 4628162, length: 17786, sha256: "084107ca997628c248f1b0ba3a7494744850df39fad813546ab8bdb82a6058da" }
161541
161552
  ];
161542
161553
  }
161543
161554
  });
Binary file
@@ -8,7 +8,7 @@
8
8
  "name": "ServiceDefinition",
9
9
  "kind": "interface",
10
10
  "jsdoc": "",
11
- "decl": "export interface ServiceDefinition<S> {\n /** Initial state for the service. Must be JSON-serializable. */\n state: S;\n\n /**\n * Actions: pure functions of `(state, ...args) → newState` (or async). The\n * platform wraps each action so calling it from JS or from `__invokeAction`\n * (native side) updates the service's state and notifies subscribers.\n */\n actions?: Record<\n string,\n // `never[]`, not `unknown[]` — same reason as defineService's constraint below:\n // parameters are contravariant, so `unknown[]` rejects every handler that narrows\n // its payload. Leaving this one loose would reintroduce the same TS2344 for anyone\n // who annotates a `ServiceDefinition<S>` explicitly instead of inferring it.\n (state: S, ...args: never[]) => S | undefined | Promise<S | undefined>\n >;\n\n /**\n * Keys a cold boot takes from `state`, never from the persisted snapshot (cold-boot\n * hydration). For UI state that must describe THIS run — an editor's one-shot seed, a\n * \"still loading\" gate, an open sheet — where last run's value would be wrong, not just stale.\n * Runtimes that honour it set `himalaya.hydrationOptOut`; an older one ignores the option.\n */\n notHydrated?: readonly (keyof S & string)[];\n\n /**\n * Keys that start each VISIT from `state`: when a cached embed shows the screen again (its warm worker\n * reclaimed), these go back to their declared values before the screen reloads. For the same kind of\n * per-run UI state as `notHydrated` (an editor's one-shot seed, an open sheet); never data a returning\n * screen should keep showing. A full app's own screens and an uncached embed ignore it.\n */\n perVisit?: readonly (keyof S & string)[];\n}"
11
+ "decl": "export interface ServiceDefinition<S> {\n /** Initial state for the service. Must be JSON-serializable. */\n state: S;\n\n /**\n * Actions: pure functions of `(state, ...args) → newState` (or async). The\n * platform wraps each action so calling it from JS or from `__invokeAction`\n * (native side) updates the service's state and notifies subscribers.\n */\n actions?: Record<\n string,\n // `never[]`, not `unknown[]` — same reason as defineService's constraint below:\n // parameters are contravariant, so `unknown[]` rejects every handler that narrows\n // its payload. Leaving this one loose would reintroduce the same TS2344 for anyone\n // who annotates a `ServiceDefinition<S>` explicitly instead of inferring it.\n (state: S, ...args: never[]) => S | undefined | Promise<S | undefined>\n >;\n\n /**\n * Keys a cold boot takes from `state`, never from the persisted snapshot (cold-boot\n * hydration). For UI state that must describe THIS run — an editor's one-shot seed, a\n * \"still loading\" gate, an open sheet — where last run's value would be wrong, not just stale.\n * Runtimes that honour it set `himalaya.hydrationOptOut`; an older one ignores the option.\n */\n notHydrated?: readonly (keyof S & string)[];\n\n /**\n * Keys that start each VISIT from `state`: when a cached embed shows the screen again (its warm worker\n * reclaimed), these go back to their declared values before the screen reloads. For the same kind of\n * per-run UI state as `notHydrated` (an editor's one-shot seed, an open sheet); never data a returning\n * screen should keep showing. A cold boot never restores them either. A full app's own screens and an\n * uncached embed ignore the reset.\n */\n perVisit?: readonly (keyof S & string)[];\n}"
12
12
  },
13
13
  {
14
14
  "name": "ServiceHandle",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wix/himalaya-cli",
3
- "version": "0.1446.0",
3
+ "version": "0.1447.0",
4
4
  "type": "module",
5
5
  "description": "himi — the Himalaya authoring CLI. Build a Tier-5 content package on your own machine, push it to the production serve as a draft, preview it on web + Himi Player, and read worker logs back — the remote dev loop. Wraps @himalaya/serve-cli for transport.",
6
6
  "keywords": [
@@ -71,5 +71,5 @@
71
71
  "artifactId": "himalaya-cli"
72
72
  }
73
73
  },
74
- "falconPackageHash": "880b0e52cdd4129fa90b221507dda4d96f5c2d0bc7780fcbb6503876"
74
+ "falconPackageHash": "ec7806275850384530f4bb3f9e2a1ee23e66bf5223389c5dc7a3446a"
75
75
  }
@@ -20,6 +20,13 @@ export interface CollectionQueryState<Row> extends CollectionPage<Row> {
20
20
  pageError: boolean;
21
21
  complete: boolean;
22
22
  error: string;
23
+ /** The rows are the source's `cached` page, shown while the live read is still out. */
24
+ provisional?: boolean;
25
+ }
26
+ /** What a collection showed, as `shownPage` hands it out and `seed` takes it back on a restored boot. */
27
+ export interface CollectionShownPage<Row> {
28
+ state: CollectionQueryState<Row>;
29
+ loadedPages: number;
23
30
  }
24
31
  export interface CollectionQuerySource<Row> {
25
32
  fetch(criteria: CollectionQuery, cursor: string): Promise<CollectionPage<Row>>;
@@ -50,13 +57,19 @@ export function createCollectionQuery<Row>(source: CollectionQuerySource<Row>, p
50
57
  let state: CollectionQueryState<Row> = { criteria: normalize({}), items: [], nextCursor: '', status: 'idle', loadingMore: false, pageError: false, complete: false, error: '' };
51
58
  /** The query a revalidation is running under: its snapshots are held back until it is done. */
52
59
  let revalidating = -1;
53
- /** What the screen shows: the last published state, restored when a revalidation fails. */
54
- let shown: { state: CollectionQueryState<Row>; loadedPages: number } | undefined;
60
+ /** The last page the screen showed whole (rows ready, nothing paging, failing or standing in): what a revalidation
61
+ * keeps on screen and restores when it fails, and what a restored boot shows. A next page loading or failing after
62
+ * it leaves those rows on screen, so it is still this. */
63
+ let lastShown: { state: CollectionQueryState<Row>; loadedPages: number } | undefined;
64
+ const sameCriteria = (a: CollectionQuery, b: CollectionQuery) => JSON.stringify(a) === JSON.stringify(b);
55
65
  let clearedByError = false;
56
66
  const snapshot = (): CollectionQueryState<Row> => ({ ...state, criteria: copyQuery(state.criteria), items: [...state.items] });
57
67
  const emit = () => {
58
68
  if (disposed || revalidating === generation) return;
59
- shown = { state: snapshot(), loadedPages };
69
+ // A settled answer replaces it, an empty one included: rows the live query proved gone are never kept.
70
+ if (state.status === 'ready' && !state.pageError && !state.loadingMore && !state.provisional) {
71
+ lastShown = state.items.length > 0 ? { state: snapshot(), loadedPages } : undefined;
72
+ }
60
73
  publish(snapshot());
61
74
  };
62
75
  const merge = (old: Row[], page: Row[]): Row[] => {
@@ -77,14 +90,14 @@ export function createCollectionQuery<Row>(source: CollectionQuerySource<Row>, p
77
90
  const criteria = copyQuery(state.criteria);
78
91
  if (!append) loadedPages = 0;
79
92
  state = { ...state, status: append ? 'ready' : 'loading', loadingMore: append, pageError: false, error: '',
80
- ...(append ? {} : { items: [], nextCursor: '', total: undefined, metadata: undefined, complete: false }) };
93
+ ...(append ? {} : { items: [], nextCursor: '', total: undefined, metadata: undefined, complete: false, provisional: false }) };
81
94
  emit();
82
95
  try {
83
96
  if (!append && source.cached) {
84
97
  const cached = await source.cached(criteria).catch(() => undefined);
85
98
  if (disposed || generation !== revision) return;
86
99
  if (cached?.items.length) {
87
- state = { ...state, items: merge([], cached.items), metadata: cached.metadata, status: 'ready', complete: false };
100
+ state = { ...state, items: merge([], cached.items), metadata: cached.metadata, status: 'ready', complete: false, provisional: true };
88
101
  emit();
89
102
  }
90
103
  }
@@ -94,17 +107,18 @@ export function createCollectionQuery<Row>(source: CollectionQuerySource<Row>, p
94
107
  state = { ...state, items: merge(append ? state.items : [], page.items), nextCursor: page.nextCursor,
95
108
  total: typeof page.total === 'number' && Number.isFinite(page.total) && page.total >= 0 ? page.total : (append ? state.total : undefined),
96
109
  metadata: page.metadata ?? (append ? state.metadata : undefined),
97
- status: 'ready', loadingMore: false, pageError: false, complete: page.complete ?? !page.nextCursor };
110
+ status: 'ready', loadingMore: false, pageError: false, complete: page.complete ?? !page.nextCursor, provisional: false };
98
111
  loadedPages++;
99
112
  } catch (error) {
100
113
  if (disposed || generation !== revision) return;
101
114
  const clear = source.clearOnError?.(error) === true;
102
115
  // Revoked access always ends what a revalidation showed, whether or not the source clears its rows.
103
116
  clearedByError = clear || /HTTP 40[13]\b/.test(String(error));
117
+ if (clearedByError) lastShown = undefined;
104
118
  if (clear) loadedPages = 0;
105
119
  state = { ...state, ...(clear ? { items: [], nextCursor: '', total: undefined, metadata: undefined } : {}),
106
120
  status: append && !clear ? 'ready' : 'error', loadingMore: false, pageError: append && !clear, complete: false,
107
- error: source.errorMessage?.(error) ?? '' };
121
+ provisional: false, error: source.errorMessage?.(error) ?? '' };
108
122
  }
109
123
  emit();
110
124
  };
@@ -117,8 +131,7 @@ export function createCollectionQuery<Row>(source: CollectionQuerySource<Row>, p
117
131
  const next = normalize(criteria);
118
132
  const same = JSON.stringify(next) === JSON.stringify(state.criteria);
119
133
  const count = restorePages && same ? Math.max(loadedPages, restoringPages, 1) : 1;
120
- const showing = shown && shown.state.status === 'ready' && shown.state.items.length > 0 && !shown.state.pageError
121
- && !shown.state.loadingMore && JSON.stringify(shown.state.criteria) === JSON.stringify(next) ? shown : undefined;
134
+ const showing = lastShown && sameCriteria(lastShown.state.criteria, next) ? lastShown : undefined;
122
135
  restoringPages = count;
123
136
  state = { ...state, criteria: next };
124
137
  if (revalidate && showing) { revalidating = generation + 1; clearedByError = false; }
@@ -140,6 +153,27 @@ export function createCollectionQuery<Row>(source: CollectionQuerySource<Row>, p
140
153
  emit();
141
154
  }
142
155
  },
156
+ /** The page the screen shows (rows ready, nothing failing or still paging), for the service to keep with its
157
+ * state; undefined when it shows none. */
158
+ shownPage(): CollectionShownPage<Row> | undefined {
159
+ if (!lastShown || !sameCriteria(lastShown.state.criteria, state.criteria)) return undefined;
160
+ const page = lastShown.state;
161
+ return { state: { ...page, criteria: copyQuery(page.criteria), items: [...page.items] }, loadedPages: lastShown.loadedPages };
162
+ },
163
+ /** A cold boot restored from disk: what the last run showed becomes what this one shows, so its first `load`
164
+ * revalidates behind it instead of starting empty. Only before the first query, and only a well-formed page. */
165
+ seed(page: unknown): boolean {
166
+ const candidate = page && typeof page === 'object' ? page as Partial<CollectionShownPage<Row>> : undefined;
167
+ const restored = candidate?.state;
168
+ if (generation !== 0 || state.status !== 'idle' || !restored || restored.status !== 'ready'
169
+ || !Array.isArray(restored.items) || restored.items.length === 0 || typeof restored.nextCursor !== 'string') return false;
170
+ try { restored.items.forEach(item => { if (!source.id(item)) throw new Error('A backend collection row requires an identity'); }); } catch { return false; }
171
+ state = { ...state, ...restored, criteria: normalize(restored.criteria ?? {}), items: [...restored.items], status: 'ready',
172
+ loadingMore: false, pageError: false, provisional: false, error: '' };
173
+ loadedPages = Math.max(1, Math.floor(Number(candidate?.loadedPages) || 1));
174
+ lastShown = { state: snapshot(), loadedPages };
175
+ return true;
176
+ },
143
177
  refresh: () => { restoringPages = 0; return fetch(false); },
144
178
  loadMore: () => fetch(true),
145
179
  retry: () => fetch(state.pageError),
@@ -142,6 +142,7 @@ export function decorateCollection(name: string, def: { state: State; actions?:
142
142
  // the query controller carries a recoverable failure. Treat each shape as
143
143
  // one collection state so the shared UI never renders a blank catalog.
144
144
  const backendState = backend?.controller.getState();
145
+ const provisional = backendState?.provisional === true;
145
146
  const errored = s.status === 'error' || s.isErrored === true || s.errored === true || backendState?.status === 'error';
146
147
  const domainError = typeof s.error === 'string' ? s.error.trim() : '';
147
148
  const queryError = typeof backendState?.error === 'string' ? backendState.error.trim() : '';
@@ -177,13 +178,17 @@ export function decorateCollection(name: string, def: { state: State; actions?:
177
178
  // un-narrowed failure. A re-query while typing keeps it (and the search field's focus).
178
179
  const hideChrome = (!narrowed && !s.nextCursor && projected.loaded < COLLECTION_CHROME_MIN_ITEMS)
179
180
  || sourceEmpty || ((loading || errored) && rows.length === 0 && !narrowed);
180
- return { ...s, collection: { ...prefs, ...projected,
181
+ // Kept with the state, so a cold boot restored from disk shows its last page and revalidates behind it.
182
+ const collectionShown = backend?.controller.shownPage();
183
+ return { ...s, ...(backend ? { collectionShown: collectionShown ?? null } : {}), collection: { ...prefs, ...projected,
181
184
  hideChrome, noActiveCriteria: !narrowed,
182
185
  viewName, viewMessage, saveMessage, noSavedViews: prefs.savedViews.length === 0,
183
186
  cursor: s.pageError === true || s.loadingMore === true ? '' : s.nextCursor,
184
187
  noMore: !s.nextCursor || s.pageError === true || s.loadingMore === true,
185
188
  noPageError: s.pageError !== true,
186
- noOffline: s.fromCache !== true,
189
+ // Rows read from the device ahead of the live read are shown as they are: "saved offline" is what stays on
190
+ // screen when the live read failed, not a first visit that is still loading.
191
+ noOffline: s.fromCache !== true || provisional,
187
192
  noError: !errored,
188
193
  noRetry: !canRetry || signInRequired,
189
194
  noSignIn: !signInRequired,
@@ -202,7 +207,7 @@ export function decorateCollection(name: string, def: { state: State; actions?:
202
207
  filtersPresented, previewPresented: !!previewId, preview: projected.items.filter(row => row.collectionId === previewId),
203
208
  persistenceLabel: persisted.available ? copyParam('collection.onDevice', 'Saved on this device') : copyParam('collection.sessionOnly', 'Choices last for this session'),
204
209
  noEmpty: loading || errored || sourceEmpty || domainNarrowed || projected.items.length > 0,
205
- summary: backend && config.backendQuery && s.fromCache
210
+ summary: backend && config.backendQuery && s.fromCache && !provisional
206
211
  ? copyParam('collection.cached', 'Saved offline · may be outdated')
207
212
  : collectionSummary({ shown: projected.items.length, loaded: projected.loaded, backend: !!(backend && config.backendQuery),
208
213
  total: typeof s.total === 'number' ? s.total : undefined, noun: config.countNoun
@@ -345,6 +350,13 @@ export function defineBackendCollection<S extends object, Row extends object>(na
345
350
  apply: (state, page) => options.apply(state as S, page as unknown as CollectionQueryState<Row>) as State,
346
351
  });
347
352
  const handle = (options.register ?? register)(name, definition);
353
+ // Registration restored the state a cold boot saved: the page it showed is this run's, before the first load. Only
354
+ // when the service's own rows came back with it: a bundle that keeps them out of the restore (its `notHydrated`)
355
+ // starts its first load as before, never from rows it chose not to restore.
356
+ const restored = (handle as { getState?: () => State }).getState?.();
357
+ const rowsKey = COLLECTIONS[name]?.rows;
358
+ const restoredRows = rowsKey ? restored?.[rowsKey] : undefined;
359
+ if (Array.isArray(restoredRows) && restoredRows.length > 0) controller.seed(restored?.collectionShown);
348
360
  publish = () => handle.collectionDataChanged();
349
361
  return handle;
350
362
  }
@@ -45,7 +45,9 @@ export function withIapPresentation<S extends object>(options: {
45
45
  return project(next);
46
46
  }]));
47
47
  const service = (options.register ?? defineService)(name, {
48
- ...definition, notHydrated: [...(definition.notHydrated ?? []), ...options.transientKeys],
48
+ // The collection's shown page goes with the rows and chrome it copies: kept out of the restore with them.
49
+ ...definition, notHydrated: [...(definition.notHydrated ?? []), ...options.transientKeys,
50
+ ...((options.transientKeys as string[]).includes('collection') ? ['collectionShown' as keyof S & string] : [])],
49
51
  actions: { ...actions, iapPublish: project, iapPoll: async () => { await commerce.poll(); return undefined; },
50
52
  iapRefresh: async () => { await commerce.loadPrices(items); return undefined; } },
51
53
  });
@@ -51696,20 +51696,6 @@ const SCREENS = runtimeSetting({
51696
51696
  }
51697
51697
  }
51698
51698
  },
51699
- {
51700
- "id": "",
51701
- "body": {
51702
- "package": "himalaya",
51703
- "type": "Text",
51704
- "props": {
51705
- "text": "$service:himiOwnerLiteManage.summaryNote",
51706
- "style": "caption",
51707
- "color": "textSecondary",
51708
- "maxLines": 2,
51709
- "collapseWhenEmpty": true
51710
- }
51711
- }
51712
- },
51713
51699
  {
51714
51700
  "id": "",
51715
51701
  "body": {
@@ -52334,6 +52320,9 @@ const SCREENS = runtimeSetting({
52334
52320
  }
52335
52321
  ]
52336
52322
  }
52323
+ },
52324
+ "modifiers": {
52325
+ "animateContentSize": true
52337
52326
  }
52338
52327
  }
52339
52328
  }
@@ -20,6 +20,13 @@ export interface CollectionQueryState<Row> extends CollectionPage<Row> {
20
20
  pageError: boolean;
21
21
  complete: boolean;
22
22
  error: string;
23
+ /** The rows are the source's `cached` page, shown while the live read is still out. */
24
+ provisional?: boolean;
25
+ }
26
+ /** What a collection showed, as `shownPage` hands it out and `seed` takes it back on a restored boot. */
27
+ export interface CollectionShownPage<Row> {
28
+ state: CollectionQueryState<Row>;
29
+ loadedPages: number;
23
30
  }
24
31
  export interface CollectionQuerySource<Row> {
25
32
  fetch(criteria: CollectionQuery, cursor: string): Promise<CollectionPage<Row>>;
@@ -50,13 +57,19 @@ export function createCollectionQuery<Row>(source: CollectionQuerySource<Row>, p
50
57
  let state: CollectionQueryState<Row> = { criteria: normalize({}), items: [], nextCursor: '', status: 'idle', loadingMore: false, pageError: false, complete: false, error: '' };
51
58
  /** The query a revalidation is running under: its snapshots are held back until it is done. */
52
59
  let revalidating = -1;
53
- /** What the screen shows: the last published state, restored when a revalidation fails. */
54
- let shown: { state: CollectionQueryState<Row>; loadedPages: number } | undefined;
60
+ /** The last page the screen showed whole (rows ready, nothing paging, failing or standing in): what a revalidation
61
+ * keeps on screen and restores when it fails, and what a restored boot shows. A next page loading or failing after
62
+ * it leaves those rows on screen, so it is still this. */
63
+ let lastShown: { state: CollectionQueryState<Row>; loadedPages: number } | undefined;
64
+ const sameCriteria = (a: CollectionQuery, b: CollectionQuery) => JSON.stringify(a) === JSON.stringify(b);
55
65
  let clearedByError = false;
56
66
  const snapshot = (): CollectionQueryState<Row> => ({ ...state, criteria: copyQuery(state.criteria), items: [...state.items] });
57
67
  const emit = () => {
58
68
  if (disposed || revalidating === generation) return;
59
- shown = { state: snapshot(), loadedPages };
69
+ // A settled answer replaces it, an empty one included: rows the live query proved gone are never kept.
70
+ if (state.status === 'ready' && !state.pageError && !state.loadingMore && !state.provisional) {
71
+ lastShown = state.items.length > 0 ? { state: snapshot(), loadedPages } : undefined;
72
+ }
60
73
  publish(snapshot());
61
74
  };
62
75
  const merge = (old: Row[], page: Row[]): Row[] => {
@@ -77,14 +90,14 @@ export function createCollectionQuery<Row>(source: CollectionQuerySource<Row>, p
77
90
  const criteria = copyQuery(state.criteria);
78
91
  if (!append) loadedPages = 0;
79
92
  state = { ...state, status: append ? 'ready' : 'loading', loadingMore: append, pageError: false, error: '',
80
- ...(append ? {} : { items: [], nextCursor: '', total: undefined, metadata: undefined, complete: false }) };
93
+ ...(append ? {} : { items: [], nextCursor: '', total: undefined, metadata: undefined, complete: false, provisional: false }) };
81
94
  emit();
82
95
  try {
83
96
  if (!append && source.cached) {
84
97
  const cached = await source.cached(criteria).catch(() => undefined);
85
98
  if (disposed || generation !== revision) return;
86
99
  if (cached?.items.length) {
87
- state = { ...state, items: merge([], cached.items), metadata: cached.metadata, status: 'ready', complete: false };
100
+ state = { ...state, items: merge([], cached.items), metadata: cached.metadata, status: 'ready', complete: false, provisional: true };
88
101
  emit();
89
102
  }
90
103
  }
@@ -94,17 +107,18 @@ export function createCollectionQuery<Row>(source: CollectionQuerySource<Row>, p
94
107
  state = { ...state, items: merge(append ? state.items : [], page.items), nextCursor: page.nextCursor,
95
108
  total: typeof page.total === 'number' && Number.isFinite(page.total) && page.total >= 0 ? page.total : (append ? state.total : undefined),
96
109
  metadata: page.metadata ?? (append ? state.metadata : undefined),
97
- status: 'ready', loadingMore: false, pageError: false, complete: page.complete ?? !page.nextCursor };
110
+ status: 'ready', loadingMore: false, pageError: false, complete: page.complete ?? !page.nextCursor, provisional: false };
98
111
  loadedPages++;
99
112
  } catch (error) {
100
113
  if (disposed || generation !== revision) return;
101
114
  const clear = source.clearOnError?.(error) === true;
102
115
  // Revoked access always ends what a revalidation showed, whether or not the source clears its rows.
103
116
  clearedByError = clear || /HTTP 40[13]\b/.test(String(error));
117
+ if (clearedByError) lastShown = undefined;
104
118
  if (clear) loadedPages = 0;
105
119
  state = { ...state, ...(clear ? { items: [], nextCursor: '', total: undefined, metadata: undefined } : {}),
106
120
  status: append && !clear ? 'ready' : 'error', loadingMore: false, pageError: append && !clear, complete: false,
107
- error: source.errorMessage?.(error) ?? '' };
121
+ provisional: false, error: source.errorMessage?.(error) ?? '' };
108
122
  }
109
123
  emit();
110
124
  };
@@ -117,8 +131,7 @@ export function createCollectionQuery<Row>(source: CollectionQuerySource<Row>, p
117
131
  const next = normalize(criteria);
118
132
  const same = JSON.stringify(next) === JSON.stringify(state.criteria);
119
133
  const count = restorePages && same ? Math.max(loadedPages, restoringPages, 1) : 1;
120
- const showing = shown && shown.state.status === 'ready' && shown.state.items.length > 0 && !shown.state.pageError
121
- && !shown.state.loadingMore && JSON.stringify(shown.state.criteria) === JSON.stringify(next) ? shown : undefined;
134
+ const showing = lastShown && sameCriteria(lastShown.state.criteria, next) ? lastShown : undefined;
122
135
  restoringPages = count;
123
136
  state = { ...state, criteria: next };
124
137
  if (revalidate && showing) { revalidating = generation + 1; clearedByError = false; }
@@ -140,6 +153,27 @@ export function createCollectionQuery<Row>(source: CollectionQuerySource<Row>, p
140
153
  emit();
141
154
  }
142
155
  },
156
+ /** The page the screen shows (rows ready, nothing failing or still paging), for the service to keep with its
157
+ * state; undefined when it shows none. */
158
+ shownPage(): CollectionShownPage<Row> | undefined {
159
+ if (!lastShown || !sameCriteria(lastShown.state.criteria, state.criteria)) return undefined;
160
+ const page = lastShown.state;
161
+ return { state: { ...page, criteria: copyQuery(page.criteria), items: [...page.items] }, loadedPages: lastShown.loadedPages };
162
+ },
163
+ /** A cold boot restored from disk: what the last run showed becomes what this one shows, so its first `load`
164
+ * revalidates behind it instead of starting empty. Only before the first query, and only a well-formed page. */
165
+ seed(page: unknown): boolean {
166
+ const candidate = page && typeof page === 'object' ? page as Partial<CollectionShownPage<Row>> : undefined;
167
+ const restored = candidate?.state;
168
+ if (generation !== 0 || state.status !== 'idle' || !restored || restored.status !== 'ready'
169
+ || !Array.isArray(restored.items) || restored.items.length === 0 || typeof restored.nextCursor !== 'string') return false;
170
+ try { restored.items.forEach(item => { if (!source.id(item)) throw new Error('A backend collection row requires an identity'); }); } catch { return false; }
171
+ state = { ...state, ...restored, criteria: normalize(restored.criteria ?? {}), items: [...restored.items], status: 'ready',
172
+ loadingMore: false, pageError: false, provisional: false, error: '' };
173
+ loadedPages = Math.max(1, Math.floor(Number(candidate?.loadedPages) || 1));
174
+ lastShown = { state: snapshot(), loadedPages };
175
+ return true;
176
+ },
143
177
  refresh: () => { restoringPages = 0; return fetch(false); },
144
178
  loadMore: () => fetch(true),
145
179
  retry: () => fetch(state.pageError),
@@ -142,6 +142,7 @@ export function decorateCollection(name: string, def: { state: State; actions?:
142
142
  // the query controller carries a recoverable failure. Treat each shape as
143
143
  // one collection state so the shared UI never renders a blank catalog.
144
144
  const backendState = backend?.controller.getState();
145
+ const provisional = backendState?.provisional === true;
145
146
  const errored = s.status === 'error' || s.isErrored === true || s.errored === true || backendState?.status === 'error';
146
147
  const domainError = typeof s.error === 'string' ? s.error.trim() : '';
147
148
  const queryError = typeof backendState?.error === 'string' ? backendState.error.trim() : '';
@@ -177,13 +178,17 @@ export function decorateCollection(name: string, def: { state: State; actions?:
177
178
  // un-narrowed failure. A re-query while typing keeps it (and the search field's focus).
178
179
  const hideChrome = (!narrowed && !s.nextCursor && projected.loaded < COLLECTION_CHROME_MIN_ITEMS)
179
180
  || sourceEmpty || ((loading || errored) && rows.length === 0 && !narrowed);
180
- return { ...s, collection: { ...prefs, ...projected,
181
+ // Kept with the state, so a cold boot restored from disk shows its last page and revalidates behind it.
182
+ const collectionShown = backend?.controller.shownPage();
183
+ return { ...s, ...(backend ? { collectionShown: collectionShown ?? null } : {}), collection: { ...prefs, ...projected,
181
184
  hideChrome, noActiveCriteria: !narrowed,
182
185
  viewName, viewMessage, saveMessage, noSavedViews: prefs.savedViews.length === 0,
183
186
  cursor: s.pageError === true || s.loadingMore === true ? '' : s.nextCursor,
184
187
  noMore: !s.nextCursor || s.pageError === true || s.loadingMore === true,
185
188
  noPageError: s.pageError !== true,
186
- noOffline: s.fromCache !== true,
189
+ // Rows read from the device ahead of the live read are shown as they are: "saved offline" is what stays on
190
+ // screen when the live read failed, not a first visit that is still loading.
191
+ noOffline: s.fromCache !== true || provisional,
187
192
  noError: !errored,
188
193
  noRetry: !canRetry || signInRequired,
189
194
  noSignIn: !signInRequired,
@@ -202,7 +207,7 @@ export function decorateCollection(name: string, def: { state: State; actions?:
202
207
  filtersPresented, previewPresented: !!previewId, preview: projected.items.filter(row => row.collectionId === previewId),
203
208
  persistenceLabel: persisted.available ? copyParam('collection.onDevice', 'Saved on this device') : copyParam('collection.sessionOnly', 'Choices last for this session'),
204
209
  noEmpty: loading || errored || sourceEmpty || domainNarrowed || projected.items.length > 0,
205
- summary: backend && config.backendQuery && s.fromCache
210
+ summary: backend && config.backendQuery && s.fromCache && !provisional
206
211
  ? copyParam('collection.cached', 'Saved offline · may be outdated')
207
212
  : collectionSummary({ shown: projected.items.length, loaded: projected.loaded, backend: !!(backend && config.backendQuery),
208
213
  total: typeof s.total === 'number' ? s.total : undefined, noun: config.countNoun
@@ -345,6 +350,13 @@ export function defineBackendCollection<S extends object, Row extends object>(na
345
350
  apply: (state, page) => options.apply(state as S, page as unknown as CollectionQueryState<Row>) as State,
346
351
  });
347
352
  const handle = (options.register ?? register)(name, definition);
353
+ // Registration restored the state a cold boot saved: the page it showed is this run's, before the first load. Only
354
+ // when the service's own rows came back with it: a bundle that keeps them out of the restore (its `notHydrated`)
355
+ // starts its first load as before, never from rows it chose not to restore.
356
+ const restored = (handle as { getState?: () => State }).getState?.();
357
+ const rowsKey = COLLECTIONS[name]?.rows;
358
+ const restoredRows = rowsKey ? restored?.[rowsKey] : undefined;
359
+ if (Array.isArray(restoredRows) && restoredRows.length > 0) controller.seed(restored?.collectionShown);
348
360
  publish = () => handle.collectionDataChanged();
349
361
  return handle;
350
362
  }
@@ -17,7 +17,7 @@ import { OWNER_MODULES, ownerModuleId } from '../_shared/owner-modules.js';
17
17
  import { localPreferences } from '../_shared/local-preferences.js';
18
18
  import { stashCopy, copyParam, copyTemplate } from '../_shared/himi-copy.js';
19
19
  import { fallbackNameFrom, selectedSiteName } from '../_shared/owner-selected-site.js';
20
- import { DASHBOARD_PATHS, deriveManage, parseAreas, recentAreas, rememberArea, withMetrics, type ManageArea, type Metric } from './logic.js';
20
+ import { DASHBOARD_PATHS, deriveManage, parseAreas, recentAreas, metricFailure, rememberArea, withMetrics, type ManageArea, type Metric, type MetricFailure } from './logic.js';
21
21
 
22
22
  interface State {
23
23
  /** The business the header names: the one picked on Sites, else the descriptor's
@@ -43,7 +43,6 @@ interface State {
43
43
  noSections: boolean;
44
44
  isLoading: boolean;
45
45
  notLoading: boolean;
46
- summaryNote: string;
47
46
  handoffError: string;
48
47
  }
49
48
 
@@ -242,20 +241,26 @@ async function loadMetrics(state: State): Promise<State> {
242
241
  return newestStarted ?? settled;
243
242
  }
244
243
 
244
+ /** A row's status when its summary could not load: why, so a missing app or a denied read is not taken for a fault. */
245
+ function failureText(failure: MetricFailure): string {
246
+ switch (failure) {
247
+ case 'notSetUp': return copyParam('ownerManage.notSetUp', 'Not set up on this site');
248
+ case 'noAccess': return copyParam('ownerManage.noAccess', 'No access');
249
+ default: return copyParam('ownerManage.summaryUnavailable', 'Couldn’t load summary');
250
+ }
251
+ }
252
+
245
253
  async function readMetrics(state: State): Promise<State> {
246
254
  const keys = [...new Set(state.areas.map(area => area.metricKey))];
247
255
  const settled = await Promise.allSettled(keys.map(metricFor));
248
256
  const metrics: Record<string, Metric | undefined> = {};
249
- const unavailable = new Set<string>();
257
+ const failures = new Map<string, string>();
250
258
  settled.forEach((result, index) => {
251
259
  if (result.status === 'fulfilled') metrics[keys[index]] = result.value;
252
- else unavailable.add(keys[index]);
260
+ else failures.set(keys[index], failureText(metricFailure(result.reason)));
253
261
  });
254
- const areas = withMetrics(state.areas, metrics, unavailable);
255
- const note = unavailable.size
256
- ? copyParam('ownerManage.partialSummary', 'Some summaries couldn’t load. You can still open every business area.')
257
- : '';
258
- return derive({ ...state, areas, isLoading: false, notLoading: true, summaryNote: note, handoffError: '' }, areas);
262
+ const areas = withMetrics(state.areas, metrics, failures);
263
+ return derive({ ...state, areas, isLoading: false, notLoading: true, handoffError: '' }, areas);
259
264
  }
260
265
 
261
266
  function findArea(state: State, arg: unknown): ManageArea | undefined {
@@ -294,7 +299,7 @@ const svc = defineService<State, {
294
299
  // 'All' is always a valid filter, so the chip row paints from the first frame
295
300
  // instead of appearing (and shifting the layout) when the areas arrive.
296
301
  groups: [{ id: 'all', label: allLabel() }], summary: '', noEmpty: true,
297
- hasRecents: false, noRecents: true, noSections: true, isLoading: true, notLoading: false, summaryNote: '', handoffError: '',
302
+ hasRecents: false, noRecents: true, noSections: true, isLoading: true, notLoading: false, handoffError: '',
298
303
  },
299
304
  actions: {
300
305
  load: async (state, arg) => {
@@ -313,7 +318,7 @@ const svc = defineService<State, {
313
318
  const parsed = (() => { try { return JSON.parse(str(arg, 'areas') || '[]'); } catch { return []; } })();
314
319
  const areas = parseAreas(parsed);
315
320
  if (state.status === 'ready' && sameAreas(state.areas, areas)) return loadMetrics(revalidating(state, areas));
316
- return loadMetrics(derive({ ...state, status: 'ready', areas, isLoading: true, notLoading: false, summaryNote: '', handoffError: '' }, areas));
321
+ return loadMetrics(derive({ ...state, status: 'ready', areas, isLoading: true, notLoading: false, handoffError: '' }, areas));
317
322
  },
318
323
  // Refresh re-reads summaries for KNOWN areas. Before the descriptor has
319
324
  // delivered them there is nothing to refresh, and settling here would open
@@ -321,7 +326,7 @@ const svc = defineService<State, {
321
326
  // it is re-read either way.
322
327
  refresh: async state => {
323
328
  currentBusinessName = selectedSiteName(fallbackBusinessName);
324
- return state.status !== 'ready' ? { ...state, businessName: currentBusinessName } : loadMetrics(derive({ ...state, isLoading: true, notLoading: false, summaryNote: '', handoffError: '' }));
329
+ return state.status !== 'ready' ? { ...state, businessName: currentBusinessName } : loadMetrics(derive({ ...state, isLoading: true, notLoading: false, handoffError: '' }));
325
330
  },
326
331
  search: (state, arg) => derive({ ...state, query: str(arg, 'value').slice(0, 256) }),
327
332
  filter: (state, arg) => {