diff --git a/src/graphql.ts b/src/graphql.ts index defb53b..5d10d4c 100644 --- a/src/graphql.ts +++ b/src/graphql.ts @@ -4,7 +4,7 @@ import { RouterContext, graphql, makeExecutableSchema, -} from "./mods"; +} from "./mods.js"; export interface ResolversProps { Query?: any; diff --git a/src/graphql/index.ts b/src/graphql/index.ts index 9135422..0b02cd6 100644 --- a/src/graphql/index.ts +++ b/src/graphql/index.ts @@ -1,6 +1,7 @@ -import { mergeTypeDefs, mergeResolvers } from "../mods"; +import { mergeTypeDefs, mergeResolvers } from "../mods.js"; +import * as sudoku from "./sudoku.js"; -const modules = [require("./sudoku")]; +const modules = [sudoku]; export const typeDefs = mergeTypeDefs(modules.map((mod) => mod.typeDefs)); export const resolvers = mergeResolvers(modules.map((mod) => mod.resolvers)); diff --git a/src/graphql/sudoku.ts b/src/graphql/sudoku.ts index add567d..8cecf9a 100644 --- a/src/graphql/sudoku.ts +++ b/src/graphql/sudoku.ts @@ -1,5 +1,5 @@ -import { gql } from "../mods"; -import { generate, GenerateArguments } from "../sudoku/index"; +import { gql } from "../mods.js"; +import { generate, GenerateArguments } from "../sudoku/index.js"; export const typeDefs = gql` """ diff --git a/src/main.ts b/src/main.ts index 5dc18e6..7d6ebc1 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,6 +1,6 @@ -import { Application, bodyParser } from "./mods"; -import { applyGraphQL } from "./graphql"; -import { typeDefs, resolvers } from "./graphql/index"; +import { Application, bodyParser } from "./mods.js"; +import { applyGraphQL } from "./graphql.js"; +import { typeDefs, resolvers } from "./graphql/index.js"; import stoppable from "stoppable"; import cors from "@koa/cors"; diff --git a/src/sudoku/index.ts b/src/sudoku/index.ts index 00e0e20..d7549af 100644 --- a/src/sudoku/index.ts +++ b/src/sudoku/index.ts @@ -1,7 +1,8 @@ -import { StaticPool, isTimeoutError } from "node-worker-threads-pool"; +import { spawn, Thread, Worker } from "threads"; import WORKERS from "physical-cpu-count"; -import { prettyPrint } from "./util"; +import { prettyPrint } from "./util.js"; + const TIMEOUT = 20000; export type Cell = number; @@ -19,46 +20,64 @@ export type Sudoku = { cells: Cell[]; }; -const pool = new StaticPool({ - size: WORKERS, - task: "./src/sudoku/worker.js", -}); +function getWorker() { + return spawn(new Worker("./worker")); +} + +const available: any = []; + +function initialize() { + console.log(`Starting ${WORKERS} worker threads`); + for (let n = 0; n < WORKERS; n++) { + getWorker().then((worker) => available.push(worker)); + } +} +initialize(); + +/** + * Awaits a promise with a timeout. + * + * @param promise the promise to await + * @param ms the timeout in milliseconds + * @param cb a callback to call when the timeout is reached. The promise is + * rejected with whatever gets returned here. + */ +function withTimeout(promise: Promise, ms: number, cb: () => any) { + let timeout: NodeJS.Timeout; + return new Promise((resolve, reject) => { + timeout = setTimeout(() => { + reject(cb()); + }, ms); + promise.then(resolve).catch(reject); + }).finally(() => clearTimeout(timeout!)); +} -let activeWorkers = 0; export async function generate( regionWidth: number, regionHeight: number, clues: number ): Promise { - if (activeWorkers >= WORKERS) { - throw new Error("No workers available. Please try again in a moment."); + const proxy = available.pop(); + if (!proxy) { + throw new Error("No workers available right now. Please try again."); } - try { - activeWorkers++; - const puzzle = await pool.exec( - { - regionWidth, - regionHeight, - clues, - }, - TIMEOUT - ); - - prettyPrint(regionWidth, regionHeight, puzzle); - - return { - regionWidth, - regionHeight, - size: (regionWidth * regionHeight) ** 2, - cells: puzzle, - }; - } catch (err) { - if (isTimeoutError(err)) { - throw new Error("Timed out. Try increasing the number of clues."); + const puzzle = await withTimeout( + proxy.generate(regionWidth, regionHeight, clues), + TIMEOUT, + () => { + Thread.terminate(proxy); + getWorker().then((worker) => available.push(worker)); + return new Error("Timed out. Try reducing the number of clues."); } - throw err; - } finally { - activeWorkers--; - } + ); + + available.push(proxy); + prettyPrint(regionWidth, regionHeight, puzzle); + return { + regionWidth, + regionHeight, + size: (regionWidth * regionHeight) ** 2, + cells: puzzle, + }; } diff --git a/src/sudoku/math.ts b/src/sudoku/math.ts index 0e08db3..a083e74 100644 --- a/src/sudoku/math.ts +++ b/src/sudoku/math.ts @@ -6,9 +6,9 @@ import { addNodeToColumn, maskRow, unmaskRow, -} from "./dlx"; -import { shuffle, range } from "./util"; -import { Cell } from "./index"; +} from "./dlx.js"; +import { shuffle, range } from "./util.js"; +import { Cell } from "./index.js"; type NodeMeta = { index: number; diff --git a/src/sudoku/util.ts b/src/sudoku/util.ts index 0920b48..bea6aff 100644 --- a/src/sudoku/util.ts +++ b/src/sudoku/util.ts @@ -1,4 +1,4 @@ -import { Cell } from "./index"; +import { Cell } from "./index.js"; export function randInt(lower: number, upper: number) { return Math.floor(Math.random() * (upper - lower)) + lower; diff --git a/src/sudoku/worker.js b/src/sudoku/worker.js index 61a0d50..a9db9b7 100644 --- a/src/sudoku/worker.js +++ b/src/sudoku/worker.js @@ -1,15 +1,13 @@ -const { SudokuMath } = require("./math"); -const { parentPort } = require("worker_threads"); +import { SudokuMath } from "./math.js"; +import { expose } from "threads/worker"; const maths = {}; -parentPort.on("message", ({ regionWidth, regionHeight, clues }) => { - const math = - maths[`${regionWidth}:${regionHeight}`] || - (maths[`${regionWidth}:${regionHeight}`] = new SudokuMath( - regionWidth, - regionHeight - )); - const puzzle = math.generate(clues); - parentPort.postMessage(puzzle); +expose({ + generate(regionWidth, regionHeight, clues) { + const key = `${regionWidth}:${regionHeight}`; + const math = + maths[key] ?? (maths[key] = new SudokuMath(regionWidth, regionHeight)); + return math.generate(clues); + }, });