scrabble-solver 2.18.3 → 2.18.5
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/.nx/workspace-data/d/server-process.json +3 -3
- package/.nx/workspace-data/file-map.json +1615 -1582
- 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 +1091 -1156
- package/.nx/workspace-data/parsed-lock-file.nodes.json +2038 -2029
- package/.nx/workspace-data/project-graph.json +3290 -3338
- package/.nx/workspace-data/source-maps.json +44 -8
- package/.oxlintrc.json +0 -1
- package/CLAUDE.md +10 -8
- package/bun.lock +707 -424
- package/lerna.json +1 -1
- package/package.json +11 -11
- package/packages/configs/package.json +3 -3
- package/packages/constants/package.json +1 -1
- package/packages/dictionaries/bin/update-dictionaries.js +2 -5
- package/packages/dictionaries/build/Dictionaries.d.ts +1 -1
- package/packages/dictionaries/build/Dictionaries.js +9 -4
- package/packages/dictionaries/build/lib/DiskCache.js +1 -1
- package/packages/dictionaries/build/lib/downloadDictionary.d.ts +1 -1
- package/packages/dictionaries/build/lib/downloadDictionary.js +12 -7
- package/packages/dictionaries/package.json +5 -5
- package/packages/dictionaries/src/Dictionaries.ts +13 -6
- package/packages/dictionaries/src/lib/DiskCache.ts +2 -2
- package/packages/dictionaries/src/lib/downloadDictionary.ts +14 -6
- package/packages/logger/README.md +18 -0
- package/packages/logger/build/constants.d.ts +3 -0
- package/packages/logger/build/constants.js +4 -1
- package/packages/logger/build/describeError.d.ts +5 -0
- package/packages/logger/build/describeError.js +23 -0
- package/packages/logger/build/events.d.ts +92 -0
- package/packages/logger/build/events.js +13 -0
- package/packages/logger/build/formatCsvRow.d.ts +2 -0
- package/packages/logger/build/formatCsvRow.js +20 -0
- package/packages/logger/build/index.d.ts +5 -1
- package/packages/logger/build/index.js +13 -3
- package/packages/logger/build/logError.d.ts +4 -0
- package/packages/logger/build/logError.js +17 -0
- package/packages/logger/build/logEvent.d.ts +3 -0
- package/packages/logger/build/logEvent.js +29 -0
- package/packages/logger/package.json +7 -8
- package/packages/logger/scripts/export-csv.ts +113 -0
- package/packages/logger/scripts/readEvents.ts +34 -0
- package/packages/logger/scripts/tsconfig.json +10 -0
- package/packages/logger/src/constants.ts +9 -0
- package/packages/logger/src/describeError.test.ts +44 -0
- package/packages/logger/src/describeError.ts +30 -0
- package/packages/logger/src/events.ts +103 -0
- package/packages/logger/src/formatCsvRow.test.ts +41 -0
- package/packages/logger/src/formatCsvRow.ts +24 -0
- package/packages/logger/src/index.ts +13 -1
- package/packages/logger/src/logError.ts +24 -0
- package/packages/logger/src/logEvent.test.ts +59 -0
- package/packages/logger/src/logEvent.ts +26 -0
- package/packages/scrabble-solver/.next/BUILD_ID +1 -1
- package/packages/scrabble-solver/.next/build-manifest.json +19 -19
- 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 +2 -2
- 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/dynamic-css-manifest.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 +4 -4
- package/packages/scrabble-solver/.next/react-loadable-manifest.json +6 -6
- package/packages/scrabble-solver/.next/routes-manifest.json +1 -1
- package/packages/scrabble-solver/.next/server/chunks/30.js +1 -1
- package/packages/scrabble-solver/.next/server/chunks/546.js +1 -1
- package/packages/scrabble-solver/.next/server/chunks/612.js +5 -0
- package/packages/scrabble-solver/.next/server/chunks/712.js +1 -1
- package/packages/scrabble-solver/.next/server/chunks/715.js +1 -0
- package/packages/scrabble-solver/.next/server/chunks/853.js +2 -2
- package/packages/scrabble-solver/.next/server/chunks/961.js +1 -1
- package/packages/scrabble-solver/.next/server/dynamic-css-manifest.js +1 -1
- package/packages/scrabble-solver/.next/server/middleware-build-manifest.js +1 -1
- package/packages/scrabble-solver/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/packages/scrabble-solver/.next/server/pages/404.html +2 -2
- package/packages/scrabble-solver/.next/server/pages/500.html +2 -2
- package/packages/scrabble-solver/.next/server/pages/_app.js.nft.json +1 -1
- package/packages/scrabble-solver/.next/server/pages/_document.js +2 -2
- 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 +2 -2
- 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 +2 -2
- 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/index.meta +1 -0
- package/packages/scrabble-solver/.next/server/pages/not-found.html +2 -2
- package/packages/scrabble-solver/.next/server/route-cache/PAGES/8a5edab282632443219e051e4ade2d1d5bbc671c781051bf1437897cbdfea0f1/$/index.html +29 -0
- package/packages/scrabble-solver/.next/server/route-cache/PAGES/8a5edab282632443219e051e4ade2d1d5bbc671c781051bf1437897cbdfea0f1/$/index.json +1 -0
- package/packages/scrabble-solver/.next/server/route-cache/PAGES/8a5edab282632443219e051e4ade2d1d5bbc671c781051bf1437897cbdfea0f1/$/index.meta +1 -0
- package/packages/scrabble-solver/.next/static/{8WnIUTe-yJvPD6stltvYI → YHx536pklAqlc9UGAXCj8}/_buildManifest.js +1 -1
- package/packages/scrabble-solver/.next/static/chunks/101-fee295c7587c0836.js +3 -0
- package/packages/scrabble-solver/.next/static/chunks/{256.6a20b23a766cfebd.js → 256.2a4fa7f278e0a97d.js} +1 -1
- package/packages/scrabble-solver/.next/static/chunks/693.d295da7c7b873986.js +1 -0
- package/packages/scrabble-solver/.next/static/chunks/827.09f660ca0a951d12.js +1 -0
- package/packages/scrabble-solver/.next/static/chunks/980.dd35e807b18063c1.js +1 -0
- package/packages/scrabble-solver/.next/static/chunks/framework-988fd6f19d2744e8.js +1 -0
- package/packages/scrabble-solver/.next/static/chunks/main-d397eb8af33c5163.js +5 -0
- package/packages/scrabble-solver/.next/static/chunks/pages/_app-0c68f165d6de8507.js +1 -0
- package/packages/scrabble-solver/.next/static/chunks/pages/index-ea21d96a88f25000.js +1 -0
- package/packages/scrabble-solver/.next/static/chunks/{webpack-d7138f1ef6b8c9d6.js → webpack-31879a8fb6dcffd0.js} +1 -1
- package/packages/scrabble-solver/.next/static/css/{92f85ce358a0a468.css → 4dbcacc1122ac6b9.css} +1 -1
- package/packages/scrabble-solver/.next/static/css/8b8087461767360a.css +1 -0
- package/packages/scrabble-solver/.next/static/css/{65bcc9440e067113.css → 98051ed25076b896.css} +1 -1
- package/packages/scrabble-solver/.next/trace +22 -22
- package/packages/scrabble-solver/.next/trace-build +1 -1
- package/packages/scrabble-solver/package.json +18 -18
- package/packages/scrabble-solver/public/service-worker.js +1 -1
- package/packages/scrabble-solver/src/api/BadRequestError.ts +6 -0
- package/packages/scrabble-solver/src/api/index.ts +2 -1
- package/packages/scrabble-solver/src/api/withApiLog.ts +85 -0
- package/packages/scrabble-solver/src/components/Button/Button.module.scss +1 -1
- package/packages/scrabble-solver/src/components/Solver/Solver.module.scss +4 -4
- package/packages/scrabble-solver/src/components/Solver/Solver.tsx +2 -2
- package/packages/scrabble-solver/src/hooks/useColumns.ts +5 -7
- package/packages/scrabble-solver/src/modals/ResultsModal/ResultsModal.tsx +2 -2
- package/packages/scrabble-solver/src/modals/WordsModal/WordsModal.module.scss +1 -1
- package/packages/scrabble-solver/src/modals/WordsModal/WordsModal.tsx +2 -2
- package/packages/scrabble-solver/src/modals/WordsModal/components/WordsTable/WordsTable.tsx +2 -2
- package/packages/scrabble-solver/src/pages/api/dictionary/[locale]/[word].ts +22 -30
- package/packages/scrabble-solver/src/pages/api/dictionary/[locale]/index.ts +36 -42
- package/packages/scrabble-solver/src/pages/api/solve.ts +32 -42
- package/packages/scrabble-solver/src/pages/api/verify.ts +35 -43
- package/packages/scrabble-solver/src/pages/api/visit.ts +38 -13
- package/packages/scrabble-solver/src/pages/index.tsx +2 -2
- package/packages/scrabble-solver/src/parameters/index.ts +0 -2
- package/packages/scrabble-solver/src/sdk/solve.ts +3 -9
- package/packages/scrabble-solver/src/sdk/verify.ts +2 -10
- package/packages/scrabble-solver/src/sdk/visit.ts +8 -3
- package/packages/scrabble-solver/src/state/sagas.ts +7 -1
- package/packages/scrabble-solver/src/styles/_tokens.scss +0 -2
- package/packages/scrabble-solver/src/styles/mixins.scss +0 -5
- package/packages/scrabble-solver/src/styles/variables.module.scss +0 -1
- package/packages/scrabble-solver/src/styles/variables.module.scss.d.ts +0 -1
- package/packages/scrabble-solver/src/styles/variables.scss +1 -1
- package/packages/scrabble-solver/src/types/api.ts +7 -0
- package/packages/scrabble-solver/tsconfig.tsbuildinfo +1 -1
- package/packages/solver/package.json +6 -6
- package/packages/types/package.json +2 -2
- package/packages/word-definitions/package.json +3 -3
- package/packages/word-lists/package.json +3 -3
- package/playwright-report/index.html +6 -6
- package/packages/gaddag/build/Gaddag.d.ts +0 -39
- package/packages/gaddag/build/Gaddag.js +0 -128
- package/packages/gaddag/build/buildGaddag.d.ts +0 -10
- package/packages/gaddag/build/buildGaddag.js +0 -360
- package/packages/gaddag/build/constants.d.ts +0 -12
- package/packages/gaddag/build/constants.js +0 -15
- package/packages/gaddag/build/index.d.ts +0 -3
- package/packages/gaddag/build/index.js +0 -13
- package/packages/logger/build/logger.d.ts +0 -1
- package/packages/logger/build/logger.js +0 -28
- package/packages/logger/scripts/stats.js +0 -73
- package/packages/logger/src/logger.ts +0 -30
- package/packages/scrabble-solver/.next/server/chunks/392.js +0 -1
- package/packages/scrabble-solver/.next/static/chunks/101-829ab8a8d310e7a0.js +0 -3
- package/packages/scrabble-solver/.next/static/chunks/693.677c88e0eb1340b5.js +0 -1
- package/packages/scrabble-solver/.next/static/chunks/827.c81b92b383a748c9.js +0 -1
- package/packages/scrabble-solver/.next/static/chunks/980.7ce17464fc56648a.js +0 -1
- package/packages/scrabble-solver/.next/static/chunks/framework-626cbd48ac5aca57.js +0 -1
- package/packages/scrabble-solver/.next/static/chunks/main-7e52da35a4beea0b.js +0 -5
- package/packages/scrabble-solver/.next/static/chunks/pages/_app-e3fe880e24c024ab.js +0 -1
- package/packages/scrabble-solver/.next/static/chunks/pages/index-b073a1686818dd26.js +0 -1
- package/packages/scrabble-solver/.next/static/css/a691348b788f8bc0.css +0 -1
- package/packages/scrabble-solver/src/api/getServerLoggingData.ts +0 -17
- package/packages/scrabble-solver/src/hooks/useIsCompactLayout.ts +0 -14
- /package/packages/scrabble-solver/.next/static/{8WnIUTe-yJvPD6stltvYI → YHx536pklAqlc9UGAXCj8}/_ssgManifest.js +0 -0
|
@@ -1,65 +1,61 @@
|
|
|
1
1
|
import { type Gaddag } from '@kamilmielnik/gaddag';
|
|
2
2
|
import { dictionaries } from '@scrabble-solver/dictionaries';
|
|
3
|
-
import {
|
|
3
|
+
import { logEvent } from '@scrabble-solver/logger';
|
|
4
4
|
import { isLocale, type Locale } from '@scrabble-solver/types';
|
|
5
5
|
import { type NextApiRequest, type NextApiResponse } from 'next';
|
|
6
6
|
import { promisify } from 'util';
|
|
7
7
|
import { gzip } from 'zlib';
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import { type ApiContext, BadRequestError, withApiLog } from '@/api';
|
|
10
10
|
|
|
11
11
|
interface RequestData {
|
|
12
12
|
locale: Locale;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
const meta = getServerLoggingData(request);
|
|
15
|
+
export default withApiLog('download', dictionary);
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
async function dictionary(request: NextApiRequest, response: NextApiResponse, { ip, getElapsedMs }: ApiContext) {
|
|
18
|
+
const { locale } = parseRequest(request);
|
|
19
|
+
const gaddag = await dictionaries.get(locale);
|
|
20
|
+
const encoding = acceptsGzip(request) ? 'gzip' : 'identity';
|
|
21
|
+
const body = encoding === 'gzip' ? await getCompressedDictionary(gaddag) : getSerializedDictionary(gaddag);
|
|
20
22
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
});
|
|
23
|
+
// no-cache makes clients store the payload but revalidate it, so unchanged
|
|
24
|
+
// re-downloads become 304s, answered by Next's built-in ETag handling.
|
|
25
|
+
response.setHeader('Cache-Control', 'no-cache');
|
|
26
|
+
response.setHeader('Content-Type', 'application/octet-stream');
|
|
27
|
+
response.setHeader('Vary', 'Accept-Encoding');
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
if (encoding === 'gzip') {
|
|
30
|
+
response.setHeader('Content-Encoding', 'gzip');
|
|
31
|
+
}
|
|
31
32
|
|
|
32
|
-
|
|
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');
|
|
33
|
+
response.status(200).send(body);
|
|
37
34
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
};
|
|
35
|
+
logEvent({
|
|
36
|
+
type: 'download',
|
|
37
|
+
ip,
|
|
38
|
+
ms: getElapsedMs(),
|
|
39
|
+
locale,
|
|
40
|
+
status: response.statusCode,
|
|
41
|
+
encoding,
|
|
42
|
+
bytes: response.statusCode === 304 ? 0 : body.byteLength,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
49
45
|
|
|
50
|
-
|
|
46
|
+
function parseRequest(request: NextApiRequest): RequestData {
|
|
51
47
|
const { locale } = request.query;
|
|
52
48
|
|
|
53
49
|
if (!isLocale(locale)) {
|
|
54
|
-
throw new
|
|
50
|
+
throw new BadRequestError('Invalid "locale" parameter');
|
|
55
51
|
}
|
|
56
52
|
|
|
57
53
|
return {
|
|
58
54
|
locale,
|
|
59
55
|
};
|
|
60
|
-
}
|
|
56
|
+
}
|
|
61
57
|
|
|
62
|
-
|
|
58
|
+
function acceptsGzip(request: NextApiRequest): boolean {
|
|
63
59
|
const acceptEncoding = request.headers['accept-encoding'];
|
|
64
60
|
const header = Array.isArray(acceptEncoding) ? acceptEncoding.join(',') : acceptEncoding;
|
|
65
61
|
|
|
@@ -86,14 +82,14 @@ const acceptsGzip = (request: NextApiRequest): boolean => {
|
|
|
86
82
|
}
|
|
87
83
|
|
|
88
84
|
return wildcardAccepts;
|
|
89
|
-
}
|
|
85
|
+
}
|
|
90
86
|
|
|
91
87
|
// Dictionaries rebuild at most daily, so each response variant is produced
|
|
92
88
|
// once and lives exactly as long as the in-memory dictionary it was made from.
|
|
93
89
|
const serializedCache = new WeakMap<Gaddag, Buffer>();
|
|
94
90
|
const compressedCache = new WeakMap<Gaddag, Promise<Buffer>>();
|
|
95
91
|
|
|
96
|
-
|
|
92
|
+
function getSerializedDictionary(gaddag: Gaddag): Buffer {
|
|
97
93
|
let serialized = serializedCache.get(gaddag);
|
|
98
94
|
|
|
99
95
|
if (!serialized) {
|
|
@@ -104,11 +100,11 @@ const getSerializedDictionary = (gaddag: Gaddag): Buffer => {
|
|
|
104
100
|
}
|
|
105
101
|
|
|
106
102
|
return serialized;
|
|
107
|
-
}
|
|
103
|
+
}
|
|
108
104
|
|
|
109
105
|
const gzipAsync = promisify(gzip);
|
|
110
106
|
|
|
111
|
-
|
|
107
|
+
function getCompressedDictionary(gaddag: Gaddag): Promise<Buffer> {
|
|
112
108
|
let compressed = compressedCache.get(gaddag);
|
|
113
109
|
|
|
114
110
|
if (!compressed) {
|
|
@@ -118,12 +114,10 @@ const getCompressedDictionary = (gaddag: Gaddag): Promise<Buffer> => {
|
|
|
118
114
|
}
|
|
119
115
|
|
|
120
116
|
return compressed;
|
|
121
|
-
}
|
|
117
|
+
}
|
|
122
118
|
|
|
123
119
|
export const config = {
|
|
124
120
|
api: {
|
|
125
121
|
responseLimit: '50mb',
|
|
126
122
|
},
|
|
127
123
|
};
|
|
128
|
-
|
|
129
|
-
export default dictionary;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { getConfig, hasConfig } from '@scrabble-solver/configs';
|
|
2
2
|
import { BLANK } from '@scrabble-solver/constants';
|
|
3
3
|
import { dictionaries } from '@scrabble-solver/dictionaries';
|
|
4
|
-
import {
|
|
4
|
+
import { logEvent } from '@scrabble-solver/logger';
|
|
5
5
|
import { solve as solveScrabble } from '@scrabble-solver/solver';
|
|
6
6
|
import {
|
|
7
7
|
Board,
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
} from '@scrabble-solver/types';
|
|
16
16
|
import { type NextApiRequest, type NextApiResponse } from 'next';
|
|
17
17
|
|
|
18
|
-
import {
|
|
18
|
+
import { type ApiContext, BadRequestError, isBoardValid, isCharacterValid, withApiLog } from '@/api';
|
|
19
19
|
import { isStringArray } from '@/lib/isStringArray';
|
|
20
20
|
|
|
21
21
|
interface RequestData {
|
|
@@ -26,71 +26,63 @@ interface RequestData {
|
|
|
26
26
|
locale: Locale;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const results = solveScrabble(gaddag, config, board, tiles);
|
|
51
|
-
response.status(200).send(results);
|
|
52
|
-
} catch (error) {
|
|
53
|
-
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
54
|
-
logger.error('solve - error', { error, meta });
|
|
55
|
-
response.status(500).send({ error: 'Server error', message });
|
|
56
|
-
}
|
|
57
|
-
};
|
|
29
|
+
export default withApiLog('solve', solve);
|
|
30
|
+
|
|
31
|
+
async function solve(request: NextApiRequest, response: NextApiResponse, { ip, getElapsedMs }: ApiContext) {
|
|
32
|
+
const { board, characters, config, game, locale } = parseRequest(request);
|
|
33
|
+
const gaddag = await dictionaries.get(locale);
|
|
34
|
+
const tiles = characters.map((character) => new Tile({ character, isBlank: character === BLANK }));
|
|
35
|
+
const results = solveScrabble(gaddag, config, board, tiles);
|
|
36
|
+
response.status(200).send(results);
|
|
37
|
+
|
|
38
|
+
logEvent({
|
|
39
|
+
type: 'solve',
|
|
40
|
+
ip,
|
|
41
|
+
ms: getElapsedMs(),
|
|
42
|
+
locale,
|
|
43
|
+
game,
|
|
44
|
+
tiles: board.getTilesCount(),
|
|
45
|
+
blanks: board.getBlanksCount(),
|
|
46
|
+
rack: characters.join(','),
|
|
47
|
+
results: results.length,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
58
50
|
|
|
59
|
-
|
|
51
|
+
function parseRequest(request: NextApiRequest): RequestData {
|
|
60
52
|
const { board: boardJson, characters, game, locale } = request.body;
|
|
61
53
|
|
|
62
54
|
if (!isLocale(locale)) {
|
|
63
|
-
throw new
|
|
55
|
+
throw new BadRequestError('Invalid "locale" parameter');
|
|
64
56
|
}
|
|
65
57
|
|
|
66
58
|
if (!isGame(game)) {
|
|
67
|
-
throw new
|
|
59
|
+
throw new BadRequestError('Invalid "game" parameter');
|
|
68
60
|
}
|
|
69
61
|
|
|
70
62
|
if (!isStringArray(characters) || characters.length === 0) {
|
|
71
|
-
throw new
|
|
63
|
+
throw new BadRequestError('Invalid "characters" parameter');
|
|
72
64
|
}
|
|
73
65
|
|
|
74
66
|
if (!hasConfig(game, locale)) {
|
|
75
|
-
throw new
|
|
67
|
+
throw new BadRequestError(`No game "${game}" in "${locale}"`);
|
|
76
68
|
}
|
|
77
69
|
|
|
78
70
|
const config = getConfig(game, locale);
|
|
79
71
|
|
|
80
72
|
for (const character of characters) {
|
|
81
73
|
if (!isCharacterValid(character)) {
|
|
82
|
-
throw new
|
|
74
|
+
throw new BadRequestError('Invalid "characters" parameter');
|
|
83
75
|
}
|
|
84
76
|
}
|
|
85
77
|
|
|
86
78
|
const blanksCount = characters.filter((character) => character === BLANK).length;
|
|
87
79
|
|
|
88
80
|
if (blanksCount > config.blanksCount) {
|
|
89
|
-
throw new
|
|
81
|
+
throw new BadRequestError('Too many blank tiles passed');
|
|
90
82
|
}
|
|
91
83
|
|
|
92
84
|
if (!isBoardJson(boardJson) || !isBoardValid(boardJson, config)) {
|
|
93
|
-
throw new
|
|
85
|
+
throw new BadRequestError('Invalid "board" parameter');
|
|
94
86
|
}
|
|
95
87
|
|
|
96
88
|
const board = Board.fromJson(boardJson);
|
|
@@ -102,12 +94,10 @@ const parseRequest = (request: NextApiRequest): RequestData => {
|
|
|
102
94
|
game,
|
|
103
95
|
locale,
|
|
104
96
|
};
|
|
105
|
-
}
|
|
97
|
+
}
|
|
106
98
|
|
|
107
99
|
export const config = {
|
|
108
100
|
api: {
|
|
109
101
|
responseLimit: '25mb',
|
|
110
102
|
},
|
|
111
103
|
};
|
|
112
|
-
|
|
113
|
-
export default solve;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { getConfig, hasConfig } from '@scrabble-solver/configs';
|
|
2
2
|
import { dictionaries } from '@scrabble-solver/dictionaries';
|
|
3
|
-
import {
|
|
3
|
+
import { logEvent } from '@scrabble-solver/logger';
|
|
4
4
|
import { Board, type Config, type Game, type Locale, isBoardJson, isGame, isLocale } from '@scrabble-solver/types';
|
|
5
5
|
import { type NextApiRequest, type NextApiResponse } from 'next';
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { type ApiContext, BadRequestError, isBoardValid, withApiLog } from '@/api';
|
|
8
8
|
import { type VerifiedWord } from '@/types';
|
|
9
9
|
|
|
10
10
|
interface RequestData {
|
|
@@ -14,63 +14,57 @@ interface RequestData {
|
|
|
14
14
|
locale: Locale;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
const meta = getServerLoggingData(request);
|
|
17
|
+
export default withApiLog('verify', verify);
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
async function verify(request: NextApiRequest, response: NextApiResponse, { ip, getElapsedMs }: ApiContext) {
|
|
20
|
+
const { board, game, locale } = parseRequest(request);
|
|
21
|
+
const gaddag = await dictionaries.get(locale);
|
|
22
|
+
const words = board.getWords().sort((a, b) => a.word.localeCompare(b.word, locale));
|
|
23
|
+
const invalidWords: VerifiedWord[] = [];
|
|
24
|
+
const validWords: VerifiedWord[] = [];
|
|
22
25
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
boardTilesCount: board.getTilesCount(),
|
|
29
|
-
game,
|
|
30
|
-
locale,
|
|
31
|
-
},
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
const gaddag = await dictionaries.get(locale);
|
|
35
|
-
const words = board.getWords().sort((a, b) => a.word.localeCompare(b.word, locale));
|
|
36
|
-
const invalidWords: VerifiedWord[] = [];
|
|
37
|
-
const validWords: VerifiedWord[] = [];
|
|
38
|
-
|
|
39
|
-
for (const word of words) {
|
|
40
|
-
if (gaddag.has(word.word)) {
|
|
41
|
-
validWords.push({ ...word, isValid: true });
|
|
42
|
-
} else {
|
|
43
|
-
invalidWords.push({ ...word, isValid: false });
|
|
44
|
-
}
|
|
26
|
+
for (const word of words) {
|
|
27
|
+
if (gaddag.has(word.word)) {
|
|
28
|
+
validWords.push({ ...word, isValid: true });
|
|
29
|
+
} else {
|
|
30
|
+
invalidWords.push({ ...word, isValid: false });
|
|
45
31
|
}
|
|
46
|
-
|
|
47
|
-
response.status(200).send({ invalidWords, validWords });
|
|
48
|
-
} catch (error) {
|
|
49
|
-
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
50
|
-
logger.error('verify - error', { error, meta });
|
|
51
|
-
response.status(500).send({ error: 'Server error', message });
|
|
52
32
|
}
|
|
53
|
-
};
|
|
54
33
|
|
|
55
|
-
|
|
34
|
+
response.status(200).send({ invalidWords, validWords });
|
|
35
|
+
|
|
36
|
+
logEvent({
|
|
37
|
+
type: 'verify',
|
|
38
|
+
ip,
|
|
39
|
+
ms: getElapsedMs(),
|
|
40
|
+
locale,
|
|
41
|
+
game,
|
|
42
|
+
tiles: board.getTilesCount(),
|
|
43
|
+
blanks: board.getBlanksCount(),
|
|
44
|
+
valid: validWords.length,
|
|
45
|
+
invalid: invalidWords.length,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function parseRequest(request: NextApiRequest): RequestData {
|
|
56
50
|
const { board: boardJson, game, locale } = request.body;
|
|
57
51
|
|
|
58
52
|
if (!isLocale(locale)) {
|
|
59
|
-
throw new
|
|
53
|
+
throw new BadRequestError('Invalid "locale" parameter');
|
|
60
54
|
}
|
|
61
55
|
|
|
62
56
|
if (!isGame(game)) {
|
|
63
|
-
throw new
|
|
57
|
+
throw new BadRequestError('Invalid "game" parameter');
|
|
64
58
|
}
|
|
65
59
|
|
|
66
60
|
if (!hasConfig(game, locale)) {
|
|
67
|
-
throw new
|
|
61
|
+
throw new BadRequestError(`No game "${game}" in "${locale}"`);
|
|
68
62
|
}
|
|
69
63
|
|
|
70
64
|
const config = getConfig(game, locale);
|
|
71
65
|
|
|
72
66
|
if (!isBoardJson(boardJson) || !isBoardValid(boardJson, config)) {
|
|
73
|
-
throw new
|
|
67
|
+
throw new BadRequestError('Invalid "board" parameter');
|
|
74
68
|
}
|
|
75
69
|
|
|
76
70
|
const board = Board.fromJson(boardJson);
|
|
@@ -81,6 +75,4 @@ const parseRequest = (request: NextApiRequest): RequestData => {
|
|
|
81
75
|
game,
|
|
82
76
|
locale,
|
|
83
77
|
};
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export default verify;
|
|
78
|
+
}
|
|
@@ -1,19 +1,44 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { logEvent } from '@scrabble-solver/logger';
|
|
2
|
+
import { isGame, isLocale, isObject } from '@scrabble-solver/types';
|
|
2
3
|
import { type NextApiRequest, type NextApiResponse } from 'next';
|
|
3
4
|
|
|
4
|
-
import {
|
|
5
|
+
import { type ApiContext, withApiLog } from '@/api';
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
interface RequestData {
|
|
8
|
+
game: string | undefined;
|
|
9
|
+
locale: string | undefined;
|
|
10
|
+
referrer: string | undefined;
|
|
11
|
+
url: string | undefined;
|
|
12
|
+
}
|
|
8
13
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
const URL_MAX_LENGTH = 256;
|
|
15
|
+
|
|
16
|
+
export default withApiLog('visit', visit);
|
|
17
|
+
|
|
18
|
+
function visit(request: NextApiRequest, response: NextApiResponse, { ip, ua }: ApiContext) {
|
|
19
|
+
const { game, locale, referrer, url } = parseRequest(request);
|
|
20
|
+
response.status(200).send(true);
|
|
21
|
+
logEvent({ type: 'visit', ip, ua, referrer, locale, game, url });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// A visit is only a ping - unexpected fields are ignored, never rejected.
|
|
25
|
+
function parseRequest(request: NextApiRequest): RequestData {
|
|
26
|
+
const body: unknown = request.body;
|
|
27
|
+
|
|
28
|
+
if (!isObject(body)) {
|
|
29
|
+
return { game: undefined, locale: undefined, referrer: undefined, url: undefined };
|
|
16
30
|
}
|
|
17
|
-
};
|
|
18
31
|
|
|
19
|
-
|
|
32
|
+
const { game, locale, referrer, url } = body;
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
game: isGame(game) ? game : undefined,
|
|
36
|
+
locale: isLocale(locale) ? locale : undefined,
|
|
37
|
+
referrer: parseUrl(referrer),
|
|
38
|
+
url: parseUrl(url),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function parseUrl(value: unknown): string | undefined {
|
|
43
|
+
return typeof value === 'string' && value.length > 0 ? value.slice(0, URL_MAX_LENGTH) : undefined;
|
|
44
|
+
}
|
|
@@ -14,9 +14,9 @@ import { NavButtons } from '@/components/NavButtons';
|
|
|
14
14
|
import { Solver } from '@/components/Solver';
|
|
15
15
|
import { useDirection } from '@/hooks/useDirection';
|
|
16
16
|
import { useEffectOnce } from '@/hooks/useEffectOnce';
|
|
17
|
-
import { useIsCompactLayout } from '@/hooks/useIsCompactLayout';
|
|
18
17
|
import { useLanguage } from '@/hooks/useLanguage';
|
|
19
18
|
import { useLocalStorage } from '@/hooks/useLocalStorage';
|
|
19
|
+
import { useMediaQuery } from '@/hooks/useMediaQuery';
|
|
20
20
|
import { LOCALE_FEATURES } from '@/i18n/constants';
|
|
21
21
|
import { schedulePreloadModals } from '@/modals/preload';
|
|
22
22
|
import { CONFIG_PENDING_CLASS, GITHUB_PROJECT_URL, NPM_PACKAGE_URL, SITE_DESCRIPTION, SITE_URL } from '@/parameters';
|
|
@@ -94,7 +94,7 @@ const Index: FunctionComponent<Props> = ({ version }) => {
|
|
|
94
94
|
const config = useTypedSelector(selectConfig);
|
|
95
95
|
const locale = useTypedSelector(selectLocale);
|
|
96
96
|
const isHydrated = useTypedSelector(selectIsHydrated);
|
|
97
|
-
const isCompactLayout =
|
|
97
|
+
const isCompactLayout = useMediaQuery('<l');
|
|
98
98
|
const previousIsCompactLayout = useRef(isCompactLayout);
|
|
99
99
|
const [modals, setModals] = useState<Record<Modal, boolean>>({
|
|
100
100
|
dictionary: false,
|
|
@@ -8,8 +8,6 @@ export const BREAKPOINTS = {
|
|
|
8
8
|
xl: Number(variables.breakpointXl),
|
|
9
9
|
};
|
|
10
10
|
|
|
11
|
-
export const BREAKPOINT_HEIGHT_L = Number(variables.breakpointHeightL);
|
|
12
|
-
|
|
13
11
|
export const GITHUB_PROJECT_URL = 'https://github.com/kamilmielnik/scrabble-solver';
|
|
14
12
|
|
|
15
13
|
export const NPM_PACKAGE_URL = 'https://www.npmjs.com/package/scrabble-solver';
|
|
@@ -1,17 +1,11 @@
|
|
|
1
|
-
import { Board,
|
|
1
|
+
import { Board, Result, type ResultJson } from '@scrabble-solver/types';
|
|
2
2
|
|
|
3
3
|
import { solveLocally } from '@/solver-worker';
|
|
4
|
+
import { type SolveRequestPayload } from '@/types';
|
|
4
5
|
|
|
5
6
|
import { fetchJson } from './fetchJson';
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
board: BoardJson;
|
|
9
|
-
characters: string[];
|
|
10
|
-
game: Game;
|
|
11
|
-
locale: Locale;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export const solve = async ({ board, characters, game, locale }: Payload): Promise<Result[]> => {
|
|
8
|
+
export const solve = async ({ board, characters, game, locale }: SolveRequestPayload): Promise<Result[]> => {
|
|
15
9
|
const payload = { board, characters, game, locale };
|
|
16
10
|
const json =
|
|
17
11
|
(await solveLocally(payload)) ??
|
|
@@ -1,22 +1,14 @@
|
|
|
1
|
-
import { type BoardJson, type Game, type Locale } from '@scrabble-solver/types';
|
|
2
|
-
|
|
3
1
|
import { verifyLocally } from '@/solver-worker';
|
|
4
|
-
import { type VerifiedWord } from '@/types';
|
|
2
|
+
import { type VerifiedWord, type VerifyRequestPayload } from '@/types';
|
|
5
3
|
|
|
6
4
|
import { fetchJson } from './fetchJson';
|
|
7
5
|
|
|
8
|
-
interface Payload {
|
|
9
|
-
board: BoardJson;
|
|
10
|
-
game: Game;
|
|
11
|
-
locale: Locale;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
6
|
interface Response {
|
|
15
7
|
invalidWords: VerifiedWord[];
|
|
16
8
|
validWords: VerifiedWord[];
|
|
17
9
|
}
|
|
18
10
|
|
|
19
|
-
export const verify = async ({ board, game, locale }:
|
|
11
|
+
export const verify = async ({ board, game, locale }: VerifyRequestPayload): Promise<Response> => {
|
|
20
12
|
const payload = { board, game, locale };
|
|
21
13
|
return (
|
|
22
14
|
(await verifyLocally(payload)) ??
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { type VisitRequestPayload } from '@/types';
|
|
2
|
+
|
|
3
|
+
import { fetchJson } from './fetchJson';
|
|
4
|
+
|
|
5
|
+
export function visit(payload: VisitRequestPayload): Promise<boolean> {
|
|
6
|
+
return fetchJson<boolean>('/api/visit', {
|
|
3
7
|
method: 'PUT',
|
|
8
|
+
body: JSON.stringify(payload),
|
|
4
9
|
});
|
|
5
|
-
}
|
|
10
|
+
}
|
|
@@ -204,7 +204,13 @@ function* hydratePersistedTranslations(locale: Locale, version: string): AnyGene
|
|
|
204
204
|
|
|
205
205
|
function* visitWhenIdle(): AnyGenerator {
|
|
206
206
|
yield call(waitForIdleOrFirstIntent);
|
|
207
|
-
yield
|
|
207
|
+
const locale = yield select(selectLocale);
|
|
208
|
+
const game = yield select(selectGame);
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
yield call(visit, { game, locale, referrer: document.referrer, url: location.href });
|
|
212
|
+
// oxlint-disable-next-line no-empty
|
|
213
|
+
} catch {}
|
|
208
214
|
}
|
|
209
215
|
|
|
210
216
|
function* prefetchDictionaryOnFirstIntent(): AnyGenerator {
|
|
@@ -10,11 +10,6 @@
|
|
|
10
10
|
),
|
|
11
11
|
$media-expressions: (
|
|
12
12
|
touch: '(hover: none)',
|
|
13
|
-
/**
|
|
14
|
-
* Keep in sync with useIsCompactLayout
|
|
15
|
-
*/
|
|
16
|
-
compact: '(max-width: #{$breakpoint-l - 1px}), (max-height: #{$breakpoint-height-l - 1px})',
|
|
17
|
-
desktop: '(min-width: #{$breakpoint-l}) and (min-height: #{$breakpoint-height-l})',
|
|
18
13
|
)
|
|
19
14
|
);
|
|
20
15
|
|
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
breakpointM: strip-unit($breakpoint-m);
|
|
7
7
|
breakpointL: strip-unit($breakpoint-l);
|
|
8
8
|
breakpointXl: strip-unit($breakpoint-xl);
|
|
9
|
-
breakpointHeightL: strip-unit($breakpoint-height-l);
|
|
10
9
|
|
|
11
10
|
borderColorLight: $border-color-light;
|
|
12
11
|
borderWidth: strip-unit($border-width);
|
|
@@ -153,7 +153,7 @@ $easeOutSine: cubic-bezier(0.61, 1, 0.88, 1);
|
|
|
153
153
|
--components-spacing: var(--spacing--l);
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
-
@include media('
|
|
156
|
+
@include media('<l') {
|
|
157
157
|
--dictionary--height: var(--dictionary--height--mobile);
|
|
158
158
|
--logo--height: #{$logo-height-small};
|
|
159
159
|
--solver-column-total: 0px;
|