Update all imports to .js for esm support, use threads.js
threads.js has better support for modules - no need to give a project-relative path to the worker file, which complicated the build. Add rudimentary thread pooling w/ execution timeout.
This commit is contained in:
		| @ -4,7 +4,7 @@ import { | ||||
|   RouterContext, | ||||
|   graphql, | ||||
|   makeExecutableSchema, | ||||
| } from "./mods"; | ||||
| } from "./mods.js"; | ||||
|  | ||||
| export interface ResolversProps { | ||||
|   Query?: any; | ||||
|  | ||||
| @ -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)); | ||||
|  | ||||
| @ -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` | ||||
|   """ | ||||
|  | ||||
| @ -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"; | ||||
|  | ||||
| @ -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<GenerateArguments, Cell[]>({ | ||||
|   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<T>(promise: Promise<T>, ms: number, cb: () => any) { | ||||
|   let timeout: NodeJS.Timeout; | ||||
|   return new Promise<T>((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<Sudoku> { | ||||
|   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 | ||||
|   const puzzle = await withTimeout<number[]>( | ||||
|     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."); | ||||
|     } | ||||
|   ); | ||||
|  | ||||
|   available.push(proxy); | ||||
|   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."); | ||||
|     } | ||||
|     throw err; | ||||
|   } finally { | ||||
|     activeWorkers--; | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -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; | ||||
|  | ||||
| @ -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; | ||||
|  | ||||
| @ -1,15 +1,16 @@ | ||||
| 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 }) => { | ||||
| expose({ | ||||
|   generate(regionWidth, regionHeight, clues) { | ||||
|     const math = | ||||
|       maths[`${regionWidth}:${regionHeight}`] || | ||||
|       (maths[`${regionWidth}:${regionHeight}`] = new SudokuMath( | ||||
|         regionWidth, | ||||
|         regionHeight | ||||
|       )); | ||||
|   const puzzle = math.generate(clues); | ||||
|   parentPort.postMessage(puzzle); | ||||
|     return math.generate(clues); | ||||
|   }, | ||||
| }); | ||||
|  | ||||
		Reference in New Issue
	
	Block a user