Add progressive web app companion for cross-platform access
Vite + TypeScript PWA that mirrors the Android app's core features: - Pre-processed shelter data (build-time UTM33N→WGS84 conversion) - Leaflet map with shelter markers, user location, and offline tiles - Canvas compass arrow (ported from DirectionArrowView.kt) - IndexedDB shelter cache with 7-day staleness check - Service worker with CacheFirst tiles and precached app shell - i18n for en, nb, nn (ported from Android strings.xml) - iOS/Android compass handling with low-pass filter - Respects user map interaction (no auto-snap on pan/zoom) - Build revision cache-breaker for reliable SW updates Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
46365b713b
commit
e8428de775
12051 changed files with 1799735 additions and 0 deletions
820
pwa/node_modules/tinypool/dist/index.js
generated
vendored
Normal file
820
pwa/node_modules/tinypool/dist/index.js
generated
vendored
Normal file
|
|
@ -0,0 +1,820 @@
|
|||
import { isMovable, isTaskQueue, isTransferable, kFieldCount, kQueueOptions, kRequestCountField, kResponseCountField, kTransferable, kValue, markMovable } from "./common-Qw-RoVFD.js";
|
||||
import { MessageChannel, MessagePort, Worker, receiveMessageOnPort } from "node:worker_threads";
|
||||
import { EventEmitterAsyncResource, once } from "node:events";
|
||||
import { AsyncResource } from "node:async_hooks";
|
||||
import { URL, fileURLToPath } from "node:url";
|
||||
import { join } from "node:path";
|
||||
import { inspect, types } from "node:util";
|
||||
import assert from "node:assert";
|
||||
import { performance } from "node:perf_hooks";
|
||||
import { readFileSync } from "node:fs";
|
||||
import os from "node:os";
|
||||
import childProcess, { fork } from "node:child_process";
|
||||
|
||||
//#region src/physicalCpuCount.ts
|
||||
function exec(command) {
|
||||
const output = childProcess.execSync(command, {
|
||||
encoding: "utf8",
|
||||
stdio: [
|
||||
null,
|
||||
null,
|
||||
null
|
||||
]
|
||||
});
|
||||
return output;
|
||||
}
|
||||
let amount;
|
||||
try {
|
||||
const platform = os.platform();
|
||||
if (platform === "linux") {
|
||||
const output1 = exec("cat /proc/cpuinfo | grep \"physical id\" | sort |uniq | wc -l");
|
||||
const output2 = exec("cat /proc/cpuinfo | grep \"core id\" | sort | uniq | wc -l");
|
||||
const physicalCpuAmount = parseInt(output1.trim(), 10);
|
||||
const physicalCoreAmount = parseInt(output2.trim(), 10);
|
||||
amount = physicalCpuAmount * physicalCoreAmount;
|
||||
} else if (platform === "darwin") {
|
||||
const output = exec("sysctl -n hw.physicalcpu_max");
|
||||
amount = parseInt(output.trim(), 10);
|
||||
} else if (platform === "win32") throw new Error();
|
||||
else {
|
||||
const cores = os.cpus().filter(function(cpu, index) {
|
||||
const hasHyperthreading = cpu.model.includes("Intel");
|
||||
const isOdd = index % 2 === 1;
|
||||
return !hasHyperthreading || isOdd;
|
||||
});
|
||||
amount = cores.length;
|
||||
}
|
||||
} catch {
|
||||
amount = os.cpus().length;
|
||||
}
|
||||
if (amount === 0) amount = os.cpus().length;
|
||||
|
||||
//#endregion
|
||||
//#region src/runtime/thread-worker.ts
|
||||
var ThreadWorker = class {
|
||||
name = "ThreadWorker";
|
||||
runtime = "worker_threads";
|
||||
initialize(options) {
|
||||
this.thread = new Worker(fileURLToPath(import.meta.url + "/../entry/worker.js"), options);
|
||||
this.threadId = this.thread.threadId;
|
||||
}
|
||||
async terminate() {
|
||||
const output = await this.thread.terminate();
|
||||
this.channel?.onClose?.();
|
||||
return output;
|
||||
}
|
||||
postMessage(message, transferListItem) {
|
||||
return this.thread.postMessage(message, transferListItem);
|
||||
}
|
||||
on(event, callback) {
|
||||
return this.thread.on(event, callback);
|
||||
}
|
||||
once(event, callback) {
|
||||
return this.thread.once(event, callback);
|
||||
}
|
||||
emit(event, ...data) {
|
||||
return this.thread.emit(event, ...data);
|
||||
}
|
||||
ref() {
|
||||
return this.thread.ref();
|
||||
}
|
||||
unref() {
|
||||
return this.thread.unref();
|
||||
}
|
||||
setChannel(channel) {
|
||||
if (channel.onMessage) throw new Error("{ runtime: 'worker_threads' } doesn't support channel.onMessage. Use transferListItem for listening to messages instead.");
|
||||
if (channel.postMessage) throw new Error("{ runtime: 'worker_threads' } doesn't support channel.postMessage. Use transferListItem for sending to messages instead.");
|
||||
if (this.channel && this.channel !== channel) this.channel.onClose?.();
|
||||
this.channel = channel;
|
||||
}
|
||||
};
|
||||
|
||||
//#endregion
|
||||
//#region src/runtime/process-worker.ts
|
||||
const __tinypool_worker_message__ = true;
|
||||
const SIGKILL_TIMEOUT = 1e3;
|
||||
var ProcessWorker = class {
|
||||
name = "ProcessWorker";
|
||||
runtime = "child_process";
|
||||
isTerminating = false;
|
||||
initialize(options) {
|
||||
this.process = fork(fileURLToPath(import.meta.url + "/../entry/process.js"), options.argv, {
|
||||
...options,
|
||||
stdio: "pipe",
|
||||
env: {
|
||||
...options.env,
|
||||
TINYPOOL_WORKER_ID: options.workerData[0].workerId.toString()
|
||||
}
|
||||
});
|
||||
process.stdout.setMaxListeners(1 + process.stdout.getMaxListeners());
|
||||
process.stderr.setMaxListeners(1 + process.stderr.getMaxListeners());
|
||||
this.process.stdout?.pipe(process.stdout);
|
||||
this.process.stderr?.pipe(process.stderr);
|
||||
this.threadId = this.process.pid;
|
||||
this.process.on("exit", this.onUnexpectedExit);
|
||||
this.waitForExit = new Promise((r) => this.process.on("exit", r));
|
||||
}
|
||||
onUnexpectedExit = () => {
|
||||
this.process.emit("error", new Error("Worker exited unexpectedly"));
|
||||
};
|
||||
async terminate() {
|
||||
this.isTerminating = true;
|
||||
this.process.off("exit", this.onUnexpectedExit);
|
||||
const sigkillTimeout = setTimeout(() => this.process.kill("SIGKILL"), SIGKILL_TIMEOUT);
|
||||
this.process.kill();
|
||||
await this.waitForExit;
|
||||
this.process.stdout?.unpipe(process.stdout);
|
||||
this.process.stderr?.unpipe(process.stderr);
|
||||
this.port?.close();
|
||||
this.channel?.onClose?.();
|
||||
clearTimeout(sigkillTimeout);
|
||||
}
|
||||
setChannel(channel) {
|
||||
if (this.channel && this.channel !== channel) this.channel.onClose?.();
|
||||
this.channel = channel;
|
||||
this.channel.onMessage?.((message) => {
|
||||
this.send(message);
|
||||
});
|
||||
}
|
||||
send(message) {
|
||||
if (!this.isTerminating) this.process.send(message);
|
||||
}
|
||||
postMessage(message, transferListItem) {
|
||||
transferListItem?.forEach((item) => {
|
||||
if (item instanceof MessagePort) {
|
||||
this.port = item;
|
||||
this.port.start();
|
||||
}
|
||||
});
|
||||
if (this.port) this.port.on("message", (message$1) => this.send({
|
||||
...message$1,
|
||||
source: "port",
|
||||
__tinypool_worker_message__
|
||||
}));
|
||||
return this.send({
|
||||
...message,
|
||||
source: "pool",
|
||||
__tinypool_worker_message__
|
||||
});
|
||||
}
|
||||
on(event, callback) {
|
||||
return this.process.on(event, (data) => {
|
||||
if (event === "error") return callback(data);
|
||||
if (!data || !data.__tinypool_worker_message__) return this.channel?.postMessage?.(data);
|
||||
if (data.source === "pool") callback(data);
|
||||
else if (data.source === "port") this.port.postMessage(data);
|
||||
});
|
||||
}
|
||||
once(event, callback) {
|
||||
return this.process.once(event, callback);
|
||||
}
|
||||
emit(event, ...data) {
|
||||
return this.process.emit(event, ...data);
|
||||
}
|
||||
ref() {
|
||||
return this.process.ref();
|
||||
}
|
||||
unref() {
|
||||
this.port?.unref();
|
||||
this.process.channel?.unref?.();
|
||||
if (hasUnref(this.process.stdout)) this.process.stdout.unref();
|
||||
if (hasUnref(this.process.stderr)) this.process.stderr.unref();
|
||||
return this.process.unref();
|
||||
}
|
||||
};
|
||||
function hasUnref(stream) {
|
||||
return stream != null && "unref" in stream && typeof stream.unref === "function";
|
||||
}
|
||||
|
||||
//#endregion
|
||||
//#region src/index.ts
|
||||
const cpuCount = amount;
|
||||
function onabort(abortSignal, listener) {
|
||||
if ("addEventListener" in abortSignal) abortSignal.addEventListener("abort", listener, { once: true });
|
||||
else abortSignal.once("abort", listener);
|
||||
}
|
||||
var AbortError = class extends Error {
|
||||
constructor() {
|
||||
super("The task has been aborted");
|
||||
}
|
||||
get name() {
|
||||
return "AbortError";
|
||||
}
|
||||
};
|
||||
var CancelError = class extends Error {
|
||||
constructor() {
|
||||
super("The task has been cancelled");
|
||||
}
|
||||
get name() {
|
||||
return "CancelError";
|
||||
}
|
||||
};
|
||||
var ArrayTaskQueue = class {
|
||||
tasks = [];
|
||||
get size() {
|
||||
return this.tasks.length;
|
||||
}
|
||||
shift() {
|
||||
return this.tasks.shift();
|
||||
}
|
||||
push(task) {
|
||||
this.tasks.push(task);
|
||||
}
|
||||
remove(task) {
|
||||
const index = this.tasks.indexOf(task);
|
||||
assert.notStrictEqual(index, -1);
|
||||
this.tasks.splice(index, 1);
|
||||
}
|
||||
cancel() {
|
||||
while (this.tasks.length > 0) {
|
||||
const task = this.tasks.pop();
|
||||
task?.cancel();
|
||||
}
|
||||
}
|
||||
};
|
||||
const kDefaultOptions = {
|
||||
filename: null,
|
||||
name: "default",
|
||||
runtime: "worker_threads",
|
||||
minThreads: Math.max(cpuCount / 2, 1),
|
||||
maxThreads: cpuCount,
|
||||
idleTimeout: 0,
|
||||
maxQueue: Infinity,
|
||||
concurrentTasksPerWorker: 1,
|
||||
useAtomics: true,
|
||||
taskQueue: new ArrayTaskQueue(),
|
||||
trackUnmanagedFds: true
|
||||
};
|
||||
const kDefaultRunOptions = {
|
||||
transferList: void 0,
|
||||
filename: null,
|
||||
signal: null,
|
||||
name: null
|
||||
};
|
||||
var DirectlyTransferable = class {
|
||||
#value;
|
||||
constructor(value) {
|
||||
this.#value = value;
|
||||
}
|
||||
get [kTransferable]() {
|
||||
return this.#value;
|
||||
}
|
||||
get [kValue]() {
|
||||
return this.#value;
|
||||
}
|
||||
};
|
||||
var ArrayBufferViewTransferable = class {
|
||||
#view;
|
||||
constructor(view) {
|
||||
this.#view = view;
|
||||
}
|
||||
get [kTransferable]() {
|
||||
return this.#view.buffer;
|
||||
}
|
||||
get [kValue]() {
|
||||
return this.#view;
|
||||
}
|
||||
};
|
||||
let taskIdCounter = 0;
|
||||
function maybeFileURLToPath(filename) {
|
||||
return filename.startsWith("file:") ? fileURLToPath(new URL(filename)) : filename;
|
||||
}
|
||||
var TaskInfo = class extends AsyncResource {
|
||||
abortListener = null;
|
||||
workerInfo = null;
|
||||
constructor(task, transferList, filename, name, callback, abortSignal, triggerAsyncId, channel) {
|
||||
super("Tinypool.Task", {
|
||||
requireManualDestroy: true,
|
||||
triggerAsyncId
|
||||
});
|
||||
this.callback = callback;
|
||||
this.task = task;
|
||||
this.transferList = transferList;
|
||||
this.cancel = () => this.callback(new CancelError(), null);
|
||||
this.channel = channel;
|
||||
if (isMovable(task)) {
|
||||
/* istanbul ignore if */
|
||||
if (this.transferList == null) this.transferList = [];
|
||||
this.transferList = this.transferList.concat(task[kTransferable]);
|
||||
this.task = task[kValue];
|
||||
}
|
||||
this.filename = filename;
|
||||
this.name = name;
|
||||
this.taskId = taskIdCounter++;
|
||||
this.abortSignal = abortSignal;
|
||||
this.created = performance.now();
|
||||
this.started = 0;
|
||||
}
|
||||
releaseTask() {
|
||||
const ret = this.task;
|
||||
this.task = null;
|
||||
return ret;
|
||||
}
|
||||
done(err, result) {
|
||||
this.emitDestroy();
|
||||
this.runInAsyncScope(this.callback, null, err, result);
|
||||
if (this.abortSignal && this.abortListener) if ("removeEventListener" in this.abortSignal && this.abortListener) this.abortSignal.removeEventListener("abort", this.abortListener);
|
||||
else this.abortSignal.off("abort", this.abortListener);
|
||||
}
|
||||
get [kQueueOptions]() {
|
||||
return kQueueOptions in this.task ? this.task[kQueueOptions] : null;
|
||||
}
|
||||
};
|
||||
var AsynchronouslyCreatedResource = class {
|
||||
onreadyListeners = [];
|
||||
markAsReady() {
|
||||
const listeners = this.onreadyListeners;
|
||||
assert(listeners !== null);
|
||||
this.onreadyListeners = null;
|
||||
for (const listener of listeners) listener();
|
||||
}
|
||||
isReady() {
|
||||
return this.onreadyListeners === null;
|
||||
}
|
||||
onReady(fn) {
|
||||
if (this.onreadyListeners === null) {
|
||||
fn();
|
||||
return;
|
||||
}
|
||||
this.onreadyListeners.push(fn);
|
||||
}
|
||||
};
|
||||
var AsynchronouslyCreatedResourcePool = class {
|
||||
pendingItems = new Set();
|
||||
readyItems = new Set();
|
||||
constructor(maximumUsage) {
|
||||
this.maximumUsage = maximumUsage;
|
||||
this.onAvailableListeners = [];
|
||||
}
|
||||
add(item) {
|
||||
this.pendingItems.add(item);
|
||||
item.onReady(() => {
|
||||
/* istanbul ignore else */
|
||||
if (this.pendingItems.has(item)) {
|
||||
this.pendingItems.delete(item);
|
||||
this.readyItems.add(item);
|
||||
this.maybeAvailable(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
delete(item) {
|
||||
this.pendingItems.delete(item);
|
||||
this.readyItems.delete(item);
|
||||
}
|
||||
findAvailable() {
|
||||
let minUsage = this.maximumUsage;
|
||||
let candidate = null;
|
||||
for (const item of this.readyItems) {
|
||||
const usage = item.currentUsage();
|
||||
if (usage === 0) return item;
|
||||
if (usage < minUsage) {
|
||||
candidate = item;
|
||||
minUsage = usage;
|
||||
}
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
*[Symbol.iterator]() {
|
||||
yield* this.pendingItems;
|
||||
yield* this.readyItems;
|
||||
}
|
||||
get size() {
|
||||
return this.pendingItems.size + this.readyItems.size;
|
||||
}
|
||||
maybeAvailable(item) {
|
||||
/* istanbul ignore else */
|
||||
if (item.currentUsage() < this.maximumUsage) for (const listener of this.onAvailableListeners) listener(item);
|
||||
}
|
||||
onAvailable(fn) {
|
||||
this.onAvailableListeners.push(fn);
|
||||
}
|
||||
};
|
||||
const Errors = {
|
||||
ThreadTermination: () => new Error("Terminating worker thread"),
|
||||
FilenameNotProvided: () => new Error("filename must be provided to run() or in options object"),
|
||||
TaskQueueAtLimit: () => new Error("Task queue is at limit"),
|
||||
NoTaskQueueAvailable: () => new Error("No task queue available and all Workers are busy")
|
||||
};
|
||||
var WorkerInfo = class extends AsynchronouslyCreatedResource {
|
||||
idleTimeout = null;
|
||||
lastSeenResponseCount = 0;
|
||||
constructor(worker, port, workerId, freeWorkerId, onMessage, filename, teardown) {
|
||||
super();
|
||||
this.worker = worker;
|
||||
this.workerId = workerId;
|
||||
this.freeWorkerId = freeWorkerId;
|
||||
this.teardown = teardown;
|
||||
this.filename = filename;
|
||||
this.port = port;
|
||||
this.port.on("message", (message) => this._handleResponse(message));
|
||||
this.onMessage = onMessage;
|
||||
this.taskInfos = new Map();
|
||||
this.sharedBuffer = new Int32Array(new SharedArrayBuffer(kFieldCount * Int32Array.BYTES_PER_ELEMENT));
|
||||
}
|
||||
async destroy(timeout) {
|
||||
let resolve;
|
||||
let reject;
|
||||
const ret = new Promise((res, rej) => {
|
||||
resolve = res;
|
||||
reject = rej;
|
||||
});
|
||||
if (this.teardown && this.filename) {
|
||||
const { teardown, filename } = this;
|
||||
await new Promise((resolve$1, reject$1) => {
|
||||
this.postTask(new TaskInfo({}, [], filename, teardown, (error, result) => error ? reject$1(error) : resolve$1(result), null, 1, void 0));
|
||||
});
|
||||
}
|
||||
const timer = timeout ? setTimeout(() => reject(new Error("Failed to terminate worker")), timeout) : null;
|
||||
this.worker.terminate().then(() => {
|
||||
if (timer !== null) clearTimeout(timer);
|
||||
this.port.close();
|
||||
this.clearIdleTimeout();
|
||||
for (const taskInfo of this.taskInfos.values()) taskInfo.done(Errors.ThreadTermination());
|
||||
this.taskInfos.clear();
|
||||
resolve();
|
||||
});
|
||||
return ret;
|
||||
}
|
||||
clearIdleTimeout() {
|
||||
if (this.idleTimeout !== null) {
|
||||
clearTimeout(this.idleTimeout);
|
||||
this.idleTimeout = null;
|
||||
}
|
||||
}
|
||||
ref() {
|
||||
this.port.ref();
|
||||
return this;
|
||||
}
|
||||
unref() {
|
||||
this.port.unref();
|
||||
return this;
|
||||
}
|
||||
_handleResponse(message) {
|
||||
this.usedMemory = message.usedMemory;
|
||||
this.onMessage(message);
|
||||
if (this.taskInfos.size === 0) this.unref();
|
||||
}
|
||||
postTask(taskInfo) {
|
||||
assert(!this.taskInfos.has(taskInfo.taskId));
|
||||
const message = {
|
||||
task: taskInfo.releaseTask(),
|
||||
taskId: taskInfo.taskId,
|
||||
filename: taskInfo.filename,
|
||||
name: taskInfo.name
|
||||
};
|
||||
try {
|
||||
if (taskInfo.channel) this.worker.setChannel?.(taskInfo.channel);
|
||||
this.port.postMessage(message, taskInfo.transferList);
|
||||
} catch (err) {
|
||||
taskInfo.done(err);
|
||||
return;
|
||||
}
|
||||
taskInfo.workerInfo = this;
|
||||
this.taskInfos.set(taskInfo.taskId, taskInfo);
|
||||
this.ref();
|
||||
this.clearIdleTimeout();
|
||||
Atomics.add(this.sharedBuffer, kRequestCountField, 1);
|
||||
Atomics.notify(this.sharedBuffer, kRequestCountField, 1);
|
||||
}
|
||||
processPendingMessages() {
|
||||
const actualResponseCount = Atomics.load(this.sharedBuffer, kResponseCountField);
|
||||
if (actualResponseCount !== this.lastSeenResponseCount) {
|
||||
this.lastSeenResponseCount = actualResponseCount;
|
||||
let entry;
|
||||
while ((entry = receiveMessageOnPort(this.port)) !== void 0) this._handleResponse(entry.message);
|
||||
}
|
||||
}
|
||||
isRunningAbortableTask() {
|
||||
if (this.taskInfos.size !== 1) return false;
|
||||
const [first] = this.taskInfos;
|
||||
const [, task] = first || [];
|
||||
return task?.abortSignal !== null;
|
||||
}
|
||||
currentUsage() {
|
||||
if (this.isRunningAbortableTask()) return Infinity;
|
||||
return this.taskInfos.size;
|
||||
}
|
||||
};
|
||||
var ThreadPool = class {
|
||||
skipQueue = [];
|
||||
completed = 0;
|
||||
start = performance.now();
|
||||
inProcessPendingMessages = false;
|
||||
startingUp = false;
|
||||
workerFailsDuringBootstrap = false;
|
||||
constructor(publicInterface, options) {
|
||||
this.publicInterface = publicInterface;
|
||||
this.taskQueue = options.taskQueue || new ArrayTaskQueue();
|
||||
const filename = options.filename ? maybeFileURLToPath(options.filename) : null;
|
||||
this.options = {
|
||||
...kDefaultOptions,
|
||||
...options,
|
||||
filename,
|
||||
maxQueue: 0
|
||||
};
|
||||
if (options.maxThreads !== void 0 && this.options.minThreads >= options.maxThreads) this.options.minThreads = options.maxThreads;
|
||||
if (options.minThreads !== void 0 && this.options.maxThreads <= options.minThreads) this.options.maxThreads = options.minThreads;
|
||||
if (options.maxQueue === "auto") this.options.maxQueue = this.options.maxThreads ** 2;
|
||||
else this.options.maxQueue = options.maxQueue ?? kDefaultOptions.maxQueue;
|
||||
this.workerIds = new Map(new Array(this.options.maxThreads).fill(0).map((_, i) => [i + 1, true]));
|
||||
this.workers = new AsynchronouslyCreatedResourcePool(this.options.concurrentTasksPerWorker);
|
||||
this.workers.onAvailable((w) => this._onWorkerAvailable(w));
|
||||
this.startingUp = true;
|
||||
this._ensureMinimumWorkers();
|
||||
this.startingUp = false;
|
||||
}
|
||||
_ensureEnoughWorkersForTaskQueue() {
|
||||
while (this.workers.size < this.taskQueue.size && this.workers.size < this.options.maxThreads) this._addNewWorker();
|
||||
}
|
||||
_ensureMaximumWorkers() {
|
||||
while (this.workers.size < this.options.maxThreads) this._addNewWorker();
|
||||
}
|
||||
_ensureMinimumWorkers() {
|
||||
while (this.workers.size < this.options.minThreads) this._addNewWorker();
|
||||
}
|
||||
_addNewWorker() {
|
||||
const workerIds = this.workerIds;
|
||||
let workerId;
|
||||
workerIds.forEach((isIdAvailable, _workerId$1) => {
|
||||
if (isIdAvailable && !workerId) {
|
||||
workerId = _workerId$1;
|
||||
workerIds.set(_workerId$1, false);
|
||||
}
|
||||
});
|
||||
const tinypoolPrivateData = { workerId };
|
||||
const worker = this.options.runtime === "child_process" ? new ProcessWorker() : new ThreadWorker();
|
||||
worker.initialize({
|
||||
env: this.options.env,
|
||||
argv: this.options.argv,
|
||||
execArgv: this.options.execArgv,
|
||||
resourceLimits: this.options.resourceLimits,
|
||||
workerData: [tinypoolPrivateData, this.options.workerData],
|
||||
trackUnmanagedFds: this.options.trackUnmanagedFds
|
||||
});
|
||||
const onMessage = (message$1) => {
|
||||
const { taskId, result } = message$1;
|
||||
const taskInfo = workerInfo.taskInfos.get(taskId);
|
||||
workerInfo.taskInfos.delete(taskId);
|
||||
if (!this.shouldRecycleWorker(taskInfo)) this.workers.maybeAvailable(workerInfo);
|
||||
/* istanbul ignore if */
|
||||
if (taskInfo === void 0) {
|
||||
const err = new Error(`Unexpected message from Worker: ${inspect(message$1)}`);
|
||||
this.publicInterface.emit("error", err);
|
||||
} else taskInfo.done(message$1.error, result);
|
||||
this._processPendingMessages();
|
||||
};
|
||||
const { port1, port2 } = new MessageChannel();
|
||||
const workerInfo = new WorkerInfo(worker, port1, workerId, () => workerIds.set(workerId, true), onMessage, this.options.filename, this.options.teardown);
|
||||
if (this.startingUp) workerInfo.markAsReady();
|
||||
const message = {
|
||||
filename: this.options.filename,
|
||||
name: this.options.name,
|
||||
port: port2,
|
||||
sharedBuffer: workerInfo.sharedBuffer,
|
||||
useAtomics: this.options.useAtomics
|
||||
};
|
||||
worker.postMessage(message, [port2]);
|
||||
worker.on("message", (message$1) => {
|
||||
if (message$1.ready === true) {
|
||||
port1.start();
|
||||
if (workerInfo.currentUsage() === 0) workerInfo.unref();
|
||||
if (!workerInfo.isReady()) workerInfo.markAsReady();
|
||||
return;
|
||||
}
|
||||
worker.emit("error", new Error(`Unexpected message on Worker: ${inspect(message$1)}`));
|
||||
});
|
||||
worker.on("error", (err) => {
|
||||
worker.ref = () => {};
|
||||
const taskInfos = [...workerInfo.taskInfos.values()];
|
||||
workerInfo.taskInfos.clear();
|
||||
this._removeWorker(workerInfo);
|
||||
if (workerInfo.isReady() && !this.workerFailsDuringBootstrap) this._ensureMinimumWorkers();
|
||||
else this.workerFailsDuringBootstrap = true;
|
||||
if (taskInfos.length > 0) for (const taskInfo of taskInfos) taskInfo.done(err, null);
|
||||
else this.publicInterface.emit("error", err);
|
||||
});
|
||||
worker.unref();
|
||||
port1.on("close", () => {
|
||||
worker.ref();
|
||||
});
|
||||
this.workers.add(workerInfo);
|
||||
}
|
||||
_processPendingMessages() {
|
||||
if (this.inProcessPendingMessages || !this.options.useAtomics) return;
|
||||
this.inProcessPendingMessages = true;
|
||||
try {
|
||||
for (const workerInfo of this.workers) workerInfo.processPendingMessages();
|
||||
} finally {
|
||||
this.inProcessPendingMessages = false;
|
||||
}
|
||||
}
|
||||
_removeWorker(workerInfo) {
|
||||
workerInfo.freeWorkerId();
|
||||
this.workers.delete(workerInfo);
|
||||
return workerInfo.destroy(this.options.terminateTimeout);
|
||||
}
|
||||
_onWorkerAvailable(workerInfo) {
|
||||
while ((this.taskQueue.size > 0 || this.skipQueue.length > 0) && workerInfo.currentUsage() < this.options.concurrentTasksPerWorker) {
|
||||
const taskInfo = this.skipQueue.shift() || this.taskQueue.shift();
|
||||
if (taskInfo.abortSignal && workerInfo.taskInfos.size > 0) {
|
||||
this.skipQueue.push(taskInfo);
|
||||
break;
|
||||
}
|
||||
const now = performance.now();
|
||||
taskInfo.started = now;
|
||||
workerInfo.postTask(taskInfo);
|
||||
this._maybeDrain();
|
||||
return;
|
||||
}
|
||||
if (workerInfo.taskInfos.size === 0 && this.workers.size > this.options.minThreads) workerInfo.idleTimeout = setTimeout(() => {
|
||||
assert.strictEqual(workerInfo.taskInfos.size, 0);
|
||||
if (this.workers.size > this.options.minThreads) this._removeWorker(workerInfo);
|
||||
}, this.options.idleTimeout).unref();
|
||||
}
|
||||
runTask(task, options) {
|
||||
let { filename, name } = options;
|
||||
const { transferList = [], signal = null, channel } = options;
|
||||
if (filename == null) filename = this.options.filename;
|
||||
if (name == null) name = this.options.name;
|
||||
if (typeof filename !== "string") return Promise.reject(Errors.FilenameNotProvided());
|
||||
filename = maybeFileURLToPath(filename);
|
||||
let resolve;
|
||||
let reject;
|
||||
const ret = new Promise((res, rej) => {
|
||||
resolve = res;
|
||||
reject = rej;
|
||||
});
|
||||
const taskInfo = new TaskInfo(task, transferList, filename, name, (err, result) => {
|
||||
this.completed++;
|
||||
if (err !== null) reject(err);
|
||||
if (this.shouldRecycleWorker(taskInfo)) this._removeWorker(taskInfo.workerInfo).then(() => this._ensureMinimumWorkers()).then(() => this._ensureEnoughWorkersForTaskQueue()).then(() => resolve(result)).catch(reject);
|
||||
else resolve(result);
|
||||
}, signal, this.publicInterface.asyncResource.asyncId(), channel);
|
||||
if (signal !== null) {
|
||||
if (signal.aborted) return Promise.reject(new AbortError());
|
||||
taskInfo.abortListener = () => {
|
||||
reject(new AbortError());
|
||||
if (taskInfo.workerInfo !== null) {
|
||||
this._removeWorker(taskInfo.workerInfo);
|
||||
this._ensureMinimumWorkers();
|
||||
} else this.taskQueue.remove(taskInfo);
|
||||
};
|
||||
onabort(signal, taskInfo.abortListener);
|
||||
}
|
||||
if (this.taskQueue.size > 0) {
|
||||
const totalCapacity = this.options.maxQueue + this.pendingCapacity();
|
||||
if (this.taskQueue.size >= totalCapacity) if (this.options.maxQueue === 0) return Promise.reject(Errors.NoTaskQueueAvailable());
|
||||
else return Promise.reject(Errors.TaskQueueAtLimit());
|
||||
else {
|
||||
if (this.workers.size < this.options.maxThreads) this._addNewWorker();
|
||||
this.taskQueue.push(taskInfo);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
let workerInfo = this.workers.findAvailable();
|
||||
if (workerInfo !== null && workerInfo.currentUsage() > 0 && signal) workerInfo = null;
|
||||
let waitingForNewWorker = false;
|
||||
if ((workerInfo === null || workerInfo.currentUsage() > 0) && this.workers.size < this.options.maxThreads) {
|
||||
this._addNewWorker();
|
||||
waitingForNewWorker = true;
|
||||
}
|
||||
if (workerInfo === null) {
|
||||
if (this.options.maxQueue <= 0 && !waitingForNewWorker) return Promise.reject(Errors.NoTaskQueueAvailable());
|
||||
else this.taskQueue.push(taskInfo);
|
||||
return ret;
|
||||
}
|
||||
const now = performance.now();
|
||||
taskInfo.started = now;
|
||||
workerInfo.postTask(taskInfo);
|
||||
this._maybeDrain();
|
||||
return ret;
|
||||
}
|
||||
shouldRecycleWorker(taskInfo) {
|
||||
if (taskInfo?.workerInfo?.shouldRecycle) return true;
|
||||
if (this.options.isolateWorkers && taskInfo?.workerInfo) return true;
|
||||
if (!this.options.isolateWorkers && this.options.maxMemoryLimitBeforeRecycle !== void 0 && (taskInfo?.workerInfo?.usedMemory || 0) > this.options.maxMemoryLimitBeforeRecycle) return true;
|
||||
return false;
|
||||
}
|
||||
pendingCapacity() {
|
||||
return this.workers.pendingItems.size * this.options.concurrentTasksPerWorker;
|
||||
}
|
||||
_maybeDrain() {
|
||||
if (this.taskQueue.size === 0 && this.skipQueue.length === 0) this.publicInterface.emit("drain");
|
||||
}
|
||||
async destroy() {
|
||||
while (this.skipQueue.length > 0) {
|
||||
const taskInfo = this.skipQueue.shift();
|
||||
taskInfo.done(new Error("Terminating worker thread"));
|
||||
}
|
||||
while (this.taskQueue.size > 0) {
|
||||
const taskInfo = this.taskQueue.shift();
|
||||
taskInfo.done(new Error("Terminating worker thread"));
|
||||
}
|
||||
const exitEvents = [];
|
||||
while (this.workers.size > 0) {
|
||||
const [workerInfo] = this.workers;
|
||||
exitEvents.push(once(workerInfo.worker, "exit"));
|
||||
this._removeWorker(workerInfo);
|
||||
}
|
||||
await Promise.all(exitEvents);
|
||||
}
|
||||
async recycleWorkers(options = {}) {
|
||||
const runtimeChanged = options?.runtime && options.runtime !== this.options.runtime;
|
||||
if (options?.runtime) this.options.runtime = options.runtime;
|
||||
if (this.options.isolateWorkers && !runtimeChanged) return;
|
||||
const exitEvents = [];
|
||||
Array.from(this.workers).filter((workerInfo) => {
|
||||
if (workerInfo.currentUsage() === 0) {
|
||||
exitEvents.push(once(workerInfo.worker, "exit"));
|
||||
this._removeWorker(workerInfo);
|
||||
} else workerInfo.shouldRecycle = true;
|
||||
});
|
||||
await Promise.all(exitEvents);
|
||||
this._ensureMinimumWorkers();
|
||||
}
|
||||
};
|
||||
var Tinypool = class extends EventEmitterAsyncResource {
|
||||
#pool;
|
||||
constructor(options = {}) {
|
||||
if (options.minThreads !== void 0 && options.minThreads > 0 && options.minThreads < 1) options.minThreads = Math.max(1, Math.floor(options.minThreads * cpuCount));
|
||||
if (options.maxThreads !== void 0 && options.maxThreads > 0 && options.maxThreads < 1) options.maxThreads = Math.max(1, Math.floor(options.maxThreads * cpuCount));
|
||||
super({
|
||||
...options,
|
||||
name: "Tinypool"
|
||||
});
|
||||
if (options.minThreads !== void 0 && options.maxThreads !== void 0 && options.minThreads > options.maxThreads) throw new RangeError("options.minThreads and options.maxThreads must not conflict");
|
||||
this.#pool = new ThreadPool(this, options);
|
||||
}
|
||||
run(task, options = kDefaultRunOptions) {
|
||||
const { transferList, filename, name, signal, runtime, channel } = options;
|
||||
return this.#pool.runTask(task, {
|
||||
transferList,
|
||||
filename,
|
||||
name,
|
||||
signal,
|
||||
runtime,
|
||||
channel
|
||||
});
|
||||
}
|
||||
async destroy() {
|
||||
await this.#pool.destroy();
|
||||
this.emitDestroy();
|
||||
}
|
||||
get options() {
|
||||
return this.#pool.options;
|
||||
}
|
||||
get threads() {
|
||||
const ret = [];
|
||||
for (const workerInfo of this.#pool.workers) ret.push(workerInfo.worker);
|
||||
return ret;
|
||||
}
|
||||
get queueSize() {
|
||||
const pool = this.#pool;
|
||||
return Math.max(pool.taskQueue.size - pool.pendingCapacity(), 0);
|
||||
}
|
||||
cancelPendingTasks() {
|
||||
const pool = this.#pool;
|
||||
pool.taskQueue.cancel();
|
||||
}
|
||||
async recycleWorkers(options = {}) {
|
||||
await this.#pool.recycleWorkers(options);
|
||||
}
|
||||
get completed() {
|
||||
return this.#pool.completed;
|
||||
}
|
||||
get duration() {
|
||||
return performance.now() - this.#pool.start;
|
||||
}
|
||||
static get isWorkerThread() {
|
||||
return process.__tinypool_state__?.isWorkerThread || false;
|
||||
}
|
||||
static get workerData() {
|
||||
return process.__tinypool_state__?.workerData || void 0;
|
||||
}
|
||||
static get version() {
|
||||
const { version } = JSON.parse(readFileSync(join(__dirname, "../package.json"), "utf-8"));
|
||||
return version;
|
||||
}
|
||||
static move(val) {
|
||||
if (val != null && typeof val === "object" && typeof val !== "function") {
|
||||
if (!isTransferable(val)) if (types.isArrayBufferView(val)) val = new ArrayBufferViewTransferable(val);
|
||||
else val = new DirectlyTransferable(val);
|
||||
markMovable(val);
|
||||
}
|
||||
return val;
|
||||
}
|
||||
static get transferableSymbol() {
|
||||
return kTransferable;
|
||||
}
|
||||
static get valueSymbol() {
|
||||
return kValue;
|
||||
}
|
||||
static get queueOptionsSymbol() {
|
||||
return kQueueOptions;
|
||||
}
|
||||
};
|
||||
const _workerId = process.__tinypool_state__?.workerId;
|
||||
var src_default = Tinypool;
|
||||
|
||||
//#endregion
|
||||
export { Tinypool, src_default as default, isMovable, isTaskQueue, isTransferable, kFieldCount, kQueueOptions, kRequestCountField, kResponseCountField, kTransferable, kValue, markMovable, _workerId as workerId };
|
||||
Loading…
Add table
Add a link
Reference in a new issue