@medalsocial/booking 0.0.0-placeholder.0 → 0.2.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 (266) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/LICENSE +201 -0
  3. package/NOTICE +6 -0
  4. package/README.md +220 -1
  5. package/dist/core/age.d.mts +34 -0
  6. package/dist/core/age.mjs +95 -0
  7. package/dist/core/age.mjs.map +1 -0
  8. package/dist/core/attempt-store.d.mts +111 -0
  9. package/dist/core/attempt-store.mjs +159 -0
  10. package/dist/core/attempt-store.mjs.map +1 -0
  11. package/dist/core/categories.d.mts +27 -0
  12. package/dist/core/categories.mjs +42 -0
  13. package/dist/core/categories.mjs.map +1 -0
  14. package/dist/core/clock.d.mts +33 -0
  15. package/dist/core/clock.mjs +251 -0
  16. package/dist/core/clock.mjs.map +1 -0
  17. package/dist/core/config.d.mts +178 -0
  18. package/dist/core/config.mjs +394 -0
  19. package/dist/core/config.mjs.map +1 -0
  20. package/dist/core/consent.d.mts +14 -0
  21. package/dist/core/consent.mjs +17 -0
  22. package/dist/core/consent.mjs.map +1 -0
  23. package/dist/core/deep-link.d.mts +40 -0
  24. package/dist/core/deep-link.mjs +160 -0
  25. package/dist/core/deep-link.mjs.map +1 -0
  26. package/dist/core/display-name.d.mts +18 -0
  27. package/dist/core/display-name.mjs +30 -0
  28. package/dist/core/display-name.mjs.map +1 -0
  29. package/dist/core/draft-store.d.mts +89 -0
  30. package/dist/core/draft-store.mjs +131 -0
  31. package/dist/core/draft-store.mjs.map +1 -0
  32. package/dist/core/dto.d.mts +46 -0
  33. package/dist/core/dto.mjs +157 -0
  34. package/dist/core/dto.mjs.map +1 -0
  35. package/dist/core/ics.d.mts +63 -0
  36. package/dist/core/ics.mjs +130 -0
  37. package/dist/core/ics.mjs.map +1 -0
  38. package/dist/core/index.d.mts +35 -0
  39. package/dist/core/index.mjs +32 -0
  40. package/dist/core/labels.d.mts +88 -0
  41. package/dist/core/labels.mjs +93 -0
  42. package/dist/core/labels.mjs.map +1 -0
  43. package/dist/core/machine.d.mts +645 -0
  44. package/dist/core/machine.mjs +988 -0
  45. package/dist/core/machine.mjs.map +1 -0
  46. package/dist/core/money.d.mts +12 -0
  47. package/dist/core/money.mjs +51 -0
  48. package/dist/core/money.mjs.map +1 -0
  49. package/dist/core/next-available.d.mts +20 -0
  50. package/dist/core/next-available.mjs +36 -0
  51. package/dist/core/next-available.mjs.map +1 -0
  52. package/dist/core/next-free.d.mts +25 -0
  53. package/dist/core/next-free.mjs +44 -0
  54. package/dist/core/next-free.mjs.map +1 -0
  55. package/dist/core/party-slots.d.mts +79 -0
  56. package/dist/core/party-slots.mjs +229 -0
  57. package/dist/core/party-slots.mjs.map +1 -0
  58. package/dist/core/paths.d.mts +11 -0
  59. package/dist/core/paths.mjs +56 -0
  60. package/dist/core/paths.mjs.map +1 -0
  61. package/dist/core/phone.d.mts +17 -0
  62. package/dist/core/phone.mjs +58 -0
  63. package/dist/core/phone.mjs.map +1 -0
  64. package/dist/core/portal/cookies.d.mts +29 -0
  65. package/dist/core/portal/dto.d.mts +140 -0
  66. package/dist/core/portal/dto.mjs +142 -0
  67. package/dist/core/portal/dto.mjs.map +1 -0
  68. package/dist/core/portal/family.d.mts +39 -0
  69. package/dist/core/portal/family.mjs +39 -0
  70. package/dist/core/portal/family.mjs.map +1 -0
  71. package/dist/core/portal/login-input.d.mts +29 -0
  72. package/dist/core/portal/login-input.mjs +29 -0
  73. package/dist/core/portal/login-input.mjs.map +1 -0
  74. package/dist/core/portal/next-path.d.mts +12 -0
  75. package/dist/core/portal/next-path.mjs +98 -0
  76. package/dist/core/portal/next-path.mjs.map +1 -0
  77. package/dist/core/portal/return-path.d.mts +7 -0
  78. package/dist/core/portal/return-path.mjs +49 -0
  79. package/dist/core/portal/return-path.mjs.map +1 -0
  80. package/dist/core/portal/session-cookie.d.mts +27 -0
  81. package/dist/core/portal/session-cookie.mjs +30 -0
  82. package/dist/core/portal/session-cookie.mjs.map +1 -0
  83. package/dist/core/portal/vipps-link.d.mts +56 -0
  84. package/dist/core/portal/vipps-link.mjs +136 -0
  85. package/dist/core/portal/vipps-link.mjs.map +1 -0
  86. package/dist/core/portal/vipps-return.d.mts +39 -0
  87. package/dist/core/portal/vipps-return.mjs +69 -0
  88. package/dist/core/portal/vipps-return.mjs.map +1 -0
  89. package/dist/core/rebook-store.d.mts +28 -0
  90. package/dist/core/rebook-store.mjs +60 -0
  91. package/dist/core/rebook-store.mjs.map +1 -0
  92. package/dist/core/restore-gate.d.mts +40 -0
  93. package/dist/core/restore-gate.mjs +96 -0
  94. package/dist/core/restore-gate.mjs.map +1 -0
  95. package/dist/core/stores.d.mts +8 -0
  96. package/dist/core/stores.mjs +23 -0
  97. package/dist/core/stores.mjs.map +1 -0
  98. package/dist/core/types.d.mts +245 -0
  99. package/dist/core/visit.d.mts +26 -0
  100. package/dist/core/visit.mjs +40 -0
  101. package/dist/core/visit.mjs.map +1 -0
  102. package/dist/core/wire.d.mts +37 -0
  103. package/dist/next/cache/memory.d.mts +14 -0
  104. package/dist/next/cache/memory.mjs +51 -0
  105. package/dist/next/cache/memory.mjs.map +1 -0
  106. package/dist/next/cache/next-data.d.mts +6 -0
  107. package/dist/next/cache/next-data.mjs +35 -0
  108. package/dist/next/cache/next-data.mjs.map +1 -0
  109. package/dist/next/cache/noop.d.mts +6 -0
  110. package/dist/next/cache/noop.mjs +15 -0
  111. package/dist/next/cache/noop.mjs.map +1 -0
  112. package/dist/next/cache/workers.d.mts +29 -0
  113. package/dist/next/cache/workers.mjs +74 -0
  114. package/dist/next/cache/workers.mjs.map +1 -0
  115. package/dist/next/catalogue.d.mts +38 -0
  116. package/dist/next/catalogue.mjs +215 -0
  117. package/dist/next/catalogue.mjs.map +1 -0
  118. package/dist/next/handler.d.mts +11 -0
  119. package/dist/next/handler.mjs +150 -0
  120. package/dist/next/handler.mjs.map +1 -0
  121. package/dist/next/index.d.mts +17 -0
  122. package/dist/next/index.mjs +14 -0
  123. package/dist/next/loaders.d.mts +79 -0
  124. package/dist/next/loaders.mjs +195 -0
  125. package/dist/next/loaders.mjs.map +1 -0
  126. package/dist/next/manage-dto.d.mts +6 -0
  127. package/dist/next/manage-dto.mjs +28 -0
  128. package/dist/next/manage-dto.mjs.map +1 -0
  129. package/dist/next/medal.d.mts +105 -0
  130. package/dist/next/medal.mjs +155 -0
  131. package/dist/next/medal.mjs.map +1 -0
  132. package/dist/next/options.d.mts +158 -0
  133. package/dist/next/options.mjs +65 -0
  134. package/dist/next/options.mjs.map +1 -0
  135. package/dist/next/portal/guardian.d.mts +42 -0
  136. package/dist/next/portal/guardian.mjs +76 -0
  137. package/dist/next/portal/guardian.mjs.map +1 -0
  138. package/dist/next/portal/medal-portal.d.mts +161 -0
  139. package/dist/next/portal/medal-portal.mjs +825 -0
  140. package/dist/next/portal/medal-portal.mjs.map +1 -0
  141. package/dist/next/portal/session.d.mts +38 -0
  142. package/dist/next/portal/session.mjs +40 -0
  143. package/dist/next/portal/session.mjs.map +1 -0
  144. package/dist/next/portal/vipps-flash.d.mts +14 -0
  145. package/dist/next/portal/vipps-flash.mjs +41 -0
  146. package/dist/next/portal/vipps-flash.mjs.map +1 -0
  147. package/dist/next/portal.d.mts +205 -0
  148. package/dist/next/portal.mjs +469 -0
  149. package/dist/next/portal.mjs.map +1 -0
  150. package/dist/next/request.d.mts +51 -0
  151. package/dist/next/request.mjs +123 -0
  152. package/dist/next/request.mjs.map +1 -0
  153. package/dist/next/routes/catalogue-routes.mjs +344 -0
  154. package/dist/next/routes/catalogue-routes.mjs.map +1 -0
  155. package/dist/next/routes/create.mjs +460 -0
  156. package/dist/next/routes/create.mjs.map +1 -0
  157. package/dist/next/routes/manage.mjs +191 -0
  158. package/dist/next/routes/manage.mjs.map +1 -0
  159. package/dist/next/routes/portal-routes.mjs +312 -0
  160. package/dist/next/routes/portal-routes.mjs.map +1 -0
  161. package/dist/next/routes/shared.mjs +162 -0
  162. package/dist/next/routes/shared.mjs.map +1 -0
  163. package/dist/next/routes/vipps-return.mjs +166 -0
  164. package/dist/next/routes/vipps-return.mjs.map +1 -0
  165. package/dist/next/runtime.d.mts +49 -0
  166. package/dist/next/runtime.mjs +94 -0
  167. package/dist/next/runtime.mjs.map +1 -0
  168. package/dist/next/seed.d.mts +71 -0
  169. package/dist/next/seed.mjs +495 -0
  170. package/dist/next/seed.mjs.map +1 -0
  171. package/dist/next/server.d.mts +26 -0
  172. package/dist/next/server.mjs +63 -0
  173. package/dist/next/server.mjs.map +1 -0
  174. package/dist/react/BookingLink.d.mts +61 -0
  175. package/dist/react/BookingLink.mjs +102 -0
  176. package/dist/react/BookingLink.mjs.map +1 -0
  177. package/dist/react/BookingWizard.d.mts +49 -0
  178. package/dist/react/BookingWizard.mjs +598 -0
  179. package/dist/react/BookingWizard.mjs.map +1 -0
  180. package/dist/react/LoginPageQuery.d.mts +37 -0
  181. package/dist/react/LoginPageQuery.mjs +106 -0
  182. package/dist/react/LoginPageQuery.mjs.map +1 -0
  183. package/dist/react/LoginSheet.d.mts +49 -0
  184. package/dist/react/LoginSheet.mjs +98 -0
  185. package/dist/react/LoginSheet.mjs.map +1 -0
  186. package/dist/react/ManageBooking.d.mts +50 -0
  187. package/dist/react/ManageBooking.mjs +197 -0
  188. package/dist/react/ManageBooking.mjs.map +1 -0
  189. package/dist/react/PortalDashboard.d.mts +10 -0
  190. package/dist/react/Provider.d.mts +82 -0
  191. package/dist/react/Provider.mjs +132 -0
  192. package/dist/react/Provider.mjs.map +1 -0
  193. package/dist/react/actions.d.mts +133 -0
  194. package/dist/react/actions.mjs +90 -0
  195. package/dist/react/actions.mjs.map +1 -0
  196. package/dist/react/index.d.mts +28 -0
  197. package/dist/react/index.mjs +20 -0
  198. package/dist/react/kit.d.mts +54 -0
  199. package/dist/react/kit.mjs +127 -0
  200. package/dist/react/kit.mjs.map +1 -0
  201. package/dist/react/labels/index.d.mts +23 -0
  202. package/dist/react/labels/index.mjs +44 -0
  203. package/dist/react/labels/index.mjs.map +1 -0
  204. package/dist/react/labels/login.d.mts +10 -0
  205. package/dist/react/labels/login.mjs +13 -0
  206. package/dist/react/labels/login.mjs.map +1 -0
  207. package/dist/react/labels/manage.d.mts +15 -0
  208. package/dist/react/labels/manage.mjs +19 -0
  209. package/dist/react/labels/manage.mjs.map +1 -0
  210. package/dist/react/labels/portal.d.mts +25 -0
  211. package/dist/react/labels/portal.mjs +41 -0
  212. package/dist/react/labels/portal.mjs.map +1 -0
  213. package/dist/react/labels/screens.d.mts +11 -0
  214. package/dist/react/labels/screens.mjs +721 -0
  215. package/dist/react/labels/screens.mjs.map +1 -0
  216. package/dist/react/labels/wizard.d.mts +76 -0
  217. package/dist/react/labels/wizard.mjs +97 -0
  218. package/dist/react/labels/wizard.mjs.map +1 -0
  219. package/dist/react/link/index.d.mts +5 -0
  220. package/dist/react/link/index.mjs +5 -0
  221. package/dist/react/portal/SessionTouch.d.mts +19 -0
  222. package/dist/react/portal/SessionTouch.mjs +40 -0
  223. package/dist/react/portal/SessionTouch.mjs.map +1 -0
  224. package/dist/react/portal/age-prompt.d.mts +10 -0
  225. package/dist/react/portal/age-prompt.mjs +21 -0
  226. package/dist/react/portal/age-prompt.mjs.map +1 -0
  227. package/dist/react/portal/book-again.d.mts +18 -0
  228. package/dist/react/portal/book-again.mjs +21 -0
  229. package/dist/react/portal/book-again.mjs.map +1 -0
  230. package/dist/react/portal/child-summary.d.mts +17 -0
  231. package/dist/react/portal/child-summary.mjs +45 -0
  232. package/dist/react/portal/child-summary.mjs.map +1 -0
  233. package/dist/react/portal/dashboard.d.mts +74 -0
  234. package/dist/react/portal/dashboard.mjs +204 -0
  235. package/dist/react/portal/dashboard.mjs.map +1 -0
  236. package/dist/react/portal/leave.d.mts +17 -0
  237. package/dist/react/portal/leave.mjs +21 -0
  238. package/dist/react/portal/leave.mjs.map +1 -0
  239. package/dist/react/portal/sections.d.mts +12 -0
  240. package/dist/react/portal/sections.mjs +48 -0
  241. package/dist/react/portal/sections.mjs.map +1 -0
  242. package/dist/react/portal/tabs.d.mts +23 -0
  243. package/dist/react/portal/tabs.mjs +36 -0
  244. package/dist/react/portal/tabs.mjs.map +1 -0
  245. package/dist/react/portal/vipps-flash.d.mts +11 -0
  246. package/dist/react/portal/vipps-flash.mjs +18 -0
  247. package/dist/react/portal/vipps-flash.mjs.map +1 -0
  248. package/dist/react/portal/wired.mjs +202 -0
  249. package/dist/react/portal/wired.mjs.map +1 -0
  250. package/dist/react/screen-labels.mjs +14 -0
  251. package/dist/react/screen-labels.mjs.map +1 -0
  252. package/dist/react/shared.d.mts +13 -0
  253. package/dist/react/shared.mjs +7 -0
  254. package/dist/react/use-next-free.d.mts +19 -0
  255. package/dist/react/use-next-free.mjs +56 -0
  256. package/dist/react/use-next-free.mjs.map +1 -0
  257. package/dist/react/useBooking.d.mts +179 -0
  258. package/dist/react/useBooking.mjs +1331 -0
  259. package/dist/react/useBooking.mjs.map +1 -0
  260. package/dist/react/wizard/adapters.mjs +189 -0
  261. package/dist/react/wizard/adapters.mjs.map +1 -0
  262. package/dist/react/wizard/index.d.mts +11 -0
  263. package/dist/react/wizard/index.mjs +6 -0
  264. package/dist/react/wizard-error.mjs +29 -0
  265. package/dist/react/wizard-error.mjs.map +1 -0
  266. package/package.json +126 -5
@@ -0,0 +1,495 @@
1
+ import { normaliseCategory } from "../core/categories.mjs";
2
+ import { createClock } from "../core/clock.mjs";
3
+ import { serviceMatches } from "../core/deep-link.mjs";
4
+ import { createDto, toBookingDayDto, toBookingSlotDto } from "../core/dto.mjs";
5
+ import { firstOpeningPerResource } from "../core/next-available.mjs";
6
+ import { inBackground, timed } from "./options.mjs";
7
+ //#region src/next/seed.ts
8
+ /**
9
+ * The booking seed: everything the first two steps of the booking page need,
10
+ * from the edge (L1) cache.
11
+ *
12
+ * WHY. Every read through `catalogue.ts` goes through the data cache, which on
13
+ * OpenNext is the regional Cache API in front of R2 plus a Durable Object round
14
+ * trip for the tag check — 0.25–0.5 s per read even when warm, and the page
15
+ * needed two or three rounds of them. A Workers Cache API read
16
+ * (`caches.default`) is a few milliseconds.
17
+ *
18
+ * WHAT. Two edge entries, read in sequence:
19
+ *
20
+ * - the CATALOGUE — `{ services, resources }` — shared by every prefetch set,
21
+ * one-minute key bucket;
22
+ * - the SEED for one prefetch set — `{ slots, schedules }` per service over
23
+ * the page's window — 30-second key bucket.
24
+ *
25
+ * Assembled, `loadBookingSeed` answers `{ services, resources, nextAvailable,
26
+ * schedules, slots, generatedAt }`. The GUARDIAN (cookie-dependent) never goes
27
+ * in; the page reads it on its own.
28
+ *
29
+ * A MISS builds from the `catalogue.ts` loaders, in parallel, so the rounding,
30
+ * the 62-day bypass and every other bound there still applies — the edge entry
31
+ * is one more layer in front, not a second way to ask Medal. An entry is only
32
+ * stored when every part of it was read; a failure is served to this request
33
+ * (absent from the seed, fetched on demand by the wizard) and is not pinned for
34
+ * the next visitor.
35
+ *
36
+ * A HIT is re-filtered to now: an entry up to 30 s old may hold a slot that has
37
+ * since started, and «next free» is derived after that filter.
38
+ *
39
+ * KEYS. `<edgePrefix>/<env>[-<version>]/…`, mapped onto storage keys by the
40
+ * edge adapter (the Workers adapter puts the site's origin in front, because
41
+ * Cache API entries belong to the zone). The environment is in the path
42
+ * because staging and production can share a zone. The seed key holds the
43
+ * local day (the window ends on a local midnight), the SORTED prefetch ids and
44
+ * the 30 s bucket. The prefetch ids are always the output of `prefetchTargets`
45
+ * over the catalogue — a deep link that names no bookable service falls back
46
+ * to the default set — so the number of distinct keys per bucket is at most
47
+ * one per bookable service plus one.
48
+ *
49
+ * STALENESS. A seed is at most 30 s older than the slot entry it was built
50
+ * from, which `catalogue.ts` bounds at ~60 s: ~90 s for a booking made
51
+ * anywhere else. The site's own create / move / cancel (and a SLOT_TAKEN)
52
+ * delete every seed key containing an affected service in this location
53
+ * (`expireBookingSeeds`), and first write a per-service expiry marker (plus
54
+ * one location-wide marker, so a hit checks one key, not one per service) so
55
+ * a build already in flight when the write landed does not store its
56
+ * pre-write seed (it still serves it to its own request), and a hit built
57
+ * before the write is not served; in OTHER locations such a seed
58
+ * stays until its bucket turns, ≤30 s. Medal re-checks every slot at submit,
59
+ * and the 409 already carries fresh openings, so a stale seed costs a retry,
60
+ * never a double booking.
61
+ */
62
+ const SEED_BUCKET_MS = 3e4;
63
+ const SEED_TTL_S = 30;
64
+ const CATALOGUE_BUCKET_MS = 6e4;
65
+ const CATALOGUE_TTL_S = 60;
66
+ /**
67
+ * STALE-WHILE-REVALIDATE. The bucketed keys above keep a location warm only
68
+ * while visitors keep coming; after a quiet minute the first visitor there
69
+ * paid a full rebuild. So each build is ALSO kept under a «latest» key without
70
+ * the bucket, and a bucket miss serves it at once while one rebuild runs
71
+ * behind the response:
72
+ *
73
+ * - the catalogue (services, stylists) for up to `CATALOGUE_STALE_MAX_S`;
74
+ * - a seed for up to `SEED_STALE_MAX_MS`, and ONLY when no create / move /
75
+ * cancel handled IN THIS LOCATION has touched its services since it was
76
+ * built (the expiry markers, kept at least that long, and awaited by the
77
+ * write routes before they answer). The markers are per location: a booking
78
+ * handled elsewhere, or made outside this site, can show as open here for at
79
+ * most `SEED_STALE_MAX_MS` — Medal answers that slot at submit with a 409
80
+ * carrying fresh openings.
81
+ *
82
+ * One refresh per key at a time in an isolate (`once`), and a refresh never
83
+ * replaces a kept copy built later than its own (`isNewer`), so a burst of
84
+ * visitors to a quiet location costs one rebuild and out-of-order finishes
85
+ * cannot put older openings back.
86
+ */
87
+ const CATALOGUE_STALE_MAX_S = 3600;
88
+ const SEED_STALE_MAX_MS = 3e5;
89
+ /**
90
+ * The per-service expiry marker: the instant this location last saw a write
91
+ * to the service (`expireBookingSeeds`). Outlives any seed build and any
92
+ * «latest» seed that could still be served stale (`SEED_STALE_MAX_MS`).
93
+ */
94
+ const MARKER_TTL_S = 600;
95
+ /**
96
+ * The location-wide marker (`anyExpiryMarkerKey`) is the instant of the last
97
+ * write to ANY service here, so a hit reads one key instead of one per
98
+ * prefetched service (up to `window.prefetchLimit`). Only when it is at or
99
+ * within this margin of a seed's build instant are the per-service markers
100
+ * read, to tell a write to one of the seed's services from a write to another
101
+ * one. The margin covers writes racing: the edge has no compare-and-set, so
102
+ * the location-wide marker can end on an earlier write whose put landed after
103
+ * a later one's; the per-service markers still hold the later. One seed
104
+ * bucket: far longer than an edge put takes, and a hit within it of a write
105
+ * costs the per-service reads it always cost before.
106
+ *
107
+ * When the location-wide marker cannot be read, or the adapter cannot store
108
+ * it, the per-service markers are read instead. When it is simply absent (no
109
+ * write here within `MARKER_TTL_S`, or its put failed while a per-service put
110
+ * succeeded) the hit is served; as with any marker that failed, the
111
+ * background deletes and the 30 s bucket still bound that seed.
112
+ */
113
+ const MARKER_RACE_MS = SEED_BUCKET_MS;
114
+ /**
115
+ * How long after the first delete the second one runs. The first can land
116
+ * BEFORE the write it answers is visible to a concurrent seed build: the slot
117
+ * tag expiry is written in the platform's deferred work, and a booking-page
118
+ * miss already in flight puts the seed it built from pre-booking slots once it
119
+ * is done. Either puts a stale seed back after the first delete; the second
120
+ * pass removes it. A build that starts after the tag expiry has landed reads
121
+ * live.
122
+ */
123
+ const SEED_RECHECK_MS = 2e3;
124
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
125
+ function createSeed(options, catalogue) {
126
+ const { config, logger } = options;
127
+ const { edge } = options.cache;
128
+ const SEED_PATH = options.cache.edgePrefix ?? "/__medal-edge/booking-seed/v1";
129
+ const RANGE_DAYS = config.window.rangeDays;
130
+ const PREFETCH_LIMIT = config.window.prefetchLimit;
131
+ const clock = createClock(config);
132
+ const { toBookingServiceDto, toBookingResourceDto } = createDto(config);
133
+ /** In-flight refreshes by edge key, shared by every request in this isolate. */
134
+ const inflight = /* @__PURE__ */ new Map();
135
+ function once(key, work) {
136
+ const running = inflight.get(key);
137
+ if (running) return running;
138
+ const started = work().finally(() => inflight.delete(key));
139
+ inflight.set(key, started);
140
+ return started;
141
+ }
142
+ /** A package key as the adapter stores it, or `null` when it cannot store now. */
143
+ function storageKey(key) {
144
+ return edge.key ? edge.key(key) : key;
145
+ }
146
+ function keyBase() {
147
+ const env = options.cache.environment || "unknown";
148
+ const version = edge.version?.();
149
+ const base = `${SEED_PATH}/${encodeURIComponent(version ? `${env}-${version}` : env)}`;
150
+ return storageKey(base) === null ? null : base;
151
+ }
152
+ function keyed(path) {
153
+ const base = keyBase();
154
+ return base === null ? null : storageKey(path(base));
155
+ }
156
+ /**
157
+ * The services worth prefetching, in order, capped at `PREFETCH_LIMIT`: the
158
+ * one the link names (it is the one the visitor is known to want), then the
159
+ * bookable `window.prefetchCategory` menu. An unknown value is ignored.
160
+ */
161
+ function prefetchTargets(services, named) {
162
+ const linked = named ? services.find((service) => service.bookableOnline && serviceMatches(service, named)) : void 0;
163
+ const prefetchCategory = config.window.prefetchCategory;
164
+ const kids = prefetchCategory === null ? [] : services.filter((service) => service.bookableOnline && normaliseCategory(config, service.category) === prefetchCategory && service.id !== linked?.id);
165
+ return [...linked ? [linked] : [], ...kids].slice(0, PREFETCH_LIMIT);
166
+ }
167
+ /** The ids a seed is keyed by: sorted, so the order of the set is not a key. */
168
+ function prefetchKey(targets) {
169
+ return [...new Set(targets.map((target) => target.id))].sort().map(encodeURIComponent).join(",");
170
+ }
171
+ function seedBucket(now) {
172
+ return Math.floor(now / SEED_BUCKET_MS);
173
+ }
174
+ function expiryMarkerKey(serviceId) {
175
+ return keyed((base) => `${base}/gen/${encodeURIComponent(serviceId)}`);
176
+ }
177
+ /** The location-wide expiry marker: the last write to any service here. */
178
+ function anyExpiryMarkerKey() {
179
+ return keyed((base) => `${base}/gen-any`);
180
+ }
181
+ function catalogueKeyAt(now) {
182
+ return keyed((base) => `${base}/catalogue/${Math.floor(now / CATALOGUE_BUCKET_MS)}`);
183
+ }
184
+ /** The catalogue kept past its bucket, for a stale-while-revalidate serve. */
185
+ function catalogueLatestKey() {
186
+ return keyed((base) => `${base}/catalogue/latest`);
187
+ }
188
+ /** One prefetch set's seed kept past its bucket, for the same local day. */
189
+ function seedLatestKeyAt(key, now) {
190
+ if (key === "") return null;
191
+ return keyed((base) => `${base}/seed-latest/${clock.dayKey(now)}/${key}`);
192
+ }
193
+ /** The seed key for one prefetch set at one instant. */
194
+ function seedKeyAt(key, now) {
195
+ if (key === "") return null;
196
+ return keyed((base) => `${base}/seed/${clock.dayKey(now)}/${key}/${seedBucket(now)}`);
197
+ }
198
+ async function edgeRead(key) {
199
+ try {
200
+ return await edge.get(key);
201
+ } catch (error) {
202
+ logger.warn({ err: error }, "Could not read the booking seed from the colo cache");
203
+ return;
204
+ }
205
+ }
206
+ function edgeWrite(key, value, ttlSeconds) {
207
+ return inBackground(edge, edge.put(key, value, ttlSeconds));
208
+ }
209
+ /** Whether a copy built at `builtAt` may replace what `key` holds now. */
210
+ async function isNewer(key, builtAt) {
211
+ const kept = await edgeRead(key);
212
+ const keptAt = kept?.generatedAt ?? kept?.builtAt;
213
+ return typeof keptAt !== "number" || builtAt >= keptAt;
214
+ }
215
+ /**
216
+ * One expiry marker, `null` when it is not set, or `failed` when the
217
+ * adapter cannot store it or the read fails.
218
+ */
219
+ async function readMarker(key, failed) {
220
+ if (key === null) return failed;
221
+ try {
222
+ const hit = await edge.get(key);
223
+ const mark = hit === void 0 ? NaN : Number(hit);
224
+ return Number.isFinite(mark) ? mark : null;
225
+ } catch {
226
+ return failed;
227
+ }
228
+ }
229
+ /**
230
+ * The latest per-service expiry marker among `targets`, or `null`. A marker
231
+ * that cannot be read counts as none: the delete passes and the 30 s bucket
232
+ * still bound it.
233
+ */
234
+ async function latestExpiry(targets) {
235
+ const valid = (await Promise.all(targets.map(({ id }) => readMarker(expiryMarkerKey(id), null)))).filter((mark) => mark !== null);
236
+ return valid.length === 0 ? null : Math.max(...valid);
237
+ }
238
+ /**
239
+ * Whether a write touching one of `targets`' services was marked at or
240
+ * after an instant — i.e. a seed built then may hold pre-write openings.
241
+ * The location-wide marker is read once, as soon as this is called (so it
242
+ * runs alongside the seed read); the per-service markers only when it does
243
+ * not rule a write out (`MARKER_RACE_MS`), and then once for every check.
244
+ */
245
+ function expiryCheck(targets) {
246
+ const any = readMarker(anyExpiryMarkerKey(), "unknown");
247
+ let perService;
248
+ return async (since) => {
249
+ const anyAt = await any;
250
+ if (anyAt === null || anyAt !== "unknown" && anyAt < since - MARKER_RACE_MS) return false;
251
+ perService ??= latestExpiry(targets);
252
+ const latest = await perService;
253
+ return latest !== null && latest >= since;
254
+ };
255
+ }
256
+ async function writeExpiryMarkers(serviceIds, at) {
257
+ const keys = [...serviceIds.map(expiryMarkerKey), anyExpiryMarkerKey()];
258
+ await Promise.all(keys.map(async (key) => {
259
+ if (key === null) return;
260
+ try {
261
+ await edge.put(key, at, MARKER_TTL_S);
262
+ } catch (error) {
263
+ logger.warn({ err: error }, "Could not mark booking seeds expired");
264
+ }
265
+ }));
266
+ }
267
+ /** The catalogue straight from `catalogue.ts`. Services failing throws. */
268
+ async function readCatalogue() {
269
+ const [services, resources] = await Promise.all([catalogue.cachedServices(), catalogue.cachedResources().then((list) => list.map(toBookingResourceDto), (error) => {
270
+ logger.warn({ err: error }, "Could not prefetch the stylists for the booking page");
271
+ return null;
272
+ })]);
273
+ return {
274
+ catalogue: {
275
+ services: services.map(toBookingServiceDto),
276
+ resources
277
+ },
278
+ complete: resources !== null
279
+ };
280
+ }
281
+ /**
282
+ * The catalogue, from the edge when there is one. Throws when the services
283
+ * cannot be read — the one failure that replaces the wizard.
284
+ */
285
+ async function loadBookingCatalogue(now = Date.now()) {
286
+ return timed(options.timing, "catalogue", async () => {
287
+ const key = catalogueKeyAt(now);
288
+ if (key === null) return (await readCatalogue()).catalogue;
289
+ const hit = await edgeRead(key);
290
+ if (hit && Array.isArray(hit.services)) return hit;
291
+ const refresh = () => once(`${key}#refresh`, async () => {
292
+ const builtAt = Date.now();
293
+ const { catalogue: built, complete } = await readCatalogue();
294
+ if (complete && built.services.length > 0) {
295
+ await edgeWrite(key, built, CATALOGUE_TTL_S);
296
+ const latest = catalogueLatestKey();
297
+ if (latest !== null && await isNewer(latest, builtAt)) await edgeWrite(latest, {
298
+ ...built,
299
+ builtAt
300
+ }, CATALOGUE_STALE_MAX_S);
301
+ }
302
+ return built;
303
+ });
304
+ const latest = catalogueLatestKey();
305
+ const stale = latest === null ? void 0 : await edgeRead(latest);
306
+ if (stale && Array.isArray(stale.services) && stale.services.length > 0) {
307
+ inBackground(edge, refresh().catch((error) => {
308
+ logger.warn({ err: error }, "Could not refresh the booking catalogue");
309
+ }));
310
+ return stale;
311
+ }
312
+ return refresh();
313
+ });
314
+ }
315
+ /** One read per service, best effort per service. */
316
+ async function prefetchEach(targets, what, readOne) {
317
+ const entries = (await Promise.all(targets.map(async (service) => {
318
+ try {
319
+ return [service.id, await readOne(service.id)];
320
+ } catch (error) {
321
+ logger.warn({
322
+ err: error,
323
+ serviceId: service.id
324
+ }, `Could not prefetch ${what}`);
325
+ return null;
326
+ }
327
+ }))).filter((entry) => entry !== null);
328
+ return {
329
+ values: Object.fromEntries(entries),
330
+ complete: entries.length === targets.length
331
+ };
332
+ }
333
+ async function buildSeed(targets, fromTs, toTs) {
334
+ const [slots, schedules] = await Promise.all([prefetchEach(targets, "availability", async (serviceId) => (await catalogue.cachedAvailability({
335
+ serviceId,
336
+ fromTs,
337
+ toTs
338
+ })).flatMap(toBookingSlotDto)), prefetchEach(targets, "the schedule", async (serviceId) => (await catalogue.cachedSchedule({
339
+ serviceId,
340
+ fromTs,
341
+ toTs
342
+ })).flatMap(toBookingDayDto))]);
343
+ return {
344
+ seed: {
345
+ slots: slots.values,
346
+ schedules: schedules.values,
347
+ generatedAt: fromTs
348
+ },
349
+ complete: slots.complete && schedules.complete
350
+ };
351
+ }
352
+ /** A stored seed as of `now`: slots that have started since it was built go. */
353
+ function freshen(seed, now, toTs) {
354
+ const slots = Object.fromEntries(Object.entries(seed.slots).map(([serviceId, list]) => [serviceId, list.filter((slot) => slot.startTs >= now && slot.startTs < toTs)]));
355
+ return {
356
+ ...seed,
357
+ slots
358
+ };
359
+ }
360
+ /**
361
+ * The seed for the page: the catalogue plus openings and hours for the
362
+ * prefetch set `named` selects. Throws only when the catalogue cannot be read.
363
+ *
364
+ * The state (hit / stale / miss / bypass) goes to the timing sink with the
365
+ * seed phase, for `x-booking-seed`.
366
+ */
367
+ async function loadBookingSeed(named, now = Date.now()) {
368
+ const loaded = await loadBookingCatalogue(now);
369
+ const { toTs } = clock.window(now, RANGE_DAYS);
370
+ const targets = prefetchTargets(loaded.services, named);
371
+ let state = "miss";
372
+ const markSeed = (next) => {
373
+ state = next;
374
+ };
375
+ const stored = await timed(options.timing, "seed", async () => {
376
+ const key = seedKeyAt(prefetchKey(targets), now);
377
+ if (key === null) {
378
+ markSeed("bypass");
379
+ return (await buildSeed(targets, now, toTs)).seed;
380
+ }
381
+ const expired = expiryCheck(targets);
382
+ const hit = await edgeRead(key);
383
+ if (hit?.slots && hit.schedules && !await expired(hit.generatedAt)) {
384
+ markSeed("hit");
385
+ return freshen(hit, now, toTs);
386
+ }
387
+ const latestKey = seedLatestKeyAt(prefetchKey(targets), now);
388
+ const rebuild = () => once(`${key}#rebuild`, async () => {
389
+ const buildStart = Date.now();
390
+ const { seed, complete } = await buildSeed(targets, now, toTs);
391
+ if (complete && !await expiryCheck(targets)(buildStart)) {
392
+ await edgeWrite(key, seed, SEED_TTL_S);
393
+ if (latestKey !== null && await isNewer(latestKey, seed.generatedAt)) await edgeWrite(latestKey, seed, Math.ceil(SEED_STALE_MAX_MS / 1e3));
394
+ }
395
+ return seed;
396
+ });
397
+ const stale = latestKey === null ? void 0 : await edgeRead(latestKey);
398
+ if (stale?.slots && stale.schedules && now - stale.generatedAt < 3e5 && !await expired(stale.generatedAt)) {
399
+ markSeed("stale");
400
+ inBackground(edge, rebuild().catch((error) => {
401
+ logger.warn({ err: error }, "Could not refresh the booking seed");
402
+ }));
403
+ return freshen(stale, now, toTs);
404
+ }
405
+ markSeed("miss");
406
+ return rebuild();
407
+ }, () => ({ state }));
408
+ const nextAvailable = loaded.resources === null ? {} : Object.fromEntries(Object.entries(stored.slots).map(([serviceId, slots]) => [serviceId, firstOpeningPerResource(slots)]));
409
+ return {
410
+ ...loaded,
411
+ slots: stored.slots,
412
+ schedules: stored.schedules,
413
+ nextAvailable,
414
+ generatedAt: stored.generatedAt,
415
+ fromTs: now,
416
+ toTs
417
+ };
418
+ }
419
+ /**
420
+ * Every prefetch set the page can key a seed by: the default one and one per
421
+ * bookable service a deep link can name. Deterministic from the catalogue.
422
+ */
423
+ function allPrefetchKeys(services) {
424
+ const sets = [prefetchTargets(services, void 0), ...services.filter((service) => service.bookableOnline).map((service) => prefetchTargets(services, service.id))];
425
+ return [...new Set(sets.filter((set) => set.length > 0).map(prefetchKey))];
426
+ }
427
+ /**
428
+ * The edge keys to delete after a write touching `serviceIds`: every prefetch
429
+ * set containing one of them, in the current and the previous 30 s bucket (a
430
+ * seed from the previous bucket can still be read by a request that started
431
+ * in it). Sets not containing an affected service are left alone.
432
+ */
433
+ function seedKeysFor(services, serviceIds, now) {
434
+ const affected = new Set(serviceIds);
435
+ if (affected.size === 0) return [];
436
+ const keys = allPrefetchKeys(services).filter((key) => key.split(",").some((id) => affected.has(decodeURIComponent(id))));
437
+ const instants = [now, now - SEED_BUCKET_MS];
438
+ return [...new Set(keys.flatMap((key) => [...instants.map((at) => seedKeyAt(key, at)), seedLatestKeyAt(key, now)]).filter((k) => k !== null))];
439
+ }
440
+ async function deleteSeeds(serviceIds, now) {
441
+ const current = catalogueKeyAt(now);
442
+ const previous = catalogueKeyAt(now - CATALOGUE_BUCKET_MS);
443
+ if (current === null || previous === null) return;
444
+ const kept = await edgeRead(current) ?? await edgeRead(previous);
445
+ const services = Array.isArray(kept?.services) ? kept.services : (await catalogue.cachedServices()).map(toBookingServiceDto);
446
+ await edge.delete(seedKeysFor(services, serviceIds, now));
447
+ }
448
+ /**
449
+ * Mark `serviceIds` expired (so an in-flight build started before now does
450
+ * not store its seed), then delete this location's seeds containing any of
451
+ * them, after a create / move / cancel or a SLOT_TAKEN — twice, the second
452
+ * time `recheckAfterMs` later (see `SEED_RECHECK_MS`). The deletes run on the
453
+ * adapter's `defer`, so they add nothing to the response; best effort — the
454
+ * write has already happened and the 30 s bucket is the backstop. A no-op
455
+ * when the edge adapter cannot store anything.
456
+ */
457
+ function expireBookingSeeds(serviceIds, now = Date.now(), recheckAfterMs = SEED_RECHECK_MS) {
458
+ const ids = [...new Set(serviceIds)];
459
+ if (ids.length === 0 || keyBase() === null) return Promise.resolve();
460
+ const pass = (at) => deleteSeeds(ids, at).catch((error) => {
461
+ logger.warn({ err: error }, "Could not expire booking seeds");
462
+ });
463
+ const marked = writeExpiryMarkers(ids, now);
464
+ inBackground(edge, (async () => {
465
+ await marked;
466
+ await pass(now);
467
+ await sleep(recheckAfterMs);
468
+ await pass(now + recheckAfterMs);
469
+ })());
470
+ return marked;
471
+ }
472
+ return {
473
+ RANGE_DAYS,
474
+ PREFETCH_LIMIT,
475
+ prefetchTargets,
476
+ prefetchKey,
477
+ seedBucket,
478
+ expiryMarkerKey,
479
+ anyExpiryMarkerKey,
480
+ catalogueKeyAt,
481
+ catalogueLatestKey,
482
+ seedLatestKeyAt,
483
+ seedKeyAt,
484
+ loadBookingCatalogue,
485
+ loadBookingSeed,
486
+ allPrefetchKeys,
487
+ seedKeysFor,
488
+ expireBookingSeeds,
489
+ resetSeedRefreshes: () => inflight.clear()
490
+ };
491
+ }
492
+ //#endregion
493
+ export { SEED_RECHECK_MS, SEED_STALE_MAX_MS, createSeed };
494
+
495
+ //# sourceMappingURL=seed.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"seed.mjs","names":[],"sources":["../../src/next/seed.ts"],"sourcesContent":["/**\n * The booking seed: everything the first two steps of the booking page need,\n * from the edge (L1) cache.\n *\n * WHY. Every read through `catalogue.ts` goes through the data cache, which on\n * OpenNext is the regional Cache API in front of R2 plus a Durable Object round\n * trip for the tag check — 0.25–0.5 s per read even when warm, and the page\n * needed two or three rounds of them. A Workers Cache API read\n * (`caches.default`) is a few milliseconds.\n *\n * WHAT. Two edge entries, read in sequence:\n *\n * - the CATALOGUE — `{ services, resources }` — shared by every prefetch set,\n * one-minute key bucket;\n * - the SEED for one prefetch set — `{ slots, schedules }` per service over\n * the page's window — 30-second key bucket.\n *\n * Assembled, `loadBookingSeed` answers `{ services, resources, nextAvailable,\n * schedules, slots, generatedAt }`. The GUARDIAN (cookie-dependent) never goes\n * in; the page reads it on its own.\n *\n * A MISS builds from the `catalogue.ts` loaders, in parallel, so the rounding,\n * the 62-day bypass and every other bound there still applies — the edge entry\n * is one more layer in front, not a second way to ask Medal. An entry is only\n * stored when every part of it was read; a failure is served to this request\n * (absent from the seed, fetched on demand by the wizard) and is not pinned for\n * the next visitor.\n *\n * A HIT is re-filtered to now: an entry up to 30 s old may hold a slot that has\n * since started, and «next free» is derived after that filter.\n *\n * KEYS. `<edgePrefix>/<env>[-<version>]/…`, mapped onto storage keys by the\n * edge adapter (the Workers adapter puts the site's origin in front, because\n * Cache API entries belong to the zone). The environment is in the path\n * because staging and production can share a zone. The seed key holds the\n * local day (the window ends on a local midnight), the SORTED prefetch ids and\n * the 30 s bucket. The prefetch ids are always the output of `prefetchTargets`\n * over the catalogue — a deep link that names no bookable service falls back\n * to the default set — so the number of distinct keys per bucket is at most\n * one per bookable service plus one.\n *\n * STALENESS. A seed is at most 30 s older than the slot entry it was built\n * from, which `catalogue.ts` bounds at ~60 s: ~90 s for a booking made\n * anywhere else. The site's own create / move / cancel (and a SLOT_TAKEN)\n * delete every seed key containing an affected service in this location\n * (`expireBookingSeeds`), and first write a per-service expiry marker (plus\n * one location-wide marker, so a hit checks one key, not one per service) so\n * a build already in flight when the write landed does not store its\n * pre-write seed (it still serves it to its own request), and a hit built\n * before the write is not served; in OTHER locations such a seed\n * stays until its bucket turns, ≤30 s. Medal re-checks every slot at submit,\n * and the 409 already carries fresh openings, so a stale seed costs a retry,\n * never a double booking.\n */\n\nimport { normaliseCategory } from '../core/categories';\nimport { createClock } from '../core/clock';\nimport type { BookingConfig } from '../core/config';\nimport { serviceMatches } from '../core/deep-link';\nimport { createDto, toBookingDayDto, toBookingSlotDto } from '../core/dto';\nimport { firstOpeningPerResource } from '../core/next-available';\nimport type {\n BookingDayDto,\n BookingResourceDto,\n BookingServiceDto,\n BookingSlotDto,\n} from '../core/types';\nimport type { Catalogue } from './catalogue';\nimport {\n type BookingCacheAdapter,\n type BookingLogger,\n type BookingTiming,\n DEFAULT_EDGE_PREFIX,\n inBackground,\n timed,\n} from './options';\n\nconst SEED_BUCKET_MS = 30_000;\nconst SEED_TTL_S = 30;\nconst CATALOGUE_BUCKET_MS = 60_000;\nconst CATALOGUE_TTL_S = 60;\n\n/**\n * STALE-WHILE-REVALIDATE. The bucketed keys above keep a location warm only\n * while visitors keep coming; after a quiet minute the first visitor there\n * paid a full rebuild. So each build is ALSO kept under a «latest» key without\n * the bucket, and a bucket miss serves it at once while one rebuild runs\n * behind the response:\n *\n * - the catalogue (services, stylists) for up to `CATALOGUE_STALE_MAX_S`;\n * - a seed for up to `SEED_STALE_MAX_MS`, and ONLY when no create / move /\n * cancel handled IN THIS LOCATION has touched its services since it was\n * built (the expiry markers, kept at least that long, and awaited by the\n * write routes before they answer). The markers are per location: a booking\n * handled elsewhere, or made outside this site, can show as open here for at\n * most `SEED_STALE_MAX_MS` — Medal answers that slot at submit with a 409\n * carrying fresh openings.\n *\n * One refresh per key at a time in an isolate (`once`), and a refresh never\n * replaces a kept copy built later than its own (`isNewer`), so a burst of\n * visitors to a quiet location costs one rebuild and out-of-order finishes\n * cannot put older openings back.\n */\nconst CATALOGUE_STALE_MAX_S = 60 * 60;\nexport const SEED_STALE_MAX_MS = 5 * 60 * 1000;\n\n/**\n * The per-service expiry marker: the instant this location last saw a write\n * to the service (`expireBookingSeeds`). Outlives any seed build and any\n * «latest» seed that could still be served stale (`SEED_STALE_MAX_MS`).\n */\nconst MARKER_TTL_S = 10 * 60;\n\n/**\n * The location-wide marker (`anyExpiryMarkerKey`) is the instant of the last\n * write to ANY service here, so a hit reads one key instead of one per\n * prefetched service (up to `window.prefetchLimit`). Only when it is at or\n * within this margin of a seed's build instant are the per-service markers\n * read, to tell a write to one of the seed's services from a write to another\n * one. The margin covers writes racing: the edge has no compare-and-set, so\n * the location-wide marker can end on an earlier write whose put landed after\n * a later one's; the per-service markers still hold the later. One seed\n * bucket: far longer than an edge put takes, and a hit within it of a write\n * costs the per-service reads it always cost before.\n *\n * When the location-wide marker cannot be read, or the adapter cannot store\n * it, the per-service markers are read instead. When it is simply absent (no\n * write here within `MARKER_TTL_S`, or its put failed while a per-service put\n * succeeded) the hit is served; as with any marker that failed, the\n * background deletes and the 30 s bucket still bound that seed.\n */\nconst MARKER_RACE_MS = SEED_BUCKET_MS;\n\n/**\n * How long after the first delete the second one runs. The first can land\n * BEFORE the write it answers is visible to a concurrent seed build: the slot\n * tag expiry is written in the platform's deferred work, and a booking-page\n * miss already in flight puts the seed it built from pre-booking slots once it\n * is done. Either puts a stale seed back after the first delete; the second\n * pass removes it. A build that starts after the tag expiry has landed reads\n * live.\n */\nexport const SEED_RECHECK_MS = 2_000;\n\nexport interface BookingCatalogue {\n services: BookingServiceDto[];\n /** `null` when the stylists could not be read; the wizard fetches them. */\n resources: BookingResourceDto[] | null;\n}\n\nexport interface BookingSeed extends BookingCatalogue {\n slots: Record<string, BookingSlotDto[]>;\n schedules: Record<string, BookingDayDto[]>;\n nextAvailable: Record<string, Record<string, number>>;\n /** When the slot/schedule part was read from `catalogue.ts`. */\n generatedAt: number;\n /** The window the slots and schedules describe. */\n fromTs: number;\n toTs: number;\n}\n\ninterface StoredSeed {\n slots: Record<string, BookingSlotDto[]>;\n schedules: Record<string, BookingDayDto[]>;\n generatedAt: number;\n}\n\n/** How a seed read ended, for `x-booking-seed` and the timing sink. */\nexport type SeedState = 'hit' | 'stale' | 'miss' | 'bypass';\n\nexport interface SeedOptions {\n config: Pick<\n BookingConfig,\n 'timeZone' | 'locale' | 'dayparts' | 'window' | 'categories' | 'fallbackCategory' | 'paths'\n >;\n cache: { edge: BookingCacheAdapter; environment: string; edgePrefix?: string };\n logger: BookingLogger;\n timing?: BookingTiming;\n}\n\nexport interface Seed {\n /** The window the wizard shows and prefetches for, in local days. */\n readonly RANGE_DAYS: number;\n /** How many services to warm the cache with. */\n readonly PREFETCH_LIMIT: number;\n prefetchTargets(services: BookingServiceDto[], named: string | undefined): BookingServiceDto[];\n prefetchKey(targets: readonly { id: string }[]): string;\n seedBucket(now: number): number;\n expiryMarkerKey(serviceId: string): string | null;\n anyExpiryMarkerKey(): string | null;\n catalogueKeyAt(now: number): string | null;\n catalogueLatestKey(): string | null;\n seedLatestKeyAt(key: string, now: number): string | null;\n seedKeyAt(key: string, now: number): string | null;\n loadBookingCatalogue(now?: number): Promise<BookingCatalogue>;\n loadBookingSeed(named: string | undefined, now?: number): Promise<BookingSeed>;\n allPrefetchKeys(services: BookingServiceDto[]): string[];\n seedKeysFor(services: BookingServiceDto[], serviceIds: Iterable<string>, now: number): string[];\n expireBookingSeeds(\n serviceIds: Iterable<string>,\n now?: number,\n recheckAfterMs?: number\n ): Promise<void>;\n /** Test seam: forget in-flight refreshes. */\n resetSeedRefreshes(): void;\n}\n\ntype SeedCatalogue = Pick<\n Catalogue,\n 'cachedServices' | 'cachedResources' | 'cachedAvailability' | 'cachedSchedule'\n>;\n\nconst sleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));\n\nexport function createSeed(options: SeedOptions, catalogue: SeedCatalogue): Seed {\n const { config, logger } = options;\n const { edge } = options.cache;\n const SEED_PATH = options.cache.edgePrefix ?? DEFAULT_EDGE_PREFIX;\n const RANGE_DAYS = config.window.rangeDays;\n const PREFETCH_LIMIT = config.window.prefetchLimit;\n const clock = createClock(config);\n const { toBookingServiceDto, toBookingResourceDto } = createDto(config);\n\n /** In-flight refreshes by edge key, shared by every request in this isolate. */\n const inflight = new Map<string, Promise<unknown>>();\n\n function once<T>(key: string, work: () => Promise<T>): Promise<T> {\n const running = inflight.get(key) as Promise<T> | undefined;\n if (running) return running;\n const started = work().finally(() => inflight.delete(key));\n inflight.set(key, started);\n return started;\n }\n\n /** A package key as the adapter stores it, or `null` when it cannot store now. */\n function storageKey(key: string): string | null {\n return edge.key ? edge.key(key) : key;\n }\n\n function keyBase(): string | null {\n const env = options.cache.environment || 'unknown';\n const version = edge.version?.();\n const base = `${SEED_PATH}/${encodeURIComponent(version ? `${env}-${version}` : env)}`;\n return storageKey(base) === null ? null : base;\n }\n\n function keyed(path: (base: string) => string): string | null {\n const base = keyBase();\n return base === null ? null : storageKey(path(base));\n }\n\n /**\n * The services worth prefetching, in order, capped at `PREFETCH_LIMIT`: the\n * one the link names (it is the one the visitor is known to want), then the\n * bookable `window.prefetchCategory` menu. An unknown value is ignored.\n */\n function prefetchTargets(\n services: BookingServiceDto[],\n named: string | undefined\n ): BookingServiceDto[] {\n const linked = named\n ? services.find((service) => service.bookableOnline && serviceMatches(service, named))\n : undefined;\n const prefetchCategory = config.window.prefetchCategory;\n const kids =\n prefetchCategory === null\n ? []\n : services.filter(\n (service) =>\n service.bookableOnline &&\n normaliseCategory(config, service.category) === prefetchCategory &&\n service.id !== linked?.id\n );\n return [...(linked ? [linked] : []), ...kids].slice(0, PREFETCH_LIMIT);\n }\n\n /** The ids a seed is keyed by: sorted, so the order of the set is not a key. */\n function prefetchKey(targets: readonly { id: string }[]): string {\n return [...new Set(targets.map((target) => target.id))]\n .sort()\n .map(encodeURIComponent)\n .join(',');\n }\n\n function seedBucket(now: number): number {\n return Math.floor(now / SEED_BUCKET_MS);\n }\n\n function expiryMarkerKey(serviceId: string): string | null {\n return keyed((base) => `${base}/gen/${encodeURIComponent(serviceId)}`);\n }\n\n /** The location-wide expiry marker: the last write to any service here. */\n function anyExpiryMarkerKey(): string | null {\n return keyed((base) => `${base}/gen-any`);\n }\n\n function catalogueKeyAt(now: number): string | null {\n return keyed((base) => `${base}/catalogue/${Math.floor(now / CATALOGUE_BUCKET_MS)}`);\n }\n\n /** The catalogue kept past its bucket, for a stale-while-revalidate serve. */\n function catalogueLatestKey(): string | null {\n return keyed((base) => `${base}/catalogue/latest`);\n }\n\n /** One prefetch set's seed kept past its bucket, for the same local day. */\n function seedLatestKeyAt(key: string, now: number): string | null {\n if (key === '') return null;\n return keyed((base) => `${base}/seed-latest/${clock.dayKey(now)}/${key}`);\n }\n\n /** The seed key for one prefetch set at one instant. */\n function seedKeyAt(key: string, now: number): string | null {\n if (key === '') return null;\n return keyed((base) => `${base}/seed/${clock.dayKey(now)}/${key}/${seedBucket(now)}`);\n }\n\n async function edgeRead<T>(key: string): Promise<T | undefined> {\n try {\n return await edge.get<T>(key);\n } catch (error) {\n logger.warn({ err: error }, 'Could not read the booking seed from the colo cache');\n return undefined;\n }\n }\n\n function edgeWrite(key: string, value: unknown, ttlSeconds: number): Promise<void> {\n return inBackground(edge, edge.put(key, value, ttlSeconds));\n }\n\n /** Whether a copy built at `builtAt` may replace what `key` holds now. */\n async function isNewer(key: string, builtAt: number): Promise<boolean> {\n const kept = await edgeRead<{ generatedAt?: number; builtAt?: number }>(key);\n const keptAt = kept?.generatedAt ?? kept?.builtAt;\n return typeof keptAt !== 'number' || builtAt >= keptAt;\n }\n\n /**\n * One expiry marker, `null` when it is not set, or `failed` when the\n * adapter cannot store it or the read fails.\n */\n async function readMarker<F>(key: string | null, failed: F): Promise<number | null | F> {\n if (key === null) return failed;\n try {\n const hit = await edge.get<unknown>(key);\n const mark = hit === undefined ? Number.NaN : Number(hit);\n return Number.isFinite(mark) ? mark : null;\n } catch {\n return failed;\n }\n }\n\n /**\n * The latest per-service expiry marker among `targets`, or `null`. A marker\n * that cannot be read counts as none: the delete passes and the 30 s bucket\n * still bound it.\n */\n async function latestExpiry(targets: readonly { id: string }[]): Promise<number | null> {\n const marks = await Promise.all(targets.map(({ id }) => readMarker(expiryMarkerKey(id), null)));\n const valid = marks.filter((mark) => mark !== null);\n return valid.length === 0 ? null : Math.max(...valid);\n }\n\n /**\n * Whether a write touching one of `targets`' services was marked at or\n * after an instant — i.e. a seed built then may hold pre-write openings.\n * The location-wide marker is read once, as soon as this is called (so it\n * runs alongside the seed read); the per-service markers only when it does\n * not rule a write out (`MARKER_RACE_MS`), and then once for every check.\n */\n function expiryCheck(targets: readonly { id: string }[]): (since: number) => Promise<boolean> {\n // Unreadable or unstorable: let the per-service markers decide.\n const any = readMarker(anyExpiryMarkerKey(), 'unknown' as const);\n let perService: Promise<number | null> | undefined;\n return async (since) => {\n const anyAt = await any;\n if (anyAt === null || (anyAt !== 'unknown' && anyAt < since - MARKER_RACE_MS)) return false;\n perService ??= latestExpiry(targets);\n const latest = await perService;\n return latest !== null && latest >= since;\n };\n }\n\n async function writeExpiryMarkers(serviceIds: string[], at: number): Promise<void> {\n const keys = [...serviceIds.map(expiryMarkerKey), anyExpiryMarkerKey()];\n await Promise.all(\n keys.map(async (key) => {\n if (key === null) return;\n try {\n await edge.put(key, at, MARKER_TTL_S);\n } catch (error) {\n logger.warn({ err: error }, 'Could not mark booking seeds expired');\n }\n })\n );\n }\n\n /** The catalogue straight from `catalogue.ts`. Services failing throws. */\n async function readCatalogue(): Promise<{ catalogue: BookingCatalogue; complete: boolean }> {\n const [services, resources] = await Promise.all([\n catalogue.cachedServices(),\n catalogue.cachedResources().then(\n (list) => list.map(toBookingResourceDto),\n (error: unknown) => {\n logger.warn({ err: error }, 'Could not prefetch the stylists for the booking page');\n return null;\n }\n ),\n ]);\n return {\n catalogue: { services: services.map(toBookingServiceDto), resources },\n complete: resources !== null,\n };\n }\n\n /**\n * The catalogue, from the edge when there is one. Throws when the services\n * cannot be read — the one failure that replaces the wizard.\n */\n async function loadBookingCatalogue(now: number = Date.now()): Promise<BookingCatalogue> {\n return timed(options.timing, 'catalogue', async () => {\n const key = catalogueKeyAt(now);\n if (key === null) return (await readCatalogue()).catalogue;\n const hit = await edgeRead<BookingCatalogue>(key);\n if (hit && Array.isArray(hit.services)) return hit;\n const refresh = (): Promise<BookingCatalogue> =>\n once(`${key}#refresh`, async () => {\n const builtAt = Date.now();\n const { catalogue: built, complete } = await readCatalogue();\n if (complete && built.services.length > 0) {\n await edgeWrite(key, built, CATALOGUE_TTL_S);\n const latest = catalogueLatestKey();\n if (latest !== null && (await isNewer(latest, builtAt))) {\n await edgeWrite(latest, { ...built, builtAt }, CATALOGUE_STALE_MAX_S);\n }\n }\n return built;\n });\n const latest = catalogueLatestKey();\n const stale = latest === null ? undefined : await edgeRead<BookingCatalogue>(latest);\n if (stale && Array.isArray(stale.services) && stale.services.length > 0) {\n void inBackground(\n edge,\n refresh().catch((error: unknown) => {\n logger.warn({ err: error }, 'Could not refresh the booking catalogue');\n })\n );\n return stale;\n }\n return refresh();\n });\n }\n\n /** One read per service, best effort per service. */\n async function prefetchEach<T>(\n targets: BookingServiceDto[],\n what: string,\n readOne: (serviceId: string) => Promise<T>\n ): Promise<{ values: Record<string, T>; complete: boolean }> {\n const fetched = await Promise.all(\n targets.map(async (service) => {\n try {\n return [service.id, await readOne(service.id)] as const;\n } catch (error) {\n logger.warn({ err: error, serviceId: service.id }, `Could not prefetch ${what}`);\n return null;\n }\n })\n );\n const entries = fetched.filter((entry) => entry !== null);\n return { values: Object.fromEntries(entries), complete: entries.length === targets.length };\n }\n\n async function buildSeed(\n targets: BookingServiceDto[],\n fromTs: number,\n toTs: number\n ): Promise<{ seed: StoredSeed; complete: boolean }> {\n const [slots, schedules] = await Promise.all([\n prefetchEach(targets, 'availability', async (serviceId) =>\n (await catalogue.cachedAvailability({ serviceId, fromTs, toTs })).flatMap(toBookingSlotDto)\n ),\n prefetchEach(targets, 'the schedule', async (serviceId) =>\n (await catalogue.cachedSchedule({ serviceId, fromTs, toTs })).flatMap(toBookingDayDto)\n ),\n ]);\n return {\n seed: { slots: slots.values, schedules: schedules.values, generatedAt: fromTs },\n complete: slots.complete && schedules.complete,\n };\n }\n\n /** A stored seed as of `now`: slots that have started since it was built go. */\n function freshen(seed: StoredSeed, now: number, toTs: number): StoredSeed {\n const slots = Object.fromEntries(\n Object.entries(seed.slots).map(([serviceId, list]) => [\n serviceId,\n list.filter((slot) => slot.startTs >= now && slot.startTs < toTs),\n ])\n );\n return { ...seed, slots };\n }\n\n /**\n * The seed for the page: the catalogue plus openings and hours for the\n * prefetch set `named` selects. Throws only when the catalogue cannot be read.\n *\n * The state (hit / stale / miss / bypass) goes to the timing sink with the\n * seed phase, for `x-booking-seed`.\n */\n async function loadBookingSeed(\n named: string | undefined,\n now: number = Date.now()\n ): Promise<BookingSeed> {\n const loaded = await loadBookingCatalogue(now);\n const { toTs } = clock.window(now, RANGE_DAYS);\n const targets = prefetchTargets(loaded.services, named);\n let state: SeedState = 'miss';\n const markSeed = (next: SeedState) => {\n state = next;\n };\n\n const stored = await timed(\n options.timing,\n 'seed',\n async (): Promise<StoredSeed> => {\n const key = seedKeyAt(prefetchKey(targets), now);\n if (key === null) {\n markSeed('bypass');\n return (await buildSeed(targets, now, toTs)).seed;\n }\n // The location-wide marker is read alongside the seed, not after it:\n // a hit costs no extra round trip, and one marker read whatever the\n // size of the prefetch set.\n const expired = expiryCheck(targets);\n const hit = await edgeRead<StoredSeed>(key);\n // A seed built before a write to one of its services was marked is\n // not served, even inside its bucket: the write route awaited that\n // marker before telling the visitor the slot was theirs, and the\n // background delete may not have run yet.\n if (hit?.slots && hit.schedules && !(await expired(hit.generatedAt))) {\n markSeed('hit');\n return freshen(hit, now, toTs);\n }\n const latestKey = seedLatestKeyAt(prefetchKey(targets), now);\n const rebuild = (): Promise<StoredSeed> =>\n once(`${key}#rebuild`, async () => {\n // Before any Medal read: a write expired at or after this instant\n // may not be in what this build reads, so its seed must not be\n // stored.\n const buildStart = Date.now();\n const { seed, complete } = await buildSeed(targets, now, toTs);\n if (complete && !(await expiryCheck(targets)(buildStart))) {\n await edgeWrite(key, seed, SEED_TTL_S);\n if (latestKey !== null && (await isNewer(latestKey, seed.generatedAt))) {\n await edgeWrite(latestKey, seed, Math.ceil(SEED_STALE_MAX_MS / 1000));\n }\n }\n return seed;\n });\n const stale = latestKey === null ? undefined : await edgeRead<StoredSeed>(latestKey);\n if (\n stale?.slots &&\n stale.schedules &&\n now - stale.generatedAt < SEED_STALE_MAX_MS &&\n !(await expired(stale.generatedAt))\n ) {\n markSeed('stale');\n void inBackground(\n edge,\n rebuild().catch((error: unknown) => {\n logger.warn({ err: error }, 'Could not refresh the booking seed');\n })\n );\n return freshen(stale, now, toTs);\n }\n markSeed('miss');\n return rebuild();\n },\n () => ({ state })\n );\n\n // Only alongside a stylist list: «next free» with nobody to hang it on is\n // not a seed the wizard can use, and it fetches both together instead.\n const nextAvailable =\n loaded.resources === null\n ? {}\n : Object.fromEntries(\n Object.entries(stored.slots).map(([serviceId, slots]) => [\n serviceId,\n firstOpeningPerResource(slots),\n ])\n );\n\n return {\n ...loaded,\n slots: stored.slots,\n schedules: stored.schedules,\n nextAvailable,\n generatedAt: stored.generatedAt,\n fromTs: now,\n toTs,\n };\n }\n\n /**\n * Every prefetch set the page can key a seed by: the default one and one per\n * bookable service a deep link can name. Deterministic from the catalogue.\n */\n function allPrefetchKeys(services: BookingServiceDto[]): string[] {\n const sets = [\n prefetchTargets(services, undefined),\n ...services\n .filter((service) => service.bookableOnline)\n .map((service) => prefetchTargets(services, service.id)),\n ];\n return [...new Set(sets.filter((set) => set.length > 0).map(prefetchKey))];\n }\n\n /**\n * The edge keys to delete after a write touching `serviceIds`: every prefetch\n * set containing one of them, in the current and the previous 30 s bucket (a\n * seed from the previous bucket can still be read by a request that started\n * in it). Sets not containing an affected service are left alone.\n */\n function seedKeysFor(\n services: BookingServiceDto[],\n serviceIds: Iterable<string>,\n now: number\n ): string[] {\n const affected = new Set(serviceIds);\n if (affected.size === 0) return [];\n const keys = allPrefetchKeys(services).filter((key) =>\n key.split(',').some((id) => affected.has(decodeURIComponent(id)))\n );\n const instants = [now, now - SEED_BUCKET_MS];\n return [\n ...new Set(\n keys\n .flatMap((key) => [\n ...instants.map((at) => seedKeyAt(key, at)),\n // The kept copy too: it must not be served stale after this write.\n seedLatestKeyAt(key, now),\n ])\n .filter((k) => k !== null)\n ),\n ];\n }\n\n async function deleteSeeds(serviceIds: string[], now: number): Promise<void> {\n const current = catalogueKeyAt(now);\n const previous = catalogueKeyAt(now - CATALOGUE_BUCKET_MS);\n // Nothing can be keyed: no seed was ever stored, so nothing to delete.\n if (current === null || previous === null) return;\n const kept =\n (await edgeRead<BookingCatalogue>(current)) ?? (await edgeRead<BookingCatalogue>(previous));\n const services = Array.isArray(kept?.services)\n ? kept.services\n : (await catalogue.cachedServices()).map(toBookingServiceDto);\n await edge.delete(seedKeysFor(services, serviceIds, now));\n }\n\n /**\n * Mark `serviceIds` expired (so an in-flight build started before now does\n * not store its seed), then delete this location's seeds containing any of\n * them, after a create / move / cancel or a SLOT_TAKEN — twice, the second\n * time `recheckAfterMs` later (see `SEED_RECHECK_MS`). The deletes run on the\n * adapter's `defer`, so they add nothing to the response; best effort — the\n * write has already happened and the 30 s bucket is the backstop. A no-op\n * when the edge adapter cannot store anything.\n */\n function expireBookingSeeds(\n serviceIds: Iterable<string>,\n now: number = Date.now(),\n recheckAfterMs: number = SEED_RECHECK_MS\n ): Promise<void> {\n const ids = [...new Set(serviceIds)];\n if (ids.length === 0 || keyBase() === null) return Promise.resolve();\n const pass = (at: number) =>\n deleteSeeds(ids, at).catch((error: unknown) => {\n logger.warn({ err: error }, 'Could not expire booking seeds');\n });\n // The marker first, and returned: a write route awaits it before\n // answering, so no kept seed from before the write passes `expiredSince`\n // in this location once the visitor has their confirmation. The deletes\n // follow in the background.\n const marked = writeExpiryMarkers(ids, now);\n void inBackground(\n edge,\n (async () => {\n await marked;\n await pass(now);\n await sleep(recheckAfterMs);\n // Re-derived at the time of the second pass: if the bucket turned in\n // between, the first pass's bucket is now the «previous» one.\n await pass(now + recheckAfterMs);\n })()\n );\n return marked;\n }\n\n return {\n RANGE_DAYS,\n PREFETCH_LIMIT,\n prefetchTargets,\n prefetchKey,\n seedBucket,\n expiryMarkerKey,\n anyExpiryMarkerKey,\n catalogueKeyAt,\n catalogueLatestKey,\n seedLatestKeyAt,\n seedKeyAt,\n loadBookingCatalogue,\n loadBookingSeed,\n allPrefetchKeys,\n seedKeysFor,\n expireBookingSeeds,\n resetSeedRefreshes: () => inflight.clear(),\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ import { BookingServerOptions } from "./options.mjs";
2
+ import "./runtime.mjs";
3
+ import { BookingHandler, createBookingHandlerFor } from "./handler.mjs";
4
+ import { BookingPageResult, ManagePageResult, PageSwitches, PortalPageResult } from "./loaders.mjs";
5
+ import { Portal } from "./portal.mjs";
6
+ //#region src/next/server.d.ts
7
+ type SearchParams = Record<string, string | string[] | undefined>;
8
+ export interface BookingServer {
9
+ handler: ReturnType<typeof createBookingHandlerFor>;
10
+ portal: Portal;
11
+ loadBookingPage(args?: PageSwitches & {
12
+ searchParams?: SearchParams;
13
+ }): Promise<BookingPageResult>;
14
+ loadManagePage(token: string, switches?: PageSwitches): Promise<ManagePageResult>;
15
+ loadPortalPage(switches?: PageSwitches): Promise<PortalPageResult>;
16
+ }
17
+ export declare function createBookingServer(options: BookingServerOptions): BookingServer;
18
+ export declare function createBookingHandler(options: BookingServerOptions): BookingHandler;
19
+ export declare function createPortal(options: BookingServerOptions): Portal;
20
+ export declare function loadBookingPage(options: BookingServerOptions, args?: PageSwitches & {
21
+ searchParams?: SearchParams;
22
+ }): Promise<BookingPageResult>;
23
+ export declare function loadManagePage(options: BookingServerOptions, token: string, switches?: PageSwitches): Promise<ManagePageResult>;
24
+ export declare function loadPortalPage(options: BookingServerOptions, switches?: PageSwitches): Promise<PortalPageResult>;
25
+ //#endregion
26
+ //# sourceMappingURL=server.d.mts.map