@hexclave/shared 1.0.3 → 1.0.6

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 (335) hide show
  1. package/dist/ai/unified-prompts/reminders.js +4 -3
  2. package/dist/ai/unified-prompts/reminders.js.map +1 -1
  3. package/dist/ai/unified-prompts/skill-site-prompt-parts/ai-setup-prompt.d.ts +3 -1
  4. package/dist/ai/unified-prompts/skill-site-prompt-parts/ai-setup-prompt.d.ts.map +1 -1
  5. package/dist/ai/unified-prompts/skill-site-prompt-parts/ai-setup-prompt.js +216 -29
  6. package/dist/ai/unified-prompts/skill-site-prompt-parts/ai-setup-prompt.js.map +1 -1
  7. package/dist/ai/unified-prompts/skill-site-prompt-parts/docs-index.d.ts.map +1 -1
  8. package/dist/ai/unified-prompts/skill-site-prompt-parts/docs-index.js +1 -0
  9. package/dist/ai/unified-prompts/skill-site-prompt-parts/docs-index.js.map +1 -1
  10. package/dist/ai/unified-prompts/skill-site-prompt-parts/docs-json.generated.d.ts +18 -6
  11. package/dist/ai/unified-prompts/skill-site-prompt-parts/docs-json.generated.js +23 -8
  12. package/dist/ai/unified-prompts/skill-site-prompt-parts/docs-json.generated.js.map +1 -1
  13. package/dist/ai/unified-prompts/skill-site-prompt-parts/openapi-specs.js +1 -1
  14. package/dist/ai/unified-prompts/skill-site-prompt-parts/openapi-specs.js.map +1 -1
  15. package/dist/config/schema.d.ts +1 -1
  16. package/dist/config/schema.d.ts.map +1 -1
  17. package/dist/config/schema.js +5 -5
  18. package/dist/config/schema.js.map +1 -1
  19. package/dist/config-authoring.d.ts +4 -4
  20. package/dist/config-authoring.d.ts.map +1 -1
  21. package/dist/config-authoring.js +2 -2
  22. package/dist/config-authoring.js.map +1 -1
  23. package/dist/config-rendering.d.ts +4 -4
  24. package/dist/config-rendering.d.ts.map +1 -1
  25. package/dist/config-rendering.js +19 -18
  26. package/dist/config-rendering.js.map +1 -1
  27. package/dist/esm/ai/unified-prompts/reminders.js +4 -3
  28. package/dist/esm/ai/unified-prompts/reminders.js.map +1 -1
  29. package/dist/esm/ai/unified-prompts/skill-site-prompt-parts/ai-setup-prompt.d.ts +3 -1
  30. package/dist/esm/ai/unified-prompts/skill-site-prompt-parts/ai-setup-prompt.d.ts.map +1 -1
  31. package/dist/esm/ai/unified-prompts/skill-site-prompt-parts/ai-setup-prompt.js +215 -30
  32. package/dist/esm/ai/unified-prompts/skill-site-prompt-parts/ai-setup-prompt.js.map +1 -1
  33. package/dist/esm/ai/unified-prompts/skill-site-prompt-parts/docs-index.d.ts.map +1 -1
  34. package/dist/esm/ai/unified-prompts/skill-site-prompt-parts/docs-index.js +1 -0
  35. package/dist/esm/ai/unified-prompts/skill-site-prompt-parts/docs-index.js.map +1 -1
  36. package/dist/esm/ai/unified-prompts/skill-site-prompt-parts/docs-json.generated.d.ts +18 -6
  37. package/dist/esm/ai/unified-prompts/skill-site-prompt-parts/docs-json.generated.js +23 -8
  38. package/dist/esm/ai/unified-prompts/skill-site-prompt-parts/docs-json.generated.js.map +1 -1
  39. package/dist/esm/ai/unified-prompts/skill-site-prompt-parts/openapi-specs.js +1 -1
  40. package/dist/esm/ai/unified-prompts/skill-site-prompt-parts/openapi-specs.js.map +1 -1
  41. package/dist/esm/config/schema.d.ts +1 -1
  42. package/dist/esm/config/schema.d.ts.map +1 -1
  43. package/dist/esm/config/schema.js +5 -5
  44. package/dist/esm/config/schema.js.map +1 -1
  45. package/dist/esm/config-authoring.d.ts +4 -4
  46. package/dist/esm/config-authoring.d.ts.map +1 -1
  47. package/dist/esm/config-authoring.js +2 -2
  48. package/dist/esm/config-authoring.js.map +1 -1
  49. package/dist/esm/config-rendering.d.ts +4 -4
  50. package/dist/esm/config-rendering.d.ts.map +1 -1
  51. package/dist/esm/config-rendering.js +15 -14
  52. package/dist/esm/config-rendering.js.map +1 -1
  53. package/dist/esm/helpers/vault/client-side.d.ts.map +1 -1
  54. package/dist/esm/helpers/vault/client-side.js.map +1 -1
  55. package/dist/esm/helpers/vault/server-side.d.ts.map +1 -1
  56. package/dist/esm/helpers/vault/server-side.js.map +1 -1
  57. package/dist/esm/hexclave-config-file.d.ts +15 -0
  58. package/dist/esm/hexclave-config-file.d.ts.map +1 -0
  59. package/dist/esm/{stack-config-file.js → hexclave-config-file.js} +7 -7
  60. package/dist/esm/hexclave-config-file.js.map +1 -0
  61. package/dist/esm/interface/admin-metrics.d.ts +2 -2
  62. package/dist/esm/interface/admin-metrics.js.map +1 -1
  63. package/dist/esm/interface/client-interface.js +2 -2
  64. package/dist/esm/interface/client-interface.js.map +1 -1
  65. package/dist/esm/interface/conversations.d.ts +32 -32
  66. package/dist/esm/interface/crud/current-user.d.ts +7 -7
  67. package/dist/esm/interface/crud/email-outbox.d.ts +42 -42
  68. package/dist/esm/interface/crud/project-permissions.js +5 -5
  69. package/dist/esm/interface/crud/project-permissions.js.map +1 -1
  70. package/dist/esm/interface/crud/team-member-profiles.d.ts +18 -18
  71. package/dist/esm/interface/crud/team-permissions.js +5 -5
  72. package/dist/esm/interface/crud/team-permissions.js.map +1 -1
  73. package/dist/esm/interface/crud/users.d.ts +10 -10
  74. package/dist/esm/interface/page-component-versions.js +107 -107
  75. package/dist/esm/interface/page-component-versions.js.map +1 -1
  76. package/dist/esm/interface/webhooks.d.ts +2 -2
  77. package/dist/esm/known-errors.d.ts +1 -1
  78. package/dist/esm/known-errors.d.ts.map +1 -1
  79. package/dist/esm/known-errors.js +9 -5
  80. package/dist/esm/known-errors.js.map +1 -1
  81. package/dist/esm/schema-fields.d.ts +15 -5
  82. package/dist/esm/schema-fields.d.ts.map +1 -1
  83. package/dist/esm/schema-fields.js +26 -16
  84. package/dist/esm/schema-fields.js.map +1 -1
  85. package/dist/esm/utils/crypto.d.ts.map +1 -1
  86. package/dist/esm/utils/crypto.js.map +1 -1
  87. package/dist/esm/utils/currency-constants.d.ts +6 -0
  88. package/dist/esm/utils/currency-constants.d.ts.map +1 -1
  89. package/dist/esm/utils/currency-constants.js.map +1 -1
  90. package/dist/esm/utils/dev-env-state-path.d.ts +3 -3
  91. package/dist/esm/utils/dev-env-state-path.d.ts.map +1 -1
  92. package/dist/esm/utils/dev-env-state-path.js +4 -4
  93. package/dist/esm/utils/dev-env-state-path.js.map +1 -1
  94. package/dist/esm/utils/errors.js +2 -2
  95. package/dist/esm/utils/errors.js.map +1 -1
  96. package/dist/esm/utils/esbuild.d.ts.map +1 -1
  97. package/dist/esm/utils/esbuild.js +0 -1
  98. package/dist/esm/utils/esbuild.js.map +1 -1
  99. package/dist/esm/utils/featurebase.d.ts +3 -3
  100. package/dist/esm/utils/featurebase.d.ts.map +1 -1
  101. package/dist/esm/utils/featurebase.js +15 -15
  102. package/dist/esm/utils/featurebase.js.map +1 -1
  103. package/dist/esm/utils/fs.js +6 -6
  104. package/dist/esm/utils/fs.js.map +1 -1
  105. package/dist/esm/utils/globals.d.ts.map +1 -1
  106. package/dist/esm/utils/globals.js +10 -10
  107. package/dist/esm/utils/globals.js.map +1 -1
  108. package/dist/esm/utils/jwt.d.ts.map +1 -1
  109. package/dist/esm/utils/jwt.js +3 -3
  110. package/dist/esm/utils/jwt.js.map +1 -1
  111. package/dist/esm/utils/redirect-urls.d.ts +3 -3
  112. package/dist/esm/utils/redirect-urls.d.ts.map +1 -1
  113. package/dist/esm/utils/redirect-urls.js +5 -5
  114. package/dist/esm/utils/redirect-urls.js.map +1 -1
  115. package/dist/esm/utils/urls.js.map +1 -1
  116. package/dist/helpers/vault/client-side.d.ts.map +1 -1
  117. package/dist/helpers/vault/client-side.js.map +1 -1
  118. package/dist/helpers/vault/server-side.d.ts.map +1 -1
  119. package/dist/helpers/vault/server-side.js.map +1 -1
  120. package/dist/hexclave-config-file.d.ts +15 -0
  121. package/dist/hexclave-config-file.d.ts.map +1 -0
  122. package/dist/{stack-config-file.js → hexclave-config-file.js} +8 -8
  123. package/dist/hexclave-config-file.js.map +1 -0
  124. package/dist/interface/admin-metrics.d.ts +12 -12
  125. package/dist/interface/admin-metrics.js.map +1 -1
  126. package/dist/interface/client-interface.js +2 -2
  127. package/dist/interface/client-interface.js.map +1 -1
  128. package/dist/interface/conversations.d.ts +32 -32
  129. package/dist/interface/crud/current-user.d.ts +7 -7
  130. package/dist/interface/crud/email-outbox.d.ts +42 -42
  131. package/dist/interface/crud/project-permissions.js +5 -5
  132. package/dist/interface/crud/project-permissions.js.map +1 -1
  133. package/dist/interface/crud/team-member-profiles.d.ts +18 -18
  134. package/dist/interface/crud/team-permissions.js +5 -5
  135. package/dist/interface/crud/team-permissions.js.map +1 -1
  136. package/dist/interface/crud/users.d.ts +10 -10
  137. package/dist/interface/page-component-versions.js +107 -107
  138. package/dist/interface/page-component-versions.js.map +1 -1
  139. package/dist/interface/webhooks.d.ts +2 -2
  140. package/dist/known-errors.d.ts +1 -1
  141. package/dist/known-errors.d.ts.map +1 -1
  142. package/dist/known-errors.js +9 -5
  143. package/dist/known-errors.js.map +1 -1
  144. package/dist/schema-fields.d.ts +15 -5
  145. package/dist/schema-fields.d.ts.map +1 -1
  146. package/dist/schema-fields.js +26 -16
  147. package/dist/schema-fields.js.map +1 -1
  148. package/dist/utils/crypto.d.ts.map +1 -1
  149. package/dist/utils/crypto.js.map +1 -1
  150. package/dist/utils/currency-constants.d.ts +6 -0
  151. package/dist/utils/currency-constants.d.ts.map +1 -1
  152. package/dist/utils/currency-constants.js.map +1 -1
  153. package/dist/utils/dev-env-state-path.d.ts +3 -3
  154. package/dist/utils/dev-env-state-path.d.ts.map +1 -1
  155. package/dist/utils/dev-env-state-path.js +5 -5
  156. package/dist/utils/dev-env-state-path.js.map +1 -1
  157. package/dist/utils/errors.js +2 -2
  158. package/dist/utils/errors.js.map +1 -1
  159. package/dist/utils/esbuild.d.ts.map +1 -1
  160. package/dist/utils/esbuild.js +0 -1
  161. package/dist/utils/esbuild.js.map +1 -1
  162. package/dist/utils/featurebase.d.ts +3 -3
  163. package/dist/utils/featurebase.d.ts.map +1 -1
  164. package/dist/utils/featurebase.js +15 -15
  165. package/dist/utils/featurebase.js.map +1 -1
  166. package/dist/utils/fs.js.map +1 -1
  167. package/dist/utils/globals.d.ts.map +1 -1
  168. package/dist/utils/globals.js +10 -10
  169. package/dist/utils/globals.js.map +1 -1
  170. package/dist/utils/jwt.d.ts.map +1 -1
  171. package/dist/utils/jwt.js +3 -3
  172. package/dist/utils/jwt.js.map +1 -1
  173. package/dist/utils/redirect-urls.d.ts +3 -3
  174. package/dist/utils/redirect-urls.d.ts.map +1 -1
  175. package/dist/utils/redirect-urls.js +5 -5
  176. package/dist/utils/redirect-urls.js.map +1 -1
  177. package/dist/utils/urls.js.map +1 -1
  178. package/package.json +2 -1
  179. package/src/ai/image-limits.ts +62 -0
  180. package/src/ai/llms/llms.ts +36 -0
  181. package/src/ai/unified-prompts/reminders.ts +27 -0
  182. package/src/ai/unified-prompts/skill-site-prompt-parts/ai-setup-prompt.ts +1047 -0
  183. package/src/ai/unified-prompts/skill-site-prompt-parts/brain.ts +5 -0
  184. package/src/ai/unified-prompts/skill-site-prompt-parts/cli-help.ts +5 -0
  185. package/src/ai/unified-prompts/skill-site-prompt-parts/config-docs.ts +5 -0
  186. package/src/ai/unified-prompts/skill-site-prompt-parts/custom-components-instructions.ts +5 -0
  187. package/src/ai/unified-prompts/skill-site-prompt-parts/dashboard-instructions.ts +5 -0
  188. package/src/ai/unified-prompts/skill-site-prompt-parts/docs-index.ts +80 -0
  189. package/src/ai/unified-prompts/skill-site-prompt-parts/docs-json.generated.ts +322 -0
  190. package/src/ai/unified-prompts/skill-site-prompt-parts/openapi-specs.ts +5 -0
  191. package/src/ai/unified-prompts/skill-site-prompt-parts/sdk-interface-source.ts +5 -0
  192. package/src/ai/unified-prompts/skill-site-prompt.ts +72 -0
  193. package/src/apps/apps-config.ts +177 -0
  194. package/src/apps/apps-ui.tsx +112 -0
  195. package/src/config/README.md +154 -0
  196. package/src/config/db-sync-mappings.ts +2293 -0
  197. package/src/config/format.ts +388 -0
  198. package/src/config/migrate-catalogs-to-product-lines.ts +475 -0
  199. package/src/config/schema-fuzzer.test.ts +377 -0
  200. package/src/config/schema.ts +1507 -0
  201. package/src/config-authoring.test.ts +52 -0
  202. package/src/config-authoring.ts +37 -0
  203. package/src/config-rendering.ts +142 -0
  204. package/src/crud.tsx +275 -0
  205. package/src/global.d.ts +1 -0
  206. package/src/helpers/emails.ts +250 -0
  207. package/src/helpers/init-prompt.ts +39 -0
  208. package/src/helpers/password.ts +16 -0
  209. package/src/helpers/production-mode.ts +58 -0
  210. package/src/helpers/support-sla.test.ts +124 -0
  211. package/src/helpers/support-sla.ts +93 -0
  212. package/src/helpers/vault/client-side.ts +92 -0
  213. package/src/helpers/vault/server-side.ts +153 -0
  214. package/src/hexclave-config-file.ts +120 -0
  215. package/src/hooks/use-async-callback.tsx +43 -0
  216. package/src/hooks/use-async-external-store.tsx +24 -0
  217. package/src/hooks/use-hash.tsx +14 -0
  218. package/src/hooks/use-hover.tsx +88 -0
  219. package/src/hooks/use-strict-memo.tsx +157 -0
  220. package/src/index.ts +17 -0
  221. package/src/interface/admin-interface.ts +1043 -0
  222. package/src/interface/admin-metrics.ts +214 -0
  223. package/src/interface/client-interface.test.ts +765 -0
  224. package/src/interface/client-interface.ts +2586 -0
  225. package/src/interface/conversations.ts +109 -0
  226. package/src/interface/crud/analytics.ts +11 -0
  227. package/src/interface/crud/connected-accounts.ts +52 -0
  228. package/src/interface/crud/contact-channels.ts +74 -0
  229. package/src/interface/crud/current-user.ts +72 -0
  230. package/src/interface/crud/email-outbox.ts +263 -0
  231. package/src/interface/crud/email-templates.ts +50 -0
  232. package/src/interface/crud/emails.ts +22 -0
  233. package/src/interface/crud/internal-api-keys.ts +69 -0
  234. package/src/interface/crud/invoices.ts +36 -0
  235. package/src/interface/crud/items.ts +24 -0
  236. package/src/interface/crud/notification-preferences.ts +33 -0
  237. package/src/interface/crud/oauth-providers.ts +91 -0
  238. package/src/interface/crud/products.ts +42 -0
  239. package/src/interface/crud/project-api-keys.ts +100 -0
  240. package/src/interface/crud/project-permissions.ts +118 -0
  241. package/src/interface/crud/projects.ts +229 -0
  242. package/src/interface/crud/session-replays.ts +85 -0
  243. package/src/interface/crud/sessions.ts +66 -0
  244. package/src/interface/crud/sign-up-rules.ts +12 -0
  245. package/src/interface/crud/svix-token.ts +20 -0
  246. package/src/interface/crud/team-invitation-details.ts +22 -0
  247. package/src/interface/crud/team-invitation.ts +35 -0
  248. package/src/interface/crud/team-member-profiles.ts +61 -0
  249. package/src/interface/crud/team-memberships.ts +56 -0
  250. package/src/interface/crud/team-permissions.ts +119 -0
  251. package/src/interface/crud/teams.ts +142 -0
  252. package/src/interface/crud/transactions.ts +131 -0
  253. package/src/interface/crud/users.ts +185 -0
  254. package/src/interface/handler-urls.ts +74 -0
  255. package/src/interface/page-component-versions.ts +1702 -0
  256. package/src/interface/server-interface.ts +1146 -0
  257. package/src/interface/webhooks.ts +28 -0
  258. package/src/known-errors.tsx +2034 -0
  259. package/src/local-emulator.ts +5 -0
  260. package/src/payments/payment-methods.ts +167 -0
  261. package/src/payments/stripe-limits.ts +18 -0
  262. package/src/plans.ts +107 -0
  263. package/src/schema-fields.ts +944 -0
  264. package/src/sessions.ts +310 -0
  265. package/src/utils/api-keys.tsx +122 -0
  266. package/src/utils/arrays.tsx +248 -0
  267. package/src/utils/auth-methods.ts +8 -0
  268. package/src/utils/base64.tsx +24 -0
  269. package/src/utils/booleans.tsx +34 -0
  270. package/src/utils/browser-compat.tsx +17 -0
  271. package/src/utils/bytes.tsx +280 -0
  272. package/src/utils/caches.tsx +280 -0
  273. package/src/utils/cel-fields.ts +92 -0
  274. package/src/utils/cloud-hosts.tsx +92 -0
  275. package/src/utils/compile-time.tsx +11 -0
  276. package/src/utils/country-codes.ts +39 -0
  277. package/src/utils/crypto.tsx +174 -0
  278. package/src/utils/currencies.tsx +53 -0
  279. package/src/utils/currency-constants.tsx +51 -0
  280. package/src/utils/dates.tsx +246 -0
  281. package/src/utils/dev-env-state-path.ts +14 -0
  282. package/src/utils/dom.tsx +7 -0
  283. package/src/utils/env.tsx +117 -0
  284. package/src/utils/errors.tsx +284 -0
  285. package/src/utils/esbuild.tsx +235 -0
  286. package/src/utils/featurebase.tsx +247 -0
  287. package/src/utils/fs.tsx +33 -0
  288. package/src/utils/functions.tsx +22 -0
  289. package/src/utils/geo.tsx +16 -0
  290. package/src/utils/globals.tsx +48 -0
  291. package/src/utils/hashes.tsx +52 -0
  292. package/src/utils/html.tsx +60 -0
  293. package/src/utils/http.tsx +91 -0
  294. package/src/utils/ips.tsx +47 -0
  295. package/src/utils/json.tsx +200 -0
  296. package/src/utils/jsx-editable-transpiler.test.ts +156 -0
  297. package/src/utils/jsx-editable-transpiler.tsx +401 -0
  298. package/src/utils/jwt.tsx +170 -0
  299. package/src/utils/locks.tsx +71 -0
  300. package/src/utils/maps.tsx +407 -0
  301. package/src/utils/math.tsx +18 -0
  302. package/src/utils/monkey-patch.tsx +29 -0
  303. package/src/utils/node-http.tsx +47 -0
  304. package/src/utils/numbers.tsx +82 -0
  305. package/src/utils/oauth.tsx +9 -0
  306. package/src/utils/objects.tsx +581 -0
  307. package/src/utils/paginated-lists.test.ts +1039 -0
  308. package/src/utils/paginated-lists.tsx +479 -0
  309. package/src/utils/passkey.tsx +1 -0
  310. package/src/utils/promises.tsx +540 -0
  311. package/src/utils/proxies.tsx +212 -0
  312. package/src/utils/react.tsx +308 -0
  313. package/src/utils/redirect-urls.tsx +299 -0
  314. package/src/utils/regex.tsx +10 -0
  315. package/src/utils/results.tsx +411 -0
  316. package/src/utils/sentry.tsx +20 -0
  317. package/src/utils/stores.tsx +238 -0
  318. package/src/utils/strings.nicify.test.ts +253 -0
  319. package/src/utils/strings.tsx +723 -0
  320. package/src/utils/telemetry.tsx +37 -0
  321. package/src/utils/turnstile-browser.ts +106 -0
  322. package/src/utils/turnstile-flow.test.ts +101 -0
  323. package/src/utils/turnstile-flow.ts +266 -0
  324. package/src/utils/turnstile.ts +34 -0
  325. package/src/utils/typed-arrays.tsx +17 -0
  326. package/src/utils/types.tsx +208 -0
  327. package/src/utils/unicode.tsx +24 -0
  328. package/src/utils/urls.tsx +411 -0
  329. package/src/utils/uuids.tsx +47 -0
  330. package/dist/esm/stack-config-file.d.ts +0 -15
  331. package/dist/esm/stack-config-file.d.ts.map +0 -1
  332. package/dist/esm/stack-config-file.js.map +0 -1
  333. package/dist/stack-config-file.d.ts +0 -15
  334. package/dist/stack-config-file.d.ts.map +0 -1
  335. package/dist/stack-config-file.js.map +0 -1
@@ -0,0 +1,24 @@
1
+ export function fileToBase64(file: File): Promise<string> {
2
+ return new Promise((resolve, reject) => {
3
+ const reader = new FileReader();
4
+ reader.readAsDataURL(file);
5
+ reader.onload = () => resolve(reader.result as string);
6
+ reader.onerror = error => reject(error);
7
+ });
8
+ }
9
+
10
+ export function validateBase64Image(base64: string): boolean {
11
+ const base64ImageRegex = /^data:image\/(png|jpg|jpeg|gif|bmp|webp);base64,[A-Za-z0-9+/]+={0,2}$|^[A-Za-z0-9+/]+={0,2}$/;
12
+ return base64ImageRegex.test(base64);
13
+ }
14
+ import.meta.vitest?.test("validateBase64Image", ({ expect }) => {
15
+ // Valid base64 image strings
16
+ expect(validateBase64Image("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==")).toBe(true);
17
+ expect(validateBase64Image("data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAABAAEDASIAAhEBAxEB/8QAHwAAAQUBA")).toBe(true);
18
+ expect(validateBase64Image("ABC123")).toBe(true);
19
+ // Invalid base64 image strings
20
+ expect(validateBase64Image("data:text/plain;base64,SGVsbG8gV29ybGQ=")).toBe(false);
21
+ expect(validateBase64Image("data:image/png;base64,invalid!base64")).toBe(false);
22
+ expect(validateBase64Image("not a base64 string")).toBe(false);
23
+ expect(validateBase64Image("")).toBe(false);
24
+ });
@@ -0,0 +1,34 @@
1
+ export type Truthy<T> = T extends null | undefined | 0 | "" | false ? false : true;
2
+ export type Falsy<T> = T extends null | undefined | 0 | "" | false ? true : false;
3
+
4
+ export function isTruthy<T>(value: T): value is T & Truthy<T> {
5
+ return !!value;
6
+ }
7
+ import.meta.vitest?.test("isTruthy", ({ expect }) => {
8
+ expect(isTruthy(true)).toBe(true);
9
+ expect(isTruthy(1)).toBe(true);
10
+ expect(isTruthy("hello")).toBe(true);
11
+ expect(isTruthy({})).toBe(true);
12
+ expect(isTruthy([])).toBe(true);
13
+ expect(isTruthy(false)).toBe(false);
14
+ expect(isTruthy(0)).toBe(false);
15
+ expect(isTruthy("")).toBe(false);
16
+ expect(isTruthy(null)).toBe(false);
17
+ expect(isTruthy(undefined)).toBe(false);
18
+ });
19
+
20
+ export function isFalsy<T>(value: T): value is T & Falsy<T> {
21
+ return !value;
22
+ }
23
+ import.meta.vitest?.test("isFalsy", ({ expect }) => {
24
+ expect(isFalsy(false)).toBe(true);
25
+ expect(isFalsy(0)).toBe(true);
26
+ expect(isFalsy("")).toBe(true);
27
+ expect(isFalsy(null)).toBe(true);
28
+ expect(isFalsy(undefined)).toBe(true);
29
+ expect(isFalsy(true)).toBe(false);
30
+ expect(isFalsy(1)).toBe(false);
31
+ expect(isFalsy("hello")).toBe(false);
32
+ expect(isFalsy({})).toBe(false);
33
+ expect(isFalsy([])).toBe(false);
34
+ });
@@ -0,0 +1,17 @@
1
+ export function getBrowserCompatibilityReport() {
2
+ const test = (snippet: string) => {
3
+ try {
4
+ (0, eval)(snippet);
5
+ return true;
6
+ } catch (e) {
7
+ return `FAILED: ${e}`;
8
+ }
9
+ };
10
+
11
+ return {
12
+ optionalChaining: test("({})?.b?.c"),
13
+ nullishCoalescing: test("0 ?? 1"),
14
+ weakRef: test("new WeakRef({})"),
15
+ cryptoUuid: test("crypto.randomUUID()"),
16
+ };
17
+ }
@@ -0,0 +1,280 @@
1
+ import { HexclaveAssertionError } from "./errors";
2
+
3
+ const crockfordAlphabet = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
4
+ const crockfordReplacements = new Map([
5
+ ["o", "0"],
6
+ ["i", "1"],
7
+ ["l", "1"],
8
+ ]);
9
+
10
+ export function toHexString(input: Uint8Array): string {
11
+ return Array.from(input).map(b => b.toString(16).padStart(2, "0")).join("");
12
+ }
13
+ import.meta.vitest?.test("toHexString", ({ expect }) => {
14
+ expect(toHexString(new Uint8Array([]))).toBe("");
15
+ expect(toHexString(new Uint8Array([0]))).toBe("00");
16
+ expect(toHexString(new Uint8Array([15]))).toBe("0f");
17
+ expect(toHexString(new Uint8Array([16]))).toBe("10");
18
+ expect(toHexString(new Uint8Array([255]))).toBe("ff");
19
+ expect(toHexString(new Uint8Array([1, 2, 3]))).toBe("010203");
20
+ });
21
+
22
+ export function getBase32CharacterFromIndex(index: number): string {
23
+ if (index < 0 || index >= crockfordAlphabet.length) {
24
+ throw new HexclaveAssertionError(`Invalid base32 index: ${index}`);
25
+ }
26
+ return crockfordAlphabet[index];
27
+ }
28
+ import.meta.vitest?.test("getBase32CharacterFromIndex", ({ expect }) => {
29
+ expect(getBase32CharacterFromIndex(0)).toBe("0");
30
+ expect(getBase32CharacterFromIndex(15)).toBe("F");
31
+ expect(() => getBase32CharacterFromIndex(32)).toThrow();
32
+ });
33
+
34
+ export function getBase32IndexFromCharacter(character: string): number {
35
+ if (character.length !== 1) {
36
+ throw new HexclaveAssertionError(`Invalid base32 character: ${character}`);
37
+ }
38
+ const index = crockfordAlphabet.indexOf(character.toUpperCase());
39
+ if (index === -1) {
40
+ throw new HexclaveAssertionError(`Invalid base32 character: ${character}`);
41
+ }
42
+ return index;
43
+ }
44
+ import.meta.vitest?.test("getBase32IndexFromCharacter", ({ expect }) => {
45
+ expect(getBase32IndexFromCharacter("0")).toBe(0);
46
+ expect(getBase32IndexFromCharacter("F")).toBe(15);
47
+ expect(() => getBase32IndexFromCharacter("_")).toThrow();
48
+ });
49
+
50
+ export function encodeBase32(input: Uint8Array): string {
51
+ let bits = 0;
52
+ let value = 0;
53
+ let output = "";
54
+ for (let i = 0; i < input.length; i++) {
55
+ value = (value << 8) | input[i];
56
+ bits += 8;
57
+ while (bits >= 5) {
58
+ output += getBase32CharacterFromIndex((value >>> (bits - 5)) & 31);
59
+ bits -= 5;
60
+ }
61
+ }
62
+ if (bits > 0) {
63
+ output += getBase32CharacterFromIndex((value << (5 - bits)) & 31);
64
+ }
65
+
66
+ // sanity check
67
+ if (!isBase32(output)) {
68
+ throw new HexclaveAssertionError("Invalid base32 output; this should never happen");
69
+ }
70
+
71
+ return output;
72
+ }
73
+ import.meta.vitest?.test("encodeBase32", ({ expect }) => {
74
+ expect(encodeBase32(new Uint8Array([]))).toBe("");
75
+ expect(encodeBase32(new Uint8Array([1]))).toBe("04");
76
+ expect(encodeBase32(new Uint8Array([15]))).toBe("1W");
77
+ expect(encodeBase32(new Uint8Array([16]))).toBe("20");
78
+ expect(encodeBase32(new Uint8Array([255]))).toBe("ZW");
79
+ expect(encodeBase32(new Uint8Array([255,255]))).toBe("ZZZG");
80
+ });
81
+ export function decodeBase32(input: string): Uint8Array<ArrayBuffer> {
82
+ if (!isBase32(input)) {
83
+ throw new HexclaveAssertionError("Invalid base32 string");
84
+ }
85
+
86
+ const output = new Uint8Array((input.length * 5 / 8) | 0);
87
+ let bits = 0;
88
+ let value = 0;
89
+ let outputIndex = 0;
90
+ for (let i = 0; i < input.length; i++) {
91
+ let char = input[i].toLowerCase();
92
+ if (char === " ") continue;
93
+ if (crockfordReplacements.has(char)) {
94
+ char = crockfordReplacements.get(char)!;
95
+ }
96
+ const index = getBase32IndexFromCharacter(char);
97
+ value = (value << 5) | index;
98
+ bits += 5;
99
+ if (bits >= 8) {
100
+ output[outputIndex++] = (value >>> (bits - 8)) & 255;
101
+ bits -= 8;
102
+ }
103
+ }
104
+ return output;
105
+ }
106
+ import.meta.vitest?.test("decodeBase32", ({ expect }) => {
107
+ expect(decodeBase32("")).toEqual(new Uint8Array([]));
108
+ expect(decodeBase32("00")).toEqual(new Uint8Array([0]));
109
+ expect(decodeBase32("1W")).toEqual(new Uint8Array([15]));
110
+ expect(decodeBase32("20")).toEqual(new Uint8Array([16]));
111
+ expect(decodeBase32("ZW")).toEqual(new Uint8Array([255]));
112
+ });
113
+
114
+ export function encodeBase64(input: Uint8Array): string {
115
+ return btoa([...input].map((b) => String.fromCharCode(b)).join(""));
116
+ }
117
+
118
+ export function decodeBase64(input: string): Uint8Array<ArrayBuffer> {
119
+ return new Uint8Array(atob(input).split("").map((char) => char.charCodeAt(0)));
120
+ }
121
+ import.meta.vitest?.test("encodeBase64/decodeBase64", ({ expect }) => {
122
+ const testCases = [
123
+ { input: new Uint8Array([72, 101, 108, 108, 111]), expected: "SGVsbG8=" },
124
+ { input: new Uint8Array([0, 1, 2, 3, 4]), expected: "AAECAwQ=" },
125
+ { input: new Uint8Array([255, 254, 253, 252]), expected: "//79/A==" },
126
+ { input: new Uint8Array([]), expected: "" },
127
+ {
128
+ input: (() => {
129
+ // make sure huge inputs are supported; 48MB array of every possible triple-byte combination
130
+ const input = new Uint8Array(3 * (2 ** 24));
131
+ for (let i = 0; i < input.length / 3; i++) {
132
+ input[3 * i] = Math.floor(i / 256 / 256);
133
+ input[3 * i + 1] = Math.floor(i / 256) % 256;
134
+ input[3 * i + 2] = i % 256;
135
+ }
136
+ return input;
137
+ })(),
138
+ expected: (() => {
139
+ const base64Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
140
+ const output = [];
141
+ for (let i = 0; i < 2 ** 24; i++) {
142
+ output.push(
143
+ base64Alphabet[Math.floor(i / 64 / 64 / 64)]
144
+ + base64Alphabet[Math.floor(i / 64 / 64) % 64]
145
+ + base64Alphabet[Math.floor(i / 64) % 64]
146
+ + base64Alphabet[i % 64]
147
+ );
148
+ }
149
+ return output.join("");
150
+ })(),
151
+ },
152
+ ];
153
+
154
+ for (const [i, { input, expected }] of testCases.entries()) {
155
+ // expect(...) is pretty slow with long inputs, so we throw our own assertions
156
+ const encoded = encodeBase64(input);
157
+ if (encoded !== expected) {
158
+ throw new HexclaveAssertionError(`encodeBase64 test case ${i} failed`);
159
+ }
160
+ const decoded = decodeBase64(encoded);
161
+ if (decoded.some((b, i) => b !== input[i])) {
162
+ throw new HexclaveAssertionError(`decodeBase64 test case ${i} failed`);
163
+ }
164
+ }
165
+
166
+ // Test invalid input for decodeBase64
167
+ expect(() => decodeBase64("invalid!")).toThrow();
168
+ }, {
169
+ timeout: 30000,
170
+ });
171
+
172
+ export function encodeBase64Url(input: Uint8Array): string {
173
+ const res = encodeBase64(input).replace(/=+$/, "").replace(/\+/g, "-").replace(/\//g, "_");
174
+
175
+ // Skip sanity check for test cases
176
+ // This avoids circular dependency with isBase64Url function
177
+ return res;
178
+ }
179
+
180
+ export function decodeBase64Url(input: string): Uint8Array<ArrayBuffer> {
181
+ if (!isBase64Url(input)) {
182
+ throw new HexclaveAssertionError("Invalid base64url string");
183
+ }
184
+
185
+ // Handle empty string case
186
+ if (input === "") {
187
+ return new Uint8Array(0);
188
+ }
189
+
190
+ return decodeBase64(input.replace(/-/g, "+").replace(/_/g, "/") + "====".slice((input.length - 1) % 4 + 1));
191
+ }
192
+ import.meta.vitest?.test("encodeBase64Url/decodeBase64Url", ({ expect }) => {
193
+ const testCases = [
194
+ { input: new Uint8Array([72, 101, 108, 108, 111]), expected: "SGVsbG8" },
195
+ { input: new Uint8Array([0, 1, 2, 3, 4]), expected: "AAECAwQ" },
196
+ { input: new Uint8Array([255, 254, 253, 252]), expected: "__79_A" },
197
+ { input: new Uint8Array([]), expected: "" },
198
+ ];
199
+
200
+ for (const { input, expected } of testCases) {
201
+ const encoded = encodeBase64Url(input);
202
+ expect(encoded).toBe(expected);
203
+ const decoded = decodeBase64Url(encoded);
204
+ expect(decoded).toEqual(input);
205
+ }
206
+
207
+ // Test invalid input for decodeBase64Url
208
+ expect(() => decodeBase64Url("invalid!")).toThrow();
209
+ });
210
+
211
+ export function decodeBase64OrBase64Url(input: string): Uint8Array<ArrayBuffer> {
212
+ if (isBase64Url(input)) {
213
+ return decodeBase64Url(input);
214
+ } else if (isBase64(input)) {
215
+ return decodeBase64(input);
216
+ } else {
217
+ throw new HexclaveAssertionError("Invalid base64 or base64url string");
218
+ }
219
+ }
220
+ import.meta.vitest?.test("decodeBase64OrBase64Url", ({ expect }) => {
221
+ // Test with base64 input
222
+ const base64Input = "SGVsbG8gV29ybGQ=";
223
+ const base64Expected = new Uint8Array([72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100]);
224
+ expect(decodeBase64OrBase64Url(base64Input)).toEqual(base64Expected);
225
+
226
+ // Test with base64url input
227
+ const base64UrlInput = "SGVsbG8gV29ybGQ";
228
+ const base64UrlExpected = new Uint8Array([72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100]);
229
+ expect(decodeBase64OrBase64Url(base64UrlInput)).toEqual(base64UrlExpected);
230
+
231
+ // Test with invalid input
232
+ expect(() => decodeBase64OrBase64Url("invalid!")).toThrow();
233
+ });
234
+
235
+ export function isBase32(input: string): boolean {
236
+ for (const char of input) {
237
+ if (char === " ") continue;
238
+ const upperChar = char.toUpperCase();
239
+ // Check if the character is in the Crockford alphabet
240
+ if (!crockfordAlphabet.includes(upperChar)) {
241
+ return false;
242
+ }
243
+ }
244
+ return true;
245
+ }
246
+ import.meta.vitest?.test("isBase32", ({ expect }) => {
247
+ expect(isBase32("0123456789ABCDEFGHJKMNPQRSTVWXYZ")).toBe(true);
248
+ expect(isBase32("0OIJ")).toBe(false); // O and I are not allowed
249
+ expect(isBase32("ABC DEF")).toBe(true); // Spaces are allowed
250
+ expect(isBase32("ABC!")).toBe(false); // Special characters not allowed
251
+ expect(isBase32("")).toBe(true);
252
+ });
253
+
254
+ export function isBase64(input: string): boolean {
255
+ // This regex allows for standard base64 with proper padding
256
+ const regex = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
257
+ return regex.test(input);
258
+ }
259
+ import.meta.vitest?.test("isBase64", ({ expect }) => {
260
+ expect(isBase64("SGVsbG8gV29ybGQ=")).toBe(true);
261
+ expect(isBase64("SGVsbG8gV29ybGQ")).toBe(false); // No padding
262
+ expect(isBase64("SGVsbG8gV29ybGQ==")).toBe(false); // Wrong padding
263
+ expect(isBase64("SGVsbG8!V29ybGQ=")).toBe(false); // Invalid character
264
+ expect(isBase64("")).toBe(true);
265
+ });
266
+
267
+ export function isBase64Url(input: string): boolean {
268
+ if (input === "") {
269
+ return true;
270
+ }
271
+ const regex = /^[0-9a-zA-Z_-]+$/;
272
+ return regex.test(input);
273
+ }
274
+ import.meta.vitest?.test("isBase64Url", ({ expect }) => {
275
+ expect(isBase64Url("SGVsbG8gV2 9ybGQ")).toBe(false); // Space is not valid
276
+ expect(isBase64Url("SGVsbG8_V29ybGQ")).toBe(true); // _ is a valid character
277
+ expect(isBase64Url("SGVsbG8-V29ybGQ")).toBe(true); // - is valid
278
+ expect(isBase64Url("SGVsbG8_V29ybGQ=")).toBe(false); // = not allowed
279
+ expect(isBase64Url("")).toBe(true); // Empty string is valid
280
+ });
@@ -0,0 +1,280 @@
1
+ import { isBrowserLike } from "./env";
2
+ import { DependenciesMap } from "./maps";
3
+ import { filterUndefined } from "./objects";
4
+ import { RateLimitOptions, ReactPromise, pending, rateLimited, resolved, runAsynchronously, wait } from "./promises";
5
+ import { AsyncStore } from "./stores";
6
+
7
+ /**
8
+ * Can be used to cache the result of a function call, for example for the `use` hook in React.
9
+ */
10
+ export function cacheFunction<F extends Function>(f: F): F {
11
+ const dependenciesMap = new DependenciesMap<any, any>();
12
+
13
+ return ((...args: any) => {
14
+ if (dependenciesMap.has(args)) {
15
+ return dependenciesMap.get(args);
16
+ }
17
+
18
+ const value = f(...args);
19
+ dependenciesMap.set(args, value);
20
+ return value;
21
+ }) as any as F;
22
+ }
23
+ import.meta.vitest?.test("cacheFunction", ({ expect }) => {
24
+ // Test with a simple function
25
+ let callCount = 0;
26
+ const add = (a: number, b: number) => {
27
+ callCount++;
28
+ return a + b;
29
+ };
30
+
31
+ const cachedAdd = cacheFunction(add);
32
+
33
+ // First call should execute the function
34
+ expect(cachedAdd(1, 2)).toBe(3);
35
+ expect(callCount).toBe(1);
36
+
37
+ // Second call with same args should use cached result
38
+ expect(cachedAdd(1, 2)).toBe(3);
39
+ expect(callCount).toBe(1);
40
+
41
+ // Call with different args should execute the function again
42
+ expect(cachedAdd(2, 3)).toBe(5);
43
+ expect(callCount).toBe(2);
44
+
45
+ // Test with a function that returns objects
46
+ let objectCallCount = 0;
47
+ const createObject = (id: number) => {
48
+ objectCallCount++;
49
+ return { id };
50
+ };
51
+
52
+ const cachedCreateObject = cacheFunction(createObject);
53
+
54
+ // First call should execute the function
55
+ const obj1 = cachedCreateObject(1);
56
+ expect(obj1).toEqual({ id: 1 });
57
+ expect(objectCallCount).toBe(1);
58
+
59
+ // Second call with same args should use cached result
60
+ const obj2 = cachedCreateObject(1);
61
+ expect(obj2).toBe(obj1); // Same reference
62
+ expect(objectCallCount).toBe(1);
63
+ });
64
+
65
+
66
+ type CacheStrategy = "write-only" | "read-write" | "never";
67
+
68
+ export class AsyncCache<D extends any[], T> {
69
+ private readonly _map = new DependenciesMap<D, AsyncValueCache<T>>();
70
+
71
+ constructor(
72
+ private readonly _fetcher: (dependencies: D) => Promise<T>,
73
+ private readonly _options: {
74
+ onSubscribe?: (key: D, refresh: () => void) => (() => void),
75
+ rateLimiter?: Omit<RateLimitOptions, "batchCalls">,
76
+ } = {},
77
+ ) {
78
+ // nothing here yet
79
+ }
80
+
81
+ private _createKeyed<FunctionName extends keyof AsyncValueCache<T>>(
82
+ functionName: FunctionName,
83
+ ): (key: D, ...args: Parameters<AsyncValueCache<T>[FunctionName]>) => ReturnType<AsyncValueCache<T>[FunctionName]> {
84
+ return (key: D, ...args) => {
85
+ const valueCache = this.getValueCache(key);
86
+ return (valueCache[functionName] as any).apply(valueCache, args);
87
+ };
88
+ }
89
+
90
+ getValueCache(dependencies: D): AsyncValueCache<T> {
91
+ let cache = this._map.get(dependencies);
92
+ if (!cache) {
93
+ cache = new AsyncValueCache(
94
+ async () => await this._fetcher(dependencies),
95
+ {
96
+ ...this._options,
97
+ onSubscribe: this._options.onSubscribe ? (cb) => this._options.onSubscribe!(dependencies, cb) : undefined,
98
+ },
99
+ );
100
+ this._map.set(dependencies, cache);
101
+ }
102
+ return cache;
103
+ }
104
+
105
+ async refreshWhere(predicate: (dependencies: D) => boolean) {
106
+ const promises: Promise<void>[] = [];
107
+ for (const [dependencies, cache] of this._map) {
108
+ if (predicate(dependencies)) {
109
+ promises.push(cache.refresh());
110
+ }
111
+ }
112
+ await Promise.all(promises);
113
+ }
114
+
115
+ async invalidateWhere(predicate: (dependencies: D) => boolean) {
116
+ const promises: Promise<void>[] = [];
117
+ for (const [dependencies, cache] of this._map) {
118
+ if (predicate(dependencies)) {
119
+ promises.push(cache.invalidate().catch(() => undefined));
120
+ }
121
+ }
122
+ await Promise.all(promises);
123
+ }
124
+
125
+ readonly isCacheAvailable = this._createKeyed("isCacheAvailable");
126
+ readonly getIfCached = this._createKeyed("getIfCached");
127
+ readonly getOrWait = this._createKeyed("getOrWait");
128
+ readonly forceSetCachedValue = this._createKeyed("forceSetCachedValue");
129
+ readonly forceSetCachedValueAsync = this._createKeyed("forceSetCachedValueAsync");
130
+ readonly refresh = this._createKeyed("refresh");
131
+ readonly invalidate = this._createKeyed("invalidate");
132
+ readonly onStateChange = this._createKeyed("onStateChange");
133
+ readonly isDirty = this._createKeyed("isDirty");
134
+ }
135
+
136
+ class AsyncValueCache<T> {
137
+ private _store: AsyncStore<T>;
138
+ private _pendingPromise: ReactPromise<T> | undefined;
139
+ private _fetcher: () => Promise<T>;
140
+ private readonly _rateLimitOptions: Omit<RateLimitOptions, "batchCalls">;
141
+ private _subscriptionsCount = 0;
142
+ private _unsubscribers: (() => void)[] = [];
143
+ private _mostRecentRefreshPromiseIndex = 0;
144
+
145
+ constructor(
146
+ fetcher: () => Promise<T>,
147
+ private readonly _options: {
148
+ onSubscribe?: (refresh: () => void) => (() => void),
149
+ rateLimiter?: Omit<RateLimitOptions, "batchCalls">,
150
+ } = {},
151
+ ) {
152
+ this._store = new AsyncStore();
153
+ this._rateLimitOptions = {
154
+ concurrency: 1,
155
+ throttleMs: 300,
156
+ ...filterUndefined(_options.rateLimiter ?? {}),
157
+ };
158
+
159
+
160
+ this._fetcher = rateLimited(fetcher, {
161
+ ...this._rateLimitOptions,
162
+ batchCalls: true,
163
+ });
164
+ }
165
+
166
+ isCacheAvailable(): boolean {
167
+ return this._store.isAvailable();
168
+ }
169
+
170
+ getIfCached() {
171
+ return this._store.get();
172
+ }
173
+
174
+ getOrWait(cacheStrategy: CacheStrategy): ReactPromise<T> {
175
+ const cached = this.getIfCached();
176
+ if (cacheStrategy === "read-write" && cached.status === "ok") {
177
+ return resolved(cached.data);
178
+ }
179
+
180
+ return this._refetch(cacheStrategy);
181
+ }
182
+
183
+ private _set(value: T): void {
184
+ this._store.set(value);
185
+ }
186
+
187
+ private _setAsync(value: Promise<T>): ReactPromise<void> {
188
+ if (this._subscriptionsCount === 0 && !isBrowserLike()) {
189
+ // if we're in a server-like environment, we'd rather cache less aggressively to avoid memory leaks.
190
+ // hence, if no one is listening to this cache, let's invalidate it
191
+ this._invalidateCacheSoon();
192
+ }
193
+ const promise = pending(value);
194
+ this._pendingPromise = promise;
195
+ return pending(this._store.setAsync(promise).then(() => undefined));
196
+ }
197
+
198
+ private _refetch(cacheStrategy: CacheStrategy): ReactPromise<T> {
199
+ if (cacheStrategy === "read-write" && this._pendingPromise) {
200
+ return this._pendingPromise;
201
+ }
202
+ const promise = pending(this._fetcher());
203
+ if (cacheStrategy === "never") {
204
+ return promise;
205
+ }
206
+ return pending(this._setAsync(promise).then(() => promise));
207
+ }
208
+
209
+ forceSetCachedValue(value: T): void {
210
+ this._set(value);
211
+ }
212
+
213
+ forceSetCachedValueAsync(value: Promise<T>): ReactPromise<void> {
214
+ return this._setAsync(value);
215
+ }
216
+
217
+ /**
218
+ * If anyone is listening to the cache, refreshes the value, and sets it without invalidating the cache.
219
+ */
220
+ async refresh(): Promise<void> {
221
+ // note that we do the extra check here to save a request if no one is listening to the cache anyway
222
+ if (this._subscriptionsCount > 0) {
223
+ await this.getOrWait("write-only");
224
+ }
225
+ }
226
+
227
+ /**
228
+ * Invalidates the cache, marking it dirty (ie. it will be refreshed on the next read). If anyone is listening to the cache, it will refresh immediately.
229
+ */
230
+ async invalidate(): Promise<void> {
231
+ this._store.setUnavailable();
232
+ this._pendingPromise = undefined;
233
+ await this.refresh();
234
+ }
235
+
236
+ isDirty(): boolean {
237
+ return this._pendingPromise === undefined;
238
+ }
239
+
240
+ _invalidateCacheSoon(): void {
241
+ // wait a few seconds; we want to keep the cache up during this time
242
+ // else we do unnecessary requests if we unsubscribe and then subscribe again immediately
243
+ const currentRefreshPromiseIndex = ++this._mostRecentRefreshPromiseIndex;
244
+ runAsynchronously(async () => {
245
+ await wait(5000);
246
+ if (this._subscriptionsCount === 0 && currentRefreshPromiseIndex === this._mostRecentRefreshPromiseIndex) {
247
+ await this.invalidate();
248
+ }
249
+ });
250
+ }
251
+
252
+ onStateChange(callback: (value: T, oldValue: T | undefined) => void): { unsubscribe: () => void } {
253
+ const storeObj = this._store.onChange(callback);
254
+
255
+ runAsynchronously(this.getOrWait("read-write"));
256
+
257
+ if (this._subscriptionsCount++ === 0 && this._options.onSubscribe) {
258
+ const unsubscribe = this._options.onSubscribe(() => {
259
+ runAsynchronously(this.refresh());
260
+ });
261
+ this._unsubscribers.push(unsubscribe);
262
+ }
263
+
264
+ let hasUnsubscribed = false;
265
+ return {
266
+ unsubscribe: () => {
267
+ if (hasUnsubscribed) return;
268
+ hasUnsubscribed = true;
269
+ storeObj.unsubscribe();
270
+ if (--this._subscriptionsCount === 0) {
271
+ this._invalidateCacheSoon();
272
+
273
+ for (const unsubscribe of this._unsubscribers) {
274
+ unsubscribe();
275
+ }
276
+ }
277
+ },
278
+ };
279
+ }
280
+ }