@ayonli/jsext
Version:
A JavaScript extension package for building strong and modern applications.
1,573 lines (1,548 loc) • 51.8 kB
JavaScript
var _a$2, _b;
const id = Symbol.for("id");
typeof ServiceWorkerGlobalScope === "function"
&& globalThis instanceof ServiceWorkerGlobalScope;
typeof SharedWorkerGlobalScope === "function"
&& globalThis instanceof SharedWorkerGlobalScope;
typeof DedicatedWorkerGlobalScope === "function"
&& globalThis instanceof DedicatedWorkerGlobalScope;
const isDeno = typeof Deno === "object" && !!((_a$2 = Deno.version) === null || _a$2 === void 0 ? void 0 : _a$2.deno);
const isBun = typeof Bun === "object" && !!Bun.version;
const isNodeLike = typeof process === "object" && !!((_b = process.versions) === null || _b === void 0 ? void 0 : _b.node) && !isDeno;
const isNode = isNodeLike && !isDeno && !isBun;
isNode && parseInt(process.version.slice(1)) < 14;
isNode && parseInt(process.version.slice(1)) < 16;
isNode && parseInt(process.version.slice(1)) < 20;
const isNodeWorkerThread = isNode
&& (process.abort.disabled === true || process.argv.includes("--worker-thread"));
const isMainThread = !isNodeWorkerThread
&& (isBun ? Bun.isMainThread : typeof WorkerGlobalScope === "undefined");
/**
* Functions for dealing with numbers.
* @module
*/
/** Returns `true` if the given value is a float number, `false` otherwise. */
/**
* Creates a generator that produces sequential numbers from `1` to
* `Number.MAX_SAFE_INTEGER`, useful for generating unique IDs.
*
* @param loop Repeat the sequence when the end is reached.
*
* @example
* ```ts
* import { serial } from "@ayonli/jsext/number";
*
* const idGenerator = serial();
*
* console.log(idGenerator.next().value); // 1
* console.log(idGenerator.next().value); // 2
* console.log(idGenerator.next().value); // 3
* ```
*/
function serial(loop = false) {
return sequence(1, Number.MAX_SAFE_INTEGER, 1, loop);
}
/**
* Creates a generator that produces sequential numbers from `min` to `max` (inclusive).
* @deprecated use {@link range} and {@link serial} instead.
*/
function* sequence(min, max, step = 1, loop = false) {
let id = min;
while (true) {
yield id;
if ((id += step) > max) {
if (loop) {
id = min;
}
else {
break;
}
}
}
}
/**
* A channel implementation that transfers data across routines, even across
* multiple threads, inspired by Golang.
* @module
*/
var _a$1;
if (typeof Symbol.dispose === "undefined") {
Object.defineProperty(Symbol, "dispose", { value: Symbol("Symbol.dispose") });
}
const idGenerator = serial(true);
/**
* A channel implementation that transfers data across routines, even across
* multiple threads, inspired by Golang.
*/
class Channel {
constructor(capacity = 0) {
this[_a$1] = idGenerator.next().value;
this.buffer = [];
this.producers = [];
this.consumers = [];
this.error = null;
this.state = 1;
if (capacity < 0) {
throw new RangeError("the capacity of a channel must not be negative");
}
this.capacity = capacity;
}
/**
* Pushes data to the channel.
*
* If there is a receiver, the data will be consumed immediately. Otherwise:
*
* - If this is an non-buffered channel, this function will block until a
* receiver is available and the data is consumed.
*
* - If this is a buffered channel, then:
* - If the buffer size is within the capacity, the data will be pushed
* to the buffer.
* - Otherwise, this function will block until there is new space for
* the data in the buffer.
*/
send(data) {
if (this.state !== 1) {
throw new Error("the channel is closed");
}
else if (this.consumers.length) {
const consume = this.consumers.shift();
return Promise.resolve(consume(null, data));
}
else if (this.capacity && this.buffer.length < this.capacity) {
this.buffer.push(data);
return Promise.resolve(undefined);
}
else {
return new Promise(resolve => {
this.producers.push(() => {
if (this.capacity) {
const _data = this.buffer.shift();
this.buffer.push(data);
resolve();
return _data;
}
else {
resolve();
return data;
}
});
});
}
}
/**
* Retrieves data from the channel.
*
* If there isn't data available at the moment, this function will block
* until new data is available.
*
* If the channel is closed, then:
*
* - If there is error set in the channel, this function throws that error
* immediately.
* - Otherwise, this function returns `undefined` immediately.
*/
recv() {
if (this.buffer.length) {
const data = this.buffer.shift();
if (this.state === 2 && !this.buffer.length) {
this.state = 0;
}
return Promise.resolve(data);
}
else if (this.producers.length) {
const produce = this.producers.shift();
if (this.state === 2 && !this.producers.length) {
this.state = 0;
}
return Promise.resolve(produce());
}
else if (this.state === 0) {
return Promise.resolve(undefined);
}
else if (this.error) {
// Error can only be consumed once, after that, that closure will
// be complete.
const { error } = this;
this.state = 0;
this.error = null;
return Promise.reject(error);
}
else if (this.state === 2) {
this.state = 0;
return Promise.resolve(undefined);
}
else {
return new Promise((resolve, reject) => {
this.consumers.push((err, data) => {
if (this.state === 2 && !this.consumers.length) {
this.state = 0;
}
err ? reject(err) : resolve(data);
});
});
}
}
/**
* Closes the channel. If `err` is supplied, it will be captured by the
* receiver.
*
* No more data shall be sent once the channel is closed.
*
* Explicitly closing the channel is not required, if the channel is no
* longer used, it will be automatically released by the GC. However, if
* the channel is used in a `for await...of...` loop, closing the channel
* will allow the loop to break automatically.
*
* Moreover, if the channel is used between parallel threads, it will no
* longer be able to release automatically, must explicitly call this
* function in order to release for GC.
*/
close(err = null) {
if (this.state !== 1) {
// prevent duplicated call
return;
}
this.state = 2;
this.error = err;
let consume;
while (consume = this.consumers.shift()) {
consume(err, undefined);
}
}
[(_a$1 = id, Symbol.asyncIterator)]() {
const channel = this;
return {
async next() {
const bufSize = channel.buffer.length;
const queueSize = channel.producers.length;
const value = await channel.recv();
return {
value: value,
done: channel.state === 0 && !bufSize && !queueSize,
};
}
};
}
[Symbol.dispose]() {
this.close();
}
/** @deprecated This method is deprecated in favor of the `send()` method. */
push(data) {
return this.send(data);
}
/** @deprecated This method is deprecated in favor of the `recv()` method. */
pop() {
return this.recv();
}
}
const channelStore = new Map();
function isChannelMessage(msg) {
return msg
&& typeof msg === "object"
&& ["send", "close"].includes(msg.type)
&& typeof msg.channelId === "number";
}
async function handleChannelMessage(msg) {
const record = channelStore.get(msg.channelId);
if (!record)
return;
if (msg.type === "send") {
await record.raw.send(msg.value);
}
else if (msg.type === "close") {
const { value: err, channelId } = msg;
record.raw.close(err);
channelStore.delete(channelId);
if (isMainThread && record.writers.length > 1) {
// distribute the channel close event to all threads
record.writers.forEach(write => {
write("close", err, channelId);
});
}
}
}
function wireChannel(channel, channelWrite) {
const channelId = channel[id];
if (!channelStore.has(channelId)) {
const send = channel.send.bind(channel);
const close = channel.close.bind(channel);
channelStore.set(channelId, {
channel,
raw: { send, close },
writers: [channelWrite],
counter: 0,
});
Object.defineProperties(channel, {
send: {
configurable: true,
writable: true,
value: async (data) => {
const record = channelStore.get(channelId);
if (record) {
const channel = record.channel;
if (channel["state"] !== 1) {
throw new Error("the channel is closed");
}
const write = record.writers[record.counter++ % record.writers.length];
await Promise.resolve(write("send", data, channelId));
}
},
},
close: {
configurable: true,
writable: true,
value: (err = null) => {
const record = channelStore.get(channelId);
if (record) {
channelStore.delete(channelId);
const channel = record.channel;
record.writers.forEach(write => {
write("close", err, channelId);
});
// recover to the original methods
Object.defineProperties(channel, {
send: {
configurable: true,
writable: true,
value: record.raw.send,
},
close: {
configurable: true,
writable: true,
value: record.raw.close,
},
});
channel.close(err);
}
},
},
});
}
else {
const record = channelStore.get(channelId);
record.writers.push(channelWrite);
}
}
function unwrapChannel(obj, channelWrite) {
var _a, _b;
const channelId = obj["@@id"];
let channel = (_a = channelStore.get(channelId)) === null || _a === void 0 ? void 0 : _a.channel;
if (!channel) {
channel = Object.assign(Object.create(Channel.prototype), {
[id]: channelId,
capacity: (_b = obj.capacity) !== null && _b !== void 0 ? _b : 0,
buffer: [],
producers: [],
consumers: [],
error: null,
state: 1,
});
}
wireChannel(channel, channelWrite);
return channel;
}
if (!Symbol.asyncIterator) {
// @ts-ignore
Symbol.asyncIterator = Symbol("Symbol.asyncIterator");
}
/**
* Checks if the given object is an IteratorLike (implemented `next`).
* @param {any} obj
* @returns {obj is { [x: string | symbol]: any; next: Function }}
*/
function isIteratorLike(obj) {
// An iterable object has a 'next' method, however including a 'next' method
// doesn't ensure the object is an iterator, it is only iterator-like.
return typeof obj === "object"
&& obj !== null
&& typeof obj.next === "function";
}
/**
* Checks if the given object is an IterableIterator (implemented both
* `@@iterator` and `next`).
* @param {any} obj
*/
function isIterableIterator(obj) {
return isIteratorLike(obj)
&& typeof obj[Symbol.iterator] === "function";
}
/**
* Checks if the given object is an AsyncIterableIterator (implemented
* both `@@asyncIterator` and `next`).
* @param {any} obj
* @returns {obj is AsyncIterableIterator<any>}
*/
function isAsyncIterableIterator(obj) {
return isIteratorLike(obj)
&& typeof obj[Symbol.asyncIterator] === "function";
}
/**
* Checks if the given object is a Generator.
* @param {any} obj
* @returns {obj is Generator}
*/
function isGenerator(obj) {
return isIterableIterator(obj)
&& hasGeneratorSpecials(obj);
}
/**
* Checks if the given object is an AsyncGenerator.
* @param {any} obj
* @returns {obj is AsyncGenerator}
*/
function isAsyncGenerator(obj) {
return isAsyncIterableIterator(obj)
&& hasGeneratorSpecials(obj);
}
/**
* @param {any} obj
*/
function hasGeneratorSpecials(obj) {
return typeof obj.return === "function"
&& typeof obj.throw === "function";
}
/**
* Utilities for encoding and decoding binary representations like hex and
* base64 strings.
* @module
*/
new TextEncoder();
/**
* Functions for dealing with byte arrays (`Uint8Array`).
* @module
*/
new TextEncoder();
new TextDecoder();
(() => {
try {
return new RegExp("^\(?:\\p{Emoji_Modifier_Base}\\p{Emoji_Modifier}?|\\p{Emoji_Presentation}|\\p{Emoji}\\uFE0F)(?:\\u200d(?:\\p{Emoji_Modifier_Base}\\p{Emoji_Modifier}?|\\p{Emoji_Presentation}|\\p{Emoji}\\uFE0F))*$", "u");
}
catch (_a) {
return new RegExp("^(\\u00a9|\\u00ae|[\\u25a0-\\u27bf]|\\ud83c[\\ud000-\\udfff]|\\ud83d[\\ud000-\\udfff]|\\ud83e[\\ud000-\\udfff])$");
}
})();
/**
* Functions for dealing with strings.
* @module
*/
const _trim = String.prototype.trim;
const _trimEnd = String.prototype.trimEnd;
const _trimStart = String.prototype.trimStart;
/**
* Removes leading and trailing spaces or custom characters of the string.
*
* @example
* ```ts
* import { trim } from "@ayonli/jsext/string";
*
* console.log(trim(" hello world ")); // "hello world"
* console.log(trim(" hello world! ", " !")); // "hello world"
* ```
*/
function trim(str, chars = "") {
if (!chars) {
return _trim.call(str);
}
else {
return trimEnd(trimStart(str, chars), chars);
}
}
/**
* Removes trailing spaces or custom characters of the string.
*
* @example
* ```ts
* import { trimEnd } from "@ayonli/jsext/string";
*
* console.log(trimEnd(" hello world ")); // " hello world"
* console.log(trimEnd(" hello world! ", " !")); // " hello world"
* ```
*/
function trimEnd(str, chars = "") {
if (!chars) {
return _trimEnd.call(str);
}
else {
let i = str.length;
while (i-- && chars.indexOf(str[i]) !== -1) { }
return str.substring(0, i + 1);
}
}
/**
* Removes leading spaces or custom characters of the string.
*
* @example
* ```ts
* import { trimStart } from "@ayonli/jsext/string";
*
* console.log(trimStart(" hello world ")); // "hello world "
* console.log(trimStart(" !hello world! ", " !")); // "hello world! "
* ```
*/
function trimStart(str, chars = "") {
if (!chars) {
return _trimStart.call(str);
}
else {
let i = 0;
do { } while (chars.indexOf(str[i]) !== -1 && ++i);
return str.substring(i);
}
}
function isVolume(path, strict = false) {
return strict ? /^[a-zA-Z]:$/.test(path) : /^[a-zA-Z]:(\\)?$/.test(path);
}
/**
* Checks if the given `path` is a Windows specific path.
* @experimental
*
* @example
* ```ts
* import { isWindowsPath } from "@ayonli/jsext/path";
*
* console.assert(isWindowsPath("C:\\Windows\\System32"));
* console.assert(isWindowsPath("c:\\Windows\\System32")); // case-insensitive on volume
* console.assert(isWindowsPath("D:/Program Files")); // forward slash is also valid
* console.assert(isWindowsPath("E:")); // volume without path is also valid
* ```
*/
function isWindowsPath(path) {
return /^[a-zA-Z]:/.test(path) && path.slice(1, 4) !== "://";
}
/**
* Checks if the given `path` is a Posix specific path.
* @experimental
*
* @example
* ```ts
* import { isPosixPath } from "@ayonli/jsext/path";
*
* console.assert(isPosixPath("/usr/bin"));
* ```
*/
function isPosixPath(path) {
return /^\//.test(path);
}
/**
* Checks if the given `path` is a file system path.
* @experimental
*
* @example
* ```ts
* import { isFsPath } from "@ayonli/jsext/path";
*
* console.assert(isFsPath("/usr/bin"));
* console.assert(isFsPath("C:\\Windows\\System32"));
* console.assert(isFsPath("./foo/bar"));
* console.assert(isFsPath("../foo/bar"));
* ```
*/
function isFsPath(path) {
return /^(\.[\/\\]|\.\.[\/\\]|[a-zA-Z]:|\/)/.test(path);
}
/**
* Checks if the given string is a URL, whether standard or non-standard.
* @experimental
*
* @example
* ```ts
* import { isUrl } from "@ayonli/jsext/path";
*
* console.assert(isUrl("http://example.com"));
* console.assert(isUrl("https://example.com?foo=bar#baz"));
* console.assert(isUrl("ftp://example.com")); // ftp url
* console.assert(isUrl("file:///C:/Windows/System32")); // file url
* console.assert(isUrl("file://localhost/C:/Windows/System32")); // file url with hostname
* console.assert(isUrl("file:///usr/bin"));
* ```
*/
function isUrl(str) {
return /^[a-z](([a-z\-]+)?:\/\/\S+|[a-z\-]+:\/\/$)/i.test(str) || isFileUrl(str);
}
/**
* Checks if the given string is a file URL, whether with or without `//`.
* @experimental
*
* @example
* ```ts
* import { isFileUrl } from "@ayonli/jsext/path";
*
* console.assert(isFileUrl("file:///C:/Windows/System32"));
* console.assert(isFileUrl("file://localhost/C:/Windows/System32"));
* console.assert(isFileUrl("file:///usr/bin"));
* console.assert(isFileUrl("file:/usr/bin"));
* console.assert(isFileUrl("file:///usr/bin?foo=bar"));
* ```
*/
function isFileUrl(str) {
return /^file:((\/\/|\/)\S+|\/?$)/i.test(str);
}
function isFileProtocol(path) {
return /^file:(\/\/)?$/i.test(path);
}
/**
* Checks if the given `path` is an absolute path.
* @experimental
*
* @example
* ```ts
* import { isAbsolute } from "@ayonli/jsext/path";
*
* console.assert(isAbsolute("/usr/bin"));
* console.assert(isAbsolute("C:\\Windows\\System32"));
* console.assert(isAbsolute("http://example.com"));
* console.assert(isAbsolute("file:///C:/Windows/System32"));
* console.assert(isAbsolute("file://localhost/C:/Windows/System32?foo=bar#baz"));
* ```
*/
function isAbsolute(path) {
return isPosixPath(path) || isWindowsPath(path) || isUrl(path);
}
/**
* Splits the `path` into well-formed segments.
* @experimental
*
* @example
* ```ts
* import { split } from "@ayonli/jsext/path";
*
* console.log(split("/usr/bin")); // ["/", "usr", "bin"]
* console.log(split("C:\\Windows\\System32")); // ["C:\\", "Windows", "System32"]
* console.log(split("file:///user/bin")); // ["file:///", "usr", "bin"]
*
* console.log(split("http://example.com/foo/bar?foo=bar#baz"));
* // ["http://example.com", "foo", "bar", "?foo=bar", "#baz"]
* ```
*/
function split(path) {
if (!path) {
return [];
}
else if (isUrl(path)) {
const { protocol, host, pathname, search, hash } = new URL(path);
let origin = protocol + "//" + host;
if (isFileProtocol(origin)) {
origin += "/";
}
if (pathname === "/") {
if (search && hash) {
return [origin, search, hash];
}
else if (search) {
return [origin, search];
}
else if (hash) {
return [origin, hash];
}
else {
return [origin];
}
}
else {
const segments = trim(decodeURI(pathname), "/").split(/[/\\]+/);
if (search && hash) {
return [origin, ...segments, search, hash];
}
else if (search) {
return [origin, ...segments, search];
}
else if (hash) {
return [origin, ...segments, hash];
}
else {
return [origin, ...segments];
}
}
}
else if (isWindowsPath(path)) {
const [_, volume, ...segments] = split("file:///" + path.replace(/[/\\]+/g, "/"));
return [volume + "\\", ...segments];
}
else if (isPosixPath(path)) {
const [_, ...segments] = split("file://" + path.replace(/[/\\]+/g, "/"));
return ["/", ...segments];
}
else { // relative path
path = path.replace(/[/\\]+/g, "/");
const [_path, query] = path.split("?");
if (query) {
const segments = _path ? trimEnd(_path, "/").split("/") : [];
const [search, hash] = query.split("#");
if (hash) {
return [...segments, "?" + search, "#" + hash];
}
else {
return [...segments, "?" + search];
}
}
else {
const [pathname, hash] = path.split("#");
const segments = pathname ? trimEnd(pathname, "/").split("/") : [];
if (hash) {
return [...segments, "#" + hash];
}
else {
return segments;
}
}
}
}
/**
* Platform-independent utility functions for dealing with file system paths and
* URLs.
*
* The functions in this module are designed to be generic and work in any
* runtime, whether server-side or browsers. They can be used for both system
* paths and URLs.
* @module
*/
/**
* Platform-specific path segment separator. The value is `\` in Windows
* server-side environments, and `/` elsewhere.
*/
const sep = (() => {
if (isDeno) {
if (Deno.build.os === "windows") {
return "\\";
}
}
else if (isNodeLike) {
if (process.platform === "win32") {
return "\\";
}
}
return "/";
})();
/**
* Returns the current working directory.
*
* **NOTE:** In the browser, this function returns the current origin and pathname.
*
* This function may fail in unsupported environments or being rejected by the
* permission system of the runtime.
*/
function cwd() {
if (isDeno) {
return Deno.cwd();
}
else if (isNodeLike) {
return process.cwd();
}
else if (typeof location === "object" && location.origin) {
return location.origin + (location.pathname === "/" ? "" : location.pathname);
}
else {
throw new Error("Unable to determine the current working directory.");
}
}
/**
* Concatenates all given `segments` into a well-formed path.
* @experimental
*
* @example
* ```ts
* import { join } from "@ayonli/jsext/path";
*
* console.log(join("foo", "bar")); // "foo/bar" or "foo\\bar" on Windows
* console.log(join("/", "foo", "bar")); // "/foo/bar"
* console.log(join("C:\\", "foo", "bar")); // "C:\\foo\\bar"
* console.log(join("file:///foo", "bar", "..")) // "file:///foo"
*
* console.log(join("http://example.com", "foo", "bar", "?query"));
* // "http://example.com/foo/bar?query"
* ```
*/
function join(...segments) {
let _paths = [];
for (let i = 0; i < segments.length; i++) {
const path = segments[i];
if (path) {
if (isAbsolute(path)) {
_paths = [];
}
_paths.push(path);
}
}
const paths = [];
for (let i = 0; i < _paths.length; i++) {
let segment = _paths[i];
for (const _segment of split(segment)) {
if (_segment === "..") {
if (!paths.length || paths.every(p => p === "..")) {
paths.push("..");
}
else if (paths.length > 2
|| (paths.length === 2 && !isAbsolute(paths[1]))
|| (paths.length === 1 && !isAbsolute(paths[0]))) {
paths.pop();
}
}
else if (_segment && _segment !== ".") {
paths.push(_segment);
}
}
}
if (!paths.length) {
return ".";
}
const start = paths[0];
const _sep = isUrl(start) || isPosixPath(start) ? "/" : isWindowsPath(start) ? "\\" : sep;
let path = "";
for (let i = 0; i < paths.length; i++) {
const segment = paths[i];
if (!path || segment[0] === "?" || segment[0] === "#") {
path += segment;
}
else if (isVolume(segment)) {
if (path) {
path += segment + "/";
}
else {
path = segment;
}
}
else {
path += (path.endsWith(_sep) ? "" : _sep) + trim(segment, "/\\");
}
}
if (/^file:\/\/\/[a-z]:$/i.test(path)) {
return path + "/";
}
else {
return path;
}
}
/**
* Resolves path `segments` into a well-formed path.
*
* This function is similar to {@link join}, except it always returns an
* absolute path based on the current working directory if the input segments
* are not absolute by themselves.
* @experimental
*/
function resolve(...segments) {
segments = segments.filter(s => s !== "");
const _cwd = cwd();
if (!segments.length) {
return _cwd;
}
segments = isAbsolute(segments[0]) ? segments : [_cwd, ...segments];
return join(...segments);
}
/**
* Converts the given URL to a file system path if it's not one already.
* @experimental
*
* @example
* ```ts
* import { toFsPath } from "@ayonli/jsext/path";
*
* console.log(toFsPath("file:///foo/bar")); // "/foo/bar"
* console.log(toFsPath("file:///c:/foo/bar")); // "c:\\foo\\bar"
* ```
*/
function toFsPath(url) {
if (isFsPath(url)) {
return url;
}
else if (isFileUrl(url)) {
url = url.replace(/^file:(\/\/)?/i, "").replace(/^\/([a-z]):/i, "$1:");
return join(url);
}
else if (!isUrl(url)) {
return resolve(url);
}
else {
throw new Error("Cannot convert a URL to a file system path.");
}
}
const urlCache = new Map();
/**
* This function is primarily used to bypass the same-origin policy for Web
* Workers in the browser, it downloads the script from the given URL and
* converts it to an object URL which can be used by the `Worker` constructor.
*
* This function can also be used in other scenarios as it also corrects the
* content-type of the response to ensure the script can be loaded properly.
*
* NOTE: This function is primarily designed for the browser, it has very little
* use on the server side.
*/
async function getObjectURL(src, mimeType = "text/javascript") {
var _a;
const isAbsolute = isUrl(src);
let cache = isAbsolute ? urlCache.get(src) : undefined;
if (cache) {
return cache;
}
// Use fetch to download the script and compose an object URL which can
// bypass the same-origin policy for web workers.
const res = await fetch(src);
if (!res.ok) {
throw new Error(`Failed to fetch resource: ${src}`);
}
let blob;
// JavaScript has more than one MIME types, so we just check it loosely.
const type = mimeType.includes("javascript") ? "javascript" : mimeType;
if ((_a = res.headers.get("content-type")) === null || _a === void 0 ? void 0 : _a.includes(type)) {
blob = await res.blob();
}
else {
// If the MIME type is not matched, we need to convert the response to
// a new Blob with the correct MIME type.
const buf = await res.arrayBuffer();
blob = new Blob([new Uint8Array(buf)], {
type: mimeType,
});
}
cache = URL.createObjectURL(blob);
isAbsolute && urlCache.set(src, cache);
return cache;
}
/**
* Utility functions for working with JavaScript modules.
* @module
*/
function interop(module, strict = undefined) {
if (typeof module === "function") {
return module().then(mod => interop(mod, strict));
}
else if (module instanceof Promise) {
return module.then(mod => interop(mod, strict));
}
else if (typeof module === "object" && module !== null && !Array.isArray(module)) {
if (typeof module["default"] === "object" &&
module["default"] !== null &&
!Array.isArray(module["default"])) {
const hasEsModule = module["__esModule"] === true
|| module["default"]["__esModule"] === true;
if (hasEsModule) {
return module["default"];
}
else if (strict) {
return module;
}
const moduleKeys = Object.getOwnPropertyNames(module)
.filter(x => x !== "default" && x !== "__esModule").sort();
const defaultKeys = Object.getOwnPropertyNames(module["default"])
.filter(x => x !== "default" && x !== "__esModule").sort();
if (String(moduleKeys) === String(defaultKeys)) {
return module["default"];
}
else if (strict === false && !moduleKeys.length) {
return module["default"];
}
}
}
return module;
}
const moduleCache = new Map();
async function resolveModule(modId, baseUrl = undefined) {
let module;
if (isNode || isBun) {
const path = baseUrl ? toFsPath(new URL(modId, baseUrl).href) : modId;
module = await import(path);
}
else {
const url = new URL(modId, baseUrl).href;
module = moduleCache.get(url);
if (!module) {
if (isDeno) {
module = await import(url);
moduleCache.set(url, module);
}
else {
try {
module = await import(url);
moduleCache.set(url, module);
}
catch (err) {
if (String(err).includes("Failed")) {
const _url = await getObjectURL(url);
module = await import(_url);
moduleCache.set(url, module);
}
else {
throw err;
}
}
}
}
}
return interop(module);
}
/**
* Functions for dealing with objects.
* @module
*/
/**
* Returns `true` if the specified object has the indicated property as its own property.
* If the property is inherited, or does not exist, the function returns `false`.
*
* @example
* ```ts
* import { hasOwn } from "@ayonli/jsext/object";
*
* const obj = { foo: "hello" };
*
* console.log(hasOwn(obj, "foo")); // true
* console.log(hasOwn(obj, "toString")); // false
* ```
*/
function hasOwn(obj, key) {
return Object.prototype.hasOwnProperty.call(obj, key);
}
function pick(obj, keys) {
return keys.reduce((result, key) => {
if (key in obj && obj[key] !== undefined) {
result[key] = obj[key];
}
return result;
}, {});
}
function omit(obj, keys) {
const allKeys = Reflect.ownKeys(obj);
const keptKeys = allKeys.filter(key => !keys.includes(key));
const result = pick(obj, keptKeys);
// special treatment for Error types
if (obj instanceof Error) {
["name", "message", "stack", "cause"].forEach(key => {
if (!keys.includes(key) &&
obj[key] !== undefined &&
!hasOwn(result, key)) {
result[key] = obj[key];
}
});
}
return result;
}
/**
* Returns `true` is the given value is a plain object, that is, an object created by
* the `Object` constructor or one with a `[[Prototype]]` of `null`.
*
* @example
* ```ts
* import { isPlainObject } from "@ayonli/jsext/object";
*
* console.log(isPlainObject({ foo: "bar" })); // true
* console.log(isPlainObject(Object.create(null))); // true
* console.log(isPlainObject(new Map([["foo", "bar"]]))); // false
* ```
*/
function isPlainObject(value) {
if (typeof value !== "object" || value === null)
return false;
const proto = Object.getPrototypeOf(value);
return proto === null || proto.constructor === Object;
}
/**
* A generic exception class, which can be used to represent any kind of error.
* It's similar to the `DOMException`, but for any JavaScript environment.
*
* @example
* ```ts
* // throw an exception with a name
* import { Exception } from "@ayonli/jsext/error";
*
* throw new Exception("The resource cannot be found", "NotFoundError");
* ```
*
* @example
* ```ts
* // throw an exception with a code
* import { Exception } from "@ayonli/jsext/error";
*
* throw new Exception("The resource cannot be found", 404);
* ```
*
* @example
* ```ts
* // rethrow an exception with a cause
* import { Exception } from "@ayonli/jsext/error";
*
* try {
* throw new Error("Something went wrong");
* } catch (error) {
* throw new Exception("An error occurred", { cause: error });
* }
* ```
*/
class Exception extends Error {
constructor(message, options = 0) {
super(message);
this.code = 0;
if (typeof options === "number") {
this.code = options;
}
else if (typeof options === "string") {
Object.defineProperty(this, "name", {
configurable: true,
enumerable: false,
writable: true,
value: options,
});
}
else {
if (options.name) {
Object.defineProperty(this, "name", {
configurable: true,
enumerable: false,
writable: true,
value: options.name,
});
}
if (options.cause) {
Object.defineProperty(this, "cause", {
configurable: true,
enumerable: false,
writable: true,
value: options.cause,
});
}
if (options.code) {
this.code = options.code;
}
}
}
}
Object.defineProperty(Exception.prototype, "name", {
configurable: true,
enumerable: false,
writable: true,
value: "Exception",
});
var _a;
if (typeof globalThis.Event !== "function") {
// @ts-ignore
globalThis.Event = (_a = class Event {
constructor(type, eventInitDict = {}) {
this.type = type;
this.eventInitDict = eventInitDict;
this.bubbles = false;
this.cancelable = false;
this.cancelBubble = false;
this.composed = false;
this.currentTarget = null;
this.defaultPrevented = false;
this.eventPhase = _a.NONE;
this.isTrusted = false;
this.returnValue = true;
this.target = null;
this.timeStamp = Date.now();
this.srcElement = null;
this.AT_TARGET = 2;
this.BUBBLING_PHASE = 3;
this.CAPTURING_PHASE = 1;
this.NONE = 0;
if (eventInitDict.bubbles !== undefined) {
this.bubbles = eventInitDict.bubbles;
}
if (eventInitDict.cancelable !== undefined) {
this.cancelable = eventInitDict.cancelable;
}
if (eventInitDict.composed !== undefined) {
this.composed = eventInitDict.composed;
}
}
composedPath() {
return [];
}
preventDefault() {
if (this.cancelable) {
this.defaultPrevented = true;
}
}
stopImmediatePropagation() {
// Do nothing
}
stopPropagation() {
this.cancelBubble = true;
}
initEvent(type, bubbles = undefined, cancelable = undefined) {
this.type = type;
this.bubbles = bubbles !== null && bubbles !== void 0 ? bubbles : false;
this.cancelable = cancelable !== null && cancelable !== void 0 ? cancelable : false;
}
},
_a.AT_TARGET = 2,
_a.BUBBLING_PHASE = 3,
_a.CAPTURING_PHASE = 1,
_a.NONE = 0,
_a);
}
if (typeof globalThis.EventTarget !== "function") {
// @ts-ignore
globalThis.EventTarget = class EventTarget {
constructor() {
this.listeners = {};
}
addEventListener(type, callback, options = {}) {
var _b;
if (!(type in this.listeners)) {
this.listeners[type] = [];
}
// @ts-ignore
this.listeners[type].push({ callback, once: (_b = options === null || options === void 0 ? void 0 : options.once) !== null && _b !== void 0 ? _b : false });
}
removeEventListener(type, callback) {
if (!(type in this.listeners)) {
return;
}
const stack = this.listeners[type];
for (let i = 0, l = stack.length; i < l; i++) {
if (stack[i].callback === callback) {
stack.splice(i, 1);
return;
}
}
if (stack.length === 0) {
delete this.listeners[type];
}
}
dispatchEvent(event) {
if (!(event.type in this.listeners)) {
return true;
}
Object.defineProperties(event, {
currentTarget: { configurable: true, value: this },
target: { configurable: true, value: this },
});
const stack = this.listeners[event.type].slice();
for (let i = 0, l = stack.length; i < l; i++) {
const listener = stack[i];
try {
listener.callback.call(this, event);
}
catch (err) {
setTimeout(() => {
throw err;
});
}
if (listener.once) {
this.removeEventListener(event.type, listener.callback);
}
}
return !event.defaultPrevented;
}
};
}
/**
* Functions for converting errors to/from other types of objects.
* @module
*/
/**
* Transforms the error to a plain object.
*
* @example
* ```ts
* import { toObject } from "@ayonli/jsext/error";
*
* const err = new Error("Something went wrong.");
*
* const obj = toObject(err);
* console.log(obj);
* // {
* // "@@type": "Error",
* // name: "Error",
* // message: "Something went wrong.",
* // stack: "Error: Something went wrong.\n at <anonymous>:1:13"
* // }
* ```
*/
function toObject(err) {
if (!(err instanceof Error) && err["name"] && err["message"]) { // Error-like
err = fromObject(err, Error);
}
const obj = {
"@@type": err.constructor.name,
...omit(err, ["toString", "toJSON", "__callSiteEvals"]),
};
if (obj["@@type"] === "AggregateError" && Array.isArray(obj["errors"])) {
obj["errors"] = obj["errors"].map(item => {
return item instanceof Error ? toObject(item) : item;
});
}
return obj;
}
function fromObject(obj, ctor = undefined) {
var _a, _b;
// @ts-ignore
if (!(obj === null || obj === void 0 ? void 0 : obj.name)) {
return null;
}
// @ts-ignore
ctor || (ctor = (globalThis[obj["@@type"] || obj.name] || globalThis[obj.name]));
if (!ctor) {
if (obj["@@type"] === "Exception") {
ctor = Exception;
}
else {
ctor = Error;
}
}
let err;
if (ctor.name === "DOMException" && typeof DOMException === "function") {
err = new ctor((_a = obj["message"]) !== null && _a !== void 0 ? _a : "", obj["name"]);
}
else {
err = Object.create(ctor.prototype, {
message: {
configurable: true,
enumerable: false,
writable: true,
value: (_b = obj["message"]) !== null && _b !== void 0 ? _b : "",
},
});
}
if (err.name !== obj["name"]) {
Object.defineProperty(err, "name", {
configurable: true,
enumerable: false,
writable: true,
value: obj["name"],
});
}
if (obj["stack"] !== undefined) {
Object.defineProperty(err, "stack", {
configurable: true,
enumerable: false,
writable: true,
value: obj["stack"],
});
}
if (obj["cause"] != undefined) {
Object.defineProperty(err, "cause", {
configurable: true,
enumerable: false,
writable: true,
value: obj["cause"],
});
}
const otherKeys = Reflect.ownKeys(obj).filter(key => ![
"@@type",
"name",
"message",
"stack",
"cause"
].includes(key));
otherKeys.forEach(key => {
var _a;
// @ts-ignore
(_a = err[key]) !== null && _a !== void 0 ? _a : (err[key] = obj[key]);
});
// @ts-ignore
if (isAggregateError(err) && Array.isArray(err["errors"])) {
err["errors"] = err["errors"].map(item => {
return isPlainObject(item) ? fromObject(item) : item;
});
}
return err;
}
/** @inner */
function isDOMException(value) {
return ((typeof DOMException === "function") && (value instanceof DOMException))
|| (value instanceof Error && value.constructor.name === "DOMException"); // Node.js v16-
}
/** @inner */
function isAggregateError(value) {
// @ts-ignore
return (typeof AggregateError === "function" && value instanceof AggregateError)
|| (value instanceof Error && value.constructor.name === "AggregateError");
}
const pendingTasks = new Map();
/**
* For some reason, in Node.js and Bun, when import expression throws an
* module/package not found error, the error can not be serialized and sent to
* the other thread properly. We need to check this situation and sent the error
* as plain object instead.
*/
function isModuleResolveError(value) {
var _a;
if (typeof value === "object" &&
typeof (value === null || value === void 0 ? void 0 : value.message) === "string" &&
/Cannot find (module|package)/.test(value === null || value === void 0 ? void 0 : value.message)) {
return (value instanceof Error) // Node.js (possibly bug)
|| ((_a = value.constructor) === null || _a === void 0 ? void 0 : _a.name) === "Error"; // Bun (doesn't inherit from Error)
}
return false;
}
function removeUnserializableProperties(obj) {
const _obj = {};
for (const key of Reflect.ownKeys(obj)) {
if (typeof obj[key] !== "bigint" && typeof obj[key] !== "function") {
_obj[key] = obj[key];
}
}
return _obj;
}
function unwrapArgs(args, channelWrite) {
return args.map(arg => {
if (isPlainObject(arg)) {
if (arg["@@type"] === "Channel" && typeof arg["@@id"] === "number") {
return unwrapChannel(arg, channelWrite);
}
else if (arg["@@type"] === "Exception"
|| arg["@@type"] === "DOMException"
|| arg["@@type"] === "AggregateError") {
return fromObject(arg);
}
}
return arg;
});
}
function wrapReturnValue(value) {
const transferable = [];
if (value instanceof ArrayBuffer) {
transferable.push(value);
}
else if ((value instanceof Exception)
|| isDOMException(value)
|| isAggregateError(value)
|| isModuleResolveError(value)) {
value = toObject(value);
}
else if (isPlainObject(value)) {
for (const key of Object.getOwnPropertyNames(value)) {
const _value = value[key];
if (_value instanceof ArrayBuffer) {
transferable.push(_value);
}
else if ((_value instanceof Exception)
|| isDOMException(_value)
|| isAggregateError(_value)
|| isModuleResolveError(_value)) {
value[key] = toObject(_value);
}
}
}
else if (Array.isArray(value)) {
value = value.map(item => {
if (item instanceof ArrayBuffer) {
transferable.push(item);
return item;
}
else if ((item instanceof Exception)
|| isDOMException(item)
|| isAggregateError(item)
|| isModuleResolveError(item)) {
return toObject(item);
}
else {
return item;
}
});
}
return { value, transferable };
}
/**
* @ignore
* @internal
*/
function isCallRequest(msg) {
return msg && typeof msg === "object"
&& ((msg.type === "call" && typeof msg.module === "string" && typeof msg.fn === "string") ||
(["next", "return", "throw"].includes(msg.type) && typeof msg.taskId === "number"))
&& Array.isArray(msg.args);
}
/**
* @ignore
* @internal
*/
async function handleCallRequest(msg, reply) {
const _reply = reply;
reply = (res) => {
if (res.type === "error") {
if ((res.error instanceof Exception) ||
isDOMException(res.error) ||
isAggregateError(res.error) ||
isModuleResolveError(res.error)) {
return _reply({
...res,
error: removeUnserializableProperties(toObject(res.error)),
});
}
try {
return _reply(res);
}
catch (_a) {
// In case the error cannot be cloned directly, fallback to
// transferring it as an object and rebuild in the main thread.
return _reply({
...res,
error: removeUnserializableProperties(toObject(res.error)),
});
}
}
else {
return _reply(res);
}
};
msg.args = unwrapArgs(msg.args, (type, msg, channelId) => {
reply({ type, value: msg, channelId });
});
try {
if (msg.taskId && ["next", "return", "throw"].includes(msg.type)) {
const req = msg;
const task = pendingTasks.get(req.taskId);
if (task) {
if (req.type === "throw") {
try {
await task.throw(req.args[0]);
}
catch (error) {
reply({ type: "error", error, taskId: req.taskId });
}
}
else if (req.type === "return") {
try {
const res = await task.return(req.args[0]);
const { value, transferable } = wrapReturnValue(res.value);
reply({
type: "yield",
value,
done: res.done,
taskId: req.taskId,
}, transferable);
}
catch (error) {
reply({ type: "error", error, taskId: req.taskId });
}
}
else { // req.type === "next"
try {
const res = await task.next(req.args[0]);
const { value, transferable } = wrapReturnValue(res.value);
reply({
type: "yield",
value,
done: res.done,
taskId: req.taskId,
}, transferable);
}
catch (error) {
reply({ type: "error", error, taskId: req.taskId });
}
}
}
else {
reply({
type: "error",
error: new ReferenceError(`task (${req.taskId}) doesn't exists`),
taskId: req.taskId,
});
}
return;
}
const req = msg;
const module = await resolveModule(req.module);
const returns = await module[req.fn](...req.args);
if (isAsyncGenerator(returns) || isGenerator(returns)) {
if (req.taskId) {
pendingTasks.set(req.taskId, returns);
reply({ type: "gen", taskId: req.taskId });
}
else {
while (true) {
try {
const res = await returns.next();