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, |   RouterContext, | ||||||
|   graphql, |   graphql, | ||||||
|   makeExecutableSchema, |   makeExecutableSchema, | ||||||
| } from "./mods"; | } from "./mods.js"; | ||||||
|  |  | ||||||
| export interface ResolversProps { | export interface ResolversProps { | ||||||
|   Query?: any; |   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 typeDefs = mergeTypeDefs(modules.map((mod) => mod.typeDefs)); | ||||||
| export const resolvers = mergeResolvers(modules.map((mod) => mod.resolvers)); | export const resolvers = mergeResolvers(modules.map((mod) => mod.resolvers)); | ||||||
|  | |||||||
| @ -1,5 +1,5 @@ | |||||||
| import { gql } from "../mods"; | import { gql } from "../mods.js"; | ||||||
| import { generate, GenerateArguments } from "../sudoku/index"; | import { generate, GenerateArguments } from "../sudoku/index.js"; | ||||||
|  |  | ||||||
| export const typeDefs = gql` | export const typeDefs = gql` | ||||||
|   """ |   """ | ||||||
|  | |||||||
| @ -1,6 +1,6 @@ | |||||||
| import { Application, bodyParser } from "./mods"; | import { Application, bodyParser } from "./mods.js"; | ||||||
| import { applyGraphQL } from "./graphql"; | import { applyGraphQL } from "./graphql.js"; | ||||||
| import { typeDefs, resolvers } from "./graphql/index"; | import { typeDefs, resolvers } from "./graphql/index.js"; | ||||||
| import stoppable from "stoppable"; | import stoppable from "stoppable"; | ||||||
|  |  | ||||||
| import cors from "@koa/cors"; | 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 WORKERS from "physical-cpu-count"; | ||||||
| import { prettyPrint } from "./util"; | import { prettyPrint } from "./util.js"; | ||||||
|  |  | ||||||
| const TIMEOUT = 20000; | const TIMEOUT = 20000; | ||||||
|  |  | ||||||
| export type Cell = number; | export type Cell = number; | ||||||
| @ -19,46 +20,64 @@ export type Sudoku = { | |||||||
|   cells: Cell[]; |   cells: Cell[]; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const pool = new StaticPool<GenerateArguments, Cell[]>({ | function getWorker() { | ||||||
|   size: WORKERS, |   return spawn(new Worker("./worker")); | ||||||
|   task: "./src/sudoku/worker.js", | } | ||||||
| }); |  | ||||||
|  | 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( | export async function generate( | ||||||
|   regionWidth: number, |   regionWidth: number, | ||||||
|   regionHeight: number, |   regionHeight: number, | ||||||
|   clues: number |   clues: number | ||||||
| ): Promise<Sudoku> { | ): Promise<Sudoku> { | ||||||
|   if (activeWorkers >= WORKERS) { |   const proxy = available.pop(); | ||||||
|     throw new Error("No workers available. Please try again in a moment."); |   if (!proxy) { | ||||||
|  |     throw new Error("No workers available right now. Please try again."); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   try { |   const puzzle = await withTimeout<number[]>( | ||||||
|     activeWorkers++; |     proxy.generate(regionWidth, regionHeight, clues), | ||||||
|     const puzzle = await pool.exec( |     TIMEOUT, | ||||||
|       { |     () => { | ||||||
|         regionWidth, |       Thread.terminate(proxy); | ||||||
|         regionHeight, |       getWorker().then((worker) => available.push(worker)); | ||||||
|         clues, |       return new Error("Timed out. Try reducing the number of 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."); |  | ||||||
|     } |     } | ||||||
|     throw err; |   ); | ||||||
|   } finally { |  | ||||||
|     activeWorkers--; |   available.push(proxy); | ||||||
|   } |   prettyPrint(regionWidth, regionHeight, puzzle); | ||||||
|  |   return { | ||||||
|  |     regionWidth, | ||||||
|  |     regionHeight, | ||||||
|  |     size: (regionWidth * regionHeight) ** 2, | ||||||
|  |     cells: puzzle, | ||||||
|  |   }; | ||||||
| } | } | ||||||
|  | |||||||
| @ -6,9 +6,9 @@ import { | |||||||
|   addNodeToColumn, |   addNodeToColumn, | ||||||
|   maskRow, |   maskRow, | ||||||
|   unmaskRow, |   unmaskRow, | ||||||
| } from "./dlx"; | } from "./dlx.js"; | ||||||
| import { shuffle, range } from "./util"; | import { shuffle, range } from "./util.js"; | ||||||
| import { Cell } from "./index"; | import { Cell } from "./index.js"; | ||||||
|  |  | ||||||
| type NodeMeta = { | type NodeMeta = { | ||||||
|   index: number; |   index: number; | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| import { Cell } from "./index"; | import { Cell } from "./index.js"; | ||||||
|  |  | ||||||
| export function randInt(lower: number, upper: number) { | export function randInt(lower: number, upper: number) { | ||||||
|   return Math.floor(Math.random() * (upper - lower)) + lower; |   return Math.floor(Math.random() * (upper - lower)) + lower; | ||||||
|  | |||||||
| @ -1,15 +1,16 @@ | |||||||
| const { SudokuMath } = require("./math"); | import { SudokuMath } from "./math.js"; | ||||||
| const { parentPort } = require("worker_threads"); | import { expose } from "threads/worker"; | ||||||
|  |  | ||||||
| const maths = {}; | const maths = {}; | ||||||
|  |  | ||||||
| parentPort.on("message", ({ regionWidth, regionHeight, clues }) => { | expose({ | ||||||
|   const math = |   generate(regionWidth, regionHeight, clues) { | ||||||
|     maths[`${regionWidth}:${regionHeight}`] || |     const math = | ||||||
|     (maths[`${regionWidth}:${regionHeight}`] = new SudokuMath( |       maths[`${regionWidth}:${regionHeight}`] || | ||||||
|       regionWidth, |       (maths[`${regionWidth}:${regionHeight}`] = new SudokuMath( | ||||||
|       regionHeight |         regionWidth, | ||||||
|     )); |         regionHeight | ||||||
|   const puzzle = math.generate(clues); |       )); | ||||||
|   parentPort.postMessage(puzzle); |     return math.generate(clues); | ||||||
|  |   }, | ||||||
| }); | }); | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user