@scrabble-solver/scrabble-solver 2.8.11 → 2.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/InjectManifest.js.nft.json +1 -0
  3. package/.next/build-manifest.json +8 -8
  4. package/.next/cache/.tsbuildinfo +1 -1
  5. package/.next/cache/eslint/.cache_8dgz12 +1 -1
  6. package/.next/cache/next-server.js.nft.json +1 -1
  7. package/.next/cache/webpack/client-production/0.pack +0 -0
  8. package/.next/cache/webpack/client-production/index.pack +0 -0
  9. package/.next/cache/webpack/edge-server-production/0.pack +0 -0
  10. package/.next/cache/webpack/edge-server-production/index.pack +0 -0
  11. package/.next/cache/webpack/server-production/0.pack +0 -0
  12. package/.next/cache/webpack/server-production/index.pack +0 -0
  13. package/.next/next-server.js.nft.json +1 -1
  14. package/.next/prerender-manifest.json +1 -1
  15. package/.next/routes-manifest.json +1 -1
  16. package/.next/server/InjectManifest.js.nft.json +1 -0
  17. package/.next/server/chunks/413.js +37 -14
  18. package/.next/server/chunks/44.js +1 -1
  19. package/.next/server/chunks/452.js +894 -0
  20. package/.next/server/chunks/515.js +9 -4
  21. package/.next/server/chunks/865.js +230 -116
  22. package/.next/server/chunks/911.js +0 -887
  23. package/.next/server/middleware-build-manifest.js +1 -1
  24. package/.next/server/pages/404.html +3 -3
  25. package/.next/server/pages/404.js.nft.json +1 -1
  26. package/.next/server/pages/500.html +2 -2
  27. package/.next/server/pages/_app.js.nft.json +1 -1
  28. package/.next/server/pages/_error.js.nft.json +1 -1
  29. package/.next/server/pages/api/dictionary/[locale]/[word].js +1 -1
  30. package/.next/server/pages/api/dictionary/[locale]/[word].js.nft.json +1 -1
  31. package/.next/server/pages/api/dictionary/[locale].js +200 -0
  32. package/.next/server/pages/api/dictionary/[locale].js.nft.json +1 -0
  33. package/.next/server/pages/api/solve.js +7 -1
  34. package/.next/server/pages/api/solve.js.nft.json +1 -1
  35. package/.next/server/pages/api/verify.js +1 -1
  36. package/.next/server/pages/api/verify.js.nft.json +1 -1
  37. package/.next/server/pages/index.html +7 -7
  38. package/.next/server/pages/index.js +79 -30
  39. package/.next/server/pages/index.js.nft.json +1 -1
  40. package/.next/server/pages/index.json +1 -1
  41. package/.next/server/pages-manifest.json +1 -0
  42. package/.next/static/{VJkrGviICslA_8zNVJ-g- → Oki2Ia4sgLw021iM7byAe}/_buildManifest.js +1 -1
  43. package/.next/static/{VJkrGviICslA_8zNVJ-g- → Oki2Ia4sgLw021iM7byAe}/_ssgManifest.js +0 -0
  44. package/.next/static/chunks/317-e7d5d859f1f95938.js +1 -0
  45. package/.next/static/chunks/pages/_app-10893b318367f97c.js +1 -0
  46. package/.next/static/chunks/pages/index-b1ffeaddd9fb64b5.js +1 -0
  47. package/.next/static/css/e3d218f4ea72c5fb.css +1 -0
  48. package/.next/trace +52 -42
  49. package/next.config.js +11 -0
  50. package/package.json +17 -11
  51. package/src/components/Board/BoardPure.tsx +2 -7
  52. package/src/components/Board/components/Cell/Cell.module.scss +1 -1
  53. package/src/components/Board/components/Cell/Cell.tsx +4 -1
  54. package/src/components/Board/components/Cell/CellPure.tsx +12 -1
  55. package/src/components/Splash/Splash.module.scss +1 -1
  56. package/src/components/SvgFontCss/SvgFontCss.tsx +2 -1
  57. package/src/pages/api/dictionary/[locale]/index.ts +53 -0
  58. package/src/pages/api/solve.ts +6 -0
  59. package/src/pages/index.tsx +14 -2
  60. package/src/sdk/fetch.ts +30 -0
  61. package/src/sdk/fetchJson.ts +10 -31
  62. package/src/sdk/getDictionary.ts +11 -0
  63. package/src/service-worker/dictionaries/constants.ts +5 -0
  64. package/src/service-worker/dictionaries/expirationManager.ts +9 -0
  65. package/src/service-worker/dictionaries/getDictionary.ts +22 -0
  66. package/src/service-worker/dictionaries/getDictionaryUrl.ts +7 -0
  67. package/src/service-worker/dictionaries/index.ts +2 -0
  68. package/src/service-worker/dictionaries/revalidateDictionary.ts +35 -0
  69. package/src/service-worker/getTrie.ts +26 -0
  70. package/src/service-worker/index.ts +22 -0
  71. package/src/service-worker/routeSolveRequests.ts +37 -0
  72. package/src/service-worker/routeVerifyRequests.ts +35 -0
  73. package/src/serviceWorkerManager.ts +26 -0
  74. package/src/state/sagas.ts +1 -0
  75. package/src/state/slices/settingsInitialState.ts +20 -1
  76. package/tsconfig.json +1 -0
  77. package/.next/static/chunks/317-8e8909dd2f587b64.js +0 -1
  78. package/.next/static/chunks/pages/_app-57c77cad0f197d93.js +0 -1
  79. package/.next/static/chunks/pages/index-d3360e075ca3c222.js +0 -1
  80. package/.next/static/css/9ac903004135f4b1.css +0 -1
package/next.config.js CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
+ const WorkboxPlugin = require('workbox-webpack-plugin');
5
6
 
6
7
  const tsConfig = fs.readFileSync(path.resolve(__dirname, 'tsconfig.json'), 'utf-8');
7
8
  const tsConfigJson = JSON.parse(tsConfig);
@@ -39,5 +40,15 @@ module.exports = {
39
40
  },
40
41
  ],
41
42
  },
43
+ plugins: [
44
+ ...config.plugins,
45
+ process.env.NODE_ENV === 'production'
46
+ ? new WorkboxPlugin.InjectManifest({
47
+ swSrc: path.join(__dirname, 'src/service-worker/index.ts'),
48
+ swDest: path.join(__dirname, 'public/service-worker.js'),
49
+ exclude: [/\.map$/, /\.next/, /_next/, /manifest/, /\.htaccess$/, /.*\/static\/.*/, /service-worker\.js$/],
50
+ })
51
+ : undefined,
52
+ ].filter(Boolean),
42
53
  }),
43
54
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scrabble-solver/scrabble-solver",
3
- "version": "2.8.11",
3
+ "version": "2.9.1",
4
4
  "description": "Scrabble Solver 2 - App",
5
5
  "engines": {
6
6
  "node": ">=16"
@@ -28,15 +28,16 @@
28
28
  "start": "env-cmd next start -p 3333"
29
29
  },
30
30
  "dependencies": {
31
+ "@kamilmielnik/trie": "^2.0.1",
31
32
  "@popperjs/core": "^2.11.6",
32
33
  "@reduxjs/toolkit": "^1.8.6",
33
- "@scrabble-solver/configs": "^2.8.11",
34
- "@scrabble-solver/constants": "^2.8.11",
35
- "@scrabble-solver/dictionaries": "^2.8.11",
36
- "@scrabble-solver/logger": "^2.8.11",
37
- "@scrabble-solver/solver": "^2.8.11",
38
- "@scrabble-solver/types": "^2.8.11",
39
- "@scrabble-solver/word-definitions": "^2.8.11",
34
+ "@scrabble-solver/configs": "^2.9.1",
35
+ "@scrabble-solver/constants": "^2.9.1",
36
+ "@scrabble-solver/dictionaries": "^2.9.1",
37
+ "@scrabble-solver/logger": "^2.9.1",
38
+ "@scrabble-solver/solver": "^2.9.1",
39
+ "@scrabble-solver/types": "^2.9.1",
40
+ "@scrabble-solver/word-definitions": "^2.9.1",
40
41
  "classnames": "^2.3.2",
41
42
  "next": "^12.3.1",
42
43
  "normalize.css": "^8.0.1",
@@ -51,7 +52,11 @@
51
52
  "redux": "^4.2.0",
52
53
  "redux-saga": "^1.2.1",
53
54
  "store2": "^2.14.2",
54
- "uuid": "^9.0.0"
55
+ "uuid": "^9.0.0",
56
+ "workbox-expiration": "^6.5.4",
57
+ "workbox-precaching": "^6.5.4",
58
+ "workbox-routing": "^6.5.4",
59
+ "workbox-window": "^6.5.4"
55
60
  },
56
61
  "devDependencies": {
57
62
  "@svgr/webpack": "^6.5.0",
@@ -66,7 +71,8 @@
66
71
  "@types/redux-saga": "^0.10.5",
67
72
  "@types/uuid": "^8.3.4",
68
73
  "env-cmd": "^10.1.0",
69
- "sass": "^1.55.0"
74
+ "sass": "^1.55.0",
75
+ "workbox-webpack-plugin": "^6.5.4"
70
76
  },
71
- "gitHead": "ab4bacd7711b47e2392da417225559effe574252"
77
+ "gitHead": "4849b4d123131fe043174f000c523868f3bd68c5"
72
78
  }
@@ -44,13 +44,7 @@ const BoardPure: FunctionComponent<Props> = ({
44
44
  onKeyDown,
45
45
  onPaste,
46
46
  }) => (
47
- <div
48
- className={classNames(styles.board, className)}
49
- ref={innerRef}
50
- onChange={onChange}
51
- onKeyDown={onKeyDown}
52
- onPaste={onPaste}
53
- >
47
+ <div className={classNames(styles.board, className)} ref={innerRef} onKeyDown={onKeyDown} onPaste={onPaste}>
54
48
  {rows.map((cells, y) => (
55
49
  <div className={styles.row} key={y}>
56
50
  {cells.map((cell, x) => (
@@ -62,6 +56,7 @@ const BoardPure: FunctionComponent<Props> = ({
62
56
  isCenter={center.x === x && center.y === y}
63
57
  key={x}
64
58
  size={cellSize}
59
+ onChange={onChange}
65
60
  onDirectionToggle={onDirectionToggle}
66
61
  onFocus={onFocus}
67
62
  />
@@ -213,7 +213,7 @@ $icon-size: 16px;
213
213
 
214
214
  .flag,
215
215
  .star {
216
- $size: 24px;
216
+ $size: 50%;
217
217
 
218
218
  width: $size;
219
219
  height: $size;
@@ -1,6 +1,6 @@
1
1
  import { EMPTY_CELL } from '@scrabble-solver/constants';
2
2
  import { Cell as CellModel } from '@scrabble-solver/types';
3
- import { FunctionComponent, RefObject, useCallback, useMemo } from 'react';
3
+ import { ChangeEventHandler, FunctionComponent, RefObject, useCallback, useMemo } from 'react';
4
4
  import { useDispatch } from 'react-redux';
5
5
 
6
6
  import { getTileSizes } from 'lib';
@@ -23,6 +23,7 @@ interface Props {
23
23
  inputRef: RefObject<HTMLInputElement>;
24
24
  isCenter: boolean;
25
25
  size: number;
26
+ onChange: ChangeEventHandler<HTMLInputElement>;
26
27
  onDirectionToggle: () => void;
27
28
  onFocus: (x: number, y: number) => void;
28
29
  }
@@ -34,6 +35,7 @@ const Cell: FunctionComponent<Props> = ({
34
35
  inputRef,
35
36
  isCenter,
36
37
  size,
38
+ onChange,
37
39
  onDirectionToggle,
38
40
  onFocus,
39
41
  }) => {
@@ -88,6 +90,7 @@ const Cell: FunctionComponent<Props> = ({
88
90
  style={style}
89
91
  tile={tile}
90
92
  translate={translate}
93
+ onChange={onChange}
91
94
  onDirectionToggleClick={handleDirectionToggleClick}
92
95
  onFocus={handleFocus}
93
96
  onToggleBlankClick={handleToggleBlankClick}
@@ -1,6 +1,14 @@
1
1
  import { Bonus, Cell, Tile as TileModel } from '@scrabble-solver/types';
2
2
  import classNames from 'classnames';
3
- import { CSSProperties, FocusEventHandler, FunctionComponent, memo, MouseEventHandler, RefObject } from 'react';
3
+ import {
4
+ ChangeEventHandler,
5
+ CSSProperties,
6
+ FocusEventHandler,
7
+ FunctionComponent,
8
+ memo,
9
+ MouseEventHandler,
10
+ RefObject,
11
+ } from 'react';
4
12
 
5
13
  import { ArrowDown, Flag, Star } from 'icons';
6
14
  import { Translate } from 'types';
@@ -25,6 +33,7 @@ interface Props {
25
33
  style?: CSSProperties;
26
34
  tile: TileModel;
27
35
  translate: Translate;
36
+ onChange: ChangeEventHandler<HTMLInputElement>;
28
37
  onDirectionToggleClick: MouseEventHandler<HTMLButtonElement>;
29
38
  onFocus: FocusEventHandler<HTMLInputElement>;
30
39
  onToggleBlankClick: MouseEventHandler<HTMLButtonElement>;
@@ -45,6 +54,7 @@ const CellPure: FunctionComponent<Props> = ({
45
54
  style,
46
55
  tile,
47
56
  translate,
57
+ onChange,
48
58
  onDirectionToggleClick,
49
59
  onFocus,
50
60
  onToggleBlankClick,
@@ -78,6 +88,7 @@ const CellPure: FunctionComponent<Props> = ({
78
88
  raised={!isEmpty}
79
89
  size={size}
80
90
  tabIndex={cell.x === 0 && cell.y === 0 ? undefined : -1}
91
+ onChange={onChange}
81
92
  onFocus={onFocus}
82
93
  />
83
94
 
@@ -1,6 +1,6 @@
1
1
  @import 'styles/animations';
2
2
 
3
- $loading-duration: 450ms;
3
+ $loading-duration: 250ms;
4
4
  $loaded-duration: 100ms;
5
5
  $hiding-duration: 200ms;
6
6
 
@@ -7,6 +7,7 @@ text {
7
7
  font-family: 'Open Sans';
8
8
  }`;
9
9
 
10
- const SvgFontCss: FunctionComponent = () => <style type="text/css">{CSS}</style>;
10
+ // eslint-disable-next-line react/no-danger
11
+ const SvgFontCss: FunctionComponent = () => <style type="text/css" dangerouslySetInnerHTML={{ __html: CSS }} />;
11
12
 
12
13
  export default SvgFontCss;
@@ -0,0 +1,53 @@
1
+ import { dictionaries } from '@scrabble-solver/dictionaries';
2
+ import logger from '@scrabble-solver/logger';
3
+ import { isLocale, Locale } from '@scrabble-solver/types';
4
+ import { NextApiRequest, NextApiResponse } from 'next';
5
+
6
+ import { getServerLoggingData } from 'api';
7
+
8
+ interface RequestData {
9
+ locale: Locale;
10
+ }
11
+
12
+ const dictionary = async (request: NextApiRequest, response: NextApiResponse): Promise<void> => {
13
+ const meta = getServerLoggingData(request);
14
+
15
+ try {
16
+ const { locale } = parseRequest(request);
17
+
18
+ logger.info('dictionary - request', {
19
+ meta,
20
+ payload: {
21
+ locale,
22
+ },
23
+ });
24
+
25
+ const trie = await dictionaries.get(locale);
26
+ response.status(200).send(trie.serialize());
27
+ } catch (error) {
28
+ const message = error instanceof Error ? error.message : 'Unknown error';
29
+ logger.error('dictionary - error', { error, meta });
30
+ response.status(500).send({ error: 'Server error', message });
31
+ throw error;
32
+ }
33
+ };
34
+
35
+ const parseRequest = (request: NextApiRequest): RequestData => {
36
+ const { locale } = request.query;
37
+
38
+ if (!isLocale(locale)) {
39
+ throw new Error('Invalid "locale" parameter');
40
+ }
41
+
42
+ return {
43
+ locale,
44
+ };
45
+ };
46
+
47
+ export const config = {
48
+ api: {
49
+ responseLimit: '25mb',
50
+ },
51
+ };
52
+
53
+ export default dictionary;
@@ -89,4 +89,10 @@ const parseRequest = (request: NextApiRequest): RequestData => {
89
89
  };
90
90
  };
91
91
 
92
+ export const config = {
93
+ api: {
94
+ responseLimit: '25mb',
95
+ },
96
+ };
97
+
92
98
  export default solve;
@@ -1,7 +1,7 @@
1
1
  import classNames from 'classnames';
2
2
  import fs from 'fs';
3
3
  import path from 'path';
4
- import { AnimationEvent, FormEvent, FunctionComponent, useState } from 'react';
4
+ import { AnimationEvent, FormEvent, FunctionComponent, useEffect, useState } from 'react';
5
5
  import Modal from 'react-modal';
6
6
  import { useDispatch } from 'react-redux';
7
7
  import { useEffectOnce, useMeasure } from 'react-use';
@@ -24,6 +24,7 @@ import {
24
24
  import { useIsTablet, useLocalStorage } from 'hooks';
25
25
  import { getCellSize } from 'lib';
26
26
  import { COMPONENTS_SPACING, COMPONENTS_SPACING_MOBILE, DICTIONARY_HEIGHT } from 'parameters';
27
+ import { registerServiceWorker } from 'serviceWorkerManager';
27
28
  import { initialize, localStorage, reset, selectConfig, solveSlice, useTypedSelector } from 'state';
28
29
 
29
30
  import styles from './index.module.scss';
@@ -47,7 +48,8 @@ const Index: FunctionComponent<Props> = ({ version }) => {
47
48
  useMeasure<HTMLDivElement>();
48
49
  const config = useTypedSelector(selectConfig);
49
50
  const cellSize = getCellSize(config, contentWidth - resultsContainerWidth, contentHeight);
50
- const isInitialized = contentWidth > 0 && boardHeight > 0 && resultsContainerWidth > 0;
51
+ const isInitializedInitial = contentWidth > 0 && boardHeight > 0 && resultsContainerWidth > 0;
52
+ const [isInitialized, setIsInitialized] = useState(isInitializedInitial);
51
53
  const resultsHeight = boardHeight - DICTIONARY_HEIGHT - (isTablet ? COMPONENTS_SPACING_MOBILE : COMPONENTS_SPACING);
52
54
 
53
55
  const handleClear = () => {
@@ -70,8 +72,18 @@ const Index: FunctionComponent<Props> = ({ version }) => {
70
72
 
71
73
  useEffectOnce(() => {
72
74
  dispatch(initialize());
75
+
76
+ setTimeout(() => {
77
+ setIsInitialized(true);
78
+ }, 100);
73
79
  });
74
80
 
81
+ useEffect(() => {
82
+ if (process.env.NODE_ENV === 'production') {
83
+ registerServiceWorker();
84
+ }
85
+ }, []);
86
+
75
87
  return (
76
88
  <>
77
89
  <div className={classNames(styles.index, { [styles.initialized]: isInitialized })}>
@@ -0,0 +1,30 @@
1
+ import { isError } from '@scrabble-solver/types';
2
+
3
+ const fetch = async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
4
+ let response: Response;
5
+
6
+ try {
7
+ response = await window.fetch(input, init);
8
+ } catch (error) {
9
+ const message = isError(error) ? error.message : 'Unknown error';
10
+ throw new Error(`Network error: ${message}`);
11
+ }
12
+
13
+ if (response.ok) {
14
+ return response;
15
+ }
16
+
17
+ try {
18
+ const json = await response.json();
19
+
20
+ if (isError(json)) {
21
+ throw new Error(json.message);
22
+ }
23
+ } finally {
24
+ // do nothing
25
+ }
26
+
27
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
28
+ };
29
+
30
+ export default fetch;
@@ -1,36 +1,15 @@
1
- import { isError } from '@scrabble-solver/types';
1
+ import fetch from './fetch';
2
2
 
3
3
  const fetchJson = async <T>(input: RequestInfo | URL, init?: RequestInit): Promise<T> => {
4
- let response: Response;
5
-
6
- try {
7
- response = await fetch(input, {
8
- ...init,
9
- headers: {
10
- 'Content-Type': 'application/json',
11
- ...init?.headers,
12
- },
13
- });
14
- } catch (error) {
15
- const message = isError(error) ? error.message : 'Unknown error';
16
- throw new Error(`Network error: ${message}`);
17
- }
18
-
19
- if (response.ok) {
20
- return response.json();
21
- }
22
-
23
- try {
24
- const json = await response.json();
25
-
26
- if (isError(json)) {
27
- throw new Error(json.message);
28
- }
29
- } finally {
30
- // do nothing
31
- }
32
-
33
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
4
+ const response = await fetch(input, {
5
+ ...init,
6
+ headers: {
7
+ 'Content-Type': 'application/json',
8
+ ...init?.headers,
9
+ },
10
+ });
11
+
12
+ return response.json();
34
13
  };
35
14
 
36
15
  export default fetchJson;
@@ -0,0 +1,11 @@
1
+ import { Trie } from '@kamilmielnik/trie';
2
+ import { Locale } from '@scrabble-solver/types';
3
+
4
+ import fetchJson from './fetchJson';
5
+
6
+ const getDictionary = async (locale: Locale): Promise<Trie> => {
7
+ const serialized = await fetchJson<string>(`/api/dictionary/${locale}`);
8
+ return Trie.deserialize(serialized);
9
+ };
10
+
11
+ export default getDictionary;
@@ -0,0 +1,5 @@
1
+ export const DICTIONARY_CACHE = 'dictionary-api-cache';
2
+
3
+ export const DAY = 24 * 60 * 60 * 1000;
4
+
5
+ export const DICTIONARY_CACHE_MAX_AGE = 1 * DAY; // eslint-disable-line no-implicit-coercion
@@ -0,0 +1,9 @@
1
+ import { CacheExpiration } from 'workbox-expiration';
2
+
3
+ import { DICTIONARY_CACHE, DICTIONARY_CACHE_MAX_AGE } from './constants';
4
+
5
+ const expirationManager = new CacheExpiration(DICTIONARY_CACHE, {
6
+ maxAgeSeconds: DICTIONARY_CACHE_MAX_AGE / 1000,
7
+ });
8
+
9
+ export default expirationManager;
@@ -0,0 +1,22 @@
1
+ import { Locale } from '@scrabble-solver/types';
2
+
3
+ import { DICTIONARY_CACHE } from './constants';
4
+ import expirationManager from './expirationManager';
5
+ import getDictionaryUrl from './getDictionaryUrl';
6
+
7
+ const getDictionary = async (locale: Locale): Promise<string | undefined> => {
8
+ await expirationManager.expireEntries();
9
+
10
+ const url = getDictionaryUrl(locale);
11
+ const cache = await caches.open(DICTIONARY_CACHE);
12
+ const cached = await cache.match(url);
13
+
14
+ if (typeof cached === 'undefined') {
15
+ return undefined;
16
+ }
17
+
18
+ const serialized = await cached.clone().text();
19
+ return serialized;
20
+ };
21
+
22
+ export default getDictionary;
@@ -0,0 +1,7 @@
1
+ import { Locale } from '@scrabble-solver/types';
2
+
3
+ const getDictionaryUrl = (locale: Locale): string => {
4
+ return `/api/dictionary/${locale}`;
5
+ };
6
+
7
+ export default getDictionaryUrl;
@@ -0,0 +1,2 @@
1
+ export { default as getDictionary } from './getDictionary';
2
+ export { default as revalidateDictionary } from './revalidateDictionary';
@@ -0,0 +1,35 @@
1
+ import { Locale } from '@scrabble-solver/types';
2
+
3
+ import { DICTIONARY_CACHE } from './constants';
4
+ import expirationManager from './expirationManager';
5
+ import getDictionaryUrl from './getDictionaryUrl';
6
+
7
+ const requests: Partial<Record<Locale, Promise<Response> | undefined>> = {};
8
+
9
+ const revalidateDictionary = async (locale: Locale): Promise<void> => {
10
+ if (requests[locale] instanceof Promise) {
11
+ return;
12
+ }
13
+
14
+ let response: Response | undefined;
15
+ const url = getDictionaryUrl(locale);
16
+ const request = fetch(url);
17
+ requests[locale] = request;
18
+
19
+ try {
20
+ response = await request;
21
+
22
+ if (!response.ok) {
23
+ requests[locale] = undefined;
24
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
25
+ }
26
+
27
+ const cache = await caches.open(DICTIONARY_CACHE);
28
+ await cache.put(url, response.clone());
29
+ await expirationManager.updateTimestamp(url);
30
+ } finally {
31
+ requests[locale] = undefined;
32
+ }
33
+ };
34
+
35
+ export default revalidateDictionary;
@@ -0,0 +1,26 @@
1
+ import { Trie } from '@kamilmielnik/trie';
2
+ import { Locale } from '@scrabble-solver/types';
3
+
4
+ import { getDictionary } from './dictionaries';
5
+
6
+ const cache: Partial<Record<Locale, { trie: Trie; dictionary: string } | undefined>> = {};
7
+
8
+ const getTrie = async (locale: Locale): Promise<Trie | undefined> => {
9
+ const dictionary = await getDictionary(locale);
10
+
11
+ if (typeof dictionary === 'undefined') {
12
+ return undefined;
13
+ }
14
+
15
+ const cached = cache[locale];
16
+
17
+ if (typeof cached === 'undefined' || cached.dictionary !== dictionary) {
18
+ const trie = Trie.deserialize(dictionary);
19
+ cache[locale] = { dictionary, trie };
20
+ return trie;
21
+ }
22
+
23
+ return cached.trie;
24
+ };
25
+
26
+ export default getTrie;
@@ -0,0 +1,22 @@
1
+ /// <reference lib="WebWorker" />
2
+
3
+ import { cleanupOutdatedCaches, precacheAndRoute } from 'workbox-precaching';
4
+
5
+ import routeSolveRequests from './routeSolveRequests';
6
+ import routeVerifyRequests from './routeVerifyRequests';
7
+
8
+ declare const self: ServiceWorkerGlobalScope;
9
+
10
+ self.addEventListener('install', () => {
11
+ self.skipWaiting();
12
+ });
13
+
14
+ self.addEventListener('activate', () => {
15
+ self.clients.claim();
16
+ });
17
+
18
+ cleanupOutdatedCaches();
19
+ // eslint-disable-next-line no-underscore-dangle
20
+ precacheAndRoute(self.__WB_MANIFEST);
21
+ routeSolveRequests();
22
+ routeVerifyRequests();
@@ -0,0 +1,37 @@
1
+ import { getConfig } from '@scrabble-solver/configs';
2
+ import { BLANK } from '@scrabble-solver/constants';
3
+ import { solve } from '@scrabble-solver/solver';
4
+ import { Board, Locale, Tile } from '@scrabble-solver/types';
5
+ import { registerRoute } from 'workbox-routing';
6
+
7
+ import { revalidateDictionary } from './dictionaries';
8
+ import getTrie from './getTrie';
9
+
10
+ const headers = {
11
+ 'Content-Type': 'application/json; charset=utf-8',
12
+ };
13
+
14
+ const routeSolveRequests = () => {
15
+ registerRoute(
16
+ ({ url }) => url.origin === location.origin && url.pathname === '/api/solve',
17
+ async ({ request }) => {
18
+ const { board, characters, configId, locale } = await request.clone().json();
19
+ const trie = await getTrie(locale);
20
+
21
+ if (!trie) {
22
+ const response = await fetch(request);
23
+ revalidateDictionary(locale);
24
+ return response;
25
+ }
26
+
27
+ const config = getConfig(configId)[locale as Locale];
28
+ const tiles = characters.map((character: string) => new Tile({ character, isBlank: character === BLANK }));
29
+ const resultsJson = solve(trie, config, Board.fromJson(board), tiles);
30
+ const json = JSON.stringify(resultsJson);
31
+ return new Response(json, { headers });
32
+ },
33
+ 'POST',
34
+ );
35
+ };
36
+
37
+ export default routeSolveRequests;
@@ -0,0 +1,35 @@
1
+ import { Board } from '@scrabble-solver/types';
2
+ import { registerRoute } from 'workbox-routing';
3
+
4
+ import { revalidateDictionary } from './dictionaries';
5
+ import getTrie from './getTrie';
6
+
7
+ const headers = {
8
+ 'Content-Type': 'application/json; charset=utf-8',
9
+ };
10
+
11
+ const routeVerifyRequests = () => {
12
+ registerRoute(
13
+ ({ url }) => url.origin === location.origin && url.pathname === '/api/verify',
14
+ async ({ request }) => {
15
+ const { board: boardJson, locale } = await request.clone().json();
16
+ const trie = await getTrie(locale);
17
+
18
+ if (!trie) {
19
+ const response = await fetch(request);
20
+ revalidateDictionary(locale);
21
+ return response;
22
+ }
23
+
24
+ const board = Board.fromJson(boardJson);
25
+ const words = board.getWords().sort((a, b) => a.localeCompare(b));
26
+ const invalidWords = words.filter((word) => !trie.has(word));
27
+ const validWords = words.filter((word) => trie.has(word));
28
+ const json = JSON.stringify({ invalidWords, validWords });
29
+ return new Response(json, { headers });
30
+ },
31
+ 'POST',
32
+ );
33
+ };
34
+
35
+ export default routeVerifyRequests;
@@ -0,0 +1,26 @@
1
+ import { Workbox } from 'workbox-window';
2
+
3
+ let serviceWorker: Workbox | null = null;
4
+
5
+ export const registerServiceWorker = () => {
6
+ if (!globalThis.navigator || !('serviceWorker' in globalThis.navigator)) {
7
+ return;
8
+ }
9
+
10
+ serviceWorker = new Workbox('/service-worker.js');
11
+ serviceWorker.register({ immediate: true });
12
+ };
13
+
14
+ export const getServiceWorker = (): Workbox | null => {
15
+ if (process.env.NODE_ENV !== 'production') {
16
+ return null;
17
+ }
18
+
19
+ if (serviceWorker) {
20
+ return serviceWorker;
21
+ }
22
+
23
+ registerServiceWorker();
24
+
25
+ return serviceWorker;
26
+ };
@@ -99,6 +99,7 @@ function* onDictionarySubmit(): AnyGenerator {
99
99
  function* onInitialize(): AnyGenerator {
100
100
  yield call(visit);
101
101
  yield* ensureProperTilesCount();
102
+ yield put(verifySlice.actions.submit());
102
103
  }
103
104
 
104
105
  function* onReset(): AnyGenerator {