@wix/himalaya-cli 0.1543.0 → 0.1545.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 (228) hide show
  1. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/agent/src/index.d.ts +5 -2
  2. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/state/src/index.d.ts +14 -0
  3. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/state/src/read.d.ts +60 -0
  4. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/auth-providers/wix/src/index.d.ts +3 -2
  5. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/activity-counters-v1/src/index.d.ts +19 -0
  6. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/ai-site-chat/src/index.d.ts +19 -0
  7. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/analytics/src/index.d.ts +42 -2
  8. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/async-jobs/src/index.d.ts +19 -0
  9. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/automations-v1/src/index.d.ts +19 -0
  10. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/badges-v2/src/index.d.ts +19 -0
  11. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/billing-tax-v1/src/index.d.ts +19 -0
  12. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/blog-v3/src/extensions.d.ts +5 -4
  13. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/blog-v3/src/index.d.ts +19 -0
  14. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/bookings-v2/src/extensions.d.ts +4 -4
  15. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/bookings-v2/src/index.d.ts +19 -0
  16. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/business-feed/src/index.d.ts +11 -0
  17. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/business-tools-v1/src/index.d.ts +19 -0
  18. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/chat-web/src/index.d.ts +22 -1
  19. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/comments-v1/src/index.d.ts +19 -0
  20. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/contacts/src/index.d.ts +19 -0
  21. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/coupons/src/index.d.ts +24 -2
  22. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/crm-tasks/src/index.d.ts +11 -0
  23. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/dashboard-setup/src/index.d.ts +11 -0
  24. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/data-collections-v1/src/index.d.ts +19 -0
  25. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/donations/src/index.d.ts +19 -0
  26. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/ecom/src/extensions.d.ts +6 -5
  27. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/ecom/src/index.d.ts +19 -0
  28. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/email-marketing-v1/src/index.d.ts +19 -0
  29. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/events/src/index.d.ts +24 -2
  30. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/faq-v2/src/index.d.ts +19 -0
  31. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/forms/src/index.d.ts +19 -0
  32. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/forum/src/index.d.ts +19 -0
  33. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/genie-chat/src/index.d.ts +11 -0
  34. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/gift-cards/src/index.d.ts +19 -0
  35. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/groups/src/index.d.ts +19 -0
  36. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/inbox/src/extensions.d.ts +2 -1
  37. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/inbox/src/index.d.ts +19 -0
  38. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/invoices/src/index.d.ts +24 -2
  39. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/loyalty/src/index.d.ts +28 -4
  40. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/marketing-v1/src/index.d.ts +19 -0
  41. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/members-feed-viewer/src/index.d.ts +23 -1
  42. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/members-v1/src/index.d.ts +25 -3
  43. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/new-releases/src/index.d.ts +23 -1
  44. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/notification-preferences/src/index.d.ts +23 -2
  45. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/notifications-v3/src/index.d.ts +19 -0
  46. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/online-programs/src/index.d.ts +25 -3
  47. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/portfolio/src/index.d.ts +25 -3
  48. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/pricing-plans/src/index.d.ts +28 -6
  49. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/redirects-v1/src/index.d.ts +19 -0
  50. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/restaurants/src/extensions.d.ts +2 -2
  51. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/restaurants/src/index.d.ts +19 -0
  52. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/reviews/src/index.d.ts +24 -2
  53. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/seo-v1/src/index.d.ts +19 -0
  54. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/site-list/src/index.d.ts +19 -0
  55. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/site-media/src/index.d.ts +11 -0
  56. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/social-feed/src/index.d.ts +23 -1
  57. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/stores-collections/src/display.d.ts +3 -2
  58. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/stores-collections/src/index.d.ts +27 -3
  59. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/stores-v1/src/catalog.d.ts +7 -7
  60. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/stores-v1/src/display.d.ts +4 -3
  61. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/stores-v1/src/extensions.d.ts +2 -1
  62. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/stores-v1/src/index.d.ts +19 -0
  63. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/table-reservations/src/index.d.ts +25 -3
  64. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/users/src/index.d.ts +23 -1
  65. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/video/src/index.d.ts +19 -0
  66. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/wix-data/src/index.d.ts +27 -3
  67. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/workflows-v1/src/index.d.ts +19 -0
  68. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk-auto/_shared/runtime.generated.d.ts +8 -0
  69. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/checkout.mjs +12 -4
  70. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/screens.mjs +36 -11
  71. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/state.mjs +206 -12
  72. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-activity-counters.mjs +19 -5
  73. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-ai-site-chat.mjs +19 -5
  74. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-analytics.mjs +21 -7
  75. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-async-jobs.mjs +19 -5
  76. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-automations.mjs +19 -5
  77. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-badges.mjs +19 -5
  78. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-billing-tax.mjs +19 -5
  79. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-blog.mjs +31 -14
  80. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-bookings.mjs +26 -12
  81. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-business-tools.mjs +20 -6
  82. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-chat-web.mjs +29 -7
  83. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-comments.mjs +28 -5
  84. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-contacts.mjs +19 -5
  85. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-coupons.mjs +14 -6
  86. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-data-collections.mjs +19 -5
  87. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-data.mjs +15 -5
  88. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-donations.mjs +19 -5
  89. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-ecom.mjs +33 -16
  90. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-email-marketing.mjs +19 -5
  91. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-events.mjs +14 -6
  92. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-faq.mjs +20 -6
  93. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-forms.mjs +19 -5
  94. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-forum.mjs +19 -5
  95. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-gift-cards.mjs +19 -5
  96. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-groups.mjs +19 -5
  97. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-inbox.mjs +21 -7
  98. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-invoices.mjs +14 -6
  99. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-loyalty.mjs +16 -8
  100. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-marketing.mjs +19 -5
  101. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-members-feed-viewer.mjs +21 -10
  102. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-members.mjs +17 -8
  103. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-new-releases.mjs +17 -5
  104. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-notification-preferences.mjs +20 -6
  105. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-notifications.mjs +19 -5
  106. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-online-programs.mjs +16 -8
  107. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-portfolio.mjs +16 -8
  108. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-pricing-plans.mjs +22 -14
  109. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-redirects.mjs +19 -5
  110. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-restaurants.mjs +22 -7
  111. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-reviews.mjs +14 -6
  112. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-rich-content-editor.mjs +3 -0
  113. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-seo.mjs +19 -5
  114. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-site-list.mjs +19 -5
  115. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-social-feed.mjs +15 -5
  116. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-stores-collections.mjs +20 -10
  117. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-stores.mjs +116 -83
  118. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-table-reservations.mjs +16 -8
  119. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-users.mjs +15 -5
  120. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-video.mjs +9 -1
  121. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-workflows.mjs +19 -5
  122. package/dist/cli.mjs +90 -57
  123. package/dist/preview-assets.generated.bin +0 -0
  124. package/fixtures/wix-canned.json +8 -8
  125. package/guides/dynamic-list.service.ts +38 -29
  126. package/guides/form-submit.service.ts +2 -1
  127. package/guides/index.json +6 -0
  128. package/guides/offline-reading.md +63 -0
  129. package/guides/one-app-to-himi-app.md +5 -3
  130. package/guides/recipe-dynamic-list.md +2 -0
  131. package/guides/recipe-four-state.md +1 -0
  132. package/guides/sdk-reference.json +225 -99
  133. package/guides/testing-himi-apps.md +2 -0
  134. package/guides/tier-5-add-a-screen.md +2 -0
  135. package/guides/wix-api-usage.md +9 -2
  136. package/package.json +2 -2
  137. package/templates/beauty-salon/dev/index.ts +4 -3
  138. package/templates/beauty-salon/tier5-src/_shared/collection-service.ts +6 -9
  139. package/templates/beauty-salon/tier5-src/_shared/iap-presentation.ts +7 -3
  140. package/templates/beauty-salon/tier5-src/himi-branded-lite-home/index.ts +1 -1
  141. package/templates/beauty-salon/tier5-src/himi-branded-lite-orders/index.ts +3 -0
  142. package/templates/beauty-salon-owner/dev/index.ts +19 -19
  143. package/templates/beauty-salon-owner/tier5-src/_shared/collection-service.ts +6 -9
  144. package/templates/beauty-salon-owner/tier5-src/_shared/owner-module-data.ts +1 -1
  145. package/templates/branded-lite/dev/index.ts +38 -20
  146. package/templates/branded-lite/tier5-src/_shared/collection-query-legacy.ts +194 -0
  147. package/templates/branded-lite/tier5-src/_shared/collection-query.ts +158 -99
  148. package/templates/branded-lite/tier5-src/_shared/collection-service.ts +31 -11
  149. package/templates/branded-lite/tier5-src/_shared/iap-presentation.ts +7 -3
  150. package/templates/branded-lite/tier5-src/_shared/read-service.ts +255 -0
  151. package/templates/branded-lite/tier5-src/himi-branded-lite-home/index.ts +1 -1
  152. package/templates/branded-lite/tier5-src/himi-branded-lite-orders/index.ts +3 -0
  153. package/templates/branded-lite/tier5-src/himi-branded-lite-shop/index.ts +202 -159
  154. package/templates/branded-lite/tier5-src/himi-store-product/index.ts +130 -201
  155. package/templates/community-club/dev/index.ts +4 -4
  156. package/templates/community-club/tier5-src/_shared/collection-service.ts +6 -9
  157. package/templates/community-club/tier5-src/_shared/iap-presentation.ts +7 -3
  158. package/templates/community-club/tier5-src/himi-branded-lite-home/index.ts +1 -1
  159. package/templates/community-club-owner/dev/index.ts +19 -19
  160. package/templates/community-club-owner/tier5-src/_shared/collection-service.ts +6 -9
  161. package/templates/community-club-owner/tier5-src/_shared/owner-module-data.ts +1 -1
  162. package/templates/events-meetups/dev/index.ts +3 -3
  163. package/templates/events-meetups/tier5-src/_shared/collection-service.ts +6 -9
  164. package/templates/events-meetups/tier5-src/_shared/iap-presentation.ts +7 -3
  165. package/templates/events-meetups/tier5-src/himi-branded-lite-home/index.ts +1 -1
  166. package/templates/events-meetups-owner/dev/index.ts +17 -17
  167. package/templates/events-meetups-owner/tier5-src/_shared/collection-service.ts +6 -9
  168. package/templates/events-meetups-owner/tier5-src/_shared/owner-module-data.ts +1 -1
  169. package/templates/gym-fitness/dev/index.ts +5 -4
  170. package/templates/gym-fitness/tier5-src/_shared/collection-service.ts +6 -9
  171. package/templates/gym-fitness/tier5-src/_shared/iap-presentation.ts +7 -3
  172. package/templates/gym-fitness/tier5-src/himi-branded-lite-home/index.ts +1 -1
  173. package/templates/gym-fitness/tier5-src/himi-branded-lite-orders/index.ts +3 -0
  174. package/templates/gym-fitness-owner/dev/index.ts +20 -20
  175. package/templates/gym-fitness-owner/tier5-src/_shared/collection-service.ts +6 -9
  176. package/templates/gym-fitness-owner/tier5-src/_shared/owner-module-data.ts +1 -1
  177. package/templates/nonprofit-cause/dev/index.ts +4 -4
  178. package/templates/nonprofit-cause/tier5-src/_shared/collection-service.ts +6 -9
  179. package/templates/nonprofit-cause/tier5-src/_shared/iap-presentation.ts +7 -3
  180. package/templates/nonprofit-cause/tier5-src/himi-branded-lite-home/index.ts +1 -1
  181. package/templates/nonprofit-cause-owner/dev/index.ts +18 -18
  182. package/templates/nonprofit-cause-owner/tier5-src/_shared/collection-service.ts +6 -9
  183. package/templates/nonprofit-cause-owner/tier5-src/_shared/owner-module-data.ts +1 -1
  184. package/templates/owner-lite/dev/index.ts +20 -20
  185. package/templates/owner-lite/tier5-src/_shared/collection-service.ts +6 -9
  186. package/templates/owner-lite/tier5-src/_shared/owner-module-data.ts +1 -1
  187. package/templates/podcast-creator/dev/index.ts +4 -4
  188. package/templates/podcast-creator/tier5-src/_shared/collection-service.ts +6 -9
  189. package/templates/podcast-creator/tier5-src/_shared/iap-presentation.ts +7 -3
  190. package/templates/podcast-creator/tier5-src/himi-branded-lite-home/index.ts +1 -1
  191. package/templates/podcast-creator-owner/dev/index.ts +19 -19
  192. package/templates/podcast-creator-owner/tier5-src/_shared/collection-service.ts +6 -9
  193. package/templates/podcast-creator-owner/tier5-src/_shared/owner-module-data.ts +1 -1
  194. package/templates/portfolio-creative/dev/index.ts +5 -4
  195. package/templates/portfolio-creative/tier5-src/_shared/collection-service.ts +6 -9
  196. package/templates/portfolio-creative/tier5-src/_shared/iap-presentation.ts +7 -3
  197. package/templates/portfolio-creative/tier5-src/himi-branded-lite-home/index.ts +1 -1
  198. package/templates/portfolio-creative/tier5-src/himi-branded-lite-orders/index.ts +3 -0
  199. package/templates/portfolio-creative-owner/dev/index.ts +19 -19
  200. package/templates/portfolio-creative-owner/tier5-src/_shared/collection-service.ts +6 -9
  201. package/templates/portfolio-creative-owner/tier5-src/_shared/owner-module-data.ts +1 -1
  202. package/templates/restaurant/dev/index.ts +3 -3
  203. package/templates/restaurant/tier5-src/_shared/collection-service.ts +6 -9
  204. package/templates/restaurant/tier5-src/_shared/iap-presentation.ts +7 -3
  205. package/templates/restaurant/tier5-src/himi-branded-lite-home/index.ts +1 -1
  206. package/templates/restaurant-owner/dev/index.ts +20 -20
  207. package/templates/restaurant-owner/tier5-src/_shared/collection-service.ts +6 -9
  208. package/templates/restaurant-owner/tier5-src/_shared/owner-module-data.ts +1 -1
  209. package/templates/retail-store/dev/index.ts +19 -1
  210. package/templates/retail-store/tier5-src/_shared/collection-query-legacy.ts +194 -0
  211. package/templates/retail-store/tier5-src/_shared/collection-query.ts +158 -99
  212. package/templates/retail-store/tier5-src/_shared/collection-service.ts +31 -11
  213. package/templates/retail-store/tier5-src/_shared/iap-presentation.ts +7 -3
  214. package/templates/retail-store/tier5-src/_shared/read-service.ts +255 -0
  215. package/templates/retail-store/tier5-src/himi-branded-lite-home/index.ts +1 -1
  216. package/templates/retail-store/tier5-src/himi-branded-lite-orders/index.ts +3 -0
  217. package/templates/retail-store/tier5-src/himi-branded-lite-shop/index.ts +202 -159
  218. package/templates/retail-store/tier5-src/himi-store-product/index.ts +130 -201
  219. package/templates/retail-store-owner/dev/index.ts +18 -18
  220. package/templates/retail-store-owner/tier5-src/_shared/collection-service.ts +6 -9
  221. package/templates/retail-store-owner/tier5-src/_shared/owner-module-data.ts +1 -1
  222. package/templates/yoga-studio/dev/index.ts +4 -4
  223. package/templates/yoga-studio/tier5-src/_shared/collection-service.ts +6 -9
  224. package/templates/yoga-studio/tier5-src/_shared/iap-presentation.ts +7 -3
  225. package/templates/yoga-studio/tier5-src/himi-branded-lite-home/index.ts +1 -1
  226. package/templates/yoga-studio-owner/dev/index.ts +18 -18
  227. package/templates/yoga-studio-owner/tier5-src/_shared/collection-service.ts +6 -9
  228. package/templates/yoga-studio-owner/tier5-src/_shared/owner-module-data.ts +1 -1
@@ -1,3 +1,6 @@
1
+ import { createReadController, initialReadStatus, type ReadMetadata, type ReadStatus } from '@himalaya/state';
2
+ import { createLegacyCollectionQuery } from './collection-query-legacy';
3
+
1
4
  /** Backend collection lifecycle shared by domain workers. The domain supplies
2
5
  * the query and row mapper; this layer never calls a domain API or mutates it. */
3
6
  export interface CollectionQuery {
@@ -12,6 +15,9 @@ export interface CollectionPage<Row> {
12
15
  total?: number;
13
16
  metadata?: Record<string, unknown>;
14
17
  complete?: boolean;
18
+ provenance?: ReadMetadata;
19
+ /** A result derived entirely from local criteria/authored configuration (e.g. no saved IDs). */
20
+ local?: boolean;
15
21
  }
16
22
  export interface CollectionQueryState<Row> extends CollectionPage<Row> {
17
23
  criteria: CollectionQuery;
@@ -20,6 +26,7 @@ export interface CollectionQueryState<Row> extends CollectionPage<Row> {
20
26
  pageError: boolean;
21
27
  complete: boolean;
22
28
  error: string;
29
+ read: ReadStatus;
23
30
  /** The rows are the source's `cached` page, shown while the live read is still out. */
24
31
  provisional?: boolean;
25
32
  }
@@ -29,7 +36,9 @@ export interface CollectionShownPage<Row> {
29
36
  loadedPages: number;
30
37
  }
31
38
  export interface CollectionQuerySource<Row> {
32
- fetch(criteria: CollectionQuery, cursor: string): Promise<CollectionPage<Row>>;
39
+ fetch(criteria: CollectionQuery, cursor: string, options?: CollectionReadOptions): Promise<CollectionPage<Row>>;
40
+ /** Source forwards cache mode and metadata to every SDK read. */
41
+ cacheRead?: boolean;
33
42
  /** Optional public cache for immediate paint; always incomplete, never a paging authority. */
34
43
  cached?(criteria: CollectionQuery): Promise<CollectionPage<Row> | undefined>;
35
44
  id(row: Row): string;
@@ -39,6 +48,10 @@ export interface CollectionQuerySource<Row> {
39
48
  * key carries the account, and a purge tears parked workers down). */
40
49
  clearOnError?(error: unknown): boolean;
41
50
  }
51
+ export interface CollectionReadOptions {
52
+ cache: 'only-if-cached' | 'reload';
53
+ onCacheResult(metadata: ReadMetadata): void;
54
+ }
42
55
  const normalize = (q: Partial<CollectionQuery>): CollectionQuery => ({
43
56
  query: typeof q.query === 'string' ? q.query.trim().slice(0, 256) : '',
44
57
  filter: typeof q.filter === 'string' ? q.filter.slice(0, 256) : 'all',
@@ -47,30 +60,33 @@ const normalize = (q: Partial<CollectionQuery>): CollectionQuery => ({
47
60
  });
48
61
  const copyQuery = (q: CollectionQuery): CollectionQuery => ({ ...q, savedIds: [...q.savedIds] });
49
62
 
50
- /** New criteria cancel the old result logically, including an in-flight next
51
- * page. Failures preserve the cursor and rows for an explicit retry. Callers
52
- * publish each snapshot through their ordinary service reducer, so loading and
53
- * filtered-empty never masquerade as a successfully completed query. */
63
+
64
+ /** Refreshes stage a complete replacement at the currently loaded depth. Old rows
65
+ * and cursors remain usable until every replacement page succeeds. Query changes
66
+ * start a separate snapshot; they never reuse a subset from another query. */
54
67
  export function createCollectionQuery<Row>(source: CollectionQuerySource<Row>, publish: (state: CollectionQueryState<Row>) => void) {
55
- let generation = 0, disposed = false;
56
- let loadedPages = 0, restoringPages = 0;
57
- let state: CollectionQueryState<Row> = { criteria: normalize({}), items: [], nextCursor: '', status: 'idle', loadingMore: false, pageError: false, complete: false, error: '' };
58
- /** The query a revalidation is running under: its snapshots are held back until it is done. */
59
- let revalidating = -1;
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;
68
+ // Only opted-in reads use the new snapshot/recovery controller. Other template collections
69
+ // keep their established query, pagination, and warm-return behavior.
70
+ if (!source.cacheRead) return createLegacyCollectionQuery(source, publish);
71
+ const sample = (globalThis as { himalaya?: { input?: Record<string, unknown> } }).himalaya?.input?.__himiOwnerPreview === 'sample';
72
+ const cacheRead = source.cacheRead && !sample;
73
+ const shouldClearOnError = (error: unknown) => source.clearOnError?.(error) === true
74
+ || /HTTP 40[13]\b|forbidden|principal.changed|identity.changed/i.test(String(error));
75
+ let generation = 0, disposed = false, loadedPages = 1, wantedPages = 1, restoringPages = 1, key = '';
76
+ let pending: Promise<void> | undefined;
77
+ let suppressPublish = false;
78
+ let state: CollectionQueryState<Row> = { criteria: normalize({}), items: [], nextCursor: '', status: 'idle',
79
+ loadingMore: false, pageError: false, complete: false, error: '', read: initialReadStatus() };
80
+ let lastShown: CollectionShownPage<Row> | undefined;
64
81
  const sameCriteria = (a: CollectionQuery, b: CollectionQuery) => JSON.stringify(a) === JSON.stringify(b);
65
- let clearedByError = false;
66
- const snapshot = (): CollectionQueryState<Row> => ({ ...state, criteria: copyQuery(state.criteria), items: [...state.items] });
82
+ const snapshot = (): CollectionQueryState<Row> => ({ ...state, read: { ...state.read }, criteria: copyQuery(state.criteria), items: [...state.items] });
67
83
  const emit = () => {
68
- if (disposed || revalidating === generation) return;
69
- // A settled answer replaces it, an empty one included: rows the live query proved gone are never kept, and a list
70
- // with nothing in it comes back as its empty state, never its loading look.
84
+ if (disposed) return;
85
+ // A settled answer replaces it, an empty one included: rows the live query proved gone are never kept.
71
86
  if (state.status === 'ready' && !state.pageError && !state.loadingMore && !state.provisional) {
72
87
  lastShown = { state: snapshot(), loadedPages };
73
88
  }
89
+ if (suppressPublish) return;
74
90
  publish(snapshot());
75
91
  };
76
92
  const merge = (old: Row[], page: Row[]): Row[] => {
@@ -84,91 +100,124 @@ export function createCollectionQuery<Row>(source: CollectionQuerySource<Row>, p
84
100
  }
85
101
  return result;
86
102
  };
87
- /** [keepRows]: a search typed over the rows on screen leaves them there until its answer arrives (status stays
88
- * `loading`, and nothing pages under the old answer), so the list is not blanked on each keystroke. */
89
- const fetch = async (append: boolean, keepRows = false): Promise<void> => {
90
- if (disposed || (append && (state.loadingMore || state.status === 'loading' || !state.nextCursor))) return;
91
- const revision = append ? generation : ++generation;
92
- const cursor = append ? state.nextCursor : '';
93
- const criteria = copyQuery(state.criteria);
94
- if (!append) loadedPages = 0;
95
- state = { ...state, status: append ? 'ready' : 'loading', loadingMore: append, pageError: false, error: '',
96
- ...(append ? {} : keepRows ? { nextCursor: '', complete: false, provisional: false }
97
- : { items: [], nextCursor: '', total: undefined, metadata: undefined, complete: false, provisional: false }) };
98
- emit();
99
- try {
100
- if (!append && source.cached) {
101
- const cached = await source.cached(criteria).catch(() => undefined);
102
- if (disposed || generation !== revision) return;
103
- if (cached?.items.length) {
104
- state = { ...state, items: merge([], cached.items), metadata: cached.metadata, status: 'ready', complete: false, provisional: true };
105
- emit();
106
- }
103
+ const combine = (a: ReadMetadata, b: ReadMetadata): ReadMetadata => ({
104
+ source: a.source === 'cache' || b.source === 'cache' ? 'cache' : 'network',
105
+ fetchedAt: Math.min(a.fetchedAt, b.fetchedAt), cacheable: a.cacheable && b.cacheable,
106
+ expiresAt: Math.min(a.expiresAt ?? 0, b.expiresAt ?? 0),
107
+ });
108
+ const readPage = async (criteria: CollectionQuery, cursor: string, cache: 'only-if-cached' | 'reload') => {
109
+ let provenance: ReadMetadata | undefined;
110
+ const page = await source.fetch(criteria, cursor, { cache, onCacheResult(value) { provenance = provenance ? combine(provenance, value) : value; } });
111
+ if (!Array.isArray(page.items) || typeof page.nextCursor !== 'string' || (cursor && page.nextCursor === cursor))
112
+ throw new Error('Invalid collection page');
113
+ if (page.local === true) provenance = { source: 'local', fetchedAt: 0, cacheable: false };
114
+ if (cacheRead && !provenance && !page.provenance) throw new Error('Read source did not provide response provenance');
115
+ // A legacy/error fallback is never a successful server refresh.
116
+ if (page.metadata?.fromCache && !provenance) throw new Error('Cache response has no policy provenance');
117
+ return { ...page, items: merge([], page.items), provenance: provenance ?? page.provenance ??
118
+ { source: 'network' as const, fetchedAt: Date.now(), cacheable: false } };
119
+ };
120
+ const reader = createReadController<CollectionPage<Row>>({
121
+ allowLocal: true,
122
+ ...(sample ? { online: () => true } : {}),
123
+ ...(!sample && (source.cached || cacheRead) ? { cached: async (readKey: string) => {
124
+ const criteria = JSON.parse(readKey) as CollectionQuery;
125
+ const page = cacheRead ? await readPage(criteria, '', 'only-if-cached') : await source.cached?.(criteria);
126
+ if (!page?.provenance || !['cache', 'local'].includes(page.provenance.source)) return undefined;
127
+ return { data: { ...page, nextCursor: '', complete: !page.nextCursor }, metadata: page.provenance };
128
+ } } : {}),
129
+ fetch: async (readKey) => {
130
+ const criteria = JSON.parse(readKey) as CollectionQuery, revision = generation, depth = wantedPages;
131
+ let page = await readPage(criteria, '', 'reload'), count = 1;
132
+ const seen = new Set<string>();
133
+ while (page.nextCursor && count < depth) {
134
+ if (disposed || revision !== generation) throw new Error('Read disposed');
135
+ if (seen.has(page.nextCursor)) throw new Error('Repeated collection cursor');
136
+ seen.add(page.nextCursor);
137
+ const next = await readPage(criteria, page.nextCursor, 'reload');
138
+ if (next.provenance.source !== 'network') throw new Error('Refresh returned cached data');
139
+ page = { ...page, items: merge(page.items, next.items), nextCursor: next.nextCursor, complete: next.complete, provenance: combine(page.provenance, next.provenance) };
140
+ count++;
107
141
  }
108
- const page = await source.fetch(criteria, cursor);
109
- if (disposed || generation !== revision) return;
110
- if (!Array.isArray(page.items) || typeof page.nextCursor !== 'string' || (cursor && page.nextCursor === cursor)) throw new Error('Invalid collection page');
111
- state = { ...state, items: merge(append ? state.items : [], page.items), nextCursor: page.nextCursor,
112
- total: typeof page.total === 'number' && Number.isFinite(page.total) && page.total >= 0 ? page.total : (append ? state.total : undefined),
113
- metadata: page.metadata ?? (append ? state.metadata : undefined),
114
- status: 'ready', loadingMore: false, pageError: false, complete: page.complete ?? !page.nextCursor, provisional: false };
142
+ if (!disposed && revision === generation) loadedPages = count;
143
+ return { data: page, metadata: page.provenance };
144
+ },
145
+ clearOnError: error => {
146
+ const clear = shouldClearOnError(error);
147
+ if (clear) lastShown = undefined;
148
+ return clear;
149
+ },
150
+ errorMessage: source.errorMessage,
151
+ publish: (page, read) => {
152
+ state = { ...state, ...(page ?? { items: [], nextCursor: '', total: undefined, metadata: undefined }),
153
+ metadata: page ? { ...page.metadata, fromCache: read.fromCache } : undefined,
154
+ read, status: read.hasSnapshot ? 'ready' : read.refreshing ? 'loading' : 'error',
155
+ complete: !!page && (page.complete ?? !page.nextCursor), loadingMore: false, pageError: false,
156
+ provisional: read.fromCache && read.refreshing,
157
+ error: read.offline && !read.hasSnapshot ? "This content isn’t available offline. Connect to load it." : read.error };
158
+ emit();
159
+ },
160
+ });
161
+ const query = (criteria: Partial<CollectionQuery>, restorePages = false, _revalidate = false): Promise<void> => {
162
+ if (disposed) return Promise.resolve();
163
+ const next = normalize(criteria), nextKey = JSON.stringify(next);
164
+ if (key === nextKey && pending) return pending;
165
+ const same = key === nextKey;
166
+ if (!same) { loadedPages = 1; restoringPages = 1; }
167
+ key = nextKey; generation++; wantedPages = same ? Math.max(1, loadedPages, restorePages ? restoringPages : 1) : 1;
168
+ state = { ...state, criteria: next };
169
+ const revision = generation;
170
+ pending = reader.load(key).finally(() => { if (revision === generation) pending = undefined; });
171
+ return pending;
172
+ };
173
+ const loadMore = async () => {
174
+ if (disposed || pending || state.loadingMore || !state.nextCursor) return;
175
+ const revision = generation, criteria = copyQuery(state.criteria), cursor = state.nextCursor;
176
+ state = { ...state, loadingMore: true, pageError: false }; emit();
177
+ try {
178
+ const page = await readPage(criteria, cursor, 'reload');
179
+ if (disposed || revision !== generation) return;
180
+ if (page.provenance.source !== 'network') throw new Error('Page returned cached data');
181
+ const provenance = state.provenance ? combine(state.provenance, page.provenance) : page.provenance;
182
+ state = { ...state, ...page, provenance, items: merge(state.items, page.items), total: page.total ?? state.total,
183
+ metadata: page.metadata ?? state.metadata, loadingMore: false, pageError: false, complete: page.complete ?? !page.nextCursor };
115
184
  loadedPages++;
185
+ wantedPages = loadedPages;
186
+ // Keep the read controller's retained snapshot aligned with appended pages.
187
+ reader.replace({ data: state, metadata: provenance });
116
188
  } catch (error) {
117
- if (disposed || generation !== revision) return;
118
- const clear = source.clearOnError?.(error) === true;
119
- // Revoked access always ends what a revalidation showed, whether or not the source clears its rows.
120
- clearedByError = clear || /HTTP 40[13]\b/.test(String(error));
121
- if (clearedByError) lastShown = undefined;
122
- if (clear) loadedPages = 0;
123
- // The rows a typed search kept on screen while it ran were never its answer: a failed one shows none of them.
124
- const drop = clear || keepRows;
125
- state = { ...state, ...(drop ? { items: [], nextCursor: '', total: undefined, metadata: undefined } : {}),
126
- status: append && !clear ? 'ready' : 'error', loadingMore: false, pageError: append && !clear, complete: false,
127
- provisional: false, error: source.errorMessage?.(error) ?? '' };
189
+ if (disposed || revision !== generation) return;
190
+ if (shouldClearOnError(error)) {
191
+ loadedPages = 1; wantedPages = 1; lastShown = undefined; reader.invalidate();
192
+ const message = source.errorMessage?.(error) || 'This content is unavailable for this account. Try signing in again.';
193
+ state = { ...state, status: 'error', loadingMore: false, pageError: false,
194
+ error: message, read: { ...state.read, error: message } };
195
+ emit();
196
+ return;
197
+ }
198
+ state = { ...state, loadingMore: false, pageError: true, complete: false,
199
+ error: source.errorMessage?.(error) || 'The next page could not load. Try again.' };
200
+ emit();
128
201
  }
129
- emit();
130
202
  };
203
+ // A late page must not repopulate a cleared/private snapshot after logout or disconnect.
204
+ const host = (globalThis as { himalaya?: { __subscribeReadLifecycle?(listener: (signal: string) => void): () => void } }).himalaya;
205
+ const stop = host?.__subscribeReadLifecycle?.(signal => {
206
+ if (signal === 'invalidate' || signal === 'offline' || signal === 'dispose') {
207
+ generation++; pending = undefined;
208
+ if (signal !== 'offline') { loadedPages = 1; wantedPages = 1; lastShown = undefined; }
209
+ if (signal === 'dispose') disposed = true;
210
+ }
211
+ });
131
212
  return {
132
213
  getState: snapshot,
133
- /** [revalidate]: a screen coming back re-queries the rows it shows. They stay on screen until every
134
- * page is in, and a failure keeps them; revoked access still clears them, and other criteria reset. */
135
- async query(criteria: Partial<CollectionQuery>, restorePages = false, revalidate = false): Promise<void> {
136
- if (disposed) return Promise.resolve();
137
- const next = normalize(criteria);
138
- const same = JSON.stringify(next) === JSON.stringify(state.criteria);
139
- const count = restorePages && same ? Math.max(loadedPages, restoringPages, 1) : 1;
140
- const showing = lastShown && sameCriteria(lastShown.state.criteria, next) ? lastShown : undefined;
141
- restoringPages = count;
142
- // Only the search text changed: the user is typing over the rows on screen, also while the last keystroke's
143
- // search is still out.
144
- const typing = !same && (state.status === 'ready' || state.status === 'loading')
145
- && sameCriteria({ ...state.criteria, query: '' }, { ...next, query: '' });
146
- state = { ...state, criteria: next };
147
- if (revalidate && showing) { revalidating = generation + 1; clearedByError = false; }
148
- const pending = fetch(false, typing);
149
- const revision = generation;
150
- await pending;
151
- // Re-appearance revalidates every previously opened page using fresh
152
- // cursors. A changed query, error or revoked access stops the restoration.
153
- while (!disposed && generation === revision && loadedPages < count && state.status === 'ready'
154
- && !state.pageError && !state.loadingMore && state.nextCursor) await fetch(true);
155
- if (generation === revision) restoringPages = 0;
156
- if (revalidating === revision) {
157
- revalidating = -1;
158
- if (generation !== revision) return;
159
- if ((state.status !== 'ready' || state.pageError) && showing && !clearedByError) {
160
- state = { ...showing.state, criteria: next };
161
- loadedPages = showing.loadedPages;
162
- }
163
- emit();
164
- }
214
+ query: (criteria: Partial<CollectionQuery>, restorePages = false, revalidate = false) => {
215
+ restoringPages = restorePages ? Math.max(1, loadedPages) : 1;
216
+ return query(criteria, restorePages, revalidate);
165
217
  },
166
- /** The page the screen shows (rows ready, nothing failing or still paging), for the service to keep with its
167
- * state; undefined when it shows none. */
168
218
  shownPage(): CollectionShownPage<Row> | undefined {
169
219
  if (!lastShown || !sameCriteria(lastShown.state.criteria, state.criteria)) return undefined;
170
- const page = lastShown.state;
171
- return { state: { ...page, criteria: copyQuery(page.criteria), items: [...page.items] }, loadedPages: lastShown.loadedPages };
220
+ return { state: { ...lastShown.state, read: { ...lastShown.state.read }, criteria: copyQuery(lastShown.state.criteria), items: [...lastShown.state.items] }, loadedPages: lastShown.loadedPages };
172
221
  },
173
222
  /** A cold boot restored from disk: what the last run showed becomes what this one shows, so its first `load`
174
223
  * revalidates behind it instead of starting empty. Only before the first query, and only a well-formed page; an
@@ -176,18 +225,28 @@ export function createCollectionQuery<Row>(source: CollectionQuerySource<Row>, p
176
225
  seed(page: unknown): boolean {
177
226
  const candidate = page && typeof page === 'object' ? page as Partial<CollectionShownPage<Row>> : undefined;
178
227
  const restored = candidate?.state;
228
+ const provenance = restored?.provenance;
179
229
  if (generation !== 0 || state.status !== 'idle' || !restored || restored.status !== 'ready'
180
- || !Array.isArray(restored.items) || typeof restored.nextCursor !== 'string') return false;
230
+ || !Array.isArray(restored.items) || typeof restored.nextCursor !== 'string'
231
+ || !provenance || !['cache', 'network'].includes(provenance.source) || !provenance.cacheable
232
+ || provenance.expiresAt === undefined || provenance.expiresAt <= Date.now()) return false;
181
233
  try { restored.items.forEach(item => { if (!source.id(item)) throw new Error('A backend collection row requires an identity'); }); } catch { return false; }
182
234
  state = { ...state, ...restored, criteria: normalize(restored.criteria ?? {}), items: [...restored.items], status: 'ready',
183
235
  loadingMore: false, pageError: false, provisional: false, error: '' };
184
236
  loadedPages = Math.max(1, Math.floor(Number(candidate?.loadedPages) || 1));
237
+ wantedPages = loadedPages;
238
+ const saved = state.provenance;
239
+ if (!saved) return false;
240
+ suppressPublish = true;
241
+ key = JSON.stringify(state.criteria);
242
+ reader.replace({ data: state, metadata: saved }, key);
243
+ suppressPublish = false;
185
244
  lastShown = { state: snapshot(), loadedPages };
186
245
  return true;
187
246
  },
188
- refresh: () => { restoringPages = 0; return fetch(false); },
189
- loadMore: () => fetch(true),
190
- retry: () => fetch(state.pageError),
191
- dispose() { disposed = true; generation++; },
247
+ refresh: () => query(state.criteria, true, true),
248
+ loadMore,
249
+ retry: () => state.pageError ? loadMore() : query(state.criteria, true, true),
250
+ dispose() { disposed = true; generation++; stop?.(); reader.dispose(); },
192
251
  };
193
252
  }
@@ -12,6 +12,8 @@ export interface CollectionSaving {
12
12
  type State = Record<string, unknown>;
13
13
  interface BackendCollection {
14
14
  restorePagesOnLoad?: boolean;
15
+ /** Opt in only for templates whose generated screens adopt the shared read status. */
16
+ readLifecycle?: boolean;
15
17
  /** Sees the load action's argument (the screen's onAppear params) BEFORE the query runs. A returned
16
18
  * `{ filter, sort }` resets the criteria for that load (a screen that always opens on one view). */
17
19
  onLoad?(arg: unknown): CollectionCriteriaReset | void;
@@ -83,6 +85,8 @@ export function decorateCollection(name: string, def: { state: State; actions?:
83
85
  })
84
86
  : [];
85
87
  let pendingFilter = '';
88
+ const requestedReadLifecycle = (def as { readLifecycle?: { refreshAction: string; statusPath: string } }).readLifecycle;
89
+ const hasReadLifecycle = !!requestedReadLifecycle || backend?.readLifecycle === true;
86
90
  const applyNavigationCriteria = (arg: unknown, state: State): boolean => {
87
91
  const input = object(arg);
88
92
  let changed = false;
@@ -144,16 +148,18 @@ export function decorateCollection(name: string, def: { state: State; actions?:
144
148
  if (previewId && !rows.some(row => collectionId(row) === previewId)) previewId = '';
145
149
  const filterChoices = [{ id: 'all', label: copyParam('collection.all', 'All') }, ...(config.rowStyle === 'flat' ? [] : [{ id: 'saved', label: copyParam('collection.saved', 'Saved') }]),
146
150
  ...projected.filters.map(label => ({ id: `value:${label}`, label: copyParam(`collection.filter.${label}`, label) }))];
147
- const loading = s.status === 'loading' || s.isLoading === true;
151
+ const readStatus = hasReadLifecycle ? s.readStatus as { hasSnapshot?: boolean; refreshing?: boolean } | undefined : undefined;
152
+ const loading = readStatus ? readStatus.refreshing === true && readStatus.hasSnapshot !== true : s.status === 'loading' || s.isLoading === true;
148
153
  // Older domain reducers expose `errored` rather than `isErrored`; backend
149
154
  // adapters can also intentionally keep their display status "ready" while
150
155
  // the query controller carries a recoverable failure. Treat each shape as
151
156
  // one collection state so the shared UI never renders a blank catalog.
152
157
  const backendState = backend?.controller.getState();
158
+ const read = backend?.readLifecycle ? s.readStatus as { hasSnapshot?: boolean; offline?: boolean; error?: string } | undefined : undefined;
153
159
  const provisional = backendState?.provisional === true;
154
- const errored = s.status === 'error' || s.isErrored === true || s.errored === true || backendState?.status === 'error';
160
+ const errored = !!read?.error || (backend?.readLifecycle === true && !!backendState?.error) || s.status === 'error' || s.isErrored === true || s.errored === true || backendState?.status === 'error';
155
161
  const domainError = typeof s.error === 'string' ? s.error.trim() : '';
156
- const queryError = typeof backendState?.error === 'string' ? backendState.error.trim() : '';
162
+ const queryError = read?.offline && !read.hasSnapshot ? 'This content isn’t available offline. Connect to load it.' : read?.error || (typeof backendState?.error === 'string' ? backendState.error.trim() : '');
157
163
  // Some domains carry their failure sentence in `emptyText` (portfolio, podcasts);
158
164
  // the screen folds that node into this block, so keep the domain's own words.
159
165
  const domainEmptyError = errored && typeof s.emptyText === 'string' ? s.emptyText.trim() : '';
@@ -178,7 +184,7 @@ export function decorateCollection(name: string, def: { state: State; actions?:
178
184
  // "All" and the collection's own default (e.g. Upcoming) are both un-narrowed: Clear
179
185
  // resets to "All", so showing it on either would offer a reset that changes nothing.
180
186
  const narrowed = !!prefs.query.trim() || (prefs.filter !== 'all' && prefs.filter !== initialCollection(config).filter) || domainNarrowed;
181
- const sourceEmpty = !loading && !errored && rows.length === 0 &&
187
+ const sourceEmpty = (backend?.readLifecycle !== true || backendState?.read.hasSnapshot === true) && !loading && !errored && rows.length === 0 &&
182
188
  (!(backend && config.backendQuery) || !narrowed) && !domainNarrowed;
183
189
  const emptySourceText = (typeof s.emptyText === 'string' && s.emptyText.trim()) || copyParam('collection.emptySource', 'Nothing here yet');
184
190
  // Chrome is noise over a small un-narrowed list (fewer than COLLECTION_CHROME_MIN_ITEMS
@@ -196,14 +202,15 @@ export function decorateCollection(name: string, def: { state: State; actions?:
196
202
  noPageError: s.pageError !== true,
197
203
  // Rows read from the device ahead of the live read are shown as they are: "saved offline" is what stays on
198
204
  // screen when the live read failed, not a first visit that is still loading.
199
- noOffline: s.fromCache !== true || provisional,
205
+ noOffline: !(backend?.readLifecycle === true && backendState?.read.stale || s.fromCache === true) || provisional,
200
206
  noError: !errored,
201
207
  noRetry: !canRetry || signInRequired,
202
208
  noSignIn: !signInRequired,
203
209
  errorTitle: signInRequired ? copyParam('collection.signInTitle', 'Sign in to see this') : copyParam('collection.loadErrorTitle', 'This collection could not load'),
204
210
  errorMessage,
205
- // A skeleton only with nothing to show: a search re-running over the rows on screen keeps them.
206
- noLoading: !loading || rows.length > 0,
211
+ isLoading: loading, notLoading: !loading,
212
+ // Legacy searches retain visible rows; opted-in reads also retain valid empty snapshots.
213
+ noLoading: !loading || rows.length > 0 || (backend?.readLifecycle === true && backendState?.read.hasSnapshot === true),
207
214
  // De-dup against a domain that paints its OWN empty block (screen-kit emptyBlock is
208
215
  // gated on the domain's top-level `noEmpty`): only one empty state ever shows.
209
216
  noEmptySource: !sourceEmpty || s.noEmpty === false,
@@ -330,7 +337,11 @@ export function decorateCollection(name: string, def: { state: State; actions?:
330
337
  };
331
338
  actions.refresh = (_s, arg) => {
332
339
  stashCopy(arg); syncSaved();
333
- return backend.controller.query(criteria()).then(() => undefined);
340
+ // Reconnect refreshes the currently visible result. Rebuild every page the
341
+ // user had already opened, using fresh cursors, before replacing the snapshot.
342
+ return (backend.readLifecycle
343
+ ? backend.controller.query(criteria(), true, true)
344
+ : backend.controller.query(criteria())).then(() => undefined);
334
345
  };
335
346
  actions.loadMore = () => backend.controller.loadMore().then(() => undefined);
336
347
  actions.retry = actions.collectionRetry = () => backend.controller.retry().then(() => undefined);
@@ -344,7 +355,10 @@ export function decorateCollection(name: string, def: { state: State; actions?:
344
355
  const retry = actions.refresh ?? actions.load;
345
356
  if (retry) actions.collectionRetry = retry;
346
357
  }
347
- return { state: derive(def.state), actions };
358
+ return { state: derive(def.state), actions,
359
+ ...(requestedReadLifecycle || backend?.readLifecycle ? { readPresentation: derive } : {}),
360
+ ...(requestedReadLifecycle ? { readLifecycle: requestedReadLifecycle }
361
+ : backend?.readLifecycle && runtime?.input?.__himiOwnerPreview !== 'sample' ? { readLifecycle: { refreshAction: 'refresh', statusPath: 'readStatus' } } : {}) };
348
362
  }
349
363
 
350
364
  /** Domain-owned page source with shared query controls. Publishing uses a normal
@@ -357,6 +371,8 @@ export function defineBackendCollection<S extends object, Row extends object>(na
357
371
  onlineWriteMessageFields?: readonly string[];
358
372
  /** Revalidate already opened pages on navigation return; explicit refresh resets. */
359
373
  restorePagesOnLoad?: boolean;
374
+ /** Opt this shared list into the read status and reconnect lifecycle. */
375
+ readLifecycle?: boolean;
360
376
  /** The load action's argument (onAppear params beyond `copy`), before the query runs — for a
361
377
  * domain setting that rides the descriptor, e.g. My Bookings' `weeklyGoal`. */
362
378
  onLoad?(arg: unknown): CollectionCriteriaReset | void;
@@ -367,12 +383,16 @@ export function defineBackendCollection<S extends object, Row extends object>(na
367
383
  actions?: Record<string, (state: S, ...args: unknown[]) => S | undefined | Promise<S | undefined>>;
368
384
  }) {
369
385
  let publish: (() => unknown) | undefined;
370
- const controller = createCollectionQuery(options.source, () => { publish?.(); });
386
+ const controller = createCollectionQuery({ ...options.source,
387
+ // Access-denial clearing belongs to the opt-in read lifecycle. Keep the legacy
388
+ // collection contract unchanged for templates that have not adopted it.
389
+ ...(options.readLifecycle ? { clearOnError: error => /HTTP (401|403)\b|forbidden|principal.changed|identity.changed/i.test(String(error)) || options.source.clearOnError?.(error) === true } : {}) }, () => { publish?.(); });
371
390
  const definition = decorateCollection(name, { state: options.state as State, actions: options.actions as Record<string, Reducer> | undefined }, options.saving, {
372
391
  restorePagesOnLoad: options.restorePagesOnLoad,
392
+ readLifecycle: options.readLifecycle,
373
393
  onLoad: options.onLoad,
374
394
  controller: controller as unknown as BackendCollection['controller'],
375
- apply: (state, page) => options.apply(state as S, page as unknown as CollectionQueryState<Row>) as State,
395
+ apply: (state, page) => ({ ...options.apply(state as S, page as unknown as CollectionQueryState<Row>), ...(options.readLifecycle ? { readStatus: page.read } : {}) }) as State,
376
396
  });
377
397
  const registerService = options.onlineWrites ? withOnlineWrites(options.register ?? register, ['confirmCancel', 'confirmOperation', 'retryOperation', 'commitWrite'], { messageFields: options.onlineWriteMessageFields ?? ['actionMessage'] }) : (options.register ?? register);
378
398
  const handle = registerService(name, definition);
@@ -103,6 +103,8 @@ export function withIapPresentation<S extends object>(options: {
103
103
  // scopeLanded is the completion of the active load, not a stale load result. It
104
104
  // must publish even if another overlapping onAppear load advanced contentVersion.
105
105
  if (!next || (content && action !== "scopeLanded" && version !== contentVersion)) return undefined;
106
+ // A committed read projection supersedes older load results still carrying a skeleton.
107
+ if (action === "readChanged" && definition.readLifecycle) contentVersion++;
106
108
  if (content) {
107
109
  options.onContent?.(next);
108
110
  const nextItems = options.items(next);
@@ -130,9 +132,11 @@ export function withIapPresentation<S extends object>(options: {
130
132
  };
131
133
  try {
132
134
  const next = reduce(state, ...args);
133
- // Keep synchronous sidecars synchronous. Adding an await lets their old
134
- // catalog snapshot commit after a newer asynchronous refresh.
135
- if (content || (next && typeof (next as Promise<S | undefined>).then === "function")) {
135
+ // A cached read projection must commit before another onAppear can consume it.
136
+ // Other content actions retain their scheduling: shop filters and panels can
137
+ // change while a load is pending, and their existing completion order matters.
138
+ const synchronousReadProjection = action === "readChanged" && !!definition.readLifecycle;
139
+ if ((content && !synchronousReadProjection) || (next && typeof (next as Promise<S | undefined>).then === "function")) {
136
140
  return (async () => {
137
141
  try { return presentResult(await next); }
138
142
  finally { finished(); }