@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
@@ -0,0 +1,194 @@
1
+ import { initialReadStatus, type ReadStatus } from '@himalaya/state';
2
+
3
+ /** Backend collection lifecycle shared by domain workers. The domain supplies
4
+ * the query and row mapper; this layer never calls a domain API or mutates it. */
5
+ export interface CollectionQuery {
6
+ query: string;
7
+ filter: string;
8
+ sort: 'default' | 'az' | 'za';
9
+ savedIds: string[];
10
+ }
11
+ export interface CollectionPage<Row> {
12
+ items: Row[];
13
+ nextCursor: string;
14
+ total?: number;
15
+ metadata?: Record<string, unknown>;
16
+ complete?: boolean;
17
+ }
18
+ export interface CollectionQueryState<Row> extends CollectionPage<Row> {
19
+ criteria: CollectionQuery;
20
+ status: 'idle' | 'loading' | 'ready' | 'error';
21
+ loadingMore: boolean;
22
+ pageError: boolean;
23
+ complete: boolean;
24
+ error: string;
25
+ read: ReadStatus;
26
+ /** The rows are the source's `cached` page, shown while the live read is still out. */
27
+ provisional?: boolean;
28
+ }
29
+ /** What a collection showed, as `shownPage` hands it out and `seed` takes it back on a restored boot. */
30
+ export interface CollectionShownPage<Row> {
31
+ state: CollectionQueryState<Row>;
32
+ loadedPages: number;
33
+ }
34
+ export interface CollectionQuerySource<Row> {
35
+ fetch(criteria: CollectionQuery, cursor: string): Promise<CollectionPage<Row>>;
36
+ /** Optional public cache for immediate paint; always incomplete, never a paging authority. */
37
+ cached?(criteria: CollectionQuery): Promise<CollectionPage<Row> | undefined>;
38
+ id(row: Row): string;
39
+ errorMessage?(error: unknown): string;
40
+ /** Private collections must discard prior rows when access or identity changes. A revalidation keeps the
41
+ * rows it shows through any other failure: safe because one worker never serves two identities (its pool
42
+ * key carries the account, and a purge tears parked workers down). */
43
+ clearOnError?(error: unknown): boolean;
44
+ }
45
+ const normalize = (q: Partial<CollectionQuery>): CollectionQuery => ({
46
+ query: typeof q.query === 'string' ? q.query.trim().slice(0, 256) : '',
47
+ filter: typeof q.filter === 'string' ? q.filter.slice(0, 256) : 'all',
48
+ sort: q.sort === 'az' || q.sort === 'za' ? q.sort : 'default',
49
+ savedIds: Array.isArray(q.savedIds) ? [...new Set(q.savedIds.filter(id => typeof id === 'string' && id.length > 0 && id.length <= 256))].slice(0, 500) : [],
50
+ });
51
+ const copyQuery = (q: CollectionQuery): CollectionQuery => ({ ...q, savedIds: [...q.savedIds] });
52
+
53
+ /** New criteria cancel the old result logically, including an in-flight next
54
+ * page. Failures preserve the cursor and rows for an explicit retry. Callers
55
+ * publish each snapshot through their ordinary service reducer, so loading and
56
+ * filtered-empty never masquerade as a successfully completed query. */
57
+ export function createLegacyCollectionQuery<Row>(source: CollectionQuerySource<Row>, publish: (state: CollectionQueryState<Row>) => void) {
58
+ let generation = 0, disposed = false;
59
+ let loadedPages = 0, restoringPages = 0;
60
+ let state: CollectionQueryState<Row> = { criteria: normalize({}), items: [], nextCursor: '', status: 'idle', loadingMore: false, pageError: false, complete: false, error: '', read: initialReadStatus() };
61
+ /** The query a revalidation is running under: its snapshots are held back until it is done. */
62
+ let revalidating = -1;
63
+ /** The last page the screen showed whole (rows ready, nothing paging, failing or standing in): what a revalidation
64
+ * keeps on screen and restores when it fails, and what a restored boot shows. A next page loading or failing after
65
+ * it leaves those rows on screen, so it is still this. */
66
+ let lastShown: { state: CollectionQueryState<Row>; loadedPages: number } | undefined;
67
+ const sameCriteria = (a: CollectionQuery, b: CollectionQuery) => JSON.stringify(a) === JSON.stringify(b);
68
+ let clearedByError = false;
69
+ const snapshot = (): CollectionQueryState<Row> => ({ ...state, read: { ...state.read }, criteria: copyQuery(state.criteria), items: [...state.items] });
70
+ const emit = () => {
71
+ if (disposed || revalidating === generation) return;
72
+ // A settled answer replaces it, an empty one included: rows the live query proved gone are never kept.
73
+ if (state.status === 'ready' && !state.pageError && !state.loadingMore && !state.provisional) {
74
+ lastShown = { state: snapshot(), loadedPages };
75
+ }
76
+ publish(snapshot());
77
+ };
78
+ const merge = (old: Row[], page: Row[]): Row[] => {
79
+ const positions = new Map<string, number>(), result: Row[] = [];
80
+ for (const item of [...old, ...page]) {
81
+ const id = source.id(item);
82
+ if (!id) throw new Error('A backend collection row requires an identity');
83
+ const index = positions.get(id);
84
+ if (index === undefined) { positions.set(id, result.length); result.push(item); }
85
+ else result[index] = item;
86
+ }
87
+ return result;
88
+ };
89
+ /** [keepRows]: a search typed over the rows on screen leaves them there until its answer arrives (status stays
90
+ * `loading`, and nothing pages under the old answer), so the list is not blanked on each keystroke. */
91
+ const fetch = async (append: boolean, keepRows = false): Promise<void> => {
92
+ if (disposed || (append && (state.loadingMore || state.status === 'loading' || !state.nextCursor))) return;
93
+ const revision = append ? generation : ++generation;
94
+ const cursor = append ? state.nextCursor : '';
95
+ const criteria = copyQuery(state.criteria);
96
+ if (!append) loadedPages = 0;
97
+ state = { ...state, status: append ? 'ready' : 'loading', loadingMore: append, pageError: false, error: '',
98
+ ...(append ? {} : keepRows ? { nextCursor: '', complete: false, provisional: false }
99
+ : { items: [], nextCursor: '', total: undefined, metadata: undefined, complete: false, provisional: false }) };
100
+ emit();
101
+ try {
102
+ if (!append && source.cached) {
103
+ const cached = await source.cached(criteria).catch(() => undefined);
104
+ if (disposed || generation !== revision) return;
105
+ if (cached?.items.length) {
106
+ state = { ...state, items: merge([], cached.items), metadata: cached.metadata, status: 'ready', complete: false, provisional: true };
107
+ emit();
108
+ }
109
+ }
110
+ const page = await source.fetch(criteria, cursor);
111
+ if (disposed || generation !== revision) return;
112
+ if (!Array.isArray(page.items) || typeof page.nextCursor !== 'string' || (cursor && page.nextCursor === cursor)) throw new Error('Invalid collection page');
113
+ state = { ...state, items: merge(append ? state.items : [], page.items), nextCursor: page.nextCursor,
114
+ total: typeof page.total === 'number' && Number.isFinite(page.total) && page.total >= 0 ? page.total : (append ? state.total : undefined),
115
+ metadata: page.metadata ?? (append ? state.metadata : undefined),
116
+ status: 'ready', loadingMore: false, pageError: false, complete: page.complete ?? !page.nextCursor, provisional: false };
117
+ loadedPages++;
118
+ } catch (error) {
119
+ if (disposed || generation !== revision) return;
120
+ const clear = source.clearOnError?.(error) === true;
121
+ // Revoked access always ends what a revalidation showed, whether or not the source clears its rows.
122
+ clearedByError = clear || /HTTP 40[13]\b/.test(String(error));
123
+ if (clearedByError) lastShown = undefined;
124
+ if (clear) loadedPages = 0;
125
+ // The rows a typed search kept on screen while it ran were never its answer: a failed one shows none of them.
126
+ const drop = clear || keepRows;
127
+ state = { ...state, ...(drop ? { items: [], nextCursor: '', total: undefined, metadata: undefined } : {}),
128
+ status: append && !clear ? 'ready' : 'error', loadingMore: false, pageError: append && !clear, complete: false,
129
+ provisional: false, error: source.errorMessage?.(error) ?? '' };
130
+ }
131
+ emit();
132
+ };
133
+ return {
134
+ getState: snapshot,
135
+ /** [revalidate]: a screen coming back re-queries the rows it shows. They stay on screen until every
136
+ * page is in, and a failure keeps them; revoked access still clears them, and other criteria reset. */
137
+ async query(criteria: Partial<CollectionQuery>, restorePages = false, revalidate = false): Promise<void> {
138
+ if (disposed) return Promise.resolve();
139
+ const next = normalize(criteria);
140
+ const same = JSON.stringify(next) === JSON.stringify(state.criteria);
141
+ const count = restorePages && same ? Math.max(loadedPages, restoringPages, 1) : 1;
142
+ const showing = lastShown && sameCriteria(lastShown.state.criteria, next) ? lastShown : undefined;
143
+ restoringPages = count;
144
+ // Only the search text changed: the user is typing over the rows on screen, also while the last keystroke's
145
+ // search is still out.
146
+ const typing = !same && (state.status === 'ready' || state.status === 'loading')
147
+ && sameCriteria({ ...state.criteria, query: '' }, { ...next, query: '' });
148
+ state = { ...state, criteria: next };
149
+ if (revalidate && showing) { revalidating = generation + 1; clearedByError = false; }
150
+ const pending = fetch(false, typing);
151
+ const revision = generation;
152
+ await pending;
153
+ // Re-appearance revalidates every previously opened page using fresh
154
+ // cursors. A changed query, error or revoked access stops the restoration.
155
+ while (!disposed && generation === revision && loadedPages < count && state.status === 'ready'
156
+ && !state.pageError && !state.loadingMore && state.nextCursor) await fetch(true);
157
+ if (generation === revision) restoringPages = 0;
158
+ if (revalidating === revision) {
159
+ revalidating = -1;
160
+ if (generation !== revision) return;
161
+ if ((state.status !== 'ready' || state.pageError) && showing && !clearedByError) {
162
+ state = { ...showing.state, criteria: next };
163
+ loadedPages = showing.loadedPages;
164
+ }
165
+ emit();
166
+ }
167
+ },
168
+ /** The page the screen shows (rows ready, nothing failing or still paging), for the service to keep with its
169
+ * state; undefined when it shows none. */
170
+ shownPage(): CollectionShownPage<Row> | undefined {
171
+ if (!lastShown || !sameCriteria(lastShown.state.criteria, state.criteria)) return undefined;
172
+ const page = lastShown.state;
173
+ return { state: { ...page, criteria: copyQuery(page.criteria), items: [...page.items] }, loadedPages: lastShown.loadedPages };
174
+ },
175
+ /** A cold boot restored from disk: what the last run showed becomes what this one shows, so its first `load`
176
+ * revalidates behind it instead of starting empty. Only before the first query, and only a well-formed page. */
177
+ seed(page: unknown): boolean {
178
+ const candidate = page && typeof page === 'object' ? page as Partial<CollectionShownPage<Row>> : undefined;
179
+ const restored = candidate?.state;
180
+ if (generation !== 0 || state.status !== 'idle' || !restored || restored.status !== 'ready'
181
+ || !Array.isArray(restored.items) || typeof restored.nextCursor !== 'string') return false;
182
+ try { restored.items.forEach(item => { if (!source.id(item)) throw new Error('A backend collection row requires an identity'); }); } catch { return false; }
183
+ state = { ...state, ...restored, criteria: normalize(restored.criteria ?? {}), items: [...restored.items], status: 'ready',
184
+ loadingMore: false, pageError: false, provisional: false, error: '', read: restored.read ? { ...restored.read } : initialReadStatus() };
185
+ loadedPages = Math.max(1, Math.floor(Number(candidate?.loadedPages) || 1));
186
+ lastShown = { state: snapshot(), loadedPages };
187
+ return true;
188
+ },
189
+ refresh: () => { restoringPages = 0; return fetch(false); },
190
+ loadMore: () => fetch(true),
191
+ retry: () => fetch(state.pageError),
192
+ dispose() { disposed = true; generation++; },
193
+ };
194
+ }
@@ -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
  }