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:
Ole-Morten Duesund 2026-03-08 17:41:38 +01:00
commit e8428de775
12051 changed files with 1799735 additions and 0 deletions

28
pwa/node_modules/tinypool/dist/common-Qw-RoVFD.js generated vendored Normal file
View file

@ -0,0 +1,28 @@
//#region src/common.ts
const kMovable = Symbol("Tinypool.kMovable");
const kTransferable = Symbol.for("Tinypool.transferable");
const kValue = Symbol.for("Tinypool.valueOf");
const kQueueOptions = Symbol.for("Tinypool.queueOptions");
function isTransferable(value) {
return value != null && typeof value === "object" && kTransferable in value && kValue in value;
}
function isMovable(value) {
return isTransferable(value) && value[kMovable] === true;
}
function markMovable(value) {
Object.defineProperty(value, kMovable, {
enumerable: false,
configurable: true,
writable: true,
value: true
});
}
function isTaskQueue(value) {
return typeof value === "object" && value !== null && "size" in value && typeof value.shift === "function" && typeof value.remove === "function" && typeof value.push === "function";
}
const kRequestCountField = 0;
const kResponseCountField = 1;
const kFieldCount = 2;
//#endregion
export { isMovable, isTaskQueue, isTransferable, kFieldCount, kQueueOptions, kRequestCountField, kResponseCountField, kTransferable, kValue, markMovable };

1
pwa/node_modules/tinypool/dist/entry/process.d.ts generated vendored Normal file
View file

@ -0,0 +1 @@
export { };

72
pwa/node_modules/tinypool/dist/entry/process.js generated vendored Normal file
View file

@ -0,0 +1,72 @@
import { stderr, stdout } from "../utils-De75vAgL.js";
import { getHandler, throwInNextTick } from "../utils-B--2TaWv.js";
//#region src/entry/process.ts
process.__tinypool_state__ = {
isChildProcess: true,
isTinypoolWorker: true,
workerData: null,
workerId: Number(process.env.TINYPOOL_WORKER_ID)
};
const memoryUsage = process.memoryUsage.bind(process);
const send = process.send.bind(process);
process.on("message", (message) => {
if (!message || !message.__tinypool_worker_message__) return;
if (message.source === "pool") {
const { filename, name } = message;
(async function() {
if (filename !== null) await getHandler(filename, name);
send({
ready: true,
source: "pool",
__tinypool_worker_message__: true
}, () => {});
})().catch(throwInNextTick);
return;
}
if (message.source === "port") {
onMessage(message).catch(throwInNextTick);
return;
}
throw new Error(`Unexpected TinypoolWorkerMessage ${JSON.stringify(message)}`);
});
async function onMessage(message) {
const { taskId, task, filename, name } = message;
let response;
try {
const handler = await getHandler(filename, name);
if (handler === null) throw new Error(`No handler function "${name}" exported from "${filename}"`);
const result = await handler(task);
response = {
source: "port",
__tinypool_worker_message__: true,
taskId,
result,
error: null,
usedMemory: memoryUsage().heapUsed
};
if (stdout()?.writableLength > 0) await new Promise((resolve) => process.stdout.write("", resolve));
if (stderr()?.writableLength > 0) await new Promise((resolve) => process.stderr.write("", resolve));
} catch (error) {
response = {
source: "port",
__tinypool_worker_message__: true,
taskId,
result: null,
error: serializeError(error),
usedMemory: memoryUsage().heapUsed
};
}
send(response);
}
function serializeError(error) {
if (error instanceof Error) return {
...error,
name: error.name,
stack: error.stack,
message: error.message
};
return String(error);
}
//#endregion

7
pwa/node_modules/tinypool/dist/entry/utils.d.ts generated vendored Normal file
View file

@ -0,0 +1,7 @@
//#region src/entry/utils.d.ts
type Handler = Function;
declare function getHandler(filename: string, name: string): Promise<Handler | null>;
declare function throwInNextTick(error: Error): void;
//#endregion
export { getHandler, throwInNextTick };

3
pwa/node_modules/tinypool/dist/entry/utils.js generated vendored Normal file
View file

@ -0,0 +1,3 @@
import { getHandler, throwInNextTick } from "../utils-B--2TaWv.js";
export { getHandler, throwInNextTick };

1
pwa/node_modules/tinypool/dist/entry/worker.d.ts generated vendored Normal file
View file

@ -0,0 +1 @@
export { };

76
pwa/node_modules/tinypool/dist/entry/worker.js generated vendored Normal file
View file

@ -0,0 +1,76 @@
import { isMovable, kRequestCountField, kResponseCountField, kTransferable, kValue } from "../common-Qw-RoVFD.js";
import { stderr, stdout } from "../utils-De75vAgL.js";
import { getHandler, throwInNextTick } from "../utils-B--2TaWv.js";
import { parentPort, receiveMessageOnPort, workerData } from "node:worker_threads";
//#region src/entry/worker.ts
const [tinypoolPrivateData, workerData$1] = workerData;
process.__tinypool_state__ = {
isWorkerThread: true,
isTinypoolWorker: true,
workerData: workerData$1,
workerId: tinypoolPrivateData.workerId
};
const memoryUsage = process.memoryUsage.bind(process);
let useAtomics = process.env.PISCINA_DISABLE_ATOMICS !== "1";
parentPort.on("message", (message) => {
useAtomics = process.env.PISCINA_DISABLE_ATOMICS === "1" ? false : message.useAtomics;
const { port, sharedBuffer, filename, name } = message;
(async function() {
if (filename !== null) await getHandler(filename, name);
const readyMessage = { ready: true };
parentPort.postMessage(readyMessage);
port.start();
port.on("message", onMessage.bind(null, port, sharedBuffer));
atomicsWaitLoop(port, sharedBuffer);
})().catch(throwInNextTick);
});
let currentTasks = 0;
let lastSeenRequestCount = 0;
function atomicsWaitLoop(port, sharedBuffer) {
if (!useAtomics) return;
while (currentTasks === 0) {
Atomics.wait(sharedBuffer, kRequestCountField, lastSeenRequestCount);
lastSeenRequestCount = Atomics.load(sharedBuffer, kRequestCountField);
let entry;
while ((entry = receiveMessageOnPort(port)) !== void 0) onMessage(port, sharedBuffer, entry.message);
}
}
function onMessage(port, sharedBuffer, message) {
currentTasks++;
const { taskId, task, filename, name } = message;
(async function() {
let response;
let transferList = [];
try {
const handler = await getHandler(filename, name);
if (handler === null) throw new Error(`No handler function "${name}" exported from "${filename}"`);
let result = await handler(task);
if (isMovable(result)) {
transferList = transferList.concat(result[kTransferable]);
result = result[kValue];
}
response = {
taskId,
result,
error: null,
usedMemory: memoryUsage().heapUsed
};
if (stdout()?.writableLength > 0) await new Promise((resolve) => process.stdout.write("", resolve));
if (stderr()?.writableLength > 0) await new Promise((resolve) => process.stderr.write("", resolve));
} catch (error) {
response = {
taskId,
result: null,
error,
usedMemory: memoryUsage().heapUsed
};
}
currentTasks--;
port.postMessage(response, transferList);
Atomics.add(sharedBuffer, kResponseCountField, 1);
atomicsWaitLoop(port, sharedBuffer);
})().catch(throwInNextTick);
}
//#endregion

195
pwa/node_modules/tinypool/dist/index.d.ts generated vendored Normal file
View file

@ -0,0 +1,195 @@
/// <reference types="node" />
import { MessagePort, TransferListItem } from "node:worker_threads";
import { EventEmitterAsyncResource } from "node:events";
//#region src/common.d.ts
/** Channel for communicating between main thread and workers */
/** Channel for communicating between main thread and workers */
interface TinypoolChannel {
/** Workers subscribing to messages */
onMessage?: (callback: (message: any) => void) => void;
/** Called with worker's messages */
postMessage?: (message: any) => void;
/** Called when channel can be closed */
onClose?: () => void;
}
interface TinypoolWorker {
runtime: string;
initialize(options: {
env?: Record<string, string>;
argv?: string[];
execArgv?: string[];
resourceLimits?: any;
workerData: TinypoolData;
trackUnmanagedFds?: boolean;
}): void;
terminate(): Promise<any>;
postMessage(message: any, transferListItem?: TransferListItem[]): void;
setChannel?: (channel: TinypoolChannel) => void;
on(event: string, listener: (...args: any[]) => void): void;
once(event: string, listener: (...args: any[]) => void): void;
emit(event: string, ...data: any[]): void;
ref?: () => void;
unref?: () => void;
threadId: number;
}
/**
* Tinypool's internal messaging between main thread and workers.
* - Utilizers can use `__tinypool_worker_message__` property to identify
* these messages and ignore them.
*/
interface TinypoolWorkerMessage<T extends 'port' | 'pool' = 'port' | 'pool'> {
__tinypool_worker_message__: true;
source: T;
}
interface StartupMessage {
filename: string | null;
name: string;
port: MessagePort;
sharedBuffer: Int32Array;
useAtomics: boolean;
}
interface RequestMessage {
taskId: number;
task: any;
filename: string;
name: string;
}
interface ReadyMessage {
ready: true;
}
interface ResponseMessage {
taskId: number;
result: any;
error: unknown | null;
usedMemory: number;
}
interface TinypoolPrivateData {
workerId: number;
}
type TinypoolData = [TinypoolPrivateData, any];
declare const kTransferable: unique symbol;
declare const kValue: unique symbol;
declare const kQueueOptions: unique symbol;
declare function isTransferable(value: any): boolean;
declare function isMovable(value: any): boolean;
declare function markMovable(value: object): void;
interface Transferable {
readonly [kTransferable]: object;
readonly [kValue]: object;
}
interface Task {
readonly [kQueueOptions]: object | null;
cancel(): void;
}
interface TaskQueue {
readonly size: number;
shift(): Task | null;
remove(task: Task): void;
push(task: Task): void;
cancel(): void;
}
declare function isTaskQueue(value: any): boolean;
declare const kRequestCountField = 0;
declare const kResponseCountField = 1;
declare const kFieldCount = 2; //#endregion
//#region src/index.d.ts
declare global {
namespace NodeJS {
interface Process {
__tinypool_state__: {
isTinypoolWorker: boolean;
isWorkerThread?: boolean;
isChildProcess?: boolean;
workerData: any;
workerId: number;
};
}
}
}
interface AbortSignalEventTargetAddOptions {
once: boolean;
}
interface AbortSignalEventTarget {
addEventListener: (name: 'abort', listener: () => void, options?: AbortSignalEventTargetAddOptions) => void;
removeEventListener: (name: 'abort', listener: () => void) => void;
aborted?: boolean;
}
interface AbortSignalEventEmitter {
off: (name: 'abort', listener: () => void) => void;
once: (name: 'abort', listener: () => void) => void;
}
type AbortSignalAny = AbortSignalEventTarget | AbortSignalEventEmitter;
type ResourceLimits = Worker extends {
resourceLimits?: infer T;
} ? T : object;
interface Options {
filename?: string | null;
runtime?: 'worker_threads' | 'child_process';
name?: string;
minThreads?: number;
maxThreads?: number;
idleTimeout?: number;
terminateTimeout?: number;
maxQueue?: number | 'auto';
concurrentTasksPerWorker?: number;
useAtomics?: boolean;
resourceLimits?: ResourceLimits;
maxMemoryLimitBeforeRecycle?: number;
argv?: string[];
execArgv?: string[];
env?: Record<string, string>;
workerData?: any;
taskQueue?: TaskQueue;
trackUnmanagedFds?: boolean;
isolateWorkers?: boolean;
teardown?: string;
}
interface FilledOptions extends Options {
filename: string | null;
name: string;
runtime: NonNullable<Options['runtime']>;
minThreads: number;
maxThreads: number;
idleTimeout: number;
maxQueue: number;
concurrentTasksPerWorker: number;
useAtomics: boolean;
taskQueue: TaskQueue;
}
interface RunOptions {
transferList?: TransferList;
channel?: TinypoolChannel;
filename?: string | null;
signal?: AbortSignalAny | null;
name?: string | null;
runtime?: Options['runtime'];
}
type TransferList = MessagePort extends {
postMessage(value: any, transferList: infer T): any;
} ? T : never;
type TransferListItem$1 = TransferList extends (infer T)[] ? T : never;
declare class Tinypool extends EventEmitterAsyncResource {
#private;
constructor(options?: Options);
run(task: any, options?: RunOptions): Promise<any>;
destroy(): Promise<void>;
get options(): FilledOptions;
get threads(): TinypoolWorker[];
get queueSize(): number;
cancelPendingTasks(): void;
recycleWorkers(options?: Pick<Options, 'runtime'>): Promise<void>;
get completed(): number;
get duration(): number;
static get isWorkerThread(): boolean;
static get workerData(): any;
static get version(): string;
static move(val: Transferable | TransferListItem$1 | ArrayBufferView | ArrayBuffer | MessagePort): MessagePort | ArrayBuffer | Transferable | ArrayBufferView;
static get transferableSymbol(): symbol;
static get valueSymbol(): symbol;
static get queueOptionsSymbol(): symbol;
}
declare const _workerId: number;
//#endregion
export { Options, ReadyMessage, RequestMessage, ResponseMessage, StartupMessage, Task, TaskQueue, Tinypool, TinypoolChannel, TinypoolData, TinypoolPrivateData, TinypoolWorker, TinypoolWorkerMessage, Transferable, Tinypool as default, isMovable, isTaskQueue, isTransferable, kFieldCount, kQueueOptions, kRequestCountField, kResponseCountField, kTransferable, kValue, markMovable, _workerId as workerId };

820
pwa/node_modules/tinypool/dist/index.js generated vendored Normal file
View 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 };

38
pwa/node_modules/tinypool/dist/utils-B--2TaWv.js generated vendored Normal file
View file

@ -0,0 +1,38 @@
import { pathToFileURL } from "node:url";
//#region src/entry/utils.ts
let importESMCached;
function getImportESM() {
if (importESMCached === void 0) importESMCached = new Function("specifier", "return import(specifier)");
return importESMCached;
}
const handlerCache = new Map();
async function getHandler(filename, name) {
let handler = handlerCache.get(`${filename}/${name}`);
if (handler !== void 0) return handler;
try {
const handlerModule = await import(filename);
handler = typeof handlerModule.default !== "function" && handlerModule.default || handlerModule;
if (typeof handler !== "function") handler = await handler[name];
} catch {}
if (typeof handler !== "function") {
handler = await getImportESM()(pathToFileURL(filename).href);
if (typeof handler !== "function") handler = await handler[name];
}
if (typeof handler !== "function") return null;
if (handlerCache.size > 1e3) {
const [handler$1] = handlerCache;
const key = handler$1[0];
handlerCache.delete(key);
}
handlerCache.set(`${filename}/${name}`, handler);
return handler;
}
function throwInNextTick(error) {
process.nextTick(() => {
throw error;
});
}
//#endregion
export { getHandler, throwInNextTick };

10
pwa/node_modules/tinypool/dist/utils-De75vAgL.js generated vendored Normal file
View file

@ -0,0 +1,10 @@
//#region src/utils.ts
function stdout() {
return console._stdout || process.stdout || void 0;
}
function stderr() {
return console._stderr || process.stderr || void 0;
}
//#endregion
export { stderr, stdout };