scrabble-solver 2.16.7 → 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 (276) hide show
  1. package/.nx/workspace-data/d/server-process.json +2 -2
  2. package/.nx/workspace-data/file-map.json +548 -572
  3. package/.nx/workspace-data/lockfile-dependencies.hash +1 -1
  4. package/.nx/workspace-data/lockfile-nodes.hash +1 -1
  5. package/.nx/workspace-data/nx_files.nxt +0 -0
  6. package/.nx/workspace-data/parsed-lock-file.dependencies.json +185 -160
  7. package/.nx/workspace-data/parsed-lock-file.nodes.json +406 -370
  8. package/.nx/workspace-data/project-graph.json +656 -552
  9. package/.nx/workspace-data/source-maps.json +96 -0
  10. package/.oxlintrc.json +6 -0
  11. package/CLAUDE.md +22 -17
  12. package/README.md +9 -0
  13. package/bun.lock +107 -133
  14. package/cypress/e2e/app.spec.cy.ts +4 -4
  15. package/cypress/e2e/features/solver-paths.ts +66 -0
  16. package/cypress/e2e/features.spec.cy.ts +1 -0
  17. package/cypress/support/lib/actions.ts +28 -9
  18. package/lerna.json +1 -1
  19. package/package.json +8 -7
  20. package/packages/configs/package.json +3 -3
  21. package/packages/constants/package.json +1 -1
  22. package/packages/dictionaries/README.md +1 -1
  23. package/packages/dictionaries/build/Dictionaries.d.ts +2 -2
  24. package/packages/dictionaries/build/Dictionaries.js +6 -6
  25. package/packages/dictionaries/build/lib/DiskCache.d.ts +11 -4
  26. package/packages/dictionaries/build/lib/DiskCache.js +27 -10
  27. package/packages/dictionaries/build/lib/LayeredCache.d.ts +4 -4
  28. package/packages/dictionaries/build/lib/LayeredCache.js +3 -3
  29. package/packages/dictionaries/build/lib/MemoryCache.d.ts +4 -4
  30. package/packages/dictionaries/build/lib/MemoryCache.js +2 -2
  31. package/packages/dictionaries/build/lib/createCacheTimestampComparator.d.ts +2 -2
  32. package/packages/dictionaries/build/lib/downloadDictionary.d.ts +2 -2
  33. package/packages/dictionaries/build/lib/downloadDictionary.js +4 -3
  34. package/packages/dictionaries/build/lib/getDictionaryFilepath.d.ts +3 -1
  35. package/packages/dictionaries/build/lib/getDictionaryFilepath.js +8 -3
  36. package/packages/dictionaries/package.json +7 -6
  37. package/packages/dictionaries/src/Dictionaries.ts +12 -12
  38. package/packages/dictionaries/src/lib/DiskCache.test.ts +85 -0
  39. package/packages/dictionaries/src/lib/DiskCache.ts +34 -14
  40. package/packages/dictionaries/src/lib/LayeredCache.ts +7 -7
  41. package/packages/dictionaries/src/lib/MemoryCache.ts +6 -6
  42. package/packages/dictionaries/src/lib/createCacheTimestampComparator.ts +2 -2
  43. package/packages/dictionaries/src/lib/downloadDictionary.ts +5 -4
  44. package/packages/dictionaries/src/lib/getDictionaryFilepath.ts +7 -2
  45. package/packages/gaddag/build/Gaddag.d.ts +39 -0
  46. package/packages/gaddag/build/Gaddag.js +128 -0
  47. package/packages/gaddag/build/buildGaddag.d.ts +10 -0
  48. package/packages/gaddag/build/buildGaddag.js +360 -0
  49. package/packages/gaddag/build/constants.d.ts +12 -0
  50. package/packages/gaddag/build/constants.js +15 -0
  51. package/packages/gaddag/build/index.d.ts +3 -0
  52. package/packages/gaddag/build/index.js +13 -0
  53. package/packages/logger/package.json +1 -1
  54. package/packages/scrabble-solver/.next/BUILD_ID +1 -1
  55. package/packages/scrabble-solver/.next/build-manifest.json +13 -13
  56. package/packages/scrabble-solver/.next/cache/.previewinfo +1 -1
  57. package/packages/scrabble-solver/.next/cache/.rscinfo +1 -1
  58. package/packages/scrabble-solver/.next/cache/.tsbuildinfo +1 -1
  59. package/packages/scrabble-solver/.next/cache/webpack/client-production/0.pack +0 -0
  60. package/packages/scrabble-solver/.next/cache/webpack/client-production/index.pack +0 -0
  61. package/packages/scrabble-solver/.next/cache/webpack/client-production/index.pack.old +0 -0
  62. package/packages/scrabble-solver/.next/cache/webpack/edge-server-production/index.pack +0 -0
  63. package/packages/scrabble-solver/.next/cache/webpack/edge-server-production/index.pack.old +0 -0
  64. package/packages/scrabble-solver/.next/cache/webpack/server-production/0.pack +0 -0
  65. package/packages/scrabble-solver/.next/cache/webpack/server-production/index.pack +0 -0
  66. package/packages/scrabble-solver/.next/cache/webpack/server-production/index.pack.old +0 -0
  67. package/packages/scrabble-solver/.next/diagnostics/framework.json +1 -1
  68. package/packages/scrabble-solver/.next/next-server.js.nft.json +1 -1
  69. package/packages/scrabble-solver/.next/prerender-manifest.json +4 -4
  70. package/packages/scrabble-solver/.next/required-server-files.js +3 -2
  71. package/packages/scrabble-solver/.next/required-server-files.json +3 -2
  72. package/packages/scrabble-solver/.next/routes-manifest.json +1 -1
  73. package/packages/scrabble-solver/.next/server/chunks/129.js +1 -0
  74. package/packages/scrabble-solver/.next/server/chunks/30.js +1 -1
  75. package/packages/scrabble-solver/.next/server/chunks/392.js +1 -1
  76. package/packages/scrabble-solver/.next/server/chunks/671.js +1 -1
  77. package/packages/scrabble-solver/.next/server/chunks/712.js +1 -1
  78. package/packages/scrabble-solver/.next/server/chunks/785.js +1 -0
  79. package/packages/scrabble-solver/.next/server/chunks/791.js +1 -1
  80. package/packages/scrabble-solver/.next/server/middleware-build-manifest.js +1 -1
  81. package/packages/scrabble-solver/.next/server/pages/404.html +1 -1
  82. package/packages/scrabble-solver/.next/server/pages/500.html +1 -1
  83. package/packages/scrabble-solver/.next/server/pages/_app.js +1 -1
  84. package/packages/scrabble-solver/.next/server/pages/_app.js.nft.json +1 -1
  85. package/packages/scrabble-solver/.next/server/pages/_error.js +1 -1
  86. package/packages/scrabble-solver/.next/server/pages/_error.js.nft.json +1 -1
  87. package/packages/scrabble-solver/.next/server/pages/api/dictionary/[locale]/[word].js +1 -1
  88. package/packages/scrabble-solver/.next/server/pages/api/dictionary/[locale]/[word].js.nft.json +1 -1
  89. package/packages/scrabble-solver/.next/server/pages/api/dictionary/[locale].js +1 -1
  90. package/packages/scrabble-solver/.next/server/pages/api/dictionary/[locale].js.nft.json +1 -1
  91. package/packages/scrabble-solver/.next/server/pages/api/solve.js +1 -1
  92. package/packages/scrabble-solver/.next/server/pages/api/solve.js.nft.json +1 -1
  93. package/packages/scrabble-solver/.next/server/pages/api/verify.js +1 -1
  94. package/packages/scrabble-solver/.next/server/pages/api/verify.js.nft.json +1 -1
  95. package/packages/scrabble-solver/.next/server/pages/index.html +1 -1
  96. package/packages/scrabble-solver/.next/server/pages/index.js +1 -1
  97. package/packages/scrabble-solver/.next/server/pages/index.js.nft.json +1 -1
  98. package/packages/scrabble-solver/.next/server/pages/index.json +1 -1
  99. package/packages/scrabble-solver/.next/server/pages/not-found.html +1 -1
  100. package/packages/scrabble-solver/.next/server/pages/not-found.js.nft.json +1 -1
  101. package/packages/scrabble-solver/.next/server/pages-manifest.json +1 -1
  102. package/packages/scrabble-solver/.next/server/webpack-runtime.js +1 -1
  103. package/packages/scrabble-solver/.next/static/{vYpMmuC_yEurZGcyGVnLf → OLE8_ZInlGH7ENU0NIOpf}/_buildManifest.js +1 -1
  104. package/packages/scrabble-solver/.next/static/chunks/583.7a46807be14c377e.js +1 -0
  105. package/packages/scrabble-solver/.next/static/chunks/830.661816bf0de8c2f1.js +1 -0
  106. package/packages/scrabble-solver/.next/static/chunks/{main-852475ca502b4e49.js → main-5b999f3f24ebfa84.js} +2 -2
  107. package/packages/scrabble-solver/.next/static/chunks/pages/_app-7024e7a27ffbb2fa.js +7 -0
  108. package/packages/scrabble-solver/.next/static/chunks/pages/{index-ec0f6aeeaf7be227.js → index-8b398b0a5d870773.js} +1 -1
  109. package/packages/scrabble-solver/.next/static/chunks/webpack-3943d624bbb47a64.js +1 -0
  110. package/packages/scrabble-solver/.next/static/css/aedeaada218d7280.css +2 -0
  111. package/packages/scrabble-solver/.next/trace +21 -21
  112. package/packages/scrabble-solver/.next/trace-build +1 -1
  113. package/packages/scrabble-solver/next.config.js +0 -6
  114. package/packages/scrabble-solver/package.json +10 -10
  115. package/packages/scrabble-solver/public/service-worker.js +2 -2
  116. package/packages/scrabble-solver/src/components/Board/components/Cell/Cell.tsx +4 -1
  117. package/packages/scrabble-solver/src/components/Board/hooks/useGrid.ts +17 -6
  118. package/packages/scrabble-solver/src/components/Dictionary/Dictionary.module.scss +7 -22
  119. package/packages/scrabble-solver/src/components/Dictionary/Dictionary.tsx +1 -8
  120. package/packages/scrabble-solver/src/components/Results/Result.tsx +0 -3
  121. package/packages/scrabble-solver/src/components/Results/Results.module.scss +0 -1
  122. package/packages/scrabble-solver/src/components/Results/Results.tsx +10 -2
  123. package/packages/scrabble-solver/src/components/Results/types.ts +3 -3
  124. package/packages/scrabble-solver/src/components/Solver/Solver.tsx +4 -3
  125. package/packages/scrabble-solver/src/components/Tooltip/useTooltip.ts +1 -1
  126. package/packages/scrabble-solver/src/pages/api/dictionary/[locale]/[word].ts +2 -2
  127. package/packages/scrabble-solver/src/pages/api/dictionary/[locale]/index.ts +80 -3
  128. package/packages/scrabble-solver/src/pages/api/solve.ts +2 -2
  129. package/packages/scrabble-solver/src/pages/api/verify.ts +12 -3
  130. package/packages/scrabble-solver/src/sdk/solve.ts +13 -7
  131. package/packages/scrabble-solver/src/sdk/verify.ts +12 -6
  132. package/packages/scrabble-solver/src/service-worker/appShell.test.ts +86 -0
  133. package/packages/scrabble-solver/src/service-worker/appShell.ts +39 -0
  134. package/packages/scrabble-solver/src/service-worker/index.ts +4 -5
  135. package/packages/scrabble-solver/src/serviceWorkerManager.ts +1 -18
  136. package/packages/scrabble-solver/src/solver-worker/client.test.ts +151 -0
  137. package/packages/scrabble-solver/src/solver-worker/client.ts +106 -0
  138. package/packages/scrabble-solver/src/solver-worker/dictionaries/constants.ts +9 -0
  139. package/packages/scrabble-solver/src/solver-worker/dictionaries/getDictionary.ts +21 -0
  140. package/packages/scrabble-solver/src/solver-worker/dictionaries/index.ts +2 -0
  141. package/packages/scrabble-solver/src/solver-worker/dictionaries/revalidateDictionary.test.ts +199 -0
  142. package/packages/scrabble-solver/src/solver-worker/dictionaries/revalidateDictionary.ts +86 -0
  143. package/packages/scrabble-solver/src/solver-worker/dictionaries/revalidatedAt.ts +43 -0
  144. package/packages/scrabble-solver/src/solver-worker/getGaddag.test.ts +109 -0
  145. package/packages/scrabble-solver/src/solver-worker/getGaddag.ts +62 -0
  146. package/packages/scrabble-solver/src/solver-worker/index.ts +1 -0
  147. package/packages/scrabble-solver/src/solver-worker/index.worker.ts +124 -0
  148. package/packages/scrabble-solver/src/solver-worker/messages.ts +20 -0
  149. package/packages/scrabble-solver/src/state/index.ts +1 -0
  150. package/packages/scrabble-solver/src/state/sagas.ts +19 -9
  151. package/packages/scrabble-solver/src/state/useTypedStore.ts +6 -0
  152. package/packages/scrabble-solver/src/types/api.ts +1 -2
  153. package/packages/scrabble-solver/tsconfig.json +4 -0
  154. package/packages/scrabble-solver/tsconfig.tsbuildinfo +1 -1
  155. package/packages/solver/README.md +24 -0
  156. package/packages/solver/benchmarks/index.ts +15 -0
  157. package/packages/solver/benchmarks/lib.ts +31 -0
  158. package/packages/solver/benchmarks/renderChart.ts +165 -0
  159. package/packages/solver/benchmarks/results/chart.svg +65 -0
  160. package/packages/solver/benchmarks/runBenchmarks.ts +66 -0
  161. package/packages/solver/benchmarks/scenarios.ts +64 -0
  162. package/packages/solver/benchmarks/types.ts +16 -0
  163. package/packages/solver/benchmarks/updateReadme.ts +53 -0
  164. package/packages/solver/build/MoveGenerator.d.ts +116 -0
  165. package/packages/solver/build/MoveGenerator.js +686 -0
  166. package/packages/solver/build/index.d.ts +0 -13
  167. package/packages/solver/build/index.js +1 -27
  168. package/packages/solver/build/solve.d.ts +2 -2
  169. package/packages/solver/build/solve.js +3 -19
  170. package/packages/solver/package.json +7 -6
  171. package/packages/solver/src/MoveGenerator.test.ts +1009 -0
  172. package/packages/solver/src/MoveGenerator.ts +926 -0
  173. package/packages/solver/src/ReferenceSolver.ts +401 -0
  174. package/packages/solver/src/index.ts +0 -13
  175. package/packages/solver/src/solve.test.ts +21 -21
  176. package/packages/solver/src/solve.ts +4 -22
  177. package/packages/solver/tsconfig.json +1 -1
  178. package/packages/types/build/Cell.d.ts +1 -0
  179. package/packages/types/build/Cell.js +3 -0
  180. package/packages/types/build/Result.d.ts +3 -2
  181. package/packages/types/build/Result.js +11 -6
  182. package/packages/types/build/ResultJson.d.ts +5 -4
  183. package/packages/types/build/index.d.ts +0 -6
  184. package/packages/types/build/index.js +1 -9
  185. package/packages/types/build/readResultCells.d.ts +5 -0
  186. package/packages/types/build/readResultCells.js +75 -0
  187. package/packages/types/package.json +4 -3
  188. package/packages/types/src/Cell.ts +4 -0
  189. package/packages/types/src/Result.ts +15 -6
  190. package/packages/types/src/ResultJson.ts +5 -5
  191. package/packages/types/src/index.ts +0 -6
  192. package/packages/types/src/readResultCells.test.ts +140 -0
  193. package/packages/types/src/readResultCells.ts +90 -0
  194. package/packages/word-definitions/package.json +2 -2
  195. package/packages/word-lists/package.json +2 -2
  196. package/packages/scrabble-solver/.next/static/chunks/pages/_app-a5bbe02710f13d05.js +0 -7
  197. package/packages/scrabble-solver/.next/static/chunks/webpack-1a7a166b34ed8d24.js +0 -1
  198. package/packages/scrabble-solver/.next/static/css/0af35adb4bab56f0.css +0 -2
  199. package/packages/scrabble-solver/src/sdk/getDictionary.ts +0 -9
  200. package/packages/scrabble-solver/src/service-worker/average.ts +0 -7
  201. package/packages/scrabble-solver/src/service-worker/dictionaries/constants.ts +0 -5
  202. package/packages/scrabble-solver/src/service-worker/dictionaries/getDictionary.ts +0 -20
  203. package/packages/scrabble-solver/src/service-worker/dictionaries/index.ts +0 -2
  204. package/packages/scrabble-solver/src/service-worker/dictionaries/revalidateDictionary.ts +0 -33
  205. package/packages/scrabble-solver/src/service-worker/getTrie.ts +0 -24
  206. package/packages/scrabble-solver/src/service-worker/routeSolveRequests.ts +0 -91
  207. package/packages/scrabble-solver/src/service-worker/routeVerifyRequests.ts +0 -37
  208. package/packages/solver/build/areDigraphsValid.d.ts +0 -2
  209. package/packages/solver/build/areDigraphsValid.js +0 -17
  210. package/packages/solver/build/fillPattern.d.ts +0 -6
  211. package/packages/solver/build/fillPattern.js +0 -52
  212. package/packages/solver/build/generateEndIndices.d.ts +0 -2
  213. package/packages/solver/build/generateEndIndices.js +0 -17
  214. package/packages/solver/build/generateHorizontalPatterns.d.ts +0 -2
  215. package/packages/solver/build/generateHorizontalPatterns.js +0 -16
  216. package/packages/solver/build/generatePattern.d.ts +0 -7
  217. package/packages/solver/build/generatePattern.js +0 -20
  218. package/packages/solver/build/generatePatterns.d.ts +0 -2
  219. package/packages/solver/build/generatePatterns.js +0 -11
  220. package/packages/solver/build/generateStartIndices.d.ts +0 -2
  221. package/packages/solver/build/generateStartIndices.js +0 -16
  222. package/packages/solver/build/generateVectors.d.ts +0 -7
  223. package/packages/solver/build/generateVectors.js +0 -7
  224. package/packages/solver/build/generateVerticalPatterns.d.ts +0 -2
  225. package/packages/solver/build/generateVerticalPatterns.js +0 -16
  226. package/packages/solver/build/getCellsScore.d.ts +0 -2
  227. package/packages/solver/build/getCellsScore.js +0 -18
  228. package/packages/solver/build/getPatternHash.d.ts +0 -2
  229. package/packages/solver/build/getPatternHash.js +0 -14
  230. package/packages/solver/build/getPatternScore.d.ts +0 -2
  231. package/packages/solver/build/getPatternScore.js +0 -22
  232. package/packages/solver/build/getUniquePatterns.d.ts +0 -2
  233. package/packages/solver/build/getUniquePatterns.js +0 -17
  234. package/packages/solver/src/areDigraphsValid.ts +0 -18
  235. package/packages/solver/src/fillPattern.test.ts +0 -107
  236. package/packages/solver/src/fillPattern.ts +0 -68
  237. package/packages/solver/src/generateEndIndices.test.ts +0 -23
  238. package/packages/solver/src/generateEndIndices.ts +0 -19
  239. package/packages/solver/src/generateHorizontalPatterns.test.ts +0 -31
  240. package/packages/solver/src/generateHorizontalPatterns.ts +0 -15
  241. package/packages/solver/src/generatePattern.ts +0 -33
  242. package/packages/solver/src/generatePatterns.test.ts +0 -13
  243. package/packages/solver/src/generatePatterns.ts +0 -11
  244. package/packages/solver/src/generateStartIndices.test.ts +0 -19
  245. package/packages/solver/src/generateStartIndices.ts +0 -17
  246. package/packages/solver/src/generateVectors.test.ts +0 -12
  247. package/packages/solver/src/generateVectors.ts +0 -10
  248. package/packages/solver/src/generateVerticalPatterns.test.ts +0 -32
  249. package/packages/solver/src/generateVerticalPatterns.ts +0 -15
  250. package/packages/solver/src/getCellsScore.ts +0 -21
  251. package/packages/solver/src/getPatternHash.test.ts +0 -17
  252. package/packages/solver/src/getPatternHash.ts +0 -12
  253. package/packages/solver/src/getPatternScore.test.ts +0 -204
  254. package/packages/solver/src/getPatternScore.ts +0 -23
  255. package/packages/solver/src/getUniquePatterns.ts +0 -19
  256. package/packages/types/build/CollisionJson.d.ts +0 -2
  257. package/packages/types/build/CollisionJson.js +0 -2
  258. package/packages/types/build/FinalPattern.d.ts +0 -6
  259. package/packages/types/build/FinalPattern.js +0 -14
  260. package/packages/types/build/HorizontalPattern.d.ts +0 -5
  261. package/packages/types/build/HorizontalPattern.js +0 -34
  262. package/packages/types/build/Pattern.d.ts +0 -20
  263. package/packages/types/build/Pattern.js +0 -50
  264. package/packages/types/build/PatternJson.d.ts +0 -6
  265. package/packages/types/build/PatternJson.js +0 -2
  266. package/packages/types/build/VerticalPattern.d.ts +0 -5
  267. package/packages/types/build/VerticalPattern.js +0 -34
  268. package/packages/types/src/CollisionJson.ts +0 -3
  269. package/packages/types/src/FinalPattern.ts +0 -14
  270. package/packages/types/src/HorizontalPattern.ts +0 -37
  271. package/packages/types/src/Pattern.ts +0 -71
  272. package/packages/types/src/PatternJson.ts +0 -7
  273. package/packages/types/src/VerticalPattern.ts +0 -37
  274. /package/packages/scrabble-solver/.next/static/{vYpMmuC_yEurZGcyGVnLf → OLE8_ZInlGH7ENU0NIOpf}/_ssgManifest.js +0 -0
  275. /package/packages/scrabble-solver/src/{service-worker → solver-worker}/dictionaries/expirationManager.ts +0 -0
  276. /package/packages/scrabble-solver/src/{service-worker → solver-worker}/dictionaries/getDictionaryUrl.ts +0 -0
@@ -0,0 +1,85 @@
1
+ import { Gaddag } from '@kamilmielnik/gaddag';
2
+ import { Locale } from '@scrabble-solver/types';
3
+ import fs from 'fs';
4
+ import os from 'os';
5
+ import path from 'path';
6
+
7
+ import { DiskCache } from './DiskCache';
8
+ import { getDictionaryFilepath, getLegacyDictionaryFilepath } from './getDictionaryFilepath';
9
+
10
+ const LOCALE = Locale.EN_US;
11
+ const WORDS = ['scrabble', 'solver', 'gaddag'];
12
+
13
+ describe('DiskCache', () => {
14
+ let directory: string;
15
+ let cache: DiskCache;
16
+
17
+ beforeEach(() => {
18
+ directory = fs.mkdtempSync(path.join(os.tmpdir(), 'dictionaries-test-'));
19
+ cache = new DiskCache(directory);
20
+ });
21
+
22
+ afterEach(() => {
23
+ fs.rmSync(directory, { force: true, recursive: true });
24
+ });
25
+
26
+ it('misses when there is no file', async () => {
27
+ expect(cache.has(LOCALE)).toBe(false);
28
+ expect(await cache.get(LOCALE)).toBeUndefined();
29
+ expect(cache.isStale(LOCALE)).toBeUndefined();
30
+ expect(cache.getLastModifiedTimestamp(LOCALE)).toBeUndefined();
31
+ });
32
+
33
+ it('round-trips a dictionary', async () => {
34
+ await cache.set(LOCALE, Gaddag.fromArray(WORDS));
35
+
36
+ expect(cache.has(LOCALE)).toBe(true);
37
+ expect(cache.isStale(LOCALE)).toBe(false);
38
+
39
+ const gaddag = await cache.get(LOCALE);
40
+
41
+ for (const word of WORDS) {
42
+ expect(gaddag?.has(word)).toBe(true);
43
+ }
44
+
45
+ expect(gaddag?.has('trie')).toBe(false);
46
+ });
47
+
48
+ it('treats a file written in an incompatible format as a cache miss and removes it', async () => {
49
+ const filepath = getDictionaryFilepath(LOCALE, directory);
50
+ fs.writeFileSync(filepath, 'serialized trie from a previous app version');
51
+
52
+ expect(cache.has(LOCALE)).toBe(true);
53
+ expect(await cache.get(LOCALE)).toBeUndefined();
54
+ expect(cache.has(LOCALE)).toBe(false);
55
+ });
56
+
57
+ it('treats a corrupted (truncated) file as a cache miss and removes it', async () => {
58
+ const serialized = Gaddag.fromArray(WORDS).serialize();
59
+ const filepath = getDictionaryFilepath(LOCALE, directory);
60
+ fs.writeFileSync(filepath, serialized.subarray(0, 10));
61
+
62
+ expect(await cache.get(LOCALE)).toBeUndefined();
63
+ expect(cache.has(LOCALE)).toBe(false);
64
+ });
65
+
66
+ it('removes the legacy pre-gaddag cache file when writing', async () => {
67
+ const legacyFilepath = getLegacyDictionaryFilepath(LOCALE, directory);
68
+ fs.writeFileSync(legacyFilepath, 'serialized trie');
69
+
70
+ await cache.set(LOCALE, Gaddag.fromArray(WORDS));
71
+
72
+ expect(fs.existsSync(legacyFilepath)).toBe(false);
73
+ expect(cache.has(LOCALE)).toBe(true);
74
+ });
75
+
76
+ it('reports files older than the stale threshold as stale', async () => {
77
+ await cache.set(LOCALE, Gaddag.fromArray(WORDS));
78
+
79
+ const filepath = getDictionaryFilepath(LOCALE, directory);
80
+ const twoDaysAgo = new Date(Date.now() - 2 * 24 * 60 * 60 * 1000);
81
+ fs.utimesSync(filepath, twoDaysAgo, twoDaysAgo);
82
+
83
+ expect(cache.isStale(LOCALE)).toBe(true);
84
+ });
85
+ });
@@ -1,26 +1,44 @@
1
- import { Trie } from '@kamilmielnik/trie';
1
+ import { Gaddag } from '@kamilmielnik/gaddag';
2
+ import { logger } from '@scrabble-solver/logger';
2
3
  import { type Locale } from '@scrabble-solver/types';
3
4
  import fs from 'fs';
4
5
 
5
- import { CACHE_STALE_THRESHOLD } from '../constants';
6
+ import { CACHE_STALE_THRESHOLD, OUTPUT_DIRECTORY } from '../constants';
6
7
  import type { Cache } from '../types';
7
8
 
8
- import { getDictionaryFilepath } from './getDictionaryFilepath';
9
+ import { getDictionaryFilepath, getLegacyDictionaryFilepath } from './getDictionaryFilepath';
9
10
 
10
- export class DiskCache implements Cache<Locale, Trie> {
11
- public async get(locale: Locale): Promise<Trie | undefined> {
11
+ export class DiskCache implements Cache<Locale, Gaddag> {
12
+ private readonly directory: string;
13
+
14
+ constructor(directory: string = OUTPUT_DIRECTORY) {
15
+ this.directory = directory;
16
+ }
17
+
18
+ /**
19
+ * Returns undefined when the cached file cannot be deserialized (it was
20
+ * written by an app version with an incompatible format, or it is corrupted)
21
+ * so that callers re-download and re-serialize the dictionary.
22
+ */
23
+ public async get(locale: Locale): Promise<Gaddag | undefined> {
12
24
  if (!this.has(locale)) {
13
25
  return undefined;
14
26
  }
15
27
 
16
- const filepath = getDictionaryFilepath(locale);
17
- const serialized = await fs.promises.readFile(filepath, 'utf-8');
18
- const trie = Trie.deserialize(serialized);
19
- return trie;
28
+ const filepath = getDictionaryFilepath(locale, this.directory);
29
+
30
+ try {
31
+ const serialized = await fs.promises.readFile(filepath);
32
+ return Gaddag.deserialize(new Uint8Array(serialized.buffer, serialized.byteOffset, serialized.byteLength));
33
+ } catch (error) {
34
+ logger.warn('DiskCache - incompatible or corrupted dictionary file, it will be rebuilt', { error, locale });
35
+ await fs.promises.rm(filepath, { force: true });
36
+ return undefined;
37
+ }
20
38
  }
21
39
 
22
40
  public getLastModifiedTimestamp(locale: Locale): number | undefined {
23
- const filepath = getDictionaryFilepath(locale);
41
+ const filepath = getDictionaryFilepath(locale, this.directory);
24
42
 
25
43
  if (!fs.existsSync(filepath)) {
26
44
  return undefined;
@@ -31,7 +49,7 @@ export class DiskCache implements Cache<Locale, Trie> {
31
49
  }
32
50
 
33
51
  public has(locale: Locale): boolean {
34
- const filepath = getDictionaryFilepath(locale);
52
+ const filepath = getDictionaryFilepath(locale, this.directory);
35
53
  return fs.existsSync(filepath);
36
54
  }
37
55
 
@@ -50,8 +68,10 @@ export class DiskCache implements Cache<Locale, Trie> {
50
68
  return timeSinceModification > CACHE_STALE_THRESHOLD;
51
69
  }
52
70
 
53
- public async set(locale: Locale, trie: Trie): Promise<void> {
54
- const filepath = getDictionaryFilepath(locale);
55
- await fs.promises.writeFile(filepath, trie.serialize());
71
+ public async set(locale: Locale, gaddag: Gaddag): Promise<void> {
72
+ const filepath = getDictionaryFilepath(locale, this.directory);
73
+ await fs.promises.writeFile(filepath, gaddag.serialize());
74
+ // Serialized-trie cache from before the GADDAG migration - remove this in #437
75
+ await fs.promises.rm(getLegacyDictionaryFilepath(locale, this.directory), { force: true });
56
76
  }
57
77
  }
@@ -1,4 +1,4 @@
1
- import { type Trie } from '@kamilmielnik/trie';
1
+ import { type Gaddag } from '@kamilmielnik/gaddag';
2
2
  import { type Locale } from '@scrabble-solver/types';
3
3
 
4
4
  import type { Cache } from '../types';
@@ -7,10 +7,10 @@ import { createCacheTimestampComparator } from './createCacheTimestampComparator
7
7
  import { DiskCache } from './DiskCache';
8
8
  import { MemoryCache } from './MemoryCache';
9
9
 
10
- export class LayeredCache implements Cache<Locale, Trie> {
10
+ export class LayeredCache implements Cache<Locale, Gaddag> {
11
11
  private readonly layers = [new MemoryCache(), new DiskCache()];
12
12
 
13
- public async get(locale: Locale): Promise<Trie | undefined> {
13
+ public async get(locale: Locale): Promise<Gaddag | undefined> {
14
14
  const cache = this.getLastModifiedLayer(locale);
15
15
 
16
16
  if (!cache) {
@@ -53,13 +53,13 @@ export class LayeredCache implements Cache<Locale, Trie> {
53
53
  return false;
54
54
  }
55
55
 
56
- public async set(locale: Locale, trie: Trie): Promise<void> {
56
+ public async set(locale: Locale, gaddag: Gaddag): Promise<void> {
57
57
  const [memoryCache, diskCache] = this.layers;
58
- await diskCache.set(locale, trie);
59
- await memoryCache.set(locale, trie);
58
+ await diskCache.set(locale, gaddag);
59
+ await memoryCache.set(locale, gaddag);
60
60
  }
61
61
 
62
- private getLastModifiedLayer(locale: Locale): Cache<Locale, Trie> | undefined {
62
+ private getLastModifiedLayer(locale: Locale): Cache<Locale, Gaddag> | undefined {
63
63
  const layers = this.layers.filter((cache) => cache.has(locale));
64
64
  const [cached] = [...layers].sort(createCacheTimestampComparator(locale));
65
65
  return cached;
@@ -1,15 +1,15 @@
1
- import { type Trie } from '@kamilmielnik/trie';
1
+ import { type Gaddag } from '@kamilmielnik/gaddag';
2
2
  import { type Locale } from '@scrabble-solver/types';
3
3
 
4
4
  import { CACHE_STALE_THRESHOLD } from '../constants';
5
5
  import type { Cache } from '../types';
6
6
 
7
- export class MemoryCache implements Cache<Locale, Trie> {
8
- private readonly cache: Partial<Record<Locale, Trie>> = {};
7
+ export class MemoryCache implements Cache<Locale, Gaddag> {
8
+ private readonly cache: Partial<Record<Locale, Gaddag>> = {};
9
9
 
10
10
  private readonly cacheTimestamps: Partial<Record<Locale, number>> = {};
11
11
 
12
- public get(locale: Locale): Promise<Trie | undefined> {
12
+ public get(locale: Locale): Promise<Gaddag | undefined> {
13
13
  return Promise.resolve(this.cache[locale]);
14
14
  }
15
15
 
@@ -32,9 +32,9 @@ export class MemoryCache implements Cache<Locale, Trie> {
32
32
  return timeSinceModification > CACHE_STALE_THRESHOLD;
33
33
  }
34
34
 
35
- public set(locale: Locale, trie: Trie): Promise<void> {
35
+ public set(locale: Locale, gaddag: Gaddag): Promise<void> {
36
36
  this.cacheTimestamps[locale] = Date.now();
37
- this.cache[locale] = trie;
37
+ this.cache[locale] = gaddag;
38
38
  return Promise.resolve();
39
39
  }
40
40
  }
@@ -1,10 +1,10 @@
1
- import { type Trie } from '@kamilmielnik/trie';
1
+ import { type Gaddag } from '@kamilmielnik/gaddag';
2
2
  import { type Locale } from '@scrabble-solver/types';
3
3
 
4
4
  import type { Cache } from '../types';
5
5
 
6
6
  export const createCacheTimestampComparator = (locale: Locale) => {
7
- return (a: Cache<Locale, Trie>, b: Cache<Locale, Trie>): number => {
7
+ return (a: Cache<Locale, Gaddag>, b: Cache<Locale, Gaddag>): number => {
8
8
  const aTimestamp = a.getLastModifiedTimestamp(locale);
9
9
  const bTimestamp = b.getLastModifiedTimestamp(locale);
10
10
 
@@ -1,12 +1,13 @@
1
- import { Trie } from '@kamilmielnik/trie';
1
+ import { Gaddag } from '@kamilmielnik/gaddag';
2
2
  import { logger } from '@scrabble-solver/logger';
3
3
  import { type Locale } from '@scrabble-solver/types';
4
4
  import { getWordList } from '@scrabble-solver/word-lists';
5
5
 
6
- export const downloadDictionary = async (locale: Locale): Promise<Trie> => {
6
+ export const downloadDictionary = async (locale: Locale): Promise<Gaddag> => {
7
7
  logger.info('downloadDictionary', { locale });
8
8
  const words = await getWordList(locale);
9
9
  logger.info('downloadDictionary - success', { locale });
10
- const trie = Trie.fromArray(words);
11
- return trie;
10
+ const gaddag = Gaddag.fromArray(words);
11
+ logger.info('downloadDictionary - gaddag built', { locale });
12
+ return gaddag;
12
13
  };
@@ -3,6 +3,11 @@ import path from 'path';
3
3
 
4
4
  import { OUTPUT_DIRECTORY } from '../constants';
5
5
 
6
- export const getDictionaryFilepath = (locale: Locale): string => {
7
- return path.resolve(OUTPUT_DIRECTORY, `${locale}.txt`);
6
+ export const getDictionaryFilepath = (locale: Locale, directory: string = OUTPUT_DIRECTORY): string => {
7
+ return path.resolve(directory, `${locale}.gaddag`);
8
+ };
9
+
10
+ /** Filepath of the serialized-trie cache used before the GADDAG migration. Remove in #437. */
11
+ export const getLegacyDictionaryFilepath = (locale: Locale, directory: string = OUTPUT_DIRECTORY): string => {
12
+ return path.resolve(directory, `${locale}.txt`);
8
13
  };
@@ -0,0 +1,39 @@
1
+ /**
2
+ * A GADDAG (Gordon, 1994) stored as flat typed arrays for speed and compact serialization.
3
+ *
4
+ * For every word `w` and every split `1 <= s <= |w|` the automaton accepts
5
+ * `reverse(w[0..s)) + ◇ + w[s..)` (the separator is omitted when `s === |w|`).
6
+ *
7
+ * States are identified by "refs": `ref = (firstArcIndex << 1) | isWordEnd`.
8
+ * A `firstArcIndex` of 0 means the state has no outgoing arcs; ref 0 means "no such state".
9
+ * Arcs of a state are contiguous; the last one is marked with {@link LAST_ARC_FLAG} in its label.
10
+ * An arc label stores the letter index (1..63, 0 = separator) in its low 6 bits.
11
+ */
12
+ export declare class Gaddag {
13
+ /** Arc labels: letter index | LAST_ARC_FLAG. Index 0 is an unused sentinel. */
14
+ readonly arcLabels: Uint8Array;
15
+ /** Arc targets: encoded state refs. Index 0 is an unused sentinel. */
16
+ readonly arcTargets: Int32Array;
17
+ /** Ref of the root state. */
18
+ readonly rootRef: number;
19
+ /** Code point of each letter index (position 0 holds the code point of letter 1). */
20
+ readonly charCodes: Int32Array;
21
+ private readonly letterByCharCode;
22
+ constructor(arcLabels: Uint8Array, arcTargets: Int32Array, rootRef: number, charCodes: Int32Array);
23
+ static deserialize(bytes: Uint8Array): Gaddag;
24
+ serialize(): Uint8Array;
25
+ /** Number of arcs (including the unused sentinel at index 0). */
26
+ get arcsCount(): number;
27
+ /**
28
+ * Follows the arc labeled with `letter` from the state `ref` points at.
29
+ * Returns the target ref, or 0 when there is no such arc.
30
+ *
31
+ * A state's arcs are stored in ascending letter order, so the scan stops as
32
+ * soon as it passes the wanted letter.
33
+ */
34
+ getArc(ref: number, letter: number): number;
35
+ /** Maps a code point to its letter index, or -1 when the character is not in the alphabet. */
36
+ getLetter(charCode: number): number;
37
+ has(word: string): boolean;
38
+ hasPrefix(prefix: string): boolean;
39
+ }
@@ -0,0 +1,128 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Gaddag = void 0;
4
+ /* eslint-disable no-bitwise */
5
+ const constants_1 = require("./constants");
6
+ /**
7
+ * A GADDAG (Gordon, 1994) stored as flat typed arrays for speed and compact serialization.
8
+ *
9
+ * For every word `w` and every split `1 <= s <= |w|` the automaton accepts
10
+ * `reverse(w[0..s)) + ◇ + w[s..)` (the separator is omitted when `s === |w|`).
11
+ *
12
+ * States are identified by "refs": `ref = (firstArcIndex << 1) | isWordEnd`.
13
+ * A `firstArcIndex` of 0 means the state has no outgoing arcs; ref 0 means "no such state".
14
+ * Arcs of a state are contiguous; the last one is marked with {@link LAST_ARC_FLAG} in its label.
15
+ * An arc label stores the letter index (1..63, 0 = separator) in its low 6 bits.
16
+ */
17
+ class Gaddag {
18
+ constructor(arcLabels, arcTargets, rootRef, charCodes) {
19
+ this.arcLabels = arcLabels;
20
+ this.arcTargets = arcTargets;
21
+ this.rootRef = rootRef;
22
+ this.charCodes = charCodes;
23
+ this.letterByCharCode = new Map();
24
+ for (let index = 0; index < charCodes.length; ++index) {
25
+ this.letterByCharCode.set(charCodes[index], index + 1);
26
+ }
27
+ }
28
+ static deserialize(bytes) {
29
+ // Note: an explicit copy (not .slice()) — Buffer.prototype.slice returns a
30
+ // view that would keep the misaligned byteOffset.
31
+ const aligned = bytes.byteOffset % 4 === 0 ? bytes : new Uint8Array(bytes);
32
+ const header = new Int32Array(aligned.buffer, aligned.byteOffset, 4);
33
+ if (header[0] !== constants_1.MAGIC) {
34
+ throw new Error('Invalid Gaddag data');
35
+ }
36
+ const letterCount = header[1];
37
+ const arcCount = header[2];
38
+ const rootRef = header[3];
39
+ const charCodes = new Int32Array(aligned.buffer, aligned.byteOffset + 16, letterCount);
40
+ const arcTargets = new Int32Array(aligned.buffer, aligned.byteOffset + 16 + 4 * letterCount, arcCount);
41
+ const arcLabels = new Uint8Array(aligned.buffer, aligned.byteOffset + 16 + 4 * (letterCount + arcCount), arcCount);
42
+ return new Gaddag(arcLabels, arcTargets, rootRef, charCodes);
43
+ }
44
+ serialize() {
45
+ const letterCount = this.charCodes.length;
46
+ const arcCount = this.arcTargets.length;
47
+ const bytes = new Uint8Array(16 + 4 * (letterCount + arcCount) + arcCount);
48
+ const header = new Int32Array(bytes.buffer, 0, 4);
49
+ header[0] = constants_1.MAGIC;
50
+ header[1] = letterCount;
51
+ header[2] = arcCount;
52
+ header[3] = this.rootRef;
53
+ new Int32Array(bytes.buffer, 16, letterCount).set(this.charCodes);
54
+ new Int32Array(bytes.buffer, 16 + 4 * letterCount, arcCount).set(this.arcTargets);
55
+ bytes.set(this.arcLabels, 16 + 4 * (letterCount + arcCount));
56
+ return bytes;
57
+ }
58
+ /** Number of arcs (including the unused sentinel at index 0). */
59
+ get arcsCount() {
60
+ return this.arcTargets.length;
61
+ }
62
+ /**
63
+ * Follows the arc labeled with `letter` from the state `ref` points at.
64
+ * Returns the target ref, or 0 when there is no such arc.
65
+ *
66
+ * A state's arcs are stored in ascending letter order, so the scan stops as
67
+ * soon as it passes the wanted letter.
68
+ */
69
+ getArc(ref, letter) {
70
+ let index = ref >>> 1;
71
+ if (index === 0) {
72
+ return 0;
73
+ }
74
+ const labels = this.arcLabels;
75
+ for (;;) {
76
+ const label = labels[index];
77
+ const arcLetter = label & constants_1.LETTER_MASK;
78
+ if (arcLetter === letter) {
79
+ return this.arcTargets[index];
80
+ }
81
+ if (arcLetter > letter || label >= constants_1.LAST_ARC_FLAG) {
82
+ return 0;
83
+ }
84
+ ++index;
85
+ }
86
+ }
87
+ /** Maps a code point to its letter index, or -1 when the character is not in the alphabet. */
88
+ getLetter(charCode) {
89
+ return this.letterByCharCode.get(charCode) ?? -1;
90
+ }
91
+ has(word) {
92
+ const length = word.length;
93
+ if (length === 0) {
94
+ return false;
95
+ }
96
+ let ref = this.rootRef;
97
+ for (let index = length - 1; index >= 0; --index) {
98
+ const letter = this.getLetter(word.charCodeAt(index));
99
+ if (letter === -1) {
100
+ return false;
101
+ }
102
+ ref = this.getArc(ref, letter);
103
+ if (ref === 0) {
104
+ return false;
105
+ }
106
+ }
107
+ return (ref & 1) === 1;
108
+ }
109
+ hasPrefix(prefix) {
110
+ const length = prefix.length;
111
+ if (length === 0) {
112
+ return true;
113
+ }
114
+ let ref = this.rootRef;
115
+ for (let index = length - 1; index >= 0; --index) {
116
+ const letter = this.getLetter(prefix.charCodeAt(index));
117
+ if (letter === -1) {
118
+ return false;
119
+ }
120
+ ref = this.getArc(ref, letter);
121
+ if (ref === 0) {
122
+ return false;
123
+ }
124
+ }
125
+ return (ref & 1) === 1 || this.getArc(ref, constants_1.SEPARATOR) !== 0;
126
+ }
127
+ }
128
+ exports.Gaddag = Gaddag;
@@ -0,0 +1,10 @@
1
+ import { Gaddag } from './Gaddag';
2
+ /**
3
+ * Builds a minimal GADDAG from a word list.
4
+ *
5
+ * The construction generates every GADDAG sequence (`reverse(prefix) [+ ◇ + suffix]`)
6
+ * as a compact `(wordIndex << 6) | splitIndex` integer, orders them with an in-place
7
+ * MSD radix sort, and feeds them to an incremental minimal-automaton builder
8
+ * (Daciuk et al., 2000) backed by typed arrays and an open-addressing state registry.
9
+ */
10
+ export declare const buildGaddag: (words: string[]) => Gaddag;