UNPKG

make-synchronized

Version:

[![Coverage][codecov_badge]][codecov_link] [![Npm Version][package_version_badge]][package_link] [![MIT License][license_badge]][license_link]

839 lines (813 loc) 23.6 kB
// source/index.js import module from "node:module"; // source/constants.js import { isMainThread, workerData } from "node:worker_threads"; var IS_SERVER = !isMainThread && Boolean(workerData?.isServer); var IS_PRODUCTION = true; var STDIO_STREAMS = ["stdout", "stderr"]; var GLOBAL_SERVER_PROPERTY = "__make-synchronized-server__"; var PING_ACTION_RESPONSE = "pong"; var WORKER_ACTION__APPLY = 1; var WORKER_ACTION__GET = 2; var WORKER_ACTION__OWN_KEYS = 3; var WORKER_ACTION__GET_INFORMATION = 4; var WORKER_ACTION__PING = 5; var VALUE_TYPE__FUNCTION = 1; var VALUE_TYPE__PRIMITIVE = 2; var VALUE_TYPE__PLAIN_OBJECT = 3; var VALUE_TYPE__UNKNOWN = 4; var VALUE_INFORMATION__FUNCTION = { type: VALUE_TYPE__FUNCTION }; var ATOMICS_WAIT_RESULT__TIMED_OUT = "timed-out"; var RESPONSE_TYPE__REJECT = 2; var RESPONSE_TYPE__TERMINATE = 3; var MODULE_TYPE__INLINE_FUNCTION = 1; // source/server.js import { parentPort } from "node:worker_threads"; // source/load-module.js import { workerData as workerData2 } from "node:worker_threads"; var moduleImportPromise; var moduleInstance; var moduleLoadError; async function loadModule() { if (moduleInstance) { return moduleInstance; } if (moduleLoadError) { throw moduleLoadError; } try { moduleInstance = await moduleImportPromise; } catch (error) { moduleLoadError = error; throw error; } return moduleImportPromise; } var initializeModule = async () => { if (workerData2.exposeSetModuleInstance) { Object.defineProperty(globalThis, GLOBAL_SERVER_PROPERTY, { enumerable: false, configurable: true, writable: false, value: { setModuleInstance(module2) { delete globalThis[GLOBAL_SERVER_PROPERTY]; if (!IS_PRODUCTION && Object.getOwnPropertyDescriptor( globalThis, GLOBAL_SERVER_PROPERTY ) !== void 0) { throw new Error("Unexpected error."); } moduleInstance = module2; } } }); return; } moduleImportPromise = import(workerData2.module.source); try { moduleInstance = await moduleImportPromise; } catch (error) { moduleLoadError = error; } }; var load_module_default = loadModule; // source/responser.js import process2 from "node:process"; import util from "node:util"; // source/data-clone-error.js function isDataCloneError(error) { return error instanceof DOMException && error.name === "DataCloneError"; } // source/atomics-wait-error.js var AtomicsWaitError = class extends Error { name = "AtomicsWaitError"; constructor(code, { semaphore, expected }) { super( code === ATOMICS_WAIT_RESULT__TIMED_OUT ? "Timed out" : "Unexpected error" ); Object.assign(this, { code, semaphore, expected }); } }; var atomics_wait_error_default = AtomicsWaitError; // source/lock.js var SIGNAL_INDEX = 0; var unlock = (semaphore) => { Atomics.add(semaphore, SIGNAL_INDEX, 1); Atomics.notify(semaphore, SIGNAL_INDEX, 1); }; var Lock = class { semaphore = new Int32Array( new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT) ); #messageCount = 0; lock(timeout) { const { semaphore } = this; const previous = this.#messageCount; while (true) { this.#messageCount = Atomics.load(semaphore, SIGNAL_INDEX); if (this.#messageCount > previous) { return; } const result = Atomics.wait(semaphore, SIGNAL_INDEX, previous, timeout); if (result === ATOMICS_WAIT_RESULT__TIMED_OUT) { throw new atomics_wait_error_default(result, { semaphore, expected: previous }); } } } }; var lock_default = Lock; // source/get-value-information.js var PRIMITIVE_VALUE_TYPES = /* @__PURE__ */ new Set([ "undefined", "boolean", "number", "bigint", "string" ]); var isPrimitive = (value) => value === null || PRIMITIVE_VALUE_TYPES.has(typeof value); function getPlainObjectPropertyInformation(object, key) { const descriptor = Object.getOwnPropertyDescriptor(object, key); if (!Object.hasOwn(descriptor, "value")) { return; } const { value } = descriptor; if (isPrimitive(value)) { return { type: VALUE_TYPE__PRIMITIVE, value }; } } function getValueInformation(value) { if (typeof value === "function") { return VALUE_INFORMATION__FUNCTION; } if (isPrimitive(value)) { return { type: VALUE_TYPE__PRIMITIVE, value }; } const information = { type: VALUE_TYPE__UNKNOWN }; if (Object.getPrototypeOf(value) === null) { information.type = VALUE_TYPE__PLAIN_OBJECT; information.isNullPrototypeObject = true; } if (value.constructor === Object) { information.type = VALUE_TYPE__PLAIN_OBJECT; } if (information.type === VALUE_TYPE__PLAIN_OBJECT) { information.properties = new Map( Object.keys(value).map((property) => [ property, getPlainObjectPropertyInformation(value, property) ]) ); } return information; } var get_value_information_default = getValueInformation; // source/property-path.js var normalizePath = (propertyOrPath = []) => Array.isArray(propertyOrPath) ? propertyOrPath : [propertyOrPath]; var hashPath = (path2) => JSON.stringify(normalizePath(path2)); // source/process-action.js function getValue(value, payload) { let receiver; for (const property of normalizePath(payload.path)) { receiver = value; value = Reflect.get(value, property, value); } return { value, receiver }; } async function processAction(action, payload) { if (action === WORKER_ACTION__PING) { return PING_ACTION_RESPONSE; } const module2 = await load_module_default(); const result = getValue(module2, payload); switch (action) { case WORKER_ACTION__GET: return result.value; case WORKER_ACTION__APPLY: return Reflect.apply(result.value, result.receiver, payload.argumentsList); case WORKER_ACTION__OWN_KEYS: return Reflect.ownKeys(result.value).filter( (key) => typeof key !== "symbol" ); case WORKER_ACTION__GET_INFORMATION: return get_value_information_default(result.value); } throw new Error(`Unknown action '${action}'.`); } var process_action_default = processAction; // source/response-message.js import process from "node:process"; function packRejectedValue(value) { if (value instanceof Error) { return { error: value, errorData: { ...value } }; } return { error: value }; } function unpackRejectedValue({ error, errorData }) { if (error instanceof Error && errorData) { return Object.assign(error, errorData); } return error; } function packResponseMessage(stdio, data, type) { let extraData; if (stdio.length !== 0) { extraData ??= {}; extraData.stdio = stdio; } const { exitCode } = process; if (typeof exitCode === "number" && exitCode !== 0) { extraData ??= {}; extraData.exitCode = exitCode; } if (type === RESPONSE_TYPE__TERMINATE) { extraData ??= {}; extraData.terminated = true; } if (type === RESPONSE_TYPE__REJECT) { extraData ??= {}; extraData.rejected = true; return [packRejectedValue(data), extraData]; } const message = [data]; if (extraData !== void 0) { message.push(extraData); } return message; } function unpackResponseMessage(message) { if (message.length === 1) { return { result: message[0] }; } const [data, extraData] = message; if (extraData.rejected) { extraData.error = unpackRejectedValue(data); } return { result: data, ...extraData }; } // source/responser.js var originalProcessExit = process2.exit; var Responser = class { #channel; #stdio = []; constructor(channel) { this.#channel = channel; process2.exit = (exitCode) => { process2.exitCode = exitCode; this.#terminate(); originalProcessExit(exitCode); }; for (const stream of STDIO_STREAMS) { process2[stream]._writev = (chunks, callback) => { for (const { chunk } of chunks) { this.#stdio.push({ stream, chunk }); } callback(); }; } } #send(data, type) { const { responsePort } = this.#channel; const stdio = this.#stdio; const message = packResponseMessage(stdio, data, type); try { responsePort.postMessage(message); } catch (postMessageError) { const error = isDataCloneError(postMessageError) ? new Error( `Cannot serialize worker response: ${util.inspect(data)}`, { cause: postMessageError } ) : postMessageError; responsePort.postMessage( packResponseMessage(stdio, error, RESPONSE_TYPE__REJECT) ); } finally { this.#finish(); } } #resolve(result) { this.#send(result); } #reject(error) { this.#send(error, RESPONSE_TYPE__REJECT); } #finish() { unlock(this.#channel.responseSemaphore); process2.exitCode = void 0; this.#stdio.length = 0; } #terminate() { this.#send(void 0, RESPONSE_TYPE__TERMINATE); } async process({ action, payload }) { try { this.#resolve(await process_action_default(action, payload)); } catch (error) { this.#reject(error); } } destroy() { this.#channel.responsePort.close(); process2.exitCode = void 0; } }; var responser_default = Responser; // source/server.js function startServer() { let responser; parentPort.on("message", ([action, payload, channel]) => { if (channel) { responser?.destroy(); responser = new responser_default(channel); } responser.process({ action, payload }); }); try { initializeModule(); } catch { } } var server_default = startServer; // source/threads-worker.js import process3 from "node:process"; import { pathToFileURL } from "node:url"; import * as util2 from "node:util"; import { Worker } from "node:worker_threads"; // source/channel.js import { MessageChannel, receiveMessageOnPort } from "node:worker_threads"; var Channel = class { mainThreadPort; workerPort; alive = true; #lock = new lock_default(); constructor() { const { port1: mainThreadPort, port2: workerPort } = new MessageChannel(); mainThreadPort.unref(); workerPort.unref(); this.mainThreadPort = mainThreadPort; this.workerPort = workerPort; } getResponse(timeout) { try { this.#lock.lock(timeout); } catch (error) { if (error instanceof atomics_wait_error_default) { this.destroy(); } throw error; } const message = this.#receiveMessage(); return unpackResponseMessage(message); } #receiveMessage() { const port = this.mainThreadPort; let lastEntry; while (true) { const entry = receiveMessageOnPort(port); if (!entry) { return lastEntry.message; } lastEntry = entry; } } get semaphore() { return this.#lock.semaphore; } destroy() { if (!this.alive) { return; } this.alive = false; this.mainThreadPort.close(); this.workerPort.close(); this.mainThreadPort = void 0; this.workerPort = void 0; } }; var channel_default = Channel; // source/wait-for-worker.js function waitForWorker(worker, lock, workerFile2) { if (IS_PRODUCTION) { return; } let lockWaitError; try { lock.lock(2e3); } catch (error) { if (error instanceof atomics_wait_error_default) { lockWaitError = error; } else { throw error; } } if (!lockWaitError) { return; } let pingError; try { worker.sendAction(WORKER_ACTION__PING, void 0, 2e3); } catch (error) { if (error instanceof atomics_wait_error_default) { pingError = error; } else { throw error; } } if (!pingError) { return; } if (lockWaitError) { throw new AggregateError( [lockWaitError, pingError], `Unexpected error, most likely caused by syntax error in '${workerFile2}'` ); } } var wait_for_worker_default = waitForWorker; // source/threads-worker.js var shouldUseLegacyEvalMode = () => { const version = process3.versions.node; if (!version) { return true; } const majorVersion = Number(version.split(".")[0]); if (majorVersion < 20) { return true; } if (majorVersion < 22 && process3.platform === "win32") { return true; } return false; }; var workerFile; var setWorkFile = (file) => { workerFile = file; }; var WORKER_OPTIONS = { // https://nodejs.org/api/worker_threads.html#new-workerfilename-options // Do not pipe `stdio`s stdout: true, stderr: true, trackUnmanagedFds: false }; if (globalThis.Bun) { delete WORKER_OPTIONS.stdout; delete WORKER_OPTIONS.stderr; } var ThreadsWorker = class { #worker; #module; #channel; #workerIsAlive; #workerOnlineLock; constructor(module2) { this.#module = module2; } #createWorker() { const module2 = this.#module; const workerData3 = { isServer: true }; const workerOptions = { workerData: workerData3, ...WORKER_OPTIONS }; let lock; if (!IS_PRODUCTION) { lock = new lock_default(); workerOptions.workerData.workerRunningSemaphore = lock.semaphore; } let worker; if (module2.type === MODULE_TYPE__INLINE_FUNCTION) { workerData3.exposeSetModuleInstance = true; workerOptions.eval = true; const workUrl = workerFile instanceof URL ? workerFile : pathToFileURL(workerFile); const setModuleInstance = ( /* Indent */ ` globalThis[${JSON.stringify(GLOBAL_SERVER_PROPERTY)}] .setModuleInstance({default: ${module2.code}}) ` ); worker = new Worker( shouldUseLegacyEvalMode() ? ( /* Indent */ ` import(${JSON.stringify(workUrl)}) .then(() => { ${setModuleInstance} }) ` ) : ( /* Indent */ ` import ${JSON.stringify(workUrl)} ${setModuleInstance} ` ), workerOptions ); } else { workerData3.module = module2; worker = new Worker(workerFile, workerOptions); } worker.unref(); this.#workerIsAlive = false; this.#workerOnlineLock = lock; return worker; } #killWorker(worker) { if (this.#worker !== worker) { return; } this.#worker = void 0; this.#workerIsAlive = false; this.#workerOnlineLock = void 0; } #createChannel() { if (this.#channel?.alive) { return false; } this.#channel = new channel_default(); return true; } sendAction(action, payload, timeout) { this.#worker ??= this.#createWorker(); if (!IS_PRODUCTION && !this.#workerIsAlive && this.#workerOnlineLock && action !== WORKER_ACTION__PING) { wait_for_worker_default(this, this.#workerOnlineLock, workerFile); this.#workerIsAlive = true; this.#workerOnlineLock = void 0; } const requestMessage = [action, payload]; const transferList = []; const worker = this.#worker; let channel = this.#channel; if (this.#createChannel()) { channel = this.#channel; const { workerPort: responsePort, semaphore: responseSemaphore } = channel; requestMessage.push({ responsePort, responseSemaphore }); transferList.push(responsePort); } try { worker.postMessage(requestMessage, transferList); } catch (postMessageError) { if (isDataCloneError(postMessageError)) { throw Object.assign( new DOMException( `Cannot serialize request data: ${util2.inspect(payload)}`, "DataCloneError" ), { requestData: payload, cause: postMessageError } ); } throw postMessageError; } const { stdio, exitCode, terminated, rejected, error, result } = channel.getResponse(timeout); if (stdio) { for (const { stream, chunk } of stdio) { process3[stream].write(chunk); } } if (terminated || exitCode) { worker.terminate(); channel.destroy(); this.#killWorker(worker); } if (rejected) { throw error; } return result; } }; var threads_worker_default = ThreadsWorker; // source/normalize-module.js import * as path from "node:path"; import * as url from "node:url"; import util3 from "node:util"; var isString = (value) => typeof value === "string"; var filenameToModuleId = (filename) => url.pathToFileURL(filename).href; function toModuleSource(module2) { const href = module2?.href; if (isString(href)) { return href; } const url2 = module2?.url; if (isString(url2)) { return url2; } const filename = module2?.filename; if (isString(filename)) { return filenameToModuleId(filename); } if (!isString(module2) || module2.startsWith(".")) { throw new TypeError( `'module' should be an 'URL', 'import.meta' or an absolute path, got '${util3.inspect(module2)}'.` ); } if (path.isAbsolute(module2)) { return filenameToModuleId(module2); } return module2; } function normalizeModule(module2) { return { source: toModuleSource(module2) }; } var normalize_module_default = normalizeModule; // source/synchronizer.js var cacheResult = (cache, cacheKey, getResult) => { if (!cache.has(cacheKey)) { cache.set(cacheKey, getResult()); } return cache.get(cacheKey); }; var cachePathResult = (cache, path2, getResult) => cacheResult(cache, hashPath(path2), getResult); var Synchronizer = class _Synchronizer { static #instances = /* @__PURE__ */ new Map(); static create(module2, { isNormalizedModule = false } = {}) { module2 = isNormalizedModule ? module2 : normalize_module_default(module2); return cacheResult( this.#instances, JSON.stringify(module2), () => new _Synchronizer(module2) ); } #worker; #synchronizedFunctionStore = /* @__PURE__ */ new Map(); #informationStore = /* @__PURE__ */ new Map(); #ownKeysStore = /* @__PURE__ */ new Map(); #plainObjectStore = /* @__PURE__ */ new Map(); constructor(module2) { this.#worker = new threads_worker_default(module2); } getInformation(path2) { return cachePathResult( this.#informationStore, path2, () => this.#worker.sendAction(WORKER_ACTION__GET_INFORMATION, { path: path2 }) ); } setKnownInformation(path2, information) { this.#informationStore.set(hashPath(path2), information); } get(path2) { const information = this.getInformation(path2); switch (information.type) { case VALUE_TYPE__FUNCTION: return this.#createSynchronizedFunction(path2); case VALUE_TYPE__PRIMITIVE: return information.value; case VALUE_TYPE__PLAIN_OBJECT: return this.#createPlainObjectProxy(path2, information); default: return this.#worker.sendAction(WORKER_ACTION__GET, { path: path2 }); } } ownKeys(path2) { return cachePathResult( this.#ownKeysStore, path2, () => this.#worker.sendAction(WORKER_ACTION__OWN_KEYS, { path: path2 }) ); } apply(path2, argumentsList) { return this.#worker.sendAction(WORKER_ACTION__APPLY, { path: path2, argumentsList }); } #createSynchronizedFunction(path2) { return cachePathResult( this.#synchronizedFunctionStore, path2, () => (...argumentsList) => this.apply(path2, argumentsList) ); } createDefaultExportFunctionProxy() { const defaultExportFunction = this.get("default"); return new Proxy(defaultExportFunction, { get: (target, property) => this.get(property) }); } #createPlainObjectProxy(path2, { isNullPrototypeObject, properties }) { path2 = normalizePath(path2); return cachePathResult(this.#plainObjectStore, path2, () => { const object = isNullPrototypeObject ? /* @__PURE__ */ Object.create(null) : {}; for (const [property, propertyInformation] of properties) { if (propertyInformation?.type === VALUE_TYPE__PRIMITIVE) { object[property] = propertyInformation.value; } else { Object.defineProperty(object, property, { get: () => this.get([...path2, property]), enumerable: true, configurable: true }); } } return new Proxy(object, { get: (target, property, receiver) => { if (typeof property === "symbol" || properties.has(property)) { return Reflect.get(target, property, receiver); } return this.get([...path2, property]); } }); }); } createModule() { const module2 = Object.create(null, { [Symbol.toStringTag]: { value: "Module", enumerable: false } }); const specifiers = this.ownKeys(); return Object.defineProperties( module2, Object.fromEntries( specifiers.map((specifier) => [ specifier, { get: () => this.get(specifier), enumerable: true } ]) ) ); } }; var synchronizer_default = Synchronizer; // source/for-exports.js function makeSynchronizedFunctions(module2, implementation) { if (IS_SERVER) { return implementation; } const synchronizer = synchronizer_default.create(module2); synchronizer.setKnownInformation( void 0, get_value_information_default(implementation) ); return new Proxy(implementation, { get: (target, property) => typeof implementation[property] === "function" ? synchronizer.get(property) : target[property] }); } function makeSynchronizedFunction(module2, implementation, specifier = "default") { if (IS_SERVER) { return implementation; } const synchronizer = synchronizer_default.create(module2); synchronizer.setKnownInformation( specifier, get_value_information_default(implementation) ); return synchronizer.get(specifier); } // source/for-inline-functions.js function makeInlineFunctionSynchronized(implementation) { const code = typeof implementation === "function" ? implementation.toString() : implementation; const synchronizer = synchronizer_default.create( { type: MODULE_TYPE__INLINE_FUNCTION, code }, { isNormalizedModule: true } ); synchronizer.setKnownInformation(void 0, VALUE_INFORMATION__FUNCTION); return synchronizer.get("default"); } // source/for-modules.js function makeDefaultExportSynchronized(module2) { return synchronizer_default.create(module2).get("default"); } function makeModuleSynchronized(module2) { return synchronizer_default.create(module2).createModule(); } // source/client.js function makeSynchronized(module2, implementation) { if (typeof module2 === "function") { return makeInlineFunctionSynchronized(module2); } if (typeof implementation === "function") { return makeSynchronizedFunction(module2, implementation); } if (implementation) { return makeSynchronizedFunctions(module2, implementation); } const synchronizer = synchronizer_default.create(module2); const defaultExportType = synchronizer.getInformation("default").type; if (defaultExportType === VALUE_TYPE__FUNCTION) { return synchronizer.createDefaultExportFunctionProxy(); } return synchronizer.createModule(); } // source/index.js module.enableCompileCache?.(); if (IS_SERVER) { try { server_default(); } catch { } } else { setWorkFile(new URL(import.meta.url)); } export { makeSynchronized as default, makeDefaultExportSynchronized, makeInlineFunctionSynchronized, makeModuleSynchronized, makeSynchronized, makeSynchronizedFunction, makeSynchronizedFunctions };