scrabble-solver 2.16.6 → 2.17.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 (293) hide show
  1. package/.github/workflows/bunx.yml +26 -2
  2. package/.github/workflows/npx.yml +26 -2
  3. package/.nx/workspace-data/d/server-process.json +3 -3
  4. package/.nx/workspace-data/file-map.json +441 -465
  5. package/.nx/workspace-data/lockfile-dependencies.hash +1 -1
  6. package/.nx/workspace-data/lockfile-nodes.hash +1 -1
  7. package/.nx/workspace-data/nx_files.nxt +0 -0
  8. package/.nx/workspace-data/parsed-lock-file.dependencies.json +2671 -2121
  9. package/.nx/workspace-data/parsed-lock-file.nodes.json +3846 -5277
  10. package/.nx/workspace-data/project-graph.json +7089 -7924
  11. package/.nx/workspace-data/source-maps.json +351 -255
  12. package/.oxlintrc.json +6 -0
  13. package/CLAUDE.md +25 -20
  14. package/README.md +9 -0
  15. package/bun.lock +1054 -2897
  16. package/bunfig.toml +6 -0
  17. package/cypress/e2e/app.spec.cy.ts +4 -4
  18. package/cypress/e2e/features/solver-paths.ts +66 -0
  19. package/cypress/e2e/features.spec.cy.ts +1 -0
  20. package/cypress/support/lib/actions.ts +28 -9
  21. package/cypress.config.ts +7 -0
  22. package/lerna.json +1 -1
  23. package/nx.json +3 -1
  24. package/package.json +18 -18
  25. package/packages/configs/package.json +4 -4
  26. package/packages/constants/package.json +2 -2
  27. package/packages/dictionaries/README.md +1 -1
  28. package/packages/dictionaries/build/Dictionaries.d.ts +2 -2
  29. package/packages/dictionaries/build/Dictionaries.js +6 -6
  30. package/packages/dictionaries/build/lib/DiskCache.d.ts +11 -4
  31. package/packages/dictionaries/build/lib/DiskCache.js +27 -10
  32. package/packages/dictionaries/build/lib/LayeredCache.d.ts +4 -4
  33. package/packages/dictionaries/build/lib/LayeredCache.js +3 -3
  34. package/packages/dictionaries/build/lib/MemoryCache.d.ts +4 -4
  35. package/packages/dictionaries/build/lib/MemoryCache.js +2 -2
  36. package/packages/dictionaries/build/lib/createCacheTimestampComparator.d.ts +2 -2
  37. package/packages/dictionaries/build/lib/downloadDictionary.d.ts +2 -2
  38. package/packages/dictionaries/build/lib/downloadDictionary.js +4 -3
  39. package/packages/dictionaries/build/lib/getDictionaryFilepath.d.ts +3 -1
  40. package/packages/dictionaries/build/lib/getDictionaryFilepath.js +8 -3
  41. package/packages/dictionaries/package.json +8 -7
  42. package/packages/dictionaries/src/Dictionaries.ts +12 -12
  43. package/packages/dictionaries/src/lib/DiskCache.test.ts +85 -0
  44. package/packages/dictionaries/src/lib/DiskCache.ts +34 -14
  45. package/packages/dictionaries/src/lib/LayeredCache.ts +7 -7
  46. package/packages/dictionaries/src/lib/MemoryCache.ts +6 -6
  47. package/packages/dictionaries/src/lib/createCacheTimestampComparator.ts +2 -2
  48. package/packages/dictionaries/src/lib/downloadDictionary.ts +5 -4
  49. package/packages/dictionaries/src/lib/getDictionaryFilepath.ts +7 -2
  50. package/packages/gaddag/build/Gaddag.d.ts +39 -0
  51. package/packages/gaddag/build/Gaddag.js +128 -0
  52. package/packages/gaddag/build/buildGaddag.d.ts +10 -0
  53. package/packages/gaddag/build/buildGaddag.js +360 -0
  54. package/packages/gaddag/build/constants.d.ts +12 -0
  55. package/packages/gaddag/build/constants.js +15 -0
  56. package/packages/gaddag/build/index.d.ts +3 -0
  57. package/packages/gaddag/build/index.js +13 -0
  58. package/packages/logger/package.json +2 -2
  59. package/packages/scrabble-solver/.next/BUILD_ID +1 -1
  60. package/packages/scrabble-solver/.next/build-manifest.json +17 -17
  61. package/packages/scrabble-solver/.next/cache/.previewinfo +1 -1
  62. package/packages/scrabble-solver/.next/cache/.rscinfo +1 -1
  63. package/packages/scrabble-solver/.next/cache/.tsbuildinfo +1 -1
  64. package/packages/scrabble-solver/.next/cache/webpack/client-production/0.pack +0 -0
  65. package/packages/scrabble-solver/.next/cache/webpack/client-production/index.pack +0 -0
  66. package/packages/scrabble-solver/.next/cache/webpack/client-production/index.pack.old +0 -0
  67. package/packages/scrabble-solver/.next/cache/webpack/edge-server-production/index.pack +0 -0
  68. package/packages/scrabble-solver/.next/cache/webpack/edge-server-production/index.pack.old +0 -0
  69. package/packages/scrabble-solver/.next/cache/webpack/server-production/0.pack +0 -0
  70. package/packages/scrabble-solver/.next/cache/webpack/server-production/index.pack +0 -0
  71. package/packages/scrabble-solver/.next/cache/webpack/server-production/index.pack.old +0 -0
  72. package/packages/scrabble-solver/.next/diagnostics/framework.json +1 -1
  73. package/packages/scrabble-solver/.next/next-minimal-server.js.nft.json +1 -1
  74. package/packages/scrabble-solver/.next/next-server.js.nft.json +1 -1
  75. package/packages/scrabble-solver/.next/prerender-manifest.json +7 -4
  76. package/packages/scrabble-solver/.next/required-server-files.js +29 -10
  77. package/packages/scrabble-solver/.next/required-server-files.json +29 -10
  78. package/packages/scrabble-solver/.next/routes-manifest.json +1 -1
  79. package/packages/scrabble-solver/.next/server/chunks/129.js +1 -0
  80. package/packages/scrabble-solver/.next/server/chunks/30.js +2 -2
  81. package/packages/scrabble-solver/.next/server/chunks/318.js +3 -3
  82. package/packages/scrabble-solver/.next/server/chunks/392.js +1 -1
  83. package/packages/scrabble-solver/.next/server/chunks/671.js +1 -1
  84. package/packages/scrabble-solver/.next/server/chunks/712.js +1 -1
  85. package/packages/scrabble-solver/.next/server/chunks/785.js +1 -0
  86. package/packages/scrabble-solver/.next/server/chunks/791.js +3 -3
  87. package/packages/scrabble-solver/.next/server/middleware-build-manifest.js +1 -1
  88. package/packages/scrabble-solver/.next/server/pages/404.html +1 -1
  89. package/packages/scrabble-solver/.next/server/pages/500.html +1 -1
  90. package/packages/scrabble-solver/.next/server/pages/_app.js +1 -1
  91. package/packages/scrabble-solver/.next/server/pages/_app.js.nft.json +1 -1
  92. package/packages/scrabble-solver/.next/server/pages/_document.js.nft.json +1 -1
  93. package/packages/scrabble-solver/.next/server/pages/_error.js +1 -1
  94. package/packages/scrabble-solver/.next/server/pages/_error.js.nft.json +1 -1
  95. package/packages/scrabble-solver/.next/server/pages/api/dictionary/[locale]/[word].js +1 -1
  96. package/packages/scrabble-solver/.next/server/pages/api/dictionary/[locale]/[word].js.nft.json +1 -1
  97. package/packages/scrabble-solver/.next/server/pages/api/dictionary/[locale].js +1 -1
  98. package/packages/scrabble-solver/.next/server/pages/api/dictionary/[locale].js.nft.json +1 -1
  99. package/packages/scrabble-solver/.next/server/pages/api/solve.js +1 -1
  100. package/packages/scrabble-solver/.next/server/pages/api/solve.js.nft.json +1 -1
  101. package/packages/scrabble-solver/.next/server/pages/api/verify.js +1 -1
  102. package/packages/scrabble-solver/.next/server/pages/api/verify.js.nft.json +1 -1
  103. package/packages/scrabble-solver/.next/server/pages/api/visit.js +1 -1
  104. package/packages/scrabble-solver/.next/server/pages/api/visit.js.nft.json +1 -1
  105. package/packages/scrabble-solver/.next/server/pages/index.html +1 -1
  106. package/packages/scrabble-solver/.next/server/pages/index.js +1 -1
  107. package/packages/scrabble-solver/.next/server/pages/index.js.nft.json +1 -1
  108. package/packages/scrabble-solver/.next/server/pages/index.json +1 -1
  109. package/packages/scrabble-solver/.next/server/pages/not-found.html +1 -1
  110. package/packages/scrabble-solver/.next/server/pages/not-found.js.nft.json +1 -1
  111. package/packages/scrabble-solver/.next/server/pages-manifest.json +1 -1
  112. package/packages/scrabble-solver/.next/server/webpack-runtime.js +1 -1
  113. package/packages/scrabble-solver/.next/static/{js6-w0PwySs2PyOSeCYfi → OLE8_ZInlGH7ENU0NIOpf}/_buildManifest.js +1 -1
  114. package/packages/scrabble-solver/.next/static/chunks/583.7a46807be14c377e.js +1 -0
  115. package/packages/scrabble-solver/.next/static/chunks/830.661816bf0de8c2f1.js +1 -0
  116. package/packages/scrabble-solver/.next/static/chunks/framework-3cf15b8041c59dd3.js +1 -0
  117. package/packages/scrabble-solver/.next/static/chunks/main-5b999f3f24ebfa84.js +5 -0
  118. package/packages/scrabble-solver/.next/static/chunks/pages/_app-7024e7a27ffbb2fa.js +7 -0
  119. package/packages/scrabble-solver/.next/static/chunks/pages/{index-bdf5e1e44cea16f6.js → index-8b398b0a5d870773.js} +1 -1
  120. package/packages/scrabble-solver/.next/static/chunks/webpack-3943d624bbb47a64.js +1 -0
  121. package/packages/scrabble-solver/.next/static/css/aedeaada218d7280.css +2 -0
  122. package/packages/scrabble-solver/.next/trace +21 -21
  123. package/packages/scrabble-solver/.next/trace-build +1 -1
  124. package/packages/scrabble-solver/.next/types/cache-life.d.ts +1 -0
  125. package/packages/scrabble-solver/.next/types/root-params.d.ts +3 -0
  126. package/packages/scrabble-solver/next-env.d.ts +1 -0
  127. package/packages/scrabble-solver/next.config.js +0 -1
  128. package/packages/scrabble-solver/package.json +19 -19
  129. package/packages/scrabble-solver/public/service-worker.js +2 -2
  130. package/packages/scrabble-solver/public/service-worker.js.map +1 -1
  131. package/packages/scrabble-solver/src/components/Board/components/Cell/Cell.tsx +4 -1
  132. package/packages/scrabble-solver/src/components/Board/hooks/useGrid.ts +17 -6
  133. package/packages/scrabble-solver/src/components/Dictionary/Dictionary.module.scss +7 -22
  134. package/packages/scrabble-solver/src/components/Dictionary/Dictionary.tsx +1 -8
  135. package/packages/scrabble-solver/src/components/Results/Result.tsx +0 -3
  136. package/packages/scrabble-solver/src/components/Results/Results.module.scss +0 -1
  137. package/packages/scrabble-solver/src/components/Results/Results.tsx +10 -2
  138. package/packages/scrabble-solver/src/components/Results/types.ts +3 -3
  139. package/packages/scrabble-solver/src/components/Solver/Solver.tsx +4 -3
  140. package/packages/scrabble-solver/src/components/Tooltip/useTooltip.ts +1 -1
  141. package/packages/scrabble-solver/src/pages/api/dictionary/[locale]/[word].ts +2 -2
  142. package/packages/scrabble-solver/src/pages/api/dictionary/[locale]/index.ts +80 -3
  143. package/packages/scrabble-solver/src/pages/api/solve.ts +2 -2
  144. package/packages/scrabble-solver/src/pages/api/verify.ts +12 -3
  145. package/packages/scrabble-solver/src/sdk/solve.ts +13 -7
  146. package/packages/scrabble-solver/src/sdk/verify.ts +12 -6
  147. package/packages/scrabble-solver/src/service-worker/appShell.test.ts +86 -0
  148. package/packages/scrabble-solver/src/service-worker/appShell.ts +39 -0
  149. package/packages/scrabble-solver/src/service-worker/index.ts +4 -5
  150. package/packages/scrabble-solver/src/serviceWorkerManager.ts +1 -18
  151. package/packages/scrabble-solver/src/solver-worker/client.test.ts +151 -0
  152. package/packages/scrabble-solver/src/solver-worker/client.ts +106 -0
  153. package/packages/scrabble-solver/src/solver-worker/dictionaries/constants.ts +9 -0
  154. package/packages/scrabble-solver/src/solver-worker/dictionaries/getDictionary.ts +21 -0
  155. package/packages/scrabble-solver/src/solver-worker/dictionaries/index.ts +2 -0
  156. package/packages/scrabble-solver/src/solver-worker/dictionaries/revalidateDictionary.test.ts +199 -0
  157. package/packages/scrabble-solver/src/solver-worker/dictionaries/revalidateDictionary.ts +86 -0
  158. package/packages/scrabble-solver/src/solver-worker/dictionaries/revalidatedAt.ts +43 -0
  159. package/packages/scrabble-solver/src/solver-worker/getGaddag.test.ts +109 -0
  160. package/packages/scrabble-solver/src/solver-worker/getGaddag.ts +62 -0
  161. package/packages/scrabble-solver/src/solver-worker/index.ts +1 -0
  162. package/packages/scrabble-solver/src/solver-worker/index.worker.ts +124 -0
  163. package/packages/scrabble-solver/src/solver-worker/messages.ts +20 -0
  164. package/packages/scrabble-solver/src/state/index.ts +1 -0
  165. package/packages/scrabble-solver/src/state/sagas.ts +19 -9
  166. package/packages/scrabble-solver/src/state/useTypedStore.ts +6 -0
  167. package/packages/scrabble-solver/src/types/api.ts +1 -2
  168. package/packages/scrabble-solver/tsconfig.json +4 -0
  169. package/packages/scrabble-solver/tsconfig.tsbuildinfo +1 -1
  170. package/packages/solver/README.md +24 -0
  171. package/packages/solver/benchmarks/index.ts +15 -0
  172. package/packages/solver/benchmarks/lib.ts +31 -0
  173. package/packages/solver/benchmarks/renderChart.ts +165 -0
  174. package/packages/solver/benchmarks/results/chart.svg +65 -0
  175. package/packages/solver/benchmarks/runBenchmarks.ts +66 -0
  176. package/packages/solver/benchmarks/scenarios.ts +64 -0
  177. package/packages/solver/benchmarks/types.ts +16 -0
  178. package/packages/solver/benchmarks/updateReadme.ts +53 -0
  179. package/packages/solver/build/MoveGenerator.d.ts +116 -0
  180. package/packages/solver/build/MoveGenerator.js +686 -0
  181. package/packages/solver/build/index.d.ts +0 -13
  182. package/packages/solver/build/index.js +1 -27
  183. package/packages/solver/build/solve.d.ts +2 -2
  184. package/packages/solver/build/solve.js +3 -19
  185. package/packages/solver/package.json +8 -7
  186. package/packages/solver/src/MoveGenerator.test.ts +1009 -0
  187. package/packages/solver/src/MoveGenerator.ts +926 -0
  188. package/packages/solver/src/ReferenceSolver.ts +401 -0
  189. package/packages/solver/src/index.ts +0 -13
  190. package/packages/solver/src/solve.test.ts +21 -21
  191. package/packages/solver/src/solve.ts +4 -22
  192. package/packages/solver/tsconfig.json +1 -1
  193. package/packages/types/build/Cell.d.ts +1 -0
  194. package/packages/types/build/Cell.js +3 -0
  195. package/packages/types/build/Result.d.ts +3 -2
  196. package/packages/types/build/Result.js +11 -6
  197. package/packages/types/build/ResultJson.d.ts +5 -4
  198. package/packages/types/build/index.d.ts +0 -6
  199. package/packages/types/build/index.js +1 -9
  200. package/packages/types/build/readResultCells.d.ts +5 -0
  201. package/packages/types/build/readResultCells.js +75 -0
  202. package/packages/types/package.json +5 -4
  203. package/packages/types/src/Cell.ts +4 -0
  204. package/packages/types/src/Result.ts +15 -6
  205. package/packages/types/src/ResultJson.ts +5 -5
  206. package/packages/types/src/index.ts +0 -6
  207. package/packages/types/src/readResultCells.test.ts +140 -0
  208. package/packages/types/src/readResultCells.ts +90 -0
  209. package/packages/word-definitions/package.json +3 -3
  210. package/packages/word-lists/package.json +4 -4
  211. package/packages/scrabble-solver/.next/static/chunks/framework-a23f7f643b59a2ca.js +0 -1
  212. package/packages/scrabble-solver/.next/static/chunks/main-92448bb08bb6bf1f.js +0 -5
  213. package/packages/scrabble-solver/.next/static/chunks/pages/_app-4ae86f006f6b9fe7.js +0 -7
  214. package/packages/scrabble-solver/.next/static/chunks/webpack-1a7a166b34ed8d24.js +0 -1
  215. package/packages/scrabble-solver/.next/static/css/0af35adb4bab56f0.css +0 -2
  216. package/packages/scrabble-solver/src/sdk/getDictionary.ts +0 -9
  217. package/packages/scrabble-solver/src/service-worker/average.ts +0 -7
  218. package/packages/scrabble-solver/src/service-worker/dictionaries/constants.ts +0 -5
  219. package/packages/scrabble-solver/src/service-worker/dictionaries/getDictionary.ts +0 -20
  220. package/packages/scrabble-solver/src/service-worker/dictionaries/index.ts +0 -2
  221. package/packages/scrabble-solver/src/service-worker/dictionaries/revalidateDictionary.ts +0 -33
  222. package/packages/scrabble-solver/src/service-worker/getTrie.ts +0 -24
  223. package/packages/scrabble-solver/src/service-worker/routeSolveRequests.ts +0 -91
  224. package/packages/scrabble-solver/src/service-worker/routeVerifyRequests.ts +0 -37
  225. package/packages/solver/build/areDigraphsValid.d.ts +0 -2
  226. package/packages/solver/build/areDigraphsValid.js +0 -17
  227. package/packages/solver/build/fillPattern.d.ts +0 -6
  228. package/packages/solver/build/fillPattern.js +0 -52
  229. package/packages/solver/build/generateEndIndices.d.ts +0 -2
  230. package/packages/solver/build/generateEndIndices.js +0 -17
  231. package/packages/solver/build/generateHorizontalPatterns.d.ts +0 -2
  232. package/packages/solver/build/generateHorizontalPatterns.js +0 -16
  233. package/packages/solver/build/generatePattern.d.ts +0 -7
  234. package/packages/solver/build/generatePattern.js +0 -20
  235. package/packages/solver/build/generatePatterns.d.ts +0 -2
  236. package/packages/solver/build/generatePatterns.js +0 -11
  237. package/packages/solver/build/generateStartIndices.d.ts +0 -2
  238. package/packages/solver/build/generateStartIndices.js +0 -16
  239. package/packages/solver/build/generateVectors.d.ts +0 -7
  240. package/packages/solver/build/generateVectors.js +0 -7
  241. package/packages/solver/build/generateVerticalPatterns.d.ts +0 -2
  242. package/packages/solver/build/generateVerticalPatterns.js +0 -16
  243. package/packages/solver/build/getCellsScore.d.ts +0 -2
  244. package/packages/solver/build/getCellsScore.js +0 -18
  245. package/packages/solver/build/getPatternHash.d.ts +0 -2
  246. package/packages/solver/build/getPatternHash.js +0 -14
  247. package/packages/solver/build/getPatternScore.d.ts +0 -2
  248. package/packages/solver/build/getPatternScore.js +0 -22
  249. package/packages/solver/build/getUniquePatterns.d.ts +0 -2
  250. package/packages/solver/build/getUniquePatterns.js +0 -17
  251. package/packages/solver/src/areDigraphsValid.ts +0 -18
  252. package/packages/solver/src/fillPattern.test.ts +0 -107
  253. package/packages/solver/src/fillPattern.ts +0 -68
  254. package/packages/solver/src/generateEndIndices.test.ts +0 -23
  255. package/packages/solver/src/generateEndIndices.ts +0 -19
  256. package/packages/solver/src/generateHorizontalPatterns.test.ts +0 -31
  257. package/packages/solver/src/generateHorizontalPatterns.ts +0 -15
  258. package/packages/solver/src/generatePattern.ts +0 -33
  259. package/packages/solver/src/generatePatterns.test.ts +0 -13
  260. package/packages/solver/src/generatePatterns.ts +0 -11
  261. package/packages/solver/src/generateStartIndices.test.ts +0 -19
  262. package/packages/solver/src/generateStartIndices.ts +0 -17
  263. package/packages/solver/src/generateVectors.test.ts +0 -12
  264. package/packages/solver/src/generateVectors.ts +0 -10
  265. package/packages/solver/src/generateVerticalPatterns.test.ts +0 -32
  266. package/packages/solver/src/generateVerticalPatterns.ts +0 -15
  267. package/packages/solver/src/getCellsScore.ts +0 -21
  268. package/packages/solver/src/getPatternHash.test.ts +0 -17
  269. package/packages/solver/src/getPatternHash.ts +0 -12
  270. package/packages/solver/src/getPatternScore.test.ts +0 -204
  271. package/packages/solver/src/getPatternScore.ts +0 -23
  272. package/packages/solver/src/getUniquePatterns.ts +0 -19
  273. package/packages/types/build/CollisionJson.d.ts +0 -2
  274. package/packages/types/build/CollisionJson.js +0 -2
  275. package/packages/types/build/FinalPattern.d.ts +0 -6
  276. package/packages/types/build/FinalPattern.js +0 -14
  277. package/packages/types/build/HorizontalPattern.d.ts +0 -5
  278. package/packages/types/build/HorizontalPattern.js +0 -34
  279. package/packages/types/build/Pattern.d.ts +0 -20
  280. package/packages/types/build/Pattern.js +0 -50
  281. package/packages/types/build/PatternJson.d.ts +0 -6
  282. package/packages/types/build/PatternJson.js +0 -2
  283. package/packages/types/build/VerticalPattern.d.ts +0 -5
  284. package/packages/types/build/VerticalPattern.js +0 -34
  285. package/packages/types/src/CollisionJson.ts +0 -3
  286. package/packages/types/src/FinalPattern.ts +0 -14
  287. package/packages/types/src/HorizontalPattern.ts +0 -37
  288. package/packages/types/src/Pattern.ts +0 -71
  289. package/packages/types/src/PatternJson.ts +0 -7
  290. package/packages/types/src/VerticalPattern.ts +0 -37
  291. /package/packages/scrabble-solver/.next/static/{js6-w0PwySs2PyOSeCYfi → OLE8_ZInlGH7ENU0NIOpf}/_ssgManifest.js +0 -0
  292. /package/packages/scrabble-solver/src/{service-worker → solver-worker}/dictionaries/expirationManager.ts +0 -0
  293. /package/packages/scrabble-solver/src/{service-worker → solver-worker}/dictionaries/getDictionaryUrl.ts +0 -0
@@ -5,6 +5,7 @@ import {
5
5
  type ChangeEventHandler,
6
6
  type FocusEventHandler,
7
7
  type FunctionComponent,
8
+ memo,
8
9
  type MouseEventHandler,
9
10
  type RefObject,
10
11
  type TouchEventHandler,
@@ -41,7 +42,7 @@ interface Props {
41
42
  onFocus: (x: number, y: number) => void;
42
43
  }
43
44
 
44
- export const Cell: FunctionComponent<Props> = ({
45
+ const CellBase: FunctionComponent<Props> = ({
45
46
  cell,
46
47
  cellBottom,
47
48
  cellLeft,
@@ -135,3 +136,5 @@ export const Cell: FunctionComponent<Props> = ({
135
136
  />
136
137
  );
137
138
  };
139
+
140
+ export const Cell = memo(CellBase);
@@ -53,6 +53,7 @@ export const useGrid = (rows: Cell[][]): [State, Actions] => {
53
53
  const [activeIndex, setActiveIndex] = useState<Point>({ x: 0, y: 0 });
54
54
  const [direction, setLastDirection] = useState<Direction>('horizontal');
55
55
  const directionRef = useLatest(direction);
56
+ const rowsRef = useLatest(rows);
56
57
 
57
58
  const safeActiveIndex = useMemo(
58
59
  () => ({
@@ -94,7 +95,7 @@ export const useGrid = (rows: Cell[][]): [State, Actions] => {
94
95
  (position: Point, value: string) => {
95
96
  const characters = value ? extractCharacters(config, value).filter((character) => character !== BLANK) : [BLANK];
96
97
  const actions: PayloadAction<unknown>[] = [];
97
- let board = new Board({ rows: rows.map((row) => row.map((cell) => cell.clone())) });
98
+ let board = new Board({ rows: rowsRef.current.map((row) => row.map((cell) => cell.clone())) });
98
99
  let { x, y } = position;
99
100
 
100
101
  const scheduleMoveFocus = () => {
@@ -188,7 +189,7 @@ export const useGrid = (rows: Cell[][]): [State, Actions] => {
188
189
  moveFocus(Math.abs(position.x - x) + Math.abs(position.y - y));
189
190
  actions.forEach(dispatch);
190
191
  },
191
- [config, directionRef, dispatch, height, moveFocus, rows, width],
192
+ [config, directionRef, dispatch, height, moveFocus, rowsRef, width],
192
193
  );
193
194
 
194
195
  const onChange = useCallback(
@@ -209,7 +210,7 @@ export const useGrid = (rows: Cell[][]): [State, Actions] => {
209
210
 
210
211
  if (value === EMPTY_CELL) {
211
212
  const { x, y } = position;
212
- const cell = rows[y][x];
213
+ const cell = rowsRef.current[y][x];
213
214
 
214
215
  if (cell.hasTile()) {
215
216
  dispatch(boardSlice.actions.toggleCellIsBlank(position));
@@ -219,7 +220,7 @@ export const useGrid = (rows: Cell[][]): [State, Actions] => {
219
220
 
220
221
  insertValue(position, value);
221
222
  },
222
- [dispatch, insertValue, moveFocus, rows, getInputRefPosition],
223
+ [dispatch, insertValue, moveFocus, rowsRef, getInputRefPosition],
223
224
  );
224
225
 
225
226
  const onDirectionToggle = useCallback(() => setLastDirection(toggleDirection), []);
@@ -308,7 +309,7 @@ export const useGrid = (rows: Cell[][]): [State, Actions] => {
308
309
  return;
309
310
  }
310
311
 
311
- const cell = rows[y][x];
312
+ const cell = rowsRef.current[y][x];
312
313
 
313
314
  if (isCtrl(event) && character === 'g') {
314
315
  event.preventDefault();
@@ -347,7 +348,17 @@ export const useGrid = (rows: Cell[][]): [State, Actions] => {
347
348
  dispatch(boardSlice.actions.toggleCellIsBlank(position));
348
349
  },
349
350
  });
350
- }, [changeActiveIndex, config, direction, dispatch, getInputRefPosition, locale, moveFocus, onDirectionToggle, rows]);
351
+ }, [
352
+ changeActiveIndex,
353
+ config,
354
+ direction,
355
+ dispatch,
356
+ getInputRefPosition,
357
+ locale,
358
+ moveFocus,
359
+ onDirectionToggle,
360
+ rowsRef,
361
+ ]);
351
362
 
352
363
  const onPaste = useCallback<ClipboardEventHandler>(
353
364
  (event) => {
@@ -2,17 +2,8 @@
2
2
 
3
3
  .dictionary {
4
4
  position: relative;
5
- transition: var(--transition);
6
5
  word-break: break-word;
7
6
 
8
- &.isAllowed {
9
- background-color: var(--color--green--light);
10
- }
11
-
12
- &.isNotAllowed {
13
- background-color: var(--color--red--light);
14
- }
15
-
16
7
  &:focus-within {
17
8
  @include focus-effect;
18
9
  }
@@ -21,6 +12,8 @@
21
12
  .content {
22
13
  @include scrollbars;
23
14
 
15
+ display: flex;
16
+ flex-direction: column;
24
17
  height: 100%;
25
18
  overflow-y: auto;
26
19
  border-radius: inherit;
@@ -31,26 +24,18 @@
31
24
  }
32
25
 
33
26
  .result {
34
- transition: var(--transition);
27
+ flex-shrink: 0;
28
+
29
+ &:last-child {
30
+ flex-grow: 1;
31
+ }
35
32
 
36
33
  &.isAllowed {
37
34
  background-color: var(--color--green--light);
38
-
39
- & + & {
40
- .content {
41
- padding-top: 0;
42
- }
43
- }
44
35
  }
45
36
 
46
37
  &.isNotAllowed {
47
38
  background-color: var(--color--red--light);
48
-
49
- & + & {
50
- .content {
51
- padding-top: 0;
52
- }
53
- }
54
39
  }
55
40
  }
56
41
 
@@ -25,16 +25,9 @@ export const Dictionary: FunctionComponent<Props> = ({ className }) => {
25
25
  const results = useTypedSelector(selectDictionaryResults);
26
26
  const isLoading = useTypedSelector(selectDictionaryIsLoading);
27
27
  const error = useTypedSelector(selectDictionaryError);
28
- const isLastAllowed = results.at(-1)?.isAllowed;
29
28
 
30
29
  return (
31
- <div
32
- className={classNames(styles.dictionary, className, {
33
- [styles.isAllowed]: isLastAllowed === true,
34
- [styles.isNotAllowed]: isLastAllowed === false,
35
- })}
36
- style={{ height: dictionaryResultsHeight }}
37
- >
30
+ <div className={classNames(styles.dictionary, className)} style={{ height: dictionaryResultsHeight }}>
38
31
  <div className={styles.content}>
39
32
  {typeof error !== 'undefined' && !isLoading && <EmptyState variant="error">{error.message}</EmptyState>}
40
33
 
@@ -28,7 +28,6 @@ export const Result = ({
28
28
  onClick = noop,
29
29
  onFocus = noop,
30
30
  onMouseEnter = noop,
31
- onMouseLeave = noop,
32
31
  style,
33
32
  }: RowComponentProps<ResultData>): ReactElement => {
34
33
  const { resultWordWidth } = useAppLayout();
@@ -44,7 +43,6 @@ export const Result = ({
44
43
 
45
44
  const handleClick: MouseEventHandler = (event) => onClick(result, event);
46
45
  const handleMouseEnter: MouseEventHandler = (event) => onMouseEnter(result, event);
47
- const handleMouseLeave: MouseEventHandler = (event) => onMouseLeave(result, event);
48
46
  const handleBlur: FocusEventHandler = (event) => onBlur(result, event);
49
47
  const handleFocus: FocusEventHandler = (event) => onFocus(result, event);
50
48
 
@@ -65,7 +63,6 @@ export const Result = ({
65
63
  onClick={handleClick}
66
64
  onFocus={handleFocus}
67
65
  onMouseEnter={handleMouseEnter}
68
- onMouseLeave={handleMouseLeave}
69
66
  >
70
67
  <span className={styles.resultContent}>
71
68
  {columns[ResultColumnId.Coordinates] && (
@@ -93,7 +93,6 @@ $row-padding-horizontal: calc(var(--spacing--m) + var(--spacing--s));
93
93
  @include button-reset;
94
94
 
95
95
  user-select: none;
96
- transition: var(--transition);
97
96
  cursor: pointer;
98
97
  font-weight: 400;
99
98
 
@@ -44,7 +44,14 @@ export const Results: FunctionComponent<Props> = ({ callbacks, className, highli
44
44
  const isOutdated = useTypedSelector(selectAreResultsOutdated);
45
45
  const error = useTypedSelector(selectSolveError);
46
46
  const itemData = useMemo<ResultData>(
47
- () => ({ ...callbacks, highlightedIndex, results }),
47
+ () => ({
48
+ highlightedIndex,
49
+ results,
50
+ onBlur: callbacks.onBlur,
51
+ onClick: callbacks.onClick,
52
+ onFocus: callbacks.onFocus,
53
+ onMouseEnter: callbacks.onMouseEnter,
54
+ }),
48
55
  [callbacks, highlightedIndex, results],
49
56
  );
50
57
  const listRef = useListRef(null);
@@ -74,6 +81,7 @@ export const Results: FunctionComponent<Props> = ({ callbacks, className, highli
74
81
  aria-busy={isLoadingDebounced}
75
82
  aria-label={translate('results')}
76
83
  className={classNames(styles.results, className)}
84
+ data-outdated={isOutdated}
77
85
  data-testid="results"
78
86
  role="region"
79
87
  >
@@ -111,7 +119,7 @@ export const Results: FunctionComponent<Props> = ({ callbacks, className, highli
111
119
  )}
112
120
 
113
121
  {!isOutdated && results.length > 0 && (
114
- <div className={styles.listContainer}>
122
+ <div className={styles.listContainer} onMouseLeave={callbacks.onMouseLeave}>
115
123
  <List
116
124
  className={classNames(styles.list, {
117
125
  [styles.outdated]: isOutdated,
@@ -1,15 +1,15 @@
1
1
  import { type Result } from '@scrabble-solver/types';
2
- import { type FocusEvent, type MouseEvent } from 'react';
2
+ import { type FocusEvent, type MouseEvent, type MouseEventHandler } from 'react';
3
3
 
4
4
  export interface ResultCallbacks {
5
5
  onBlur?: (result: Result, event: FocusEvent) => void;
6
6
  onClick?: (result: Result, event: MouseEvent) => void;
7
7
  onFocus?: (result: Result, event: FocusEvent) => void;
8
8
  onMouseEnter?: (result: Result, event: MouseEvent) => void;
9
- onMouseLeave?: (result: Result, event: MouseEvent) => void;
9
+ onMouseLeave?: MouseEventHandler;
10
10
  }
11
11
 
12
- export interface ResultData extends ResultCallbacks {
12
+ export interface ResultData extends Omit<ResultCallbacks, 'onMouseLeave'> {
13
13
  highlightedIndex?: number;
14
14
  results: Result[] | undefined;
15
15
  }
@@ -14,6 +14,7 @@ import {
14
14
  solveSlice,
15
15
  useTranslate,
16
16
  useTypedSelector,
17
+ useTypedStore,
17
18
  } from '@/state';
18
19
 
19
20
  import { Alert } from '../Alert';
@@ -38,13 +39,13 @@ const SolverBase: FunctionComponent<Props> = ({ className, onShowResults }) => {
38
39
  const { maxControlsWidth, showCompactControls, tileSize } = useAppLayout();
39
40
  const error = useTypedSelector(selectSolveError);
40
41
  const isOutdated = useTypedSelector(selectAreResultsOutdated);
41
- const resultCandidate = useTypedSelector(selectResultCandidate);
42
42
  const results = useTypedSelector(selectProcessedResults);
43
+ const store = useTypedStore();
43
44
  const [bestResult] = results || [];
44
45
  const touchCallbacks = useMemo(
45
46
  () => ({
46
47
  onClick: (result: Result) => {
47
- const isSelected = result === resultCandidate;
48
+ const isSelected = result === selectResultCandidate(store.getState());
48
49
 
49
50
  if (isSelected) {
50
51
  dispatch(resultsSlice.actions.applyResult(result));
@@ -53,7 +54,7 @@ const SolverBase: FunctionComponent<Props> = ({ className, onShowResults }) => {
53
54
  }
54
55
  },
55
56
  }),
56
- [dispatch, resultCandidate],
57
+ [dispatch, store],
57
58
  );
58
59
  const mouseCallbacks = useMemo(
59
60
  () => ({
@@ -37,7 +37,7 @@ export const useTooltip = ({ placement = 'top' }: TooltipOptions = {}) => {
37
37
  const { delay } = useDelayGroup(context);
38
38
  const hover = useHover(context, { move: false, delay });
39
39
  const focus = useFocus(context);
40
- const dismiss = useDismiss(context);
40
+ const dismiss = useDismiss(context, { ancestorScroll: true });
41
41
  const role = useRole(context, { role: 'tooltip' });
42
42
  const interactions = useInteractions([hover, focus, dismiss, role]);
43
43
 
@@ -30,8 +30,8 @@ const dictionary = async (request: NextApiRequest, response: NextApiResponse): P
30
30
  },
31
31
  });
32
32
 
33
- const trie = await dictionaries.get(locale);
34
- const results = await Promise.all(words.map((word) => getWordDefinition(locale, word, trie.has(word))));
33
+ const gaddag = await dictionaries.get(locale);
34
+ const results = await Promise.all(words.map((word) => getWordDefinition(locale, word, gaddag.has(word))));
35
35
  response.status(200).send(results.map((result) => result.toJson()));
36
36
  } catch (error) {
37
37
  const message = error instanceof Error ? error.message : 'Unknown error';
@@ -1,7 +1,10 @@
1
+ import { type Gaddag } from '@kamilmielnik/gaddag';
1
2
  import { dictionaries } from '@scrabble-solver/dictionaries';
2
3
  import { logger } from '@scrabble-solver/logger';
3
4
  import { isLocale, type Locale } from '@scrabble-solver/types';
4
5
  import { type NextApiRequest, type NextApiResponse } from 'next';
6
+ import { promisify } from 'util';
7
+ import { gzip } from 'zlib';
5
8
 
6
9
  import { getServerLoggingData } from '@/api';
7
10
 
@@ -22,8 +25,21 @@ const dictionary = async (request: NextApiRequest, response: NextApiResponse): P
22
25
  },
23
26
  });
24
27
 
25
- const trie = await dictionaries.get(locale);
26
- response.status(200).send(trie.serialize());
28
+ const gaddag = await dictionaries.get(locale);
29
+ const isGzip = acceptsGzip(request);
30
+ const body = isGzip ? await getCompressedDictionary(gaddag) : getSerializedDictionary(gaddag);
31
+
32
+ // no-cache makes clients store the payload but revalidate it, so unchanged
33
+ // re-downloads become 304s, answered by Next's built-in ETag handling.
34
+ response.setHeader('Cache-Control', 'no-cache');
35
+ response.setHeader('Content-Type', 'application/octet-stream');
36
+ response.setHeader('Vary', 'Accept-Encoding');
37
+
38
+ if (isGzip) {
39
+ response.setHeader('Content-Encoding', 'gzip');
40
+ }
41
+
42
+ response.status(200).send(body);
27
43
  } catch (error) {
28
44
  const message = error instanceof Error ? error.message : 'Unknown error';
29
45
  logger.error('dictionary - error', { error, meta });
@@ -43,9 +59,70 @@ const parseRequest = (request: NextApiRequest): RequestData => {
43
59
  };
44
60
  };
45
61
 
62
+ const acceptsGzip = (request: NextApiRequest): boolean => {
63
+ const acceptEncoding = request.headers['accept-encoding'];
64
+ const header = Array.isArray(acceptEncoding) ? acceptEncoding.join(',') : acceptEncoding;
65
+
66
+ if (typeof header !== 'string') {
67
+ return false;
68
+ }
69
+
70
+ let wildcardAccepts = false;
71
+
72
+ for (const entry of header.split(',')) {
73
+ const [encoding, ...parameters] = entry.split(';').map((token) => token.trim().toLowerCase());
74
+ const isRefused = parameters.some((parameter) => {
75
+ const [name, quality] = parameter.split('=').map((token) => token.trim());
76
+ return name === 'q' && parseFloat(quality) === 0;
77
+ });
78
+
79
+ if (encoding === 'gzip' || encoding === 'x-gzip') {
80
+ return !isRefused;
81
+ }
82
+
83
+ if (encoding === '*') {
84
+ wildcardAccepts = !isRefused;
85
+ }
86
+ }
87
+
88
+ return wildcardAccepts;
89
+ };
90
+
91
+ // Dictionaries rebuild at most daily, so each response variant is produced
92
+ // once and lives exactly as long as the in-memory dictionary it was made from.
93
+ const serializedCache = new WeakMap<Gaddag, Buffer>();
94
+ const compressedCache = new WeakMap<Gaddag, Promise<Buffer>>();
95
+
96
+ const getSerializedDictionary = (gaddag: Gaddag): Buffer => {
97
+ let serialized = serializedCache.get(gaddag);
98
+
99
+ if (!serialized) {
100
+ // serialize() returns freshly allocated bytes, so the Buffer wraps them without copying.
101
+ const bytes = gaddag.serialize();
102
+ serialized = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
103
+ serializedCache.set(gaddag, serialized);
104
+ }
105
+
106
+ return serialized;
107
+ };
108
+
109
+ const gzipAsync = promisify(gzip);
110
+
111
+ const getCompressedDictionary = (gaddag: Gaddag): Promise<Buffer> => {
112
+ let compressed = compressedCache.get(gaddag);
113
+
114
+ if (!compressed) {
115
+ compressed = gzipAsync(gaddag.serialize());
116
+ compressedCache.set(gaddag, compressed);
117
+ compressed.catch(() => compressedCache.delete(gaddag));
118
+ }
119
+
120
+ return compressed;
121
+ };
122
+
46
123
  export const config = {
47
124
  api: {
48
- responseLimit: '25mb',
125
+ responseLimit: '50mb',
49
126
  },
50
127
  };
51
128
 
@@ -45,9 +45,9 @@ const solve = async (request: NextApiRequest, response: NextApiResponse): Promis
45
45
  },
46
46
  });
47
47
 
48
- const trie = await dictionaries.get(locale);
48
+ const gaddag = await dictionaries.get(locale);
49
49
  const tiles = characters.map((character) => new Tile({ character, isBlank: character === BLANK }));
50
- const results = solveScrabble(trie, config, board, tiles);
50
+ const results = solveScrabble(gaddag, config, board, tiles);
51
51
  response.status(200).send(results);
52
52
  } catch (error) {
53
53
  const message = error instanceof Error ? error.message : 'Unknown error';
@@ -30,10 +30,19 @@ const verify = async (request: NextApiRequest, response: NextApiResponse): Promi
30
30
  },
31
31
  });
32
32
 
33
- const trie = await dictionaries.get(locale);
33
+ const gaddag = await dictionaries.get(locale);
34
34
  const words = board.getWords().sort((a, b) => a.localeCompare(b, locale));
35
- const invalidWords = words.filter((word) => !trie.has(word));
36
- const validWords = words.filter((word) => trie.has(word));
35
+ const invalidWords: string[] = [];
36
+ const validWords: string[] = [];
37
+
38
+ for (const word of words) {
39
+ if (gaddag.has(word)) {
40
+ validWords.push(word);
41
+ } else {
42
+ invalidWords.push(word);
43
+ }
44
+ }
45
+
37
46
  response.status(200).send({ invalidWords, validWords });
38
47
  } catch (error) {
39
48
  const message = error instanceof Error ? error.message : 'Unknown error';
@@ -1,19 +1,25 @@
1
- import { type BoardJson, type Locale, Result, type ResultJson } from '@scrabble-solver/types';
1
+ import { Board, type BoardJson, type Game, type Locale, Result, type ResultJson } from '@scrabble-solver/types';
2
+
3
+ import { solveLocally } from '@/solver-worker';
2
4
 
3
5
  import { fetchJson } from './fetchJson';
4
6
 
5
7
  interface Payload {
6
8
  board: BoardJson;
7
9
  characters: string[];
8
- game: string;
10
+ game: Game;
9
11
  locale: Locale;
10
12
  }
11
13
 
12
14
  export const solve = async ({ board, characters, game, locale }: Payload): Promise<Result[]> => {
13
- const json = await fetchJson<ResultJson[]>('/api/solve', {
14
- method: 'POST',
15
- body: JSON.stringify({ board, characters, game, locale }),
16
- });
15
+ const payload = { board, characters, game, locale };
16
+ const json =
17
+ (await solveLocally(payload)) ??
18
+ (await fetchJson<ResultJson[]>('/api/solve', {
19
+ method: 'POST',
20
+ body: JSON.stringify(payload),
21
+ }));
17
22
 
18
- return json.map(Result.fromJson);
23
+ const decodedBoard = Board.fromJson(board);
24
+ return json.map((result) => Result.fromJson(result, decodedBoard));
19
25
  };
@@ -1,10 +1,12 @@
1
- import { type BoardJson, type Locale } from '@scrabble-solver/types';
1
+ import { type BoardJson, type Game, type Locale } from '@scrabble-solver/types';
2
+
3
+ import { verifyLocally } from '@/solver-worker';
2
4
 
3
5
  import { fetchJson } from './fetchJson';
4
6
 
5
7
  interface Payload {
6
8
  board: BoardJson;
7
- game: string;
9
+ game: Game;
8
10
  locale: Locale;
9
11
  }
10
12
 
@@ -14,8 +16,12 @@ interface Response {
14
16
  }
15
17
 
16
18
  export const verify = async ({ board, game, locale }: Payload): Promise<Response> => {
17
- return fetchJson<Response>('/api/verify', {
18
- method: 'POST',
19
- body: JSON.stringify({ board, game, locale }),
20
- });
19
+ const payload = { board, game, locale };
20
+ return (
21
+ (await verifyLocally(payload)) ??
22
+ fetchJson<Response>('/api/verify', {
23
+ method: 'POST',
24
+ body: JSON.stringify(payload),
25
+ })
26
+ );
21
27
  };
@@ -0,0 +1,86 @@
1
+ import { afterAll, beforeEach, describe, expect, it } from 'bun:test';
2
+
3
+ import { cacheAppShell, respondToNavigation } from './appShell';
4
+
5
+ const entries = new Map<string, Response>();
6
+
7
+ let respond: () => Promise<Response> = () => Promise.resolve(html('shell'));
8
+
9
+ const globals = { caches: globalThis.caches, fetch: globalThis.fetch };
10
+
11
+ Object.assign(globalThis, {
12
+ caches: createCacheStorage(entries),
13
+ fetch: () => respond(),
14
+ });
15
+
16
+ afterAll(() => {
17
+ Object.assign(globalThis, globals);
18
+ });
19
+
20
+ describe('respondToNavigation', () => {
21
+ beforeEach(() => {
22
+ entries.clear();
23
+ respond = () => Promise.resolve(html('shell'));
24
+ });
25
+
26
+ it('serves the network response while online', async () => {
27
+ await cacheAppShell();
28
+ respond = () => Promise.resolve(html('deployed shell'));
29
+ const response = await respondToNavigation({ request: navigation('/') });
30
+ expect(await response.text()).toEqual('deployed shell');
31
+ });
32
+
33
+ it('serves the shell cached on install when the network is gone', async () => {
34
+ await cacheAppShell();
35
+ respond = () => Promise.reject(new Error('offline'));
36
+ const response = await respondToNavigation({ request: navigation('/') });
37
+ expect(await response.text()).toEqual('shell');
38
+ });
39
+
40
+ it('serves the shell for any navigated path', async () => {
41
+ await cacheAppShell();
42
+ respond = () => Promise.reject(new Error('offline'));
43
+ const response = await respondToNavigation({ request: navigation('/not-found') });
44
+ expect(await response.text()).toEqual('shell');
45
+ });
46
+
47
+ /**
48
+ * The cached shell links to the chunks precached next to it, so a navigation
49
+ * must not replace it with HTML from a build this worker did not precache.
50
+ */
51
+ it('keeps the installed shell when a newer one is served from the network', async () => {
52
+ await cacheAppShell();
53
+ respond = () => Promise.resolve(html('deployed shell'));
54
+ await respondToNavigation({ request: navigation('/') });
55
+
56
+ respond = () => Promise.reject(new Error('offline'));
57
+ const response = await respondToNavigation({ request: navigation('/') });
58
+ expect(await response.text()).toEqual('shell');
59
+ });
60
+
61
+ it('rethrows when there is nothing to fall back on', async () => {
62
+ respond = () => Promise.reject(new Error('offline'));
63
+ const error = await respondToNavigation({ request: navigation('/') }).catch((reason: Error) => reason);
64
+ expect(error).toEqual(new Error('offline'));
65
+ });
66
+ });
67
+
68
+ function navigation(url: string): Request {
69
+ return new Request(new URL(url, 'https://scrabble-solver.org'));
70
+ }
71
+
72
+ function html(body: string): Response {
73
+ return new Response(body, { headers: { 'Content-Type': 'text/html; charset=utf-8' } });
74
+ }
75
+
76
+ function createCacheStorage(cacheEntries: Map<string, Response>): CacheStorage {
77
+ return {
78
+ open: (name: string) =>
79
+ Promise.resolve({
80
+ add: async (url: string) => {
81
+ cacheEntries.set(`${name} ${url}`, await globalThis.fetch(url));
82
+ },
83
+ match: (url: string) => Promise.resolve(cacheEntries.get(`${name} ${url}`)?.clone()),
84
+ }),
85
+ } as unknown as CacheStorage;
86
+ }
@@ -0,0 +1,39 @@
1
+ import { NavigationRoute, registerRoute } from 'workbox-routing';
2
+
3
+ const APP_SHELL_CACHE = 'app-shell';
4
+ const APP_SHELL_URL = '/';
5
+
6
+ /**
7
+ * Caching the shell on install instead of on navigation pins it to the same build
8
+ * as the chunks precached alongside it, and covers the first visit, whose
9
+ * navigation happens before this worker controls the page.
10
+ */
11
+ export async function cacheAppShell(): Promise<void> {
12
+ const cache = await caches.open(APP_SHELL_CACHE);
13
+ await cache.add(APP_SHELL_URL);
14
+ }
15
+
16
+ export function routeNavigations(): void {
17
+ registerRoute(new NavigationRoute(respondToNavigation, { denylist: [/^\/api\//] }));
18
+ }
19
+
20
+ /**
21
+ * A deploy has to reach online users on their next navigation (see the revalidation
22
+ * headers in next.config.js), so the shell only stands in when the network is gone.
23
+ * Solving keeps working from there, off the dictionary the solver worker holds in
24
+ * the Cache API.
25
+ */
26
+ export async function respondToNavigation({ request }: { request: Request }): Promise<Response> {
27
+ try {
28
+ return await fetch(request);
29
+ } catch (error) {
30
+ const cache = await caches.open(APP_SHELL_CACHE);
31
+ const appShell = await cache.match(APP_SHELL_URL);
32
+
33
+ if (!appShell) {
34
+ throw error;
35
+ }
36
+
37
+ return appShell;
38
+ }
39
+ }
@@ -2,12 +2,12 @@
2
2
 
3
3
  import { cleanupOutdatedCaches, precacheAndRoute } from 'workbox-precaching';
4
4
 
5
- import { routeSolveRequests } from './routeSolveRequests';
6
- import { routeVerifyRequests } from './routeVerifyRequests';
5
+ import { cacheAppShell, routeNavigations } from './appShell';
7
6
 
8
7
  declare const self: ServiceWorkerGlobalScope;
9
8
 
10
- self.addEventListener('install', () => {
9
+ self.addEventListener('install', (event) => {
10
+ event.waitUntil(cacheAppShell());
11
11
  // eslint-disable-next-line @typescript-eslint/no-floating-promises
12
12
  self.skipWaiting();
13
13
  });
@@ -19,5 +19,4 @@ self.addEventListener('activate', () => {
19
19
 
20
20
  cleanupOutdatedCaches();
21
21
  precacheAndRoute(self.__WB_MANIFEST);
22
- routeSolveRequests();
23
- routeVerifyRequests();
22
+ routeNavigations();