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.
- package/.github/workflows/bunx.yml +26 -2
- package/.github/workflows/npx.yml +26 -2
- package/.nx/workspace-data/d/server-process.json +3 -3
- package/.nx/workspace-data/file-map.json +441 -465
- package/.nx/workspace-data/lockfile-dependencies.hash +1 -1
- package/.nx/workspace-data/lockfile-nodes.hash +1 -1
- package/.nx/workspace-data/nx_files.nxt +0 -0
- package/.nx/workspace-data/parsed-lock-file.dependencies.json +2671 -2121
- package/.nx/workspace-data/parsed-lock-file.nodes.json +3846 -5277
- package/.nx/workspace-data/project-graph.json +7089 -7924
- package/.nx/workspace-data/source-maps.json +351 -255
- package/.oxlintrc.json +6 -0
- package/CLAUDE.md +25 -20
- package/README.md +9 -0
- package/bun.lock +1054 -2897
- package/bunfig.toml +6 -0
- package/cypress/e2e/app.spec.cy.ts +4 -4
- package/cypress/e2e/features/solver-paths.ts +66 -0
- package/cypress/e2e/features.spec.cy.ts +1 -0
- package/cypress/support/lib/actions.ts +28 -9
- package/cypress.config.ts +7 -0
- package/lerna.json +1 -1
- package/nx.json +3 -1
- package/package.json +18 -18
- package/packages/configs/package.json +4 -4
- package/packages/constants/package.json +2 -2
- package/packages/dictionaries/README.md +1 -1
- package/packages/dictionaries/build/Dictionaries.d.ts +2 -2
- package/packages/dictionaries/build/Dictionaries.js +6 -6
- package/packages/dictionaries/build/lib/DiskCache.d.ts +11 -4
- package/packages/dictionaries/build/lib/DiskCache.js +27 -10
- package/packages/dictionaries/build/lib/LayeredCache.d.ts +4 -4
- package/packages/dictionaries/build/lib/LayeredCache.js +3 -3
- package/packages/dictionaries/build/lib/MemoryCache.d.ts +4 -4
- package/packages/dictionaries/build/lib/MemoryCache.js +2 -2
- package/packages/dictionaries/build/lib/createCacheTimestampComparator.d.ts +2 -2
- package/packages/dictionaries/build/lib/downloadDictionary.d.ts +2 -2
- package/packages/dictionaries/build/lib/downloadDictionary.js +4 -3
- package/packages/dictionaries/build/lib/getDictionaryFilepath.d.ts +3 -1
- package/packages/dictionaries/build/lib/getDictionaryFilepath.js +8 -3
- package/packages/dictionaries/package.json +8 -7
- package/packages/dictionaries/src/Dictionaries.ts +12 -12
- package/packages/dictionaries/src/lib/DiskCache.test.ts +85 -0
- package/packages/dictionaries/src/lib/DiskCache.ts +34 -14
- package/packages/dictionaries/src/lib/LayeredCache.ts +7 -7
- package/packages/dictionaries/src/lib/MemoryCache.ts +6 -6
- package/packages/dictionaries/src/lib/createCacheTimestampComparator.ts +2 -2
- package/packages/dictionaries/src/lib/downloadDictionary.ts +5 -4
- package/packages/dictionaries/src/lib/getDictionaryFilepath.ts +7 -2
- package/packages/gaddag/build/Gaddag.d.ts +39 -0
- package/packages/gaddag/build/Gaddag.js +128 -0
- package/packages/gaddag/build/buildGaddag.d.ts +10 -0
- package/packages/gaddag/build/buildGaddag.js +360 -0
- package/packages/gaddag/build/constants.d.ts +12 -0
- package/packages/gaddag/build/constants.js +15 -0
- package/packages/gaddag/build/index.d.ts +3 -0
- package/packages/gaddag/build/index.js +13 -0
- package/packages/logger/package.json +2 -2
- package/packages/scrabble-solver/.next/BUILD_ID +1 -1
- package/packages/scrabble-solver/.next/build-manifest.json +17 -17
- package/packages/scrabble-solver/.next/cache/.previewinfo +1 -1
- package/packages/scrabble-solver/.next/cache/.rscinfo +1 -1
- package/packages/scrabble-solver/.next/cache/.tsbuildinfo +1 -1
- package/packages/scrabble-solver/.next/cache/webpack/client-production/0.pack +0 -0
- package/packages/scrabble-solver/.next/cache/webpack/client-production/index.pack +0 -0
- package/packages/scrabble-solver/.next/cache/webpack/client-production/index.pack.old +0 -0
- package/packages/scrabble-solver/.next/cache/webpack/edge-server-production/index.pack +0 -0
- package/packages/scrabble-solver/.next/cache/webpack/edge-server-production/index.pack.old +0 -0
- package/packages/scrabble-solver/.next/cache/webpack/server-production/0.pack +0 -0
- package/packages/scrabble-solver/.next/cache/webpack/server-production/index.pack +0 -0
- package/packages/scrabble-solver/.next/cache/webpack/server-production/index.pack.old +0 -0
- package/packages/scrabble-solver/.next/diagnostics/framework.json +1 -1
- package/packages/scrabble-solver/.next/next-minimal-server.js.nft.json +1 -1
- package/packages/scrabble-solver/.next/next-server.js.nft.json +1 -1
- package/packages/scrabble-solver/.next/prerender-manifest.json +7 -4
- package/packages/scrabble-solver/.next/required-server-files.js +29 -10
- package/packages/scrabble-solver/.next/required-server-files.json +29 -10
- package/packages/scrabble-solver/.next/routes-manifest.json +1 -1
- package/packages/scrabble-solver/.next/server/chunks/129.js +1 -0
- package/packages/scrabble-solver/.next/server/chunks/30.js +2 -2
- package/packages/scrabble-solver/.next/server/chunks/318.js +3 -3
- package/packages/scrabble-solver/.next/server/chunks/392.js +1 -1
- package/packages/scrabble-solver/.next/server/chunks/671.js +1 -1
- package/packages/scrabble-solver/.next/server/chunks/712.js +1 -1
- package/packages/scrabble-solver/.next/server/chunks/785.js +1 -0
- package/packages/scrabble-solver/.next/server/chunks/791.js +3 -3
- package/packages/scrabble-solver/.next/server/middleware-build-manifest.js +1 -1
- package/packages/scrabble-solver/.next/server/pages/404.html +1 -1
- package/packages/scrabble-solver/.next/server/pages/500.html +1 -1
- package/packages/scrabble-solver/.next/server/pages/_app.js +1 -1
- package/packages/scrabble-solver/.next/server/pages/_app.js.nft.json +1 -1
- package/packages/scrabble-solver/.next/server/pages/_document.js.nft.json +1 -1
- package/packages/scrabble-solver/.next/server/pages/_error.js +1 -1
- package/packages/scrabble-solver/.next/server/pages/_error.js.nft.json +1 -1
- package/packages/scrabble-solver/.next/server/pages/api/dictionary/[locale]/[word].js +1 -1
- package/packages/scrabble-solver/.next/server/pages/api/dictionary/[locale]/[word].js.nft.json +1 -1
- package/packages/scrabble-solver/.next/server/pages/api/dictionary/[locale].js +1 -1
- package/packages/scrabble-solver/.next/server/pages/api/dictionary/[locale].js.nft.json +1 -1
- package/packages/scrabble-solver/.next/server/pages/api/solve.js +1 -1
- package/packages/scrabble-solver/.next/server/pages/api/solve.js.nft.json +1 -1
- package/packages/scrabble-solver/.next/server/pages/api/verify.js +1 -1
- package/packages/scrabble-solver/.next/server/pages/api/verify.js.nft.json +1 -1
- package/packages/scrabble-solver/.next/server/pages/api/visit.js +1 -1
- package/packages/scrabble-solver/.next/server/pages/api/visit.js.nft.json +1 -1
- package/packages/scrabble-solver/.next/server/pages/index.html +1 -1
- package/packages/scrabble-solver/.next/server/pages/index.js +1 -1
- package/packages/scrabble-solver/.next/server/pages/index.js.nft.json +1 -1
- package/packages/scrabble-solver/.next/server/pages/index.json +1 -1
- package/packages/scrabble-solver/.next/server/pages/not-found.html +1 -1
- package/packages/scrabble-solver/.next/server/pages/not-found.js.nft.json +1 -1
- package/packages/scrabble-solver/.next/server/pages-manifest.json +1 -1
- package/packages/scrabble-solver/.next/server/webpack-runtime.js +1 -1
- package/packages/scrabble-solver/.next/static/{js6-w0PwySs2PyOSeCYfi → OLE8_ZInlGH7ENU0NIOpf}/_buildManifest.js +1 -1
- package/packages/scrabble-solver/.next/static/chunks/583.7a46807be14c377e.js +1 -0
- package/packages/scrabble-solver/.next/static/chunks/830.661816bf0de8c2f1.js +1 -0
- package/packages/scrabble-solver/.next/static/chunks/framework-3cf15b8041c59dd3.js +1 -0
- package/packages/scrabble-solver/.next/static/chunks/main-5b999f3f24ebfa84.js +5 -0
- package/packages/scrabble-solver/.next/static/chunks/pages/_app-7024e7a27ffbb2fa.js +7 -0
- package/packages/scrabble-solver/.next/static/chunks/pages/{index-bdf5e1e44cea16f6.js → index-8b398b0a5d870773.js} +1 -1
- package/packages/scrabble-solver/.next/static/chunks/webpack-3943d624bbb47a64.js +1 -0
- package/packages/scrabble-solver/.next/static/css/aedeaada218d7280.css +2 -0
- package/packages/scrabble-solver/.next/trace +21 -21
- package/packages/scrabble-solver/.next/trace-build +1 -1
- package/packages/scrabble-solver/.next/types/cache-life.d.ts +1 -0
- package/packages/scrabble-solver/.next/types/root-params.d.ts +3 -0
- package/packages/scrabble-solver/next-env.d.ts +1 -0
- package/packages/scrabble-solver/next.config.js +0 -1
- package/packages/scrabble-solver/package.json +19 -19
- package/packages/scrabble-solver/public/service-worker.js +2 -2
- package/packages/scrabble-solver/public/service-worker.js.map +1 -1
- package/packages/scrabble-solver/src/components/Board/components/Cell/Cell.tsx +4 -1
- package/packages/scrabble-solver/src/components/Board/hooks/useGrid.ts +17 -6
- package/packages/scrabble-solver/src/components/Dictionary/Dictionary.module.scss +7 -22
- package/packages/scrabble-solver/src/components/Dictionary/Dictionary.tsx +1 -8
- package/packages/scrabble-solver/src/components/Results/Result.tsx +0 -3
- package/packages/scrabble-solver/src/components/Results/Results.module.scss +0 -1
- package/packages/scrabble-solver/src/components/Results/Results.tsx +10 -2
- package/packages/scrabble-solver/src/components/Results/types.ts +3 -3
- package/packages/scrabble-solver/src/components/Solver/Solver.tsx +4 -3
- package/packages/scrabble-solver/src/components/Tooltip/useTooltip.ts +1 -1
- package/packages/scrabble-solver/src/pages/api/dictionary/[locale]/[word].ts +2 -2
- package/packages/scrabble-solver/src/pages/api/dictionary/[locale]/index.ts +80 -3
- package/packages/scrabble-solver/src/pages/api/solve.ts +2 -2
- package/packages/scrabble-solver/src/pages/api/verify.ts +12 -3
- package/packages/scrabble-solver/src/sdk/solve.ts +13 -7
- package/packages/scrabble-solver/src/sdk/verify.ts +12 -6
- package/packages/scrabble-solver/src/service-worker/appShell.test.ts +86 -0
- package/packages/scrabble-solver/src/service-worker/appShell.ts +39 -0
- package/packages/scrabble-solver/src/service-worker/index.ts +4 -5
- package/packages/scrabble-solver/src/serviceWorkerManager.ts +1 -18
- package/packages/scrabble-solver/src/solver-worker/client.test.ts +151 -0
- package/packages/scrabble-solver/src/solver-worker/client.ts +106 -0
- package/packages/scrabble-solver/src/solver-worker/dictionaries/constants.ts +9 -0
- package/packages/scrabble-solver/src/solver-worker/dictionaries/getDictionary.ts +21 -0
- package/packages/scrabble-solver/src/solver-worker/dictionaries/index.ts +2 -0
- package/packages/scrabble-solver/src/solver-worker/dictionaries/revalidateDictionary.test.ts +199 -0
- package/packages/scrabble-solver/src/solver-worker/dictionaries/revalidateDictionary.ts +86 -0
- package/packages/scrabble-solver/src/solver-worker/dictionaries/revalidatedAt.ts +43 -0
- package/packages/scrabble-solver/src/solver-worker/getGaddag.test.ts +109 -0
- package/packages/scrabble-solver/src/solver-worker/getGaddag.ts +62 -0
- package/packages/scrabble-solver/src/solver-worker/index.ts +1 -0
- package/packages/scrabble-solver/src/solver-worker/index.worker.ts +124 -0
- package/packages/scrabble-solver/src/solver-worker/messages.ts +20 -0
- package/packages/scrabble-solver/src/state/index.ts +1 -0
- package/packages/scrabble-solver/src/state/sagas.ts +19 -9
- package/packages/scrabble-solver/src/state/useTypedStore.ts +6 -0
- package/packages/scrabble-solver/src/types/api.ts +1 -2
- package/packages/scrabble-solver/tsconfig.json +4 -0
- package/packages/scrabble-solver/tsconfig.tsbuildinfo +1 -1
- package/packages/solver/README.md +24 -0
- package/packages/solver/benchmarks/index.ts +15 -0
- package/packages/solver/benchmarks/lib.ts +31 -0
- package/packages/solver/benchmarks/renderChart.ts +165 -0
- package/packages/solver/benchmarks/results/chart.svg +65 -0
- package/packages/solver/benchmarks/runBenchmarks.ts +66 -0
- package/packages/solver/benchmarks/scenarios.ts +64 -0
- package/packages/solver/benchmarks/types.ts +16 -0
- package/packages/solver/benchmarks/updateReadme.ts +53 -0
- package/packages/solver/build/MoveGenerator.d.ts +116 -0
- package/packages/solver/build/MoveGenerator.js +686 -0
- package/packages/solver/build/index.d.ts +0 -13
- package/packages/solver/build/index.js +1 -27
- package/packages/solver/build/solve.d.ts +2 -2
- package/packages/solver/build/solve.js +3 -19
- package/packages/solver/package.json +8 -7
- package/packages/solver/src/MoveGenerator.test.ts +1009 -0
- package/packages/solver/src/MoveGenerator.ts +926 -0
- package/packages/solver/src/ReferenceSolver.ts +401 -0
- package/packages/solver/src/index.ts +0 -13
- package/packages/solver/src/solve.test.ts +21 -21
- package/packages/solver/src/solve.ts +4 -22
- package/packages/solver/tsconfig.json +1 -1
- package/packages/types/build/Cell.d.ts +1 -0
- package/packages/types/build/Cell.js +3 -0
- package/packages/types/build/Result.d.ts +3 -2
- package/packages/types/build/Result.js +11 -6
- package/packages/types/build/ResultJson.d.ts +5 -4
- package/packages/types/build/index.d.ts +0 -6
- package/packages/types/build/index.js +1 -9
- package/packages/types/build/readResultCells.d.ts +5 -0
- package/packages/types/build/readResultCells.js +75 -0
- package/packages/types/package.json +5 -4
- package/packages/types/src/Cell.ts +4 -0
- package/packages/types/src/Result.ts +15 -6
- package/packages/types/src/ResultJson.ts +5 -5
- package/packages/types/src/index.ts +0 -6
- package/packages/types/src/readResultCells.test.ts +140 -0
- package/packages/types/src/readResultCells.ts +90 -0
- package/packages/word-definitions/package.json +3 -3
- package/packages/word-lists/package.json +4 -4
- package/packages/scrabble-solver/.next/static/chunks/framework-a23f7f643b59a2ca.js +0 -1
- package/packages/scrabble-solver/.next/static/chunks/main-92448bb08bb6bf1f.js +0 -5
- package/packages/scrabble-solver/.next/static/chunks/pages/_app-4ae86f006f6b9fe7.js +0 -7
- package/packages/scrabble-solver/.next/static/chunks/webpack-1a7a166b34ed8d24.js +0 -1
- package/packages/scrabble-solver/.next/static/css/0af35adb4bab56f0.css +0 -2
- package/packages/scrabble-solver/src/sdk/getDictionary.ts +0 -9
- package/packages/scrabble-solver/src/service-worker/average.ts +0 -7
- package/packages/scrabble-solver/src/service-worker/dictionaries/constants.ts +0 -5
- package/packages/scrabble-solver/src/service-worker/dictionaries/getDictionary.ts +0 -20
- package/packages/scrabble-solver/src/service-worker/dictionaries/index.ts +0 -2
- package/packages/scrabble-solver/src/service-worker/dictionaries/revalidateDictionary.ts +0 -33
- package/packages/scrabble-solver/src/service-worker/getTrie.ts +0 -24
- package/packages/scrabble-solver/src/service-worker/routeSolveRequests.ts +0 -91
- package/packages/scrabble-solver/src/service-worker/routeVerifyRequests.ts +0 -37
- package/packages/solver/build/areDigraphsValid.d.ts +0 -2
- package/packages/solver/build/areDigraphsValid.js +0 -17
- package/packages/solver/build/fillPattern.d.ts +0 -6
- package/packages/solver/build/fillPattern.js +0 -52
- package/packages/solver/build/generateEndIndices.d.ts +0 -2
- package/packages/solver/build/generateEndIndices.js +0 -17
- package/packages/solver/build/generateHorizontalPatterns.d.ts +0 -2
- package/packages/solver/build/generateHorizontalPatterns.js +0 -16
- package/packages/solver/build/generatePattern.d.ts +0 -7
- package/packages/solver/build/generatePattern.js +0 -20
- package/packages/solver/build/generatePatterns.d.ts +0 -2
- package/packages/solver/build/generatePatterns.js +0 -11
- package/packages/solver/build/generateStartIndices.d.ts +0 -2
- package/packages/solver/build/generateStartIndices.js +0 -16
- package/packages/solver/build/generateVectors.d.ts +0 -7
- package/packages/solver/build/generateVectors.js +0 -7
- package/packages/solver/build/generateVerticalPatterns.d.ts +0 -2
- package/packages/solver/build/generateVerticalPatterns.js +0 -16
- package/packages/solver/build/getCellsScore.d.ts +0 -2
- package/packages/solver/build/getCellsScore.js +0 -18
- package/packages/solver/build/getPatternHash.d.ts +0 -2
- package/packages/solver/build/getPatternHash.js +0 -14
- package/packages/solver/build/getPatternScore.d.ts +0 -2
- package/packages/solver/build/getPatternScore.js +0 -22
- package/packages/solver/build/getUniquePatterns.d.ts +0 -2
- package/packages/solver/build/getUniquePatterns.js +0 -17
- package/packages/solver/src/areDigraphsValid.ts +0 -18
- package/packages/solver/src/fillPattern.test.ts +0 -107
- package/packages/solver/src/fillPattern.ts +0 -68
- package/packages/solver/src/generateEndIndices.test.ts +0 -23
- package/packages/solver/src/generateEndIndices.ts +0 -19
- package/packages/solver/src/generateHorizontalPatterns.test.ts +0 -31
- package/packages/solver/src/generateHorizontalPatterns.ts +0 -15
- package/packages/solver/src/generatePattern.ts +0 -33
- package/packages/solver/src/generatePatterns.test.ts +0 -13
- package/packages/solver/src/generatePatterns.ts +0 -11
- package/packages/solver/src/generateStartIndices.test.ts +0 -19
- package/packages/solver/src/generateStartIndices.ts +0 -17
- package/packages/solver/src/generateVectors.test.ts +0 -12
- package/packages/solver/src/generateVectors.ts +0 -10
- package/packages/solver/src/generateVerticalPatterns.test.ts +0 -32
- package/packages/solver/src/generateVerticalPatterns.ts +0 -15
- package/packages/solver/src/getCellsScore.ts +0 -21
- package/packages/solver/src/getPatternHash.test.ts +0 -17
- package/packages/solver/src/getPatternHash.ts +0 -12
- package/packages/solver/src/getPatternScore.test.ts +0 -204
- package/packages/solver/src/getPatternScore.ts +0 -23
- package/packages/solver/src/getUniquePatterns.ts +0 -19
- package/packages/types/build/CollisionJson.d.ts +0 -2
- package/packages/types/build/CollisionJson.js +0 -2
- package/packages/types/build/FinalPattern.d.ts +0 -6
- package/packages/types/build/FinalPattern.js +0 -14
- package/packages/types/build/HorizontalPattern.d.ts +0 -5
- package/packages/types/build/HorizontalPattern.js +0 -34
- package/packages/types/build/Pattern.d.ts +0 -20
- package/packages/types/build/Pattern.js +0 -50
- package/packages/types/build/PatternJson.d.ts +0 -6
- package/packages/types/build/PatternJson.js +0 -2
- package/packages/types/build/VerticalPattern.d.ts +0 -5
- package/packages/types/build/VerticalPattern.js +0 -34
- package/packages/types/src/CollisionJson.ts +0 -3
- package/packages/types/src/FinalPattern.ts +0 -14
- package/packages/types/src/HorizontalPattern.ts +0 -37
- package/packages/types/src/Pattern.ts +0 -71
- package/packages/types/src/PatternJson.ts +0 -7
- package/packages/types/src/VerticalPattern.ts +0 -37
- /package/packages/scrabble-solver/.next/static/{js6-w0PwySs2PyOSeCYfi → OLE8_ZInlGH7ENU0NIOpf}/_ssgManifest.js +0 -0
- /package/packages/scrabble-solver/src/{service-worker → solver-worker}/dictionaries/expirationManager.ts +0 -0
- /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
|
-
|
|
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:
|
|
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,
|
|
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 =
|
|
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,
|
|
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 =
|
|
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
|
-
}, [
|
|
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
|
-
|
|
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] && (
|
|
@@ -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
|
-
() => ({
|
|
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?:
|
|
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 ===
|
|
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,
|
|
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
|
|
34
|
-
const results = await Promise.all(words.map((word) => getWordDefinition(locale, 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
|
|
26
|
-
|
|
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: '
|
|
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
|
|
48
|
+
const gaddag = await dictionaries.get(locale);
|
|
49
49
|
const tiles = characters.map((character) => new Tile({ character, isBlank: character === BLANK }));
|
|
50
|
-
const results = solveScrabble(
|
|
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
|
|
33
|
+
const gaddag = await dictionaries.get(locale);
|
|
34
34
|
const words = board.getWords().sort((a, b) => a.localeCompare(b, locale));
|
|
35
|
-
const invalidWords =
|
|
36
|
-
const validWords =
|
|
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:
|
|
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
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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 {
|
|
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
|
-
|
|
23
|
-
routeVerifyRequests();
|
|
22
|
+
routeNavigations();
|