scrabble-solver 2.18.2 → 2.18.4

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 (183) hide show
  1. package/.nx/workspace-data/d/server-process.json +3 -3
  2. package/.nx/workspace-data/file-map.json +1472 -1435
  3. package/.nx/workspace-data/lockfile-dependencies.hash +1 -1
  4. package/.nx/workspace-data/lockfile-nodes.hash +1 -1
  5. package/.nx/workspace-data/nx_files.nxt +0 -0
  6. package/.nx/workspace-data/parsed-lock-file.dependencies.json +528 -713
  7. package/.nx/workspace-data/parsed-lock-file.nodes.json +965 -1370
  8. package/.nx/workspace-data/project-graph.json +1561 -2156
  9. package/.nx/workspace-data/source-maps.json +48 -8
  10. package/.oxlintrc.json +0 -1
  11. package/CLAUDE.md +10 -8
  12. package/README.md +1 -1
  13. package/bun.lock +230 -279
  14. package/lerna.json +1 -1
  15. package/package.json +7 -6
  16. package/packages/configs/build/games/index.d.ts +1 -0
  17. package/packages/configs/build/games/index.js +1 -0
  18. package/packages/configs/build/games/wyrazy.d.ts +19 -0
  19. package/packages/configs/build/games/wyrazy.js +73 -0
  20. package/packages/configs/build/languages/polish.d.ts +1 -0
  21. package/packages/configs/build/languages/polish.js +39 -1
  22. package/packages/configs/package.json +5 -4
  23. package/packages/configs/src/games/index.ts +1 -0
  24. package/packages/configs/src/games/wyrazy.ts +71 -0
  25. package/packages/configs/src/languages/polish.ts +40 -1
  26. package/packages/constants/package.json +1 -1
  27. package/packages/dictionaries/bin/update-dictionaries.js +2 -5
  28. package/packages/dictionaries/build/Dictionaries.d.ts +1 -1
  29. package/packages/dictionaries/build/Dictionaries.js +9 -4
  30. package/packages/dictionaries/build/lib/DiskCache.js +1 -1
  31. package/packages/dictionaries/build/lib/downloadDictionary.d.ts +1 -1
  32. package/packages/dictionaries/build/lib/downloadDictionary.js +12 -7
  33. package/packages/dictionaries/package.json +4 -4
  34. package/packages/dictionaries/src/Dictionaries.ts +13 -6
  35. package/packages/dictionaries/src/lib/DiskCache.ts +2 -2
  36. package/packages/dictionaries/src/lib/downloadDictionary.ts +14 -6
  37. package/packages/logger/README.md +18 -0
  38. package/packages/logger/build/constants.d.ts +3 -0
  39. package/packages/logger/build/constants.js +4 -1
  40. package/packages/logger/build/describeError.d.ts +5 -0
  41. package/packages/logger/build/describeError.js +23 -0
  42. package/packages/logger/build/events.d.ts +92 -0
  43. package/packages/logger/build/events.js +13 -0
  44. package/packages/logger/build/formatCsvRow.d.ts +2 -0
  45. package/packages/logger/build/formatCsvRow.js +20 -0
  46. package/packages/logger/build/index.d.ts +5 -1
  47. package/packages/logger/build/index.js +13 -3
  48. package/packages/logger/build/logError.d.ts +4 -0
  49. package/packages/logger/build/logError.js +17 -0
  50. package/packages/logger/build/logEvent.d.ts +3 -0
  51. package/packages/logger/build/logEvent.js +29 -0
  52. package/packages/logger/package.json +7 -8
  53. package/packages/logger/scripts/export-csv.ts +113 -0
  54. package/packages/logger/scripts/readEvents.ts +34 -0
  55. package/packages/logger/scripts/tsconfig.json +10 -0
  56. package/packages/logger/src/constants.ts +9 -0
  57. package/packages/logger/src/describeError.test.ts +44 -0
  58. package/packages/logger/src/describeError.ts +30 -0
  59. package/packages/logger/src/events.ts +103 -0
  60. package/packages/logger/src/formatCsvRow.test.ts +41 -0
  61. package/packages/logger/src/formatCsvRow.ts +24 -0
  62. package/packages/logger/src/index.ts +13 -1
  63. package/packages/logger/src/logError.ts +24 -0
  64. package/packages/logger/src/logEvent.test.ts +59 -0
  65. package/packages/logger/src/logEvent.ts +26 -0
  66. package/packages/scrabble-solver/.next/BUILD_ID +1 -1
  67. package/packages/scrabble-solver/.next/build-manifest.json +15 -15
  68. package/packages/scrabble-solver/.next/cache/.previewinfo +1 -1
  69. package/packages/scrabble-solver/.next/cache/.rscinfo +1 -1
  70. package/packages/scrabble-solver/.next/cache/.tsbuildinfo +1 -1
  71. package/packages/scrabble-solver/.next/cache/webpack/client-production/0.pack +0 -0
  72. package/packages/scrabble-solver/.next/cache/webpack/client-production/index.pack +0 -0
  73. package/packages/scrabble-solver/.next/cache/webpack/client-production/index.pack.old +0 -0
  74. package/packages/scrabble-solver/.next/cache/webpack/edge-server-production/index.pack +0 -0
  75. package/packages/scrabble-solver/.next/cache/webpack/edge-server-production/index.pack.old +0 -0
  76. package/packages/scrabble-solver/.next/cache/webpack/server-production/0.pack +0 -0
  77. package/packages/scrabble-solver/.next/cache/webpack/server-production/index.pack +0 -0
  78. package/packages/scrabble-solver/.next/cache/webpack/server-production/index.pack.old +0 -0
  79. package/packages/scrabble-solver/.next/diagnostics/framework.json +1 -1
  80. package/packages/scrabble-solver/.next/dynamic-css-manifest.json +1 -1
  81. package/packages/scrabble-solver/.next/prerender-manifest.json +4 -4
  82. package/packages/scrabble-solver/.next/react-loadable-manifest.json +6 -6
  83. package/packages/scrabble-solver/.next/routes-manifest.json +1 -1
  84. package/packages/scrabble-solver/.next/server/chunks/546.js +1 -1
  85. package/packages/scrabble-solver/.next/server/chunks/612.js +5 -0
  86. package/packages/scrabble-solver/.next/server/chunks/67.js +1 -1
  87. package/packages/scrabble-solver/.next/server/chunks/712.js +1 -1
  88. package/packages/scrabble-solver/.next/server/chunks/715.js +1 -0
  89. package/packages/scrabble-solver/.next/server/chunks/812.js +1 -1
  90. package/packages/scrabble-solver/.next/server/chunks/961.js +1 -1
  91. package/packages/scrabble-solver/.next/server/dynamic-css-manifest.js +1 -1
  92. package/packages/scrabble-solver/.next/server/middleware-build-manifest.js +1 -1
  93. package/packages/scrabble-solver/.next/server/middleware-react-loadable-manifest.js +1 -1
  94. package/packages/scrabble-solver/.next/server/pages/404.html +3 -3
  95. package/packages/scrabble-solver/.next/server/pages/500.html +3 -3
  96. package/packages/scrabble-solver/.next/server/pages/_app.js.nft.json +1 -1
  97. package/packages/scrabble-solver/.next/server/pages/_document.js +2 -2
  98. package/packages/scrabble-solver/.next/server/pages/_document.js.nft.json +1 -1
  99. package/packages/scrabble-solver/.next/server/pages/api/dictionary/[locale]/[word].js +2 -2
  100. package/packages/scrabble-solver/.next/server/pages/api/dictionary/[locale]/[word].js.nft.json +1 -1
  101. package/packages/scrabble-solver/.next/server/pages/api/dictionary/[locale].js +1 -1
  102. package/packages/scrabble-solver/.next/server/pages/api/dictionary/[locale].js.nft.json +1 -1
  103. package/packages/scrabble-solver/.next/server/pages/api/solve.js +1 -1
  104. package/packages/scrabble-solver/.next/server/pages/api/solve.js.nft.json +1 -1
  105. package/packages/scrabble-solver/.next/server/pages/api/verify.js +1 -1
  106. package/packages/scrabble-solver/.next/server/pages/api/verify.js.nft.json +1 -1
  107. package/packages/scrabble-solver/.next/server/pages/api/visit.js +1 -1
  108. package/packages/scrabble-solver/.next/server/pages/api/visit.js.nft.json +1 -1
  109. package/packages/scrabble-solver/.next/server/pages/index.html +3 -3
  110. package/packages/scrabble-solver/.next/server/pages/index.js +1 -1
  111. package/packages/scrabble-solver/.next/server/pages/index.js.nft.json +1 -1
  112. package/packages/scrabble-solver/.next/server/pages/index.json +1 -1
  113. package/packages/scrabble-solver/.next/server/pages/not-found.html +3 -3
  114. package/packages/scrabble-solver/.next/server/pages/not-found.js.nft.json +1 -1
  115. package/packages/scrabble-solver/.next/static/{nvGZh1gPuX98L07YmxlYD → TAqjh9l5VJQOfJ4SzgPk2}/_buildManifest.js +1 -1
  116. package/packages/scrabble-solver/.next/static/chunks/101-f341f70c10d71212.js +3 -0
  117. package/packages/scrabble-solver/.next/static/chunks/256.6a20b23a766cfebd.js +1 -0
  118. package/packages/scrabble-solver/.next/static/chunks/693.d295da7c7b873986.js +1 -0
  119. package/packages/scrabble-solver/.next/static/chunks/980.dd35e807b18063c1.js +1 -0
  120. package/packages/scrabble-solver/.next/static/chunks/{main-7e52da35a4beea0b.js → main-6e9ac37e28d2b899.js} +1 -1
  121. package/packages/scrabble-solver/.next/static/chunks/pages/_app-9340d51f5a6787fc.js +1 -0
  122. package/packages/scrabble-solver/.next/static/chunks/pages/index-ea21d96a88f25000.js +1 -0
  123. package/packages/scrabble-solver/.next/static/chunks/{webpack-9f5c9d275348829b.js → webpack-d73aa4aae49cd800.js} +1 -1
  124. package/packages/scrabble-solver/.next/static/css/{92f85ce358a0a468.css → 4dbcacc1122ac6b9.css} +1 -1
  125. package/packages/scrabble-solver/.next/static/css/8b8087461767360a.css +1 -0
  126. package/packages/scrabble-solver/.next/static/css/{65bcc9440e067113.css → 98051ed25076b896.css} +1 -1
  127. package/packages/scrabble-solver/.next/trace +22 -21
  128. package/packages/scrabble-solver/.next/trace-build +1 -1
  129. package/packages/scrabble-solver/package.json +10 -10
  130. package/packages/scrabble-solver/public/llms.txt +1 -1
  131. package/packages/scrabble-solver/public/service-worker.js +1 -1
  132. package/packages/scrabble-solver/src/api/BadRequestError.ts +6 -0
  133. package/packages/scrabble-solver/src/api/index.ts +2 -1
  134. package/packages/scrabble-solver/src/api/withApiLog.ts +85 -0
  135. package/packages/scrabble-solver/src/components/Button/Button.module.scss +1 -1
  136. package/packages/scrabble-solver/src/components/Solver/Solver.module.scss +4 -4
  137. package/packages/scrabble-solver/src/components/Solver/Solver.tsx +2 -2
  138. package/packages/scrabble-solver/src/hooks/useColumns.ts +5 -7
  139. package/packages/scrabble-solver/src/modals/ResultsModal/ResultsModal.tsx +2 -2
  140. package/packages/scrabble-solver/src/modals/WordsModal/WordsModal.module.scss +1 -1
  141. package/packages/scrabble-solver/src/modals/WordsModal/WordsModal.tsx +2 -2
  142. package/packages/scrabble-solver/src/modals/WordsModal/components/WordsTable/WordsTable.tsx +2 -2
  143. package/packages/scrabble-solver/src/pages/api/dictionary/[locale]/[word].ts +22 -30
  144. package/packages/scrabble-solver/src/pages/api/dictionary/[locale]/index.ts +36 -42
  145. package/packages/scrabble-solver/src/pages/api/solve.ts +32 -42
  146. package/packages/scrabble-solver/src/pages/api/verify.ts +35 -43
  147. package/packages/scrabble-solver/src/pages/api/visit.ts +38 -13
  148. package/packages/scrabble-solver/src/pages/index.tsx +3 -3
  149. package/packages/scrabble-solver/src/parameters/index.ts +0 -2
  150. package/packages/scrabble-solver/src/sdk/solve.ts +3 -9
  151. package/packages/scrabble-solver/src/sdk/verify.ts +2 -10
  152. package/packages/scrabble-solver/src/sdk/visit.ts +8 -3
  153. package/packages/scrabble-solver/src/state/sagas.ts +7 -1
  154. package/packages/scrabble-solver/src/styles/_tokens.scss +0 -2
  155. package/packages/scrabble-solver/src/styles/mixins.scss +0 -5
  156. package/packages/scrabble-solver/src/styles/variables.module.scss +0 -1
  157. package/packages/scrabble-solver/src/styles/variables.module.scss.d.ts +0 -1
  158. package/packages/scrabble-solver/src/styles/variables.scss +1 -1
  159. package/packages/scrabble-solver/src/types/api.ts +7 -0
  160. package/packages/scrabble-solver/tsconfig.tsbuildinfo +1 -1
  161. package/packages/solver/package.json +5 -5
  162. package/packages/types/build/Game.d.ts +2 -1
  163. package/packages/types/build/Game.js +1 -0
  164. package/packages/types/package.json +2 -2
  165. package/packages/types/src/Game.ts +1 -0
  166. package/packages/word-definitions/package.json +2 -2
  167. package/packages/word-lists/package.json +2 -2
  168. package/playwright-report/index.html +1 -1
  169. package/packages/logger/build/logger.d.ts +0 -1
  170. package/packages/logger/build/logger.js +0 -28
  171. package/packages/logger/scripts/stats.js +0 -52
  172. package/packages/logger/src/logger.ts +0 -30
  173. package/packages/scrabble-solver/.next/server/chunks/392.js +0 -1
  174. package/packages/scrabble-solver/.next/static/chunks/101-829ab8a8d310e7a0.js +0 -3
  175. package/packages/scrabble-solver/.next/static/chunks/256.66ff9fb6d7db9ec7.js +0 -1
  176. package/packages/scrabble-solver/.next/static/chunks/693.677c88e0eb1340b5.js +0 -1
  177. package/packages/scrabble-solver/.next/static/chunks/980.7ce17464fc56648a.js +0 -1
  178. package/packages/scrabble-solver/.next/static/chunks/pages/_app-f9ec81278ffa7b1a.js +0 -1
  179. package/packages/scrabble-solver/.next/static/chunks/pages/index-b5bf018100015277.js +0 -1
  180. package/packages/scrabble-solver/.next/static/css/a691348b788f8bc0.css +0 -1
  181. package/packages/scrabble-solver/src/api/getServerLoggingData.ts +0 -17
  182. package/packages/scrabble-solver/src/hooks/useIsCompactLayout.ts +0 -14
  183. /package/packages/scrabble-solver/.next/static/{nvGZh1gPuX98L07YmxlYD → TAqjh9l5VJQOfJ4SzgPk2}/_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 { logger } from '@scrabble-solver/logger';
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 { getServerLoggingData } from '@/api';
9
+ import { type ApiContext, BadRequestError, withApiLog } from '@/api';
10
10
 
11
11
  interface RequestData {
12
12
  locale: Locale;
13
13
  }
14
14
 
15
- const dictionary = async (request: NextApiRequest, response: NextApiResponse): Promise<void> => {
16
- const meta = getServerLoggingData(request);
15
+ export default withApiLog('download', dictionary);
17
16
 
18
- try {
19
- const { locale } = parseRequest(request);
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
- logger.info('dictionary - request', {
22
- meta,
23
- payload: {
24
- locale,
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
- const gaddag = await dictionaries.get(locale);
29
- const isGzip = acceptsGzip(request);
30
- const body = isGzip ? await getCompressedDictionary(gaddag) : getSerializedDictionary(gaddag);
29
+ if (encoding === 'gzip') {
30
+ response.setHeader('Content-Encoding', 'gzip');
31
+ }
31
32
 
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');
33
+ response.status(200).send(body);
37
34
 
38
- if (isGzip) {
39
- response.setHeader('Content-Encoding', 'gzip');
40
- }
41
-
42
- response.status(200).send(body);
43
- } catch (error) {
44
- const message = error instanceof Error ? error.message : 'Unknown error';
45
- logger.error('dictionary - error', { error, meta });
46
- response.status(500).send({ error: 'Server error', message });
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
- const parseRequest = (request: NextApiRequest): RequestData => {
46
+ function parseRequest(request: NextApiRequest): RequestData {
51
47
  const { locale } = request.query;
52
48
 
53
49
  if (!isLocale(locale)) {
54
- throw new Error('Invalid "locale" parameter');
50
+ throw new BadRequestError('Invalid "locale" parameter');
55
51
  }
56
52
 
57
53
  return {
58
54
  locale,
59
55
  };
60
- };
56
+ }
61
57
 
62
- const acceptsGzip = (request: NextApiRequest): boolean => {
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
- const getSerializedDictionary = (gaddag: Gaddag): Buffer => {
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
- const getCompressedDictionary = (gaddag: Gaddag): Promise<Buffer> => {
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 { logger } from '@scrabble-solver/logger';
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 { getServerLoggingData, isBoardValid, isCharacterValid } from '@/api';
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
- const solve = async (request: NextApiRequest, response: NextApiResponse): Promise<void> => {
30
- const meta = getServerLoggingData(request);
31
-
32
- try {
33
- const { board, characters, config, game, locale } = parseRequest(request);
34
-
35
- logger.info('solve - request', {
36
- meta,
37
- payload: {
38
- board: board.toString(),
39
- configId: config.game,
40
- boardBlanksCount: board.getBlanksCount(),
41
- boardTilesCount: board.getTilesCount(),
42
- characters: characters.join(''),
43
- game,
44
- locale,
45
- },
46
- });
47
-
48
- const gaddag = await dictionaries.get(locale);
49
- const tiles = characters.map((character) => new Tile({ character, isBlank: character === BLANK }));
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
- const parseRequest = (request: NextApiRequest): RequestData => {
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 Error('Invalid "locale" parameter');
55
+ throw new BadRequestError('Invalid "locale" parameter');
64
56
  }
65
57
 
66
58
  if (!isGame(game)) {
67
- throw new Error('Invalid "game" parameter');
59
+ throw new BadRequestError('Invalid "game" parameter');
68
60
  }
69
61
 
70
62
  if (!isStringArray(characters) || characters.length === 0) {
71
- throw new Error('Invalid "characters" parameter');
63
+ throw new BadRequestError('Invalid "characters" parameter');
72
64
  }
73
65
 
74
66
  if (!hasConfig(game, locale)) {
75
- throw new Error(`No game "${game}" in "${locale}"`);
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 Error('Invalid "characters" parameter');
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 Error('Too many blank tiles passed');
81
+ throw new BadRequestError('Too many blank tiles passed');
90
82
  }
91
83
 
92
84
  if (!isBoardJson(boardJson) || !isBoardValid(boardJson, config)) {
93
- throw new Error('Invalid "board" parameter');
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 { logger } from '@scrabble-solver/logger';
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 { getServerLoggingData, isBoardValid } from '@/api';
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
- const verify = async (request: NextApiRequest, response: NextApiResponse): Promise<void> => {
18
- const meta = getServerLoggingData(request);
17
+ export default withApiLog('verify', verify);
19
18
 
20
- try {
21
- const { board, game, locale } = parseRequest(request);
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
- logger.info('verify - request', {
24
- meta,
25
- payload: {
26
- board: board.toString(),
27
- boardBlanksCount: board.getBlanksCount(),
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
- const parseRequest = (request: NextApiRequest): RequestData => {
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 Error('Invalid "locale" parameter');
53
+ throw new BadRequestError('Invalid "locale" parameter');
60
54
  }
61
55
 
62
56
  if (!isGame(game)) {
63
- throw new Error('Invalid "game" parameter');
57
+ throw new BadRequestError('Invalid "game" parameter');
64
58
  }
65
59
 
66
60
  if (!hasConfig(game, locale)) {
67
- throw new Error(`No game "${game}" in "${locale}"`);
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 Error('Invalid "board" parameter');
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 { logger } from '@scrabble-solver/logger';
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 { getServerLoggingData } from '@/api';
5
+ import { type ApiContext, withApiLog } from '@/api';
5
6
 
6
- const visit = (request: NextApiRequest, response: NextApiResponse): void => {
7
- const meta = getServerLoggingData(request);
7
+ interface RequestData {
8
+ game: string | undefined;
9
+ locale: string | undefined;
10
+ referrer: string | undefined;
11
+ url: string | undefined;
12
+ }
8
13
 
9
- try {
10
- logger.info('visit - request', { meta });
11
- response.status(200).send(true);
12
- } catch (error) {
13
- const message = error instanceof Error ? error.message : 'Unknown error';
14
- logger.error('visit - error', { error, meta });
15
- response.status(500).send({ error: 'Server error', message });
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
- export default visit;
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';
@@ -74,7 +74,7 @@ const WEB_APPLICATION_JSON_LD = JSON.stringify({
74
74
  inLanguage: ['en-US', 'en-GB', 'de-DE', 'es-ES', 'fa-IR', 'fr-FR', 'pl-PL', 'ro-RO', 'tr-TR'],
75
75
  featureList: [
76
76
  'Finds the highest-scoring words for a given board and rack',
77
- 'Supports Scrabble, Super Scrabble, Scrabble Duel, Letter League, Crossplay, Literaki, and Kelimelik',
77
+ 'Supports Scrabble, Super Scrabble, Scrabble Duel, Letter League, Crossplay, Literaki, Kelimelik, and Wyrazy',
78
78
  'Dictionaries and word definitions in 8 languages',
79
79
  'Works offline once a dictionary is cached',
80
80
  'Free, open source, no ads, no sign-up',
@@ -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 = useIsCompactLayout();
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, type BoardJson, type Game, type Locale, Result, type ResultJson } from '@scrabble-solver/types';
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
- interface Payload {
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 }: Payload): Promise<Response> => {
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
- export const visit = (): Promise<Response> => {
2
- return fetch('/api/visit', {
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 call(visit);
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,8 +10,6 @@ $breakpoint-m: 992px;
10
10
  $breakpoint-l: 1200px;
11
11
  $breakpoint-xl: 1400px;
12
12
 
13
- $breakpoint-height-l: 860px;
14
-
15
13
  $border-color: #cdcdcd;
16
14
  $border-color-light: #d9d9d9;
17
15
  $border-radius: 5px;
@@ -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);
@@ -2,7 +2,6 @@
2
2
  declare const styles: {
3
3
  readonly borderColorLight: string;
4
4
  readonly borderWidth: string;
5
- readonly breakpointHeightL: string;
6
5
  readonly breakpointL: string;
7
6
  readonly breakpointM: string;
8
7
  readonly breakpointS: string;
@@ -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('compact') {
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;
@@ -12,3 +12,10 @@ export interface VerifyRequestPayload {
12
12
  game: Game;
13
13
  locale: Locale;
14
14
  }
15
+
16
+ export interface VisitRequestPayload {
17
+ game: Game;
18
+ locale: Locale;
19
+ referrer: string;
20
+ url: string;
21
+ }