@scrabble-solver/scrabble-solver 2.16.7 → 2.18.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.
- package/.next/BUILD_ID +1 -1
- package/.next/build-manifest.json +22 -20
- package/.next/cache/.previewinfo +1 -1
- package/.next/cache/.rscinfo +1 -1
- package/.next/cache/.tsbuildinfo +1 -1
- package/.next/cache/webpack/client-production/0.pack +0 -0
- package/.next/cache/webpack/client-production/index.pack +0 -0
- package/.next/cache/webpack/client-production/index.pack.old +0 -0
- package/.next/cache/webpack/edge-server-production/index.pack +0 -0
- package/.next/cache/webpack/edge-server-production/index.pack.old +0 -0
- package/.next/cache/webpack/server-production/0.pack +0 -0
- package/.next/cache/webpack/server-production/index.pack +0 -0
- package/.next/cache/webpack/server-production/index.pack.old +0 -0
- package/.next/diagnostics/framework.json +1 -1
- package/.next/dynamic-css-manifest.json +8 -1
- package/.next/next-minimal-server.js.nft.json +1 -1
- package/.next/next-server.js.nft.json +1 -1
- package/.next/prerender-manifest.json +4 -4
- package/.next/react-loadable-manifest.json +176 -1
- package/.next/required-server-files.js +3 -2
- package/.next/required-server-files.json +3 -2
- package/.next/routes-manifest.json +1 -1
- package/.next/server/chunks/30.js +1 -1
- package/.next/server/chunks/309.js +1 -0
- package/.next/server/chunks/316.js +1 -0
- package/.next/server/chunks/344.js +1 -0
- package/.next/server/chunks/371.js +1 -0
- package/.next/server/chunks/392.js +1 -1
- package/.next/server/chunks/465.js +1 -0
- package/.next/server/chunks/546.js +1 -0
- package/.next/server/chunks/623.js +1 -0
- package/.next/server/chunks/624.js +1 -0
- package/.next/server/chunks/641.js +1 -0
- package/.next/server/chunks/671.js +1 -1
- package/.next/server/chunks/692.js +1 -0
- package/.next/server/chunks/705.js +1 -0
- package/.next/server/chunks/712.js +1 -1
- package/.next/server/chunks/846.js +1 -0
- package/.next/server/chunks/853.js +15 -0
- package/.next/server/chunks/886.js +1 -0
- package/.next/server/chunks/916.js +1 -0
- package/.next/server/chunks/953.js +1 -0
- package/.next/server/chunks/961.js +1 -0
- package/.next/server/chunks/968.js +1 -0
- package/.next/server/dynamic-css-manifest.js +1 -1
- package/.next/server/middleware-build-manifest.js +1 -1
- package/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/.next/server/pages/404.html +28 -1
- package/.next/server/pages/500.html +28 -1
- package/.next/server/pages/_app.js +1 -1
- package/.next/server/pages/_app.js.nft.json +1 -1
- package/.next/server/pages/_document.js +28 -1
- package/.next/server/pages/_document.js.nft.json +1 -1
- package/.next/server/pages/_error.js +28 -1
- package/.next/server/pages/_error.js.nft.json +1 -1
- package/.next/server/pages/api/dictionary/[locale]/[word].js +1 -1
- package/.next/server/pages/api/dictionary/[locale]/[word].js.nft.json +1 -1
- package/.next/server/pages/api/dictionary/[locale].js +1 -1
- package/.next/server/pages/api/dictionary/[locale].js.nft.json +1 -1
- package/.next/server/pages/api/solve.js +1 -1
- package/.next/server/pages/api/solve.js.nft.json +1 -1
- package/.next/server/pages/api/verify.js +1 -1
- package/.next/server/pages/api/verify.js.nft.json +1 -1
- package/.next/server/pages/api/visit.js.nft.json +1 -1
- package/.next/server/pages/index.html +28 -1
- package/.next/server/pages/index.js +30 -1
- package/.next/server/pages/index.js.nft.json +1 -1
- package/.next/server/pages/index.json +1 -1
- package/.next/server/pages/not-found.html +28 -1
- package/.next/server/pages/not-found.js.nft.json +1 -1
- package/.next/server/pages-manifest.json +1 -1
- package/.next/server/webpack-api-runtime.js +1 -1
- package/.next/server/webpack-runtime.js +1 -1
- package/.next/static/FL82obwzFHRzvV6z76I-U/_buildManifest.js +1 -0
- package/.next/static/chunks/101-829ab8a8d310e7a0.js +3 -0
- package/.next/static/chunks/106.e414e88694add27f.js +1 -0
- package/.next/static/chunks/127.125d896a19061d71.js +1 -0
- package/.next/static/chunks/18.8b91e137d239e15c.js +1 -0
- package/.next/static/chunks/277.6785dfaf22fdb0b7.js +1 -0
- package/.next/static/chunks/307.67f52f13e3d60d4e.js +1 -0
- package/.next/static/chunks/341.79535ba85bf1986f.js +1 -0
- package/.next/static/chunks/363.01eed3233f92b6a4.js +1 -0
- package/.next/static/chunks/422.1559ae6756912ca5.js +1 -0
- package/.next/static/chunks/541.dcd9e8b804f88da5.js +1 -0
- package/.next/static/chunks/583.7a46807be14c377e.js +1 -0
- package/.next/static/chunks/631.98d42abad0a60af6.js +1 -0
- package/.next/static/chunks/663.d2c9ca36b26695b8.js +1 -0
- package/.next/static/chunks/711.ba7b8f900dfd40d0.js +1 -0
- package/.next/static/chunks/744.b06c9a3de4a1d877.js +1 -0
- package/.next/static/chunks/820.97b82f751ca49579.js +1 -0
- package/.next/static/chunks/865.a9b563a45848a83c.js +1 -0
- package/.next/static/chunks/912.ff8037ba9d4db5c8.js +1 -0
- package/.next/static/chunks/939.19b33a3d3c351d73.js +1 -0
- package/.next/static/chunks/990.61296e5870b91f11.js +1 -0
- package/.next/static/chunks/framework-626cbd48ac5aca57.js +1 -0
- package/.next/static/chunks/main-b92e08f3fc96dd8d.js +5 -0
- package/.next/static/chunks/pages/_app-ba5d628e81a15461.js +1 -0
- package/.next/static/chunks/pages/{_error-afeda7c5231b6c3c.js → _error-f0290a98eb3ed0ec.js} +1 -1
- package/.next/static/chunks/pages/index-5f9fa6ac3cd3d97b.js +1 -0
- package/.next/static/chunks/pages/not-found-23d61fd3ed791cad.js +1 -0
- package/.next/static/chunks/webpack-e34c3ae12275e978.js +1 -0
- package/.next/static/css/2d03f0da2c678dfc.css +1 -0
- package/.next/static/css/7b6724d2df70f48f.css +1 -0
- package/.next/static/css/abc2425d3ba607bc.css +1 -0
- package/.next/static/css/b736be3bd02b5d99.css +1 -0
- package/.next/static/css/b74cd7de82dabcdd.css +1 -0
- package/.next/static/css/c403ce739a9c0d83.css +1 -0
- package/.next/static/css/ca311e6c030663d4.css +1 -0
- package/.next/static/css/dee878eb64099d92.css +1 -0
- package/.next/static/css/fbfdce70731d0d2b.css +1 -0
- package/.next/trace +21 -21
- package/.next/trace-build +1 -1
- package/empty-module.js +2 -0
- package/next.config.js +13 -8
- package/package.json +35 -16
- package/public/llms.txt +11 -0
- package/public/robots.txt +4 -0
- package/scripts/generate-scss-types.ts +112 -0
- package/src/@types/scss.d.ts +6 -9
- package/src/@types/svg.d.ts +1 -1
- package/src/components/Alert/Alert.module.scss.d.ts +13 -0
- package/src/components/Alert/Alert.tsx +4 -1
- package/src/components/Badge/Badge.module.scss.d.ts +6 -0
- package/src/components/Board/Board.module.scss +10 -0
- package/src/components/Board/Board.module.scss.d.ts +17 -0
- package/src/components/Board/Board.tsx +14 -26
- package/src/components/Board/BoardPure.tsx +54 -49
- package/src/components/Board/components/Actions/Actions.module.scss +18 -18
- package/src/components/Board/components/Actions/Actions.module.scss.d.ts +7 -0
- package/src/components/Board/components/Actions/Actions.tsx +6 -3
- package/src/components/Board/components/Actions/lib.ts +3 -1
- package/src/components/Board/components/Cell/Cell.module.scss.d.ts +13 -0
- package/src/components/Board/components/Cell/Cell.tsx +32 -34
- package/src/components/Board/components/InputPrompt/InputPrompt.module.scss.d.ts +10 -0
- package/src/components/Board/components/InputPrompt/InputPrompt.tsx +1 -1
- package/src/components/Board/components/ToggleDirectionButton/ToggleDirectionButton.module.scss.d.ts +8 -0
- package/src/components/Board/components/ToggleDirectionButton/ToggleDirectionButton.tsx +2 -2
- package/src/components/Board/getBoardBackground.ts +175 -0
- package/src/components/Board/hooks/index.ts +0 -1
- package/src/components/Board/hooks/useBoardStyle.ts +10 -20
- package/src/components/Board/hooks/useFloatingActions.ts +3 -6
- package/src/components/Board/hooks/useGrid.ts +23 -9
- package/src/components/Board/selectors.ts +1 -1
- package/src/components/Button/Button.module.scss.d.ts +12 -0
- package/src/components/Dictionary/Dictionary.module.scss +8 -22
- package/src/components/Dictionary/Dictionary.module.scss.d.ts +14 -0
- package/src/components/Dictionary/Dictionary.tsx +1 -10
- package/src/components/DictionaryInput/DictionaryInput.module.scss.d.ts +7 -0
- package/src/components/DictionaryInput/DictionaryInput.tsx +1 -1
- package/src/components/EmptyState/EmptyState.module.scss.d.ts +7 -0
- package/src/components/EmptyState/EmptyState.tsx +1 -1
- package/src/components/IconButton/IconButton.module.scss.d.ts +8 -0
- package/src/components/Key/Key.module.scss.d.ts +6 -0
- package/src/components/Keys/Arrows/Arrows.module.scss.d.ts +11 -0
- package/src/components/Keys/Arrows/Arrows.tsx +4 -1
- package/src/components/Keys/index.tsx +1 -1
- package/src/components/Loading/Loading.module.scss.d.ts +9 -0
- package/src/components/Loading/Loading.tsx +1 -1
- package/src/components/Logo/Logo.tsx +4 -9
- package/src/components/Logo/LogoBlueprint.tsx +1 -0
- package/src/components/Modal/Modal.module.scss.d.ts +15 -0
- package/src/components/Modal/Modal.tsx +16 -3
- package/src/components/Modal/components/Section/Section.module.scss.d.ts +7 -0
- package/src/components/NavButtons/NavButtons.module.scss +20 -0
- package/src/components/NavButtons/NavButtons.module.scss.d.ts +13 -0
- package/src/components/NavButtons/NavButtons.tsx +29 -45
- package/src/components/NotFound/NotFound.module.scss.d.ts +9 -0
- package/src/components/NotFound/NotFound.tsx +1 -1
- package/src/components/PlainTiles/PlainTiles.module.scss.d.ts +11 -0
- package/src/components/PlainTiles/PlainTiles.tsx +0 -1
- package/src/components/PlainTiles/Tile.tsx +3 -4
- package/src/components/PlainTiles/lib.ts +4 -19
- package/src/components/PlainTiles/types.ts +0 -1
- package/src/components/Progress/Progress.module.scss +7 -0
- package/src/components/Progress/Progress.module.scss.d.ts +6 -0
- package/src/components/Progress/Progress.tsx +2 -11
- package/src/components/Rack/Rack.module.scss +3 -0
- package/src/components/Rack/Rack.module.scss.d.ts +10 -0
- package/src/components/Rack/Rack.tsx +8 -11
- package/src/components/Rack/components/InputPrompt/InputPrompt.module.scss +2 -0
- package/src/components/Rack/components/InputPrompt/InputPrompt.module.scss.d.ts +7 -0
- package/src/components/Rack/components/InputPrompt/InputPrompt.tsx +1 -13
- package/src/components/Rack/components/InputPrompt/lib.ts +1 -1
- package/src/components/Rack/components/RackTile/RackTile.module.scss.d.ts +6 -0
- package/src/components/Rack/components/RackTile/RackTile.tsx +5 -4
- package/src/components/Rack/selectors.ts +1 -1
- package/src/components/Radio/Radio.module.scss.d.ts +12 -0
- package/src/components/Results/Cell.tsx +3 -12
- package/src/components/Results/Header.tsx +9 -18
- package/src/components/Results/HeaderButton.tsx +4 -5
- package/src/components/Results/Result.tsx +4 -15
- package/src/components/Results/Results.module.scss +4 -7
- package/src/components/Results/Results.module.scss.d.ts +27 -0
- package/src/components/Results/Results.tsx +14 -5
- package/src/components/Results/SolveButton.tsx +1 -1
- package/src/components/Results/types.ts +3 -3
- package/src/components/ResultsInput/ResultsInput.module.scss.d.ts +7 -0
- package/src/components/ResultsInput/ResultsInput.tsx +1 -1
- package/src/components/Solver/Solver.module.scss +6 -5
- package/src/components/Solver/Solver.module.scss.d.ts +20 -0
- package/src/components/Solver/Solver.tsx +23 -24
- package/src/components/Solver/components/InsertButton/InsertButton.module.scss.d.ts +7 -0
- package/src/components/Solver/components/InsertButton/InsertButton.tsx +1 -1
- package/src/components/Solver/components/ResultCandidatePicker/ResultCandidatePicker.module.scss.d.ts +18 -0
- package/src/components/Solver/components/ResultCandidatePicker/ResultCandidatePicker.tsx +3 -1
- package/src/components/Spinner/Spinner.module.scss.d.ts +10 -0
- package/src/components/Tile/Tile.module.scss +3 -0
- package/src/components/Tile/Tile.module.scss.d.ts +19 -0
- package/src/components/Tile/Tile.tsx +5 -14
- package/src/components/Tile/TilePure.tsx +2 -12
- package/src/components/Tooltip/Tooltip.module.scss.d.ts +7 -0
- package/src/components/Tooltip/Tooltip.tsx +14 -3
- package/src/components/Tooltip/useTooltip.ts +2 -1
- package/src/hooks/useColumns.ts +4 -7
- package/src/hooks/useDeferredRender.ts +20 -0
- package/src/hooks/useDirection.ts +4 -3
- package/src/hooks/useLanguage.ts +2 -2
- package/src/hooks/useLocalStorage.ts +32 -8
- package/src/hooks/useMedia.ts +26 -26
- package/src/i18n/constants.ts +2 -15
- package/src/i18n/i18n.ts +18 -17
- package/src/i18n/languages/english.json +0 -1
- package/src/i18n/languages/french.json +0 -1
- package/src/i18n/languages/german.json +0 -1
- package/src/i18n/languages/persian.json +0 -1
- package/src/i18n/languages/polish.json +0 -1
- package/src/i18n/languages/romanian.json +0 -1
- package/src/i18n/languages/spanish.json +0 -1
- package/src/i18n/languages/turkish.json +0 -1
- package/src/i18n/localeIcons.ts +24 -0
- package/src/lib/localeTransliterate.test.ts +5 -0
- package/src/lib/localeTransliterate.ts +26 -6
- package/src/lib/waitForIdleOrFirstIntent.ts +33 -0
- package/src/modals/DictionaryModal/DictionaryModal.module.scss.d.ts +8 -0
- package/src/modals/DictionaryModal/DictionaryModal.tsx +3 -1
- package/src/modals/KeyMapModal/KeyMapModal.tsx +3 -1
- package/src/modals/KeyMapModal/components/Mapping/Mapping.module.scss.d.ts +11 -0
- package/src/modals/MenuModal/MenuModal.module.scss +5 -0
- package/src/modals/MenuModal/MenuModal.module.scss.d.ts +10 -0
- package/src/modals/MenuModal/MenuModal.tsx +16 -5
- package/src/modals/RemainingTilesModal/RemainingTilesModal.module.scss +4 -1
- package/src/modals/RemainingTilesModal/RemainingTilesModal.module.scss.d.ts +9 -0
- package/src/modals/RemainingTilesModal/RemainingTilesModal.tsx +4 -6
- package/src/modals/RemainingTilesModal/components/Character/Character.module.scss +1 -0
- package/src/modals/RemainingTilesModal/components/Character/Character.module.scss.d.ts +12 -0
- package/src/modals/RemainingTilesModal/components/Character/Character.tsx +5 -12
- package/src/modals/ResultsModal/ResultsModal.module.scss.d.ts +8 -0
- package/src/modals/ResultsModal/ResultsModal.tsx +8 -4
- package/src/modals/SettingsModal/SettingsModal.tsx +2 -2
- package/src/modals/SettingsModal/components/AutoGroupTilesSetting/AutoGroupTilesSetting.module.scss.d.ts +7 -0
- package/src/modals/SettingsModal/components/AutoGroupTilesSetting/AutoGroupTilesSetting.tsx +1 -1
- package/src/modals/SettingsModal/components/ConfigSetting/ConfigSetting.module.scss.d.ts +7 -0
- package/src/modals/SettingsModal/components/ConfigSetting/ConfigSetting.tsx +1 -1
- package/src/modals/SettingsModal/components/HighlightUnreachableCellsSetting/HighlightUnreachableCellsSetting.module.scss.d.ts +7 -0
- package/src/modals/SettingsModal/components/HighlightUnreachableCellsSetting/HighlightUnreachableCellsSetting.tsx +1 -1
- package/src/modals/SettingsModal/components/InputModeSetting/InputModeSetting.module.scss.d.ts +7 -0
- package/src/modals/SettingsModal/components/InputModeSetting/InputModeSetting.tsx +1 -1
- package/src/modals/SettingsModal/components/LocaleSetting/LocaleSetting.module.scss.d.ts +8 -0
- package/src/modals/SettingsModal/components/LocaleSetting/LocaleSetting.tsx +24 -21
- package/src/modals/SettingsModal/components/RemoveCellFiltersSetting/RemoveCellFiltersSetting.module.scss.d.ts +7 -0
- package/src/modals/SettingsModal/components/RemoveCellFiltersSetting/RemoveCellFiltersSetting.tsx +1 -1
- package/src/modals/SettingsModal/components/ShowCoordinatesSetting/ShowCoordinatesSetting.module.scss.d.ts +7 -0
- package/src/modals/SettingsModal/components/ShowCoordinatesSetting/ShowCoordinatesSetting.tsx +1 -5
- package/src/modals/WordsModal/WordsModal.module.scss.d.ts +11 -0
- package/src/modals/WordsModal/WordsModal.tsx +4 -2
- package/src/modals/preload.ts +24 -0
- package/src/pages/_app.tsx +5 -8
- package/src/pages/_document.tsx +91 -5
- package/src/pages/api/dictionary/[locale]/[word].ts +2 -2
- package/src/pages/api/dictionary/[locale]/index.ts +80 -3
- package/src/pages/api/solve.ts +3 -3
- package/src/pages/api/verify.ts +12 -3
- package/src/pages/index.module.scss +1 -0
- package/src/pages/index.module.scss.d.ts +11 -0
- package/src/pages/index.tsx +98 -38
- package/src/pages/not-found.tsx +1 -1
- package/src/parameters/index.ts +24 -93
- package/src/sdk/solve.ts +13 -7
- package/src/sdk/verify.ts +12 -6
- package/src/service-worker/appShell.test.ts +86 -0
- package/src/service-worker/appShell.ts +39 -0
- package/src/service-worker/index.ts +4 -5
- package/src/serviceWorkerManager.ts +5 -19
- package/src/solver-worker/client.test.ts +151 -0
- package/src/solver-worker/client.ts +106 -0
- package/src/solver-worker/dictionaries/constants.ts +9 -0
- package/src/solver-worker/dictionaries/getDictionary.ts +21 -0
- package/src/solver-worker/dictionaries/index.ts +2 -0
- package/src/solver-worker/dictionaries/revalidateDictionary.test.ts +199 -0
- package/src/solver-worker/dictionaries/revalidateDictionary.ts +86 -0
- package/src/solver-worker/dictionaries/revalidatedAt.ts +43 -0
- package/src/solver-worker/getGaddag.test.ts +109 -0
- package/src/solver-worker/getGaddag.ts +62 -0
- package/src/solver-worker/index.ts +1 -0
- package/src/solver-worker/index.worker.ts +124 -0
- package/src/solver-worker/messages.ts +20 -0
- package/src/state/actions.ts +1 -1
- package/src/state/app/index.ts +4 -0
- package/src/state/app/initialState.ts +6 -0
- package/src/state/app/selectors.ts +5 -0
- package/src/state/app/slice.ts +20 -0
- package/src/state/app/types.ts +4 -0
- package/src/state/board/initialState.ts +1 -2
- package/src/state/i18n/index.ts +4 -0
- package/src/state/i18n/initialState.ts +5 -0
- package/src/state/i18n/selectors.ts +3 -0
- package/src/state/i18n/slice.ts +16 -0
- package/src/state/i18n/types.ts +7 -0
- package/src/state/index.ts +3 -0
- package/src/state/lib.ts +1 -1
- package/src/state/localStorage.ts +86 -29
- package/src/state/rack/initialState.ts +1 -2
- package/src/state/rack/slice.ts +4 -1
- package/src/state/results/lib.ts +5 -5
- package/src/state/results/selectors.ts +1 -1
- package/src/state/sagas.ts +139 -13
- package/src/state/selectors.ts +1 -1
- package/src/state/settings/initialState.ts +7 -10
- package/src/state/settings/selectors.ts +7 -2
- package/src/state/settings/slice.ts +5 -2
- package/src/state/store.ts +4 -0
- package/src/state/types.ts +4 -0
- package/src/state/useTypedStore.ts +6 -0
- package/src/styles/_tokens.scss +1 -0
- package/src/styles/global.scss +9 -0
- package/src/styles/variables.module.scss +0 -21
- package/src/styles/variables.module.scss.d.ts +27 -0
- package/src/styles/variables.scss +46 -2
- package/src/types/api.ts +1 -2
- package/src/types/index.ts +1 -2
- package/tsconfig.json +4 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/.next/server/chunks/318.js +0 -3
- package/.next/server/chunks/791.js +0 -15
- package/.next/static/chunks/framework-3cf15b8041c59dd3.js +0 -1
- package/.next/static/chunks/main-852475ca502b4e49.js +0 -5
- package/.next/static/chunks/pages/_app-a5bbe02710f13d05.js +0 -7
- package/.next/static/chunks/pages/index-ec0f6aeeaf7be227.js +0 -1
- package/.next/static/chunks/pages/not-found-9e076427bad69d90.js +0 -1
- package/.next/static/chunks/webpack-1a7a166b34ed8d24.js +0 -1
- package/.next/static/css/0af35adb4bab56f0.css +0 -2
- package/.next/static/css/a78e04fe95452380.css +0 -1
- package/.next/static/vYpMmuC_yEurZGcyGVnLf/_buildManifest.js +0 -1
- package/src/app-layout/AppLayoutContext.tsx +0 -25
- package/src/app-layout/index.ts +0 -1
- package/src/app-layout/useAppLayoutValue.ts +0 -112
- package/src/components/Board/hooks/useBackgroundImage.tsx +0 -279
- package/src/components/index.ts +0 -26
- package/src/hooks/index.ts +0 -12
- package/src/hooks/useOnWindowResize.ts +0 -11
- package/src/hooks/useViewportSize.ts +0 -17
- package/src/i18n/index.ts +0 -2
- package/src/icons/Flag.svg +0 -4
- package/src/icons/Star.svg +0 -4
- package/src/icons/index.ts +0 -49
- package/src/lib/getTileSizes.ts +0 -13
- package/src/lib/index.ts +0 -24
- package/src/modals/index.ts +0 -7
- package/src/sdk/getDictionary.ts +0 -9
- package/src/service-worker/average.ts +0 -7
- package/src/service-worker/dictionaries/constants.ts +0 -5
- package/src/service-worker/dictionaries/getDictionary.ts +0 -20
- package/src/service-worker/dictionaries/index.ts +0 -2
- package/src/service-worker/dictionaries/revalidateDictionary.ts +0 -33
- package/src/service-worker/getTrie.ts +0 -24
- package/src/service-worker/routeSolveRequests.ts +0 -91
- package/src/service-worker/routeVerifyRequests.ts +0 -37
- /package/.next/static/{vYpMmuC_yEurZGcyGVnLf → FL82obwzFHRzvV6z76I-U}/_ssgManifest.js +0 -0
- /package/src/{service-worker → solver-worker}/dictionaries/expirationManager.ts +0 -0
- /package/src/{service-worker → solver-worker}/dictionaries/getDictionaryUrl.ts +0 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { Gaddag } from '@kamilmielnik/gaddag';
|
|
2
|
+
import { Locale } from '@scrabble-solver/types';
|
|
3
|
+
import { describe, expect, it, mock } from 'bun:test';
|
|
4
|
+
|
|
5
|
+
import type * as getGaddagModule from './getGaddag';
|
|
6
|
+
|
|
7
|
+
let dictionary: Response | undefined;
|
|
8
|
+
const deletedLocales: Locale[] = [];
|
|
9
|
+
|
|
10
|
+
await mock.module('./dictionaries', () => ({
|
|
11
|
+
deleteDictionary: (locale: Locale) => {
|
|
12
|
+
deletedLocales.push(locale);
|
|
13
|
+
return Promise.resolve();
|
|
14
|
+
},
|
|
15
|
+
getDictionary: () => Promise.resolve(dictionary),
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
const { getGaddag }: typeof getGaddagModule = await import('./getGaddag');
|
|
19
|
+
|
|
20
|
+
describe('getGaddag', () => {
|
|
21
|
+
it('returns undefined when there is no cached dictionary', async () => {
|
|
22
|
+
dictionary = undefined;
|
|
23
|
+
expect(await getGaddag(Locale.EN_US)).toBeUndefined();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('deserializes a cached dictionary', async () => {
|
|
27
|
+
dictionary = createDictionary(['scrabble', 'solver']);
|
|
28
|
+
const gaddag = await getGaddag(Locale.EN_US);
|
|
29
|
+
|
|
30
|
+
expect(gaddag?.has('scrabble')).toBe(true);
|
|
31
|
+
expect(gaddag?.has('trie')).toBe(false);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('deletes a dictionary cached by an app version with an incompatible format', async () => {
|
|
35
|
+
dictionary = new Response('serialized trie from a previous app version');
|
|
36
|
+
|
|
37
|
+
expect(await getGaddag(Locale.PL_PL)).toBeUndefined();
|
|
38
|
+
expect(deletedLocales).toContain(Locale.PL_PL);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('deletes a corrupted dictionary', async () => {
|
|
42
|
+
dictionary = new Response(Gaddag.fromArray(['scrabble']).serialize().subarray(0, 10));
|
|
43
|
+
|
|
44
|
+
expect(await getGaddag(Locale.FA_IR)).toBeUndefined();
|
|
45
|
+
expect(deletedLocales).toContain(Locale.FA_IR);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('reuses the deserialized dictionary while the cached response is unchanged', async () => {
|
|
49
|
+
dictionary = createDictionary(['scrabble'], { etag: '"v1"' });
|
|
50
|
+
const first = await getGaddag(Locale.EN_GB);
|
|
51
|
+
dictionary = createDictionary(['scrabble'], { etag: '"v1"' });
|
|
52
|
+
const second = await getGaddag(Locale.EN_GB);
|
|
53
|
+
|
|
54
|
+
expect(first?.has('scrabble')).toBe(true);
|
|
55
|
+
expect(second).toBe(first);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('reuses the deserialized dictionary when a revalidation only refreshes the Date header', async () => {
|
|
59
|
+
dictionary = createDictionary(['scrabble'], { date: 'Thu, 06 Aug 2026 00:00:00 GMT', etag: '"v1"' });
|
|
60
|
+
const first = await getGaddag(Locale.FR_FR);
|
|
61
|
+
dictionary = createDictionary(['scrabble'], { date: 'Fri, 07 Aug 2026 00:00:00 GMT', etag: '"v1"' });
|
|
62
|
+
const second = await getGaddag(Locale.FR_FR);
|
|
63
|
+
|
|
64
|
+
expect(first?.has('scrabble')).toBe(true);
|
|
65
|
+
expect(second).toBe(first);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('picks up a dictionary another tab wrote into the shared cache', async () => {
|
|
69
|
+
dictionary = createDictionary(['scrabble'], { etag: '"v1"' });
|
|
70
|
+
const first = await getGaddag(Locale.TR_TR);
|
|
71
|
+
dictionary = createDictionary(['solver'], { etag: '"v2"' });
|
|
72
|
+
const second = await getGaddag(Locale.TR_TR);
|
|
73
|
+
|
|
74
|
+
expect(first?.has('scrabble')).toBe(true);
|
|
75
|
+
expect(second).not.toBe(first);
|
|
76
|
+
expect(second?.has('solver')).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('falls back to the Date header for responses without an ETag', async () => {
|
|
80
|
+
dictionary = createDictionary(['scrabble'], { date: 'Thu, 06 Aug 2026 00:00:00 GMT' });
|
|
81
|
+
const first = await getGaddag(Locale.DE_DE);
|
|
82
|
+
dictionary = createDictionary(['scrabble'], { date: 'Thu, 06 Aug 2026 00:00:00 GMT' });
|
|
83
|
+
const second = await getGaddag(Locale.DE_DE);
|
|
84
|
+
dictionary = createDictionary(['scrabble'], { date: 'Fri, 07 Aug 2026 00:00:00 GMT' });
|
|
85
|
+
const third = await getGaddag(Locale.DE_DE);
|
|
86
|
+
|
|
87
|
+
expect(first?.has('scrabble')).toBe(true);
|
|
88
|
+
expect(second).toBe(first);
|
|
89
|
+
expect(third).not.toBe(first);
|
|
90
|
+
expect(third?.has('scrabble')).toBe(true);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('memoizes each locale independently', async () => {
|
|
94
|
+
dictionary = createDictionary(['scrabble'], { etag: '"es"' });
|
|
95
|
+
const spanish = await getGaddag(Locale.ES_ES);
|
|
96
|
+
dictionary = createDictionary(['solver'], { etag: '"ro"' });
|
|
97
|
+
const romanian = await getGaddag(Locale.RO_RO);
|
|
98
|
+
dictionary = createDictionary(['scrabble'], { etag: '"es"' });
|
|
99
|
+
const spanishAgain = await getGaddag(Locale.ES_ES);
|
|
100
|
+
|
|
101
|
+
expect(spanish?.has('scrabble')).toBe(true);
|
|
102
|
+
expect(romanian?.has('solver')).toBe(true);
|
|
103
|
+
expect(spanishAgain).toBe(spanish);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
function createDictionary(words: string[], headers: Record<string, string> = {}): Response {
|
|
108
|
+
return new Response(Gaddag.fromArray(words).serialize(), { headers });
|
|
109
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Gaddag } from '@kamilmielnik/gaddag';
|
|
2
|
+
import { type Locale } from '@scrabble-solver/types';
|
|
3
|
+
|
|
4
|
+
import { deleteDictionary, getDictionary } from './dictionaries';
|
|
5
|
+
|
|
6
|
+
interface DeserializedDictionary {
|
|
7
|
+
gaddag: Gaddag;
|
|
8
|
+
version: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const deserializedDictionaries: Partial<Record<Locale, DeserializedDictionary>> = {};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Returns undefined when there is no cached dictionary or when the cached
|
|
15
|
+
* dictionary cannot be deserialized (it was downloaded by an app version with
|
|
16
|
+
* an incompatible format). An undeserializable entry is deleted before
|
|
17
|
+
* returning, so the revalidation the caller triggers next downloads a fresh
|
|
18
|
+
* dictionary instead of getting a 304 for the broken bytes.
|
|
19
|
+
*/
|
|
20
|
+
export async function getGaddag(locale: Locale): Promise<Gaddag | undefined> {
|
|
21
|
+
const response = await getDictionary(locale);
|
|
22
|
+
|
|
23
|
+
if (typeof response === 'undefined') {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const version = readVersion(response);
|
|
28
|
+
const deserialized = deserializedDictionaries[locale];
|
|
29
|
+
|
|
30
|
+
if (deserialized && deserialized.version === version) {
|
|
31
|
+
return deserialized.gaddag;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const gaddag = Gaddag.deserialize(new Uint8Array(await response.arrayBuffer()));
|
|
36
|
+
|
|
37
|
+
if (typeof version !== 'undefined') {
|
|
38
|
+
deserializedDictionaries[locale] = { gaddag, version };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return gaddag;
|
|
42
|
+
} catch {
|
|
43
|
+
delete deserializedDictionaries[locale];
|
|
44
|
+
await deleteDictionary(locale);
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Read from the cached response rather than tracked in memory, so a dictionary
|
|
51
|
+
* another tab downloaded into the shared cache is picked up here too.
|
|
52
|
+
*/
|
|
53
|
+
function readVersion(response: Response): string | undefined {
|
|
54
|
+
const etag = response.headers.get('etag');
|
|
55
|
+
|
|
56
|
+
if (etag !== null) {
|
|
57
|
+
return etag;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const parts = ['date', 'content-length'].map((name) => response.headers.get(name) ?? '');
|
|
61
|
+
return parts.every((part) => part === '') ? undefined : parts.join('|');
|
|
62
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { prefetchDictionary, solveLocally, verifyLocally } from './client';
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/// <reference lib="WebWorker" />
|
|
2
|
+
|
|
3
|
+
import { getConfig } from '@scrabble-solver/configs';
|
|
4
|
+
import { BLANK } from '@scrabble-solver/constants';
|
|
5
|
+
import { solve } from '@scrabble-solver/solver';
|
|
6
|
+
import { Board, type Locale, Tile } from '@scrabble-solver/types';
|
|
7
|
+
|
|
8
|
+
import { type SolveRequestPayload, type VerifyRequestPayload } from '@/types';
|
|
9
|
+
|
|
10
|
+
import { revalidateDictionary } from './dictionaries';
|
|
11
|
+
import { getGaddag } from './getGaddag';
|
|
12
|
+
import { type SolverWorkerRequest, type SolverWorkerResponse } from './messages';
|
|
13
|
+
|
|
14
|
+
declare const self: DedicatedWorkerGlobalScope;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Solves run synchronously on the single worker thread, so a burst of requests
|
|
18
|
+
* would queue stale solves ahead of the latest one.
|
|
19
|
+
*/
|
|
20
|
+
let latestSolveId = 0;
|
|
21
|
+
|
|
22
|
+
self.addEventListener('message', ({ data }: MessageEvent<SolverWorkerRequest>) => {
|
|
23
|
+
if (data.type === 'solve') {
|
|
24
|
+
latestSolveId = data.id;
|
|
25
|
+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
26
|
+
respond(data.id, () => handleSolve(data.id, data.payload));
|
|
27
|
+
} else if (data.type === 'verify') {
|
|
28
|
+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
29
|
+
respond(data.id, () => handleVerify(data.id, data.payload));
|
|
30
|
+
} else if (data.type === 'prefetch') {
|
|
31
|
+
prefetchDictionary(data.locale).catch(() => undefined);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
async function handleSolve(id: number, payload: SolveRequestPayload): Promise<SolverWorkerResponse> {
|
|
36
|
+
const { board, characters, game, locale } = payload;
|
|
37
|
+
const gaddag = await getGaddag(locale);
|
|
38
|
+
revalidateInBackground(locale);
|
|
39
|
+
|
|
40
|
+
if (!gaddag) {
|
|
41
|
+
return { id, outcome: 'unavailable' };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
await yieldToQueuedMessages();
|
|
45
|
+
|
|
46
|
+
if (latestSolveId !== id) {
|
|
47
|
+
return { id, outcome: 'superseded' };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const config = getConfig(game, locale);
|
|
51
|
+
const tiles = characters.map((character) => new Tile({ character, isBlank: character === BLANK }));
|
|
52
|
+
return { data: solve(gaddag, config, Board.fromJson(board), tiles), id, outcome: 'answered' };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function handleVerify(
|
|
56
|
+
id: number,
|
|
57
|
+
{ board: boardJson, locale }: VerifyRequestPayload,
|
|
58
|
+
): Promise<SolverWorkerResponse> {
|
|
59
|
+
const gaddag = await getGaddag(locale);
|
|
60
|
+
revalidateInBackground(locale);
|
|
61
|
+
|
|
62
|
+
if (!gaddag) {
|
|
63
|
+
return { id, outcome: 'unavailable' };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const board = Board.fromJson(boardJson);
|
|
67
|
+
const words = board.getWords().sort((a, b) => a.localeCompare(b, locale));
|
|
68
|
+
const invalidWords: string[] = [];
|
|
69
|
+
const validWords: string[] = [];
|
|
70
|
+
|
|
71
|
+
for (const word of words) {
|
|
72
|
+
if (gaddag.has(word)) {
|
|
73
|
+
validWords.push(word);
|
|
74
|
+
} else {
|
|
75
|
+
invalidWords.push(word);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return { data: { invalidWords, validWords }, id, outcome: 'answered' };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Deserializes the dictionary ahead of the first solve, so it does not pay the
|
|
84
|
+
* cold-start cost. Warms even when revalidation fails (e.g. offline) - a
|
|
85
|
+
* previously cached dictionary can still be deserialized.
|
|
86
|
+
*/
|
|
87
|
+
async function prefetchDictionary(locale: Locale): Promise<void> {
|
|
88
|
+
try {
|
|
89
|
+
await revalidateDictionary(locale);
|
|
90
|
+
} catch {
|
|
91
|
+
// Solving falls back to the server until a later revalidation succeeds.
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
await getGaddag(locale);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Revalidation failures are expected (offline, server down) and must not
|
|
99
|
+
* surface as unhandled rejections - solving keeps using the cached dictionary
|
|
100
|
+
* until a later revalidation succeeds.
|
|
101
|
+
*/
|
|
102
|
+
function revalidateInBackground(locale: Locale): void {
|
|
103
|
+
revalidateDictionary(locale).catch(() => undefined);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function respond(id: number, handle: () => Promise<SolverWorkerResponse>): Promise<void> {
|
|
107
|
+
try {
|
|
108
|
+
self.postMessage(await handle());
|
|
109
|
+
} catch {
|
|
110
|
+
self.postMessage({ id, outcome: 'unavailable' } satisfies SolverWorkerResponse);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Messages queued behind this handler dispatch only between macrotasks - a
|
|
116
|
+
* MessageChannel round-trip lets them register before an expensive solve.
|
|
117
|
+
*/
|
|
118
|
+
function yieldToQueuedMessages(): Promise<void> {
|
|
119
|
+
return new Promise((resolve) => {
|
|
120
|
+
const { port1, port2 } = new MessageChannel();
|
|
121
|
+
port1.onmessage = () => resolve();
|
|
122
|
+
port2.postMessage(null);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type Locale, type ResultJson } from '@scrabble-solver/types';
|
|
2
|
+
|
|
3
|
+
import { type SolveRequestPayload, type VerifyRequestPayload } from '@/types';
|
|
4
|
+
|
|
5
|
+
export interface VerifyResult {
|
|
6
|
+
invalidWords: string[];
|
|
7
|
+
validWords: string[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type SolverWorkerRequest =
|
|
11
|
+
| { id: number; type: 'prefetch'; locale: Locale }
|
|
12
|
+
| { id: number; type: 'solve'; payload: SolveRequestPayload }
|
|
13
|
+
| { id: number; type: 'verify'; payload: VerifyRequestPayload };
|
|
14
|
+
|
|
15
|
+
export type SolverWorkerResponse =
|
|
16
|
+
| { id: number; outcome: 'answered'; data: ResultJson[] | VerifyResult }
|
|
17
|
+
/** No cached dictionary, or an internal error - the caller falls back to the server. */
|
|
18
|
+
| { id: number; outcome: 'unavailable' }
|
|
19
|
+
/** A newer solve replaced this one - the newer answer is the one worth waiting for. */
|
|
20
|
+
| { id: number; outcome: 'superseded' };
|
package/src/state/actions.ts
CHANGED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { createSlice } from '@reduxjs/toolkit';
|
|
2
|
+
|
|
3
|
+
import { initialize } from '../actions';
|
|
4
|
+
|
|
5
|
+
import { appInitialState } from './initialState';
|
|
6
|
+
|
|
7
|
+
export const appSlice = createSlice({
|
|
8
|
+
initialState: appInitialState,
|
|
9
|
+
name: 'app',
|
|
10
|
+
reducers: {
|
|
11
|
+
hydrated: (state) => {
|
|
12
|
+
state.isHydrated = true;
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
extraReducers: (builder) => {
|
|
16
|
+
builder.addCase(initialize, (state, action) => {
|
|
17
|
+
state.version = action.payload.version;
|
|
18
|
+
});
|
|
19
|
+
},
|
|
20
|
+
});
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { getConfig } from '@scrabble-solver/configs';
|
|
2
2
|
import { Board } from '@scrabble-solver/types';
|
|
3
3
|
|
|
4
|
-
import { localStorage } from '../localStorage';
|
|
5
4
|
import { settingsInitialState } from '../settings';
|
|
6
5
|
|
|
7
6
|
import type { BoardState } from './types';
|
|
@@ -9,7 +8,7 @@ import type { BoardState } from './types';
|
|
|
9
8
|
const { game, locale } = settingsInitialState;
|
|
10
9
|
const { boardHeight, boardWidth } = getConfig(game, locale);
|
|
11
10
|
|
|
12
|
-
export const boardInitialState: BoardState =
|
|
11
|
+
export const boardInitialState: BoardState = Board.create(boardWidth, boardHeight);
|
|
13
12
|
|
|
14
13
|
// const createOxyphenbutazone = () => {
|
|
15
14
|
// // Tiles: oypbaze
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { createSlice, type PayloadAction } from '@reduxjs/toolkit';
|
|
2
|
+
import { type Locale } from '@scrabble-solver/types';
|
|
3
|
+
|
|
4
|
+
import type { Translations } from '@/types';
|
|
5
|
+
|
|
6
|
+
import { i18nInitialState } from './initialState';
|
|
7
|
+
|
|
8
|
+
export const i18nSlice = createSlice({
|
|
9
|
+
initialState: i18nInitialState,
|
|
10
|
+
name: 'i18n',
|
|
11
|
+
reducers: {
|
|
12
|
+
loaded: (state, action: PayloadAction<{ locale: Locale; translations: Translations }>) => {
|
|
13
|
+
state.translations[action.payload.locale] = action.payload.translations;
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
});
|
package/src/state/index.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
export * from './actions';
|
|
2
|
+
export * from './app';
|
|
2
3
|
export * from './board';
|
|
3
4
|
export * from './cellFilters';
|
|
4
5
|
export * from './dictionary';
|
|
5
6
|
export * from './hoveredTile';
|
|
7
|
+
export * from './i18n';
|
|
6
8
|
export * from './localStorage';
|
|
7
9
|
export * from './rack';
|
|
8
10
|
export * from './results';
|
|
@@ -12,4 +14,5 @@ export * from './solve';
|
|
|
12
14
|
export * from './store';
|
|
13
15
|
export * from './useTranslate';
|
|
14
16
|
export * from './useTypedSelector';
|
|
17
|
+
export * from './useTypedStore';
|
|
15
18
|
export * from './verify';
|
package/src/state/lib.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { BLANK } from '@scrabble-solver/constants';
|
|
2
2
|
import { type Board, type Config } from '@scrabble-solver/types';
|
|
3
3
|
|
|
4
|
-
import { createKeyComparator } from '@/lib';
|
|
4
|
+
import { createKeyComparator } from '@/lib/createKeyComparator';
|
|
5
5
|
import { type RemainingTile } from '@/types';
|
|
6
6
|
|
|
7
7
|
export const getRemainingTiles = (
|
|
@@ -1,13 +1,20 @@
|
|
|
1
|
-
import { Board, type BoardJson } from '@scrabble-solver/types';
|
|
1
|
+
import { Board, type BoardJson, isObject, type Locale } from '@scrabble-solver/types';
|
|
2
2
|
import store2 from 'store2';
|
|
3
3
|
|
|
4
|
-
import type { Rack } from '@/types';
|
|
4
|
+
import type { Rack, Translations } from '@/types';
|
|
5
5
|
|
|
6
6
|
import type { SettingsState } from './settings/types';
|
|
7
7
|
|
|
8
8
|
const BOARD = 'board';
|
|
9
9
|
const RACK = 'rack';
|
|
10
10
|
const SETTINGS = 'settings';
|
|
11
|
+
const TRANSLATIONS = 'translations';
|
|
12
|
+
|
|
13
|
+
interface PersistedTranslations {
|
|
14
|
+
locale: Locale;
|
|
15
|
+
translations: Translations;
|
|
16
|
+
version: string;
|
|
17
|
+
}
|
|
11
18
|
|
|
12
19
|
const LEGACY_KEYS: Record<keyof SettingsState, string> = {
|
|
13
20
|
autoGroupTiles: 'auto-group-tiles',
|
|
@@ -21,33 +28,15 @@ const LEGACY_KEYS: Record<keyof SettingsState, string> = {
|
|
|
21
28
|
|
|
22
29
|
const store = store2.namespace('scrabble-solver');
|
|
23
30
|
|
|
24
|
-
/**
|
|
25
|
-
* Introduced in 2.15.26 on 2026/04/27.
|
|
26
|
-
* Life expectancy: 1y.
|
|
27
|
-
*/
|
|
28
|
-
const migrateLegacySettings = (): Partial<SettingsState> => {
|
|
29
|
-
const settings: Partial<SettingsState> = {};
|
|
30
|
-
let hasLegacy = false;
|
|
31
|
-
|
|
32
|
-
for (const [setting, legacyKey] of Object.entries(LEGACY_KEYS) as [keyof SettingsState, string][]) {
|
|
33
|
-
if (store.has(legacyKey)) {
|
|
34
|
-
settings[setting] = store.get(legacyKey);
|
|
35
|
-
store.remove(legacyKey);
|
|
36
|
-
hasLegacy = true;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
if (hasLegacy) {
|
|
41
|
-
store.set(SETTINGS, settings, true);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return settings;
|
|
45
|
-
};
|
|
46
|
-
|
|
47
31
|
export const localStorage = {
|
|
48
32
|
getBoard(): Board | undefined {
|
|
49
|
-
|
|
50
|
-
|
|
33
|
+
try {
|
|
34
|
+
const serialized = store.get(BOARD) as string | undefined;
|
|
35
|
+
return serialized ? Board.fromJson(JSON.parse(serialized) as BoardJson) : undefined;
|
|
36
|
+
} catch {
|
|
37
|
+
store.remove(BOARD);
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
51
40
|
},
|
|
52
41
|
|
|
53
42
|
setBoard(board: Board | undefined): void {
|
|
@@ -56,7 +45,14 @@ export const localStorage = {
|
|
|
56
45
|
},
|
|
57
46
|
|
|
58
47
|
getRack(): Rack | undefined {
|
|
59
|
-
|
|
48
|
+
const rack = store.get(RACK) as Rack | undefined;
|
|
49
|
+
|
|
50
|
+
if (rack !== undefined && !Array.isArray(rack)) {
|
|
51
|
+
store.remove(RACK);
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return rack;
|
|
60
56
|
},
|
|
61
57
|
|
|
62
58
|
setRack(rack: Rack | undefined): void {
|
|
@@ -65,10 +61,71 @@ export const localStorage = {
|
|
|
65
61
|
|
|
66
62
|
getSettings(): Partial<SettingsState> {
|
|
67
63
|
const stored = store.get(SETTINGS) as Partial<SettingsState> | undefined;
|
|
68
|
-
|
|
64
|
+
|
|
65
|
+
if (stored !== undefined && !isObject(stored)) {
|
|
66
|
+
store.remove(SETTINGS);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return migrateHiddenShowCoordinates(isObject(stored) ? stored : migrateLegacySettings());
|
|
69
70
|
},
|
|
70
71
|
|
|
71
72
|
setSettings(settings: SettingsState): void {
|
|
72
73
|
store.set(SETTINGS, settings, true);
|
|
73
74
|
},
|
|
75
|
+
|
|
76
|
+
getTranslations(locale: Locale, version: string): Translations | undefined {
|
|
77
|
+
const stored = store.get(TRANSLATIONS) as PersistedTranslations | undefined;
|
|
78
|
+
|
|
79
|
+
if (typeof stored === 'undefined') {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!isObject(stored) || stored.locale !== locale || stored.version !== version || !isObject(stored.translations)) {
|
|
84
|
+
store.remove(TRANSLATIONS);
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return stored.translations;
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
setTranslations(locale: Locale, version: string, translations: Translations): void {
|
|
92
|
+
store.set(TRANSLATIONS, { locale, translations, version } satisfies PersistedTranslations, true);
|
|
93
|
+
},
|
|
74
94
|
};
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Introduced in 2.15.26 on 2026/04/27.
|
|
98
|
+
* Life expectancy: 1y.
|
|
99
|
+
*/
|
|
100
|
+
function migrateLegacySettings(): Partial<SettingsState> {
|
|
101
|
+
const settings: Partial<SettingsState> = {};
|
|
102
|
+
let hasLegacy = false;
|
|
103
|
+
|
|
104
|
+
for (const [setting, legacyKey] of Object.entries(LEGACY_KEYS) as [keyof SettingsState, string][]) {
|
|
105
|
+
if (store.has(legacyKey)) {
|
|
106
|
+
settings[setting] = store.get(legacyKey);
|
|
107
|
+
store.remove(legacyKey);
|
|
108
|
+
hasLegacy = true;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (hasLegacy) {
|
|
113
|
+
store.set(SETTINGS, settings, true);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return settings;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The 'hidden' showCoordinates option was removed on 2026/08/13.
|
|
121
|
+
* Life expectancy: 1y.
|
|
122
|
+
*/
|
|
123
|
+
function migrateHiddenShowCoordinates(settings: Partial<SettingsState>): Partial<SettingsState> {
|
|
124
|
+
if (String(settings.showCoordinates) !== 'hidden') {
|
|
125
|
+
return settings;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const migrated: Partial<SettingsState> = { ...settings, showCoordinates: 'original' };
|
|
129
|
+
store.set(SETTINGS, migrated, true);
|
|
130
|
+
return migrated;
|
|
131
|
+
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { getConfig } from '@scrabble-solver/configs';
|
|
2
2
|
|
|
3
|
-
import { localStorage } from '../localStorage';
|
|
4
3
|
import { settingsInitialState } from '../settings';
|
|
5
4
|
|
|
6
5
|
import type { RackState } from './types';
|
|
@@ -9,4 +8,4 @@ const defaultConfig = getConfig(settingsInitialState.game, settingsInitialState.
|
|
|
9
8
|
|
|
10
9
|
export const rackDefaultState: RackState = Array(defaultConfig.rackSize).fill(null);
|
|
11
10
|
|
|
12
|
-
export const rackInitialState: RackState =
|
|
11
|
+
export const rackInitialState: RackState = rackDefaultState;
|
package/src/state/rack/slice.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { createSlice, type PayloadAction } from '@reduxjs/toolkit';
|
|
2
2
|
import { type Tile } from '@scrabble-solver/types';
|
|
3
3
|
|
|
4
|
-
import { arrayEquals
|
|
4
|
+
import { arrayEquals } from '@/lib/arrayEquals';
|
|
5
|
+
import { createNullMovingComparator } from '@/lib/createNullMovingComparator';
|
|
6
|
+
import { inverseDirection } from '@/lib/inverseDirection';
|
|
7
|
+
import { zipCharactersAndTiles } from '@/lib/zipCharactersAndTiles';
|
|
5
8
|
import { type AutoGroupTiles, type Rack } from '@/types';
|
|
6
9
|
|
|
7
10
|
import { rackDefaultState, rackInitialState } from './initialState';
|
package/src/state/results/lib.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { type Result, type ShowCoordinates } from '@scrabble-solver/types';
|
|
2
2
|
|
|
3
|
-
import { createKeyComparator
|
|
3
|
+
import { createKeyComparator } from '@/lib/createKeyComparator';
|
|
4
|
+
import { createRegExp } from '@/lib/createRegExp';
|
|
5
|
+
import { createStringComparator } from '@/lib/createStringComparator';
|
|
6
|
+
import { getCoordinate } from '@/lib/getCoordinate';
|
|
7
|
+
import { reverseComparator } from '@/lib/reverseComparator';
|
|
4
8
|
import {
|
|
5
9
|
type CellFilter,
|
|
6
10
|
type Comparator,
|
|
@@ -11,10 +15,6 @@ import {
|
|
|
11
15
|
} from '@/types';
|
|
12
16
|
|
|
13
17
|
export const getCoordinates = (result: Result, showCoordinates: ShowCoordinates): string => {
|
|
14
|
-
if (showCoordinates === 'hidden') {
|
|
15
|
-
return '';
|
|
16
|
-
}
|
|
17
|
-
|
|
18
18
|
const firstCell = result.cells[0];
|
|
19
19
|
const x = getCoordinate(firstCell.x, showCoordinates === 'original' ? 'letter' : 'number');
|
|
20
20
|
const y = getCoordinate(firstCell.y, showCoordinates === 'original' ? 'number' : 'letter');
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createSelector } from '@reduxjs/toolkit';
|
|
2
2
|
|
|
3
|
-
import { createRegExp } from '@/lib';
|
|
3
|
+
import { createRegExp } from '@/lib/createRegExp';
|
|
4
4
|
|
|
5
5
|
import { selectCellFilters } from '../cellFilters';
|
|
6
6
|
import { selectLocale, selectShowCoordinates } from '../settings';
|