smol update

This commit is contained in:
xananax prozaxx 2023-06-07 18:51:18 +02:00
parent eadca78080
commit cde1e58a47
10 changed files with 212 additions and 26 deletions

View File

@ -69,8 +69,15 @@ export const createTrackedResponse = (
const start = readerPromise.resolve; const start = readerPromise.resolve;
return Object.assign(successPromise, { /** @type {Promise<Response>} */
return {
start, start,
/** @type {Promise<Response>["then"]} */
then: successPromise.then.bind(successPromise),
/** @type {Promise["catch"]} */
catch: successPromise.catch.bind(successPromise),
/** @type {Promise["finally"]} */
finally: successPromise.finally.bind(successPromise),
get response() { get response() {
return response; return response;
}, },
@ -89,5 +96,7 @@ export const createTrackedResponse = (
get isUnknown() { get isUnknown() {
return success === false && failed === false; return success === false && failed === false;
}, },
});
}; };
};
export default createTrackedResponse;

View File

@ -24,4 +24,4 @@ export const deferredPromise = () => {
return Object.assign(promise, {resolve, reject}); return Object.assign(promise, {resolve, reject});
} }
export default DeferredPromise export default deferredPromise

View File

@ -0,0 +1,25 @@
//@ts-check
import {getReasonableUuid} from "./getReasonableUuid.mjs"
/**
* Returns an id that is guaranteed to not exist in the current loaded page.
* Only usable in the browser, after the page has loaded.
* Using it outside the browser or prior to loading will yield a pseudo-unique id,
* but not guaranteed unique.
* Ids are deterministic, so calling this function in the same order will always return
* the same set of ids.
* @see {uuid}
* @param {string} prefix any prefix you like, it helps discriminate ids
*/
export const getPageUniqueId = (prefix = "") => {
let id = prefix;
let limit = 99999;
while (
typeof document !== "undefined" &&
document.getElementById((id = `${prefix}${getReasonableUuid()}`)) &&
limit-- > 0
);
return id;
};
export default getPageUniqueId

View File

@ -0,0 +1,12 @@
//@ts-check
/**
* A reasonably unique id.
* Deterministic, it will always return the same id for the same call order
*/
export const getReasonableUuid = (() => {
let start = 100000000;
return () => (start++).toString(36);
})();
export default getReasonableUuid

View File

@ -20,11 +20,14 @@ import {
import { getElementByCSSSelector } from "./getElementByCSSSelector.mjs"; import { getElementByCSSSelector } from "./getElementByCSSSelector.mjs";
import { getElementById } from "./getElementById.mjs"; import { getElementById } from "./getElementById.mjs";
import { getFirstTitleContent } from "./getFirstTitleContent.mjs"; import { getFirstTitleContent } from "./getFirstTitleContent.mjs";
import { getPageUniqueId } from "./getPageUniqueId.mjs";
import { getReasonableUuid } from "./getReasonableUuid.mjs";
import { identity, awaitedIdentity } from "./identity.mjs"; import { identity, awaitedIdentity } from "./identity.mjs";
import { html } from "./html.mjs"; import { html } from "./html.mjs";
import { isExternalUrl } from "./isExternalUrl.mjs"; import { isExternalUrl } from "./isExternalUrl.mjs";
import { isLocalHost } from "./isLocalHost.mjs"; import { isLocalHost } from "./isLocalHost.mjs";
import { isNotNull } from "./isNotNull.mjs"; import { isNotNull } from "./isNotNull.mjs";
import { makeBoundConsole } from "./makeBoundConsole.mjs";
import { makeEventEmitter } from "./makeEventEmitter.mjs"; import { makeEventEmitter } from "./makeEventEmitter.mjs";
import { makeFileLoader, makeFileLoadersTracker } from "./makeFileLoader.mjs"; import { makeFileLoader, makeFileLoadersTracker } from "./makeFileLoader.mjs";
import { makeFileSizeFetcher } from "./makeFileSizeFetcher.mjs"; import { makeFileSizeFetcher } from "./makeFileSizeFetcher.mjs";
@ -39,6 +42,14 @@ import {
onDocumentKeyDown, onDocumentKeyDown,
onDocumentKey, onDocumentKey,
} from "./onDocumentKey.mjs"; } from "./onDocumentKey.mjs";
import {
basename,
filename,
stripExtension,
dirName,
extension,
metadata,
} from "./path.mjs";
import { percentFromProgress } from "./percentFromProgress.mjs"; import { percentFromProgress } from "./percentFromProgress.mjs";
import { print, makeTemplate } from "./print.mjs"; import { print, makeTemplate } from "./print.mjs";
import { import {
@ -71,12 +82,15 @@ export {
getElementByCSSSelector, getElementByCSSSelector,
getElementById, getElementById,
getFirstTitleContent, getFirstTitleContent,
getPageUniqueId,
getReasonableUuid,
html, html,
identity, identity,
awaitedIdentity, awaitedIdentity,
isExternalUrl, isExternalUrl,
isLocalHost, isLocalHost,
isNotNull, isNotNull,
makeBoundConsole,
makeEventEmitter, makeEventEmitter,
makeFileLoader, makeFileLoader,
makeFileLoadersTracker, makeFileLoadersTracker,
@ -90,6 +104,12 @@ export {
onDocumentKeyUp, onDocumentKeyUp,
onDocumentKeyDown, onDocumentKeyDown,
onDocumentKey, onDocumentKey,
basename,
filename,
stripExtension,
dirName,
extension,
metadata,
percentFromProgress, percentFromProgress,
print, print,
makeTemplate, makeTemplate,

View File

@ -0,0 +1,19 @@
//@ts-check
const methods = /** @type {('warn' & keyof typeof console)[]} */(["log", "warn", "error"])
/**
* Returns console methods that can be used standalone (without requiring `console`).
* Optional prefix will prepend every call with the provided prefix
* @param {any} prefix
*/
export const makeBoundConsole = (prefix = "") => {
const [log, warn, error] = methods.map(
(fn) =>
(/** @type {any} */ msg) =>
console[fn](prefix, msg)
);
return { log, warn, error };
};
export default makeBoundConsole

View File

@ -3,8 +3,9 @@
import { createTrackedResponse } from "./createTrackedResponse.mjs"; import { createTrackedResponse } from "./createTrackedResponse.mjs";
import { decodeContentLength } from "./decodeContentLength.mjs"; import { decodeContentLength } from "./decodeContentLength.mjs";
import { retryPromise } from "./retryPromise.mjs"; import { retryPromise } from "./retryPromise.mjs";
import { makeFileSizeFetcher } from "./makeFileSizeFetcher.mjs";
import { makeSignal } from "./makeSignal.mjs"; import { makeSignal } from "./makeSignal.mjs";
import { fetchContentLength } from "./fetchContentLength.mjs";
import { metadata } from "./path.mjs";
/** /**
* @typedef {{ * @typedef {{
@ -14,6 +15,11 @@ import { makeSignal } from "./makeSignal.mjs";
* }} FileLoaderOptions * }} FileLoaderOptions
*/ */
/**
* @template T
* @typedef {import("./makeSignal.mjs").Signal<T>} Signal<T>
*/
/** /**
* @typedef {ReturnType<typeof makeFileLoader>} FileLoader * @typedef {ReturnType<typeof makeFileLoader>} FileLoader
*/ */
@ -36,24 +42,46 @@ export const makeFileLoader = (
path, path,
{ total = 0, onProgress, signal } = {} { total = 0, onProgress, signal } = {}
) => { ) => {
const progress = makeSignal({ received: 0, total: 0 }); const progress = makeSignal({ path, received: 0, total: 0 });
/** @type {Signal<Response>} */
const done = makeSignal();
/** @type {Signal<Error>} */
const failed = makeSignal();
if (onProgress) { if (onProgress) {
progress.connect(onProgress, { signal }); progress.connect(onProgress, { signal });
} }
const createResponse = () => const createResponse = () => {
createTrackedResponse( const response = createTrackedResponse(
(received) => progress.emit({ received, total }), (received) => progress.emit({ path, received, total }),
signal signal
); );
response.then(done.emit);
response.catch(failed.emit);
return response;
};
let responsePromise = createResponse(); let responsePromise = createResponse();
/** /**
* Retrieves the file size if `total` was not provided. * Retrieves the file size if `total` was not provided.
*/ */
const fetchSize = makeFileSizeFetcher(path, total); const fetchSize = (() => {
/**
* @type {Promise<number> | null}
*/
let totalPromise;
const fetchSize = () =>
(totalPromise = total
? Promise.resolve(total)
: fetchContentLength(path).then(
(fetchedTotal) => (total = fetchedTotal)
));
return fetchSize;
})();
/** /**
* Fetches a file, dispatching progress events while doing so. * Fetches a file, dispatching progress events while doing so.
@ -102,9 +130,12 @@ export const makeFileLoader = (
}; };
return { return {
...metadata(path),
fetchSize, fetchSize,
load, load,
unload, unload,
done,
failed,
progress, progress,
get path() { get path() {
return path; return path;
@ -112,6 +143,9 @@ export const makeFileLoader = (
get total() { get total() {
return total; return total;
}, },
set total(newTotal) {
total = newTotal;
},
get received() { get received() {
return responsePromise.received; return responsePromise.received;
}, },
@ -145,14 +179,12 @@ export const makeFileLoadersTracker = (files) => {
* Called every time any file's status changes. * Called every time any file's status changes.
* Updates the total and received amounts. * Updates the total and received amounts.
*/ */
const update = () => { const update = (props) => {
let _total = 0; let _total = 0;
let _received = 0; let _received = 0;
files.forEach((fileLoader) => {
files.forEach((thing) => { _total += fileLoader.total;
_total += thing.total; _received += fileLoader.received;
_received += thing.received;
console.log(thing.path, thing.received, thing.total);
}); });
if (total != _total || received != _received) { if (total != _total || received != _received) {
total = _total; total = _total;
@ -161,15 +193,30 @@ export const makeFileLoadersTracker = (files) => {
} }
}; };
files.forEach((loader) => { files.forEach((fileLoader) => {
loader.then(decrease); fileLoader.done.connect(decrease);
loader.progress.connect(update); fileLoader.progress.connect(update);
}); });
/**
* Runs `fetchSize` on all files to ensure all have a size
*/
const ensureSize = () => const ensureSize = () =>
Promise.all(files.map(({ fetchSize }) => fetchSize())); Promise.all(files.map(({ fetchSize }) => fetchSize()));
const loadAll = () => Promise.all(files.map(({ load }) => load())); /**
* Loads all files. Loading happens once per file, so if it was called before, this is a
* no-op (for each particular file).
* If a file does not have a total set, then a small header fetch can optionally happen
* to fetch the initial size.
* @param {boolean} [andEnsureSize] set it to `false` to skip fetching sizes for files
* without a specified total
* @returns
*/
const loadAll = (andEnsureSize = true) =>
(andEnsureSize ? ensureSize() : Promise.resolve()).then(() =>
Promise.all(files.map(({ load }) => load()))
);
return { done, progress, ensureSize, loadAll }; return { done, progress, ensureSize, loadAll };
}; };

View File

@ -7,6 +7,16 @@
/** /**
* @typedef {{signal?: AbortSignal, once?: boolean}} ListenerOptions * @typedef {{signal?: AbortSignal, once?: boolean}} ListenerOptions
*/ */
/**
* @template T
* @typedef {{
* connect(listener: Listener<T>, options?: ListenerOptions): () => boolean,
* disconnect(listener: Listener<T>): boolean,
* emit(args: T): void
* disable(): void
* }} Signal<T>
*/
/** /**
* Returns an event emitter for a specific signal * Returns an event emitter for a specific signal
@ -14,7 +24,7 @@
* automatic typing where applicable. * automatic typing where applicable.
* @template T * @template T
* @param {T} [_initial] * @param {T} [_initial]
* @returns * @returns {Signal<T>}
*/ */
export const makeSignal = (_initial) => { export const makeSignal = (_initial) => {
/** @type {Set<Listener<T>>} */ /** @type {Set<Listener<T>>} */

View File

@ -1,14 +1,19 @@
//@ts-check //@ts-check
const pickFirstArg = (fst, ..._none) => JSON.stringify(fst)
/** /**
* Caches the result of a function. * Caches the result of a function.
* The cache is available as `.cache` in case there's a need to clear anything. * The cache is available as `.cache` in case there's a need to clear anything.
* Uses the first parameter as a key by default, you can change this behavior by passing a custom
* hash function.
* @template {(...args: any[]) => any} T * @template {(...args: any[]) => any} T
* @param {T} functionToMemoize * @param {T} functionToMemoize
* @param {(...args: Parameters<T>) => string|number} [hashFunction]
* @returns * @returns
*/ */
export const memoize = (functionToMemoize) => { export const memoize = (functionToMemoize, hashFunction = pickFirstArg) => {
/** @type {Map<Parameters<T>[0], ReturnType<T>>} */ /** @type {Map<string|number, ReturnType<T>>} */
const cache = new Map() const cache = new Map()
/** /**
* *
@ -16,15 +21,14 @@ export const memoize = (functionToMemoize) => {
* @returns {ReturnType<T>} * @returns {ReturnType<T>}
*/ */
const memoized = (...args) => { const memoized = (...args) => {
const key = args[0] const key = hashFunction(...args)
if(!cache.has(key)){ if(!cache.has(key)){
cache.set(key, functionToMemoize(...args)) cache.set(key, functionToMemoize(...args))
} }
//@ts-ignore
return cache.get(key) return cache.get(key)
} }
memoized.map = cache
return memoized return Object.assign(memoized, { cache })
} }
export default memoize export default memoize

40
modules/utils/path.mjs Normal file
View File

@ -0,0 +1,40 @@
/**
*
* @param {string} str
* @param {string} [sep] defaults to `/`
*/
export const filename = (str, sep = '/') => str.slice(str.lastIndexOf(sep) + 1)
/**
* @param {string} str
*/
export const stripExtension = (str) => str.slice(0,str.lastIndexOf('.'))
/**
*
* @param {string} str
* @param {string} [sep] defaults to `/`
*/
export const basename = (str, sep = '/') => stripExtension(filename(str, sep))
/**
* @param {string} str
*/
export const extension = (str) => str.slice(str.lastIndexOf('.') + 1)
/**
* @param {string} str
* @param {string} [sep] defaults to `/`
*/
export const dirName = (str, sep = '/') => str.slice(0, str.lastIndexOf(sep) + 1)
export const metadata = (str, sep = '/') => ({
basename: basename(str, sep),
filename: filename(str, sep),
extension: extension(str),
dirName: dirName(str),
fullPath: str
})
export default { basename, filename, stripExtension, dirName, extension, metadata }