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
JavaScript
// 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
};