smol update
This commit is contained in:
parent
eadca78080
commit
cde1e58a47
@ -69,8 +69,15 @@ export const createTrackedResponse = (
|
||||
|
||||
const start = readerPromise.resolve;
|
||||
|
||||
return Object.assign(successPromise, {
|
||||
/** @type {Promise<Response>} */
|
||||
return {
|
||||
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() {
|
||||
return response;
|
||||
},
|
||||
@ -89,5 +96,7 @@ export const createTrackedResponse = (
|
||||
get isUnknown() {
|
||||
return success === false && failed === false;
|
||||
},
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export default createTrackedResponse;
|
||||
|
@ -24,4 +24,4 @@ export const deferredPromise = () => {
|
||||
return Object.assign(promise, {resolve, reject});
|
||||
}
|
||||
|
||||
export default DeferredPromise
|
||||
export default deferredPromise
|
25
modules/utils/getPageUniqueId.mjs
Normal file
25
modules/utils/getPageUniqueId.mjs
Normal 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
|
12
modules/utils/getReasonableUuid.mjs
Normal file
12
modules/utils/getReasonableUuid.mjs
Normal 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
|
@ -20,11 +20,14 @@ import {
|
||||
import { getElementByCSSSelector } from "./getElementByCSSSelector.mjs";
|
||||
import { getElementById } from "./getElementById.mjs";
|
||||
import { getFirstTitleContent } from "./getFirstTitleContent.mjs";
|
||||
import { getPageUniqueId } from "./getPageUniqueId.mjs";
|
||||
import { getReasonableUuid } from "./getReasonableUuid.mjs";
|
||||
import { identity, awaitedIdentity } from "./identity.mjs";
|
||||
import { html } from "./html.mjs";
|
||||
import { isExternalUrl } from "./isExternalUrl.mjs";
|
||||
import { isLocalHost } from "./isLocalHost.mjs";
|
||||
import { isNotNull } from "./isNotNull.mjs";
|
||||
import { makeBoundConsole } from "./makeBoundConsole.mjs";
|
||||
import { makeEventEmitter } from "./makeEventEmitter.mjs";
|
||||
import { makeFileLoader, makeFileLoadersTracker } from "./makeFileLoader.mjs";
|
||||
import { makeFileSizeFetcher } from "./makeFileSizeFetcher.mjs";
|
||||
@ -39,6 +42,14 @@ import {
|
||||
onDocumentKeyDown,
|
||||
onDocumentKey,
|
||||
} from "./onDocumentKey.mjs";
|
||||
import {
|
||||
basename,
|
||||
filename,
|
||||
stripExtension,
|
||||
dirName,
|
||||
extension,
|
||||
metadata,
|
||||
} from "./path.mjs";
|
||||
import { percentFromProgress } from "./percentFromProgress.mjs";
|
||||
import { print, makeTemplate } from "./print.mjs";
|
||||
import {
|
||||
@ -71,12 +82,15 @@ export {
|
||||
getElementByCSSSelector,
|
||||
getElementById,
|
||||
getFirstTitleContent,
|
||||
getPageUniqueId,
|
||||
getReasonableUuid,
|
||||
html,
|
||||
identity,
|
||||
awaitedIdentity,
|
||||
isExternalUrl,
|
||||
isLocalHost,
|
||||
isNotNull,
|
||||
makeBoundConsole,
|
||||
makeEventEmitter,
|
||||
makeFileLoader,
|
||||
makeFileLoadersTracker,
|
||||
@ -90,6 +104,12 @@ export {
|
||||
onDocumentKeyUp,
|
||||
onDocumentKeyDown,
|
||||
onDocumentKey,
|
||||
basename,
|
||||
filename,
|
||||
stripExtension,
|
||||
dirName,
|
||||
extension,
|
||||
metadata,
|
||||
percentFromProgress,
|
||||
print,
|
||||
makeTemplate,
|
||||
|
19
modules/utils/makeBoundConsole.mjs
Normal file
19
modules/utils/makeBoundConsole.mjs
Normal 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
|
@ -3,8 +3,9 @@
|
||||
import { createTrackedResponse } from "./createTrackedResponse.mjs";
|
||||
import { decodeContentLength } from "./decodeContentLength.mjs";
|
||||
import { retryPromise } from "./retryPromise.mjs";
|
||||
import { makeFileSizeFetcher } from "./makeFileSizeFetcher.mjs";
|
||||
import { makeSignal } from "./makeSignal.mjs";
|
||||
import { fetchContentLength } from "./fetchContentLength.mjs";
|
||||
import { metadata } from "./path.mjs";
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
@ -14,6 +15,11 @@ import { makeSignal } from "./makeSignal.mjs";
|
||||
* }} FileLoaderOptions
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @typedef {import("./makeSignal.mjs").Signal<T>} Signal<T>
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {ReturnType<typeof makeFileLoader>} FileLoader
|
||||
*/
|
||||
@ -36,24 +42,46 @@ export const makeFileLoader = (
|
||||
path,
|
||||
{ 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) {
|
||||
progress.connect(onProgress, { signal });
|
||||
}
|
||||
|
||||
const createResponse = () =>
|
||||
createTrackedResponse(
|
||||
(received) => progress.emit({ received, total }),
|
||||
const createResponse = () => {
|
||||
const response = createTrackedResponse(
|
||||
(received) => progress.emit({ path, received, total }),
|
||||
signal
|
||||
);
|
||||
response.then(done.emit);
|
||||
response.catch(failed.emit);
|
||||
return response;
|
||||
};
|
||||
|
||||
let responsePromise = createResponse();
|
||||
|
||||
/**
|
||||
* 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.
|
||||
@ -102,9 +130,12 @@ export const makeFileLoader = (
|
||||
};
|
||||
|
||||
return {
|
||||
...metadata(path),
|
||||
fetchSize,
|
||||
load,
|
||||
unload,
|
||||
done,
|
||||
failed,
|
||||
progress,
|
||||
get path() {
|
||||
return path;
|
||||
@ -112,6 +143,9 @@ export const makeFileLoader = (
|
||||
get total() {
|
||||
return total;
|
||||
},
|
||||
set total(newTotal) {
|
||||
total = newTotal;
|
||||
},
|
||||
get received() {
|
||||
return responsePromise.received;
|
||||
},
|
||||
@ -145,14 +179,12 @@ export const makeFileLoadersTracker = (files) => {
|
||||
* Called every time any file's status changes.
|
||||
* Updates the total and received amounts.
|
||||
*/
|
||||
const update = () => {
|
||||
const update = (props) => {
|
||||
let _total = 0;
|
||||
let _received = 0;
|
||||
|
||||
files.forEach((thing) => {
|
||||
_total += thing.total;
|
||||
_received += thing.received;
|
||||
console.log(thing.path, thing.received, thing.total);
|
||||
files.forEach((fileLoader) => {
|
||||
_total += fileLoader.total;
|
||||
_received += fileLoader.received;
|
||||
});
|
||||
if (total != _total || received != _received) {
|
||||
total = _total;
|
||||
@ -161,15 +193,30 @@ export const makeFileLoadersTracker = (files) => {
|
||||
}
|
||||
};
|
||||
|
||||
files.forEach((loader) => {
|
||||
loader.then(decrease);
|
||||
loader.progress.connect(update);
|
||||
files.forEach((fileLoader) => {
|
||||
fileLoader.done.connect(decrease);
|
||||
fileLoader.progress.connect(update);
|
||||
});
|
||||
|
||||
/**
|
||||
* Runs `fetchSize` on all files to ensure all have a size
|
||||
*/
|
||||
const ensureSize = () =>
|
||||
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 };
|
||||
};
|
||||
|
@ -7,6 +7,16 @@
|
||||
/**
|
||||
* @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
|
||||
@ -14,7 +24,7 @@
|
||||
* automatic typing where applicable.
|
||||
* @template T
|
||||
* @param {T} [_initial]
|
||||
* @returns
|
||||
* @returns {Signal<T>}
|
||||
*/
|
||||
export const makeSignal = (_initial) => {
|
||||
/** @type {Set<Listener<T>>} */
|
||||
|
@ -1,14 +1,19 @@
|
||||
//@ts-check
|
||||
|
||||
const pickFirstArg = (fst, ..._none) => JSON.stringify(fst)
|
||||
|
||||
/**
|
||||
* Caches the result of a function.
|
||||
* 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
|
||||
* @param {T} functionToMemoize
|
||||
* @param {(...args: Parameters<T>) => string|number} [hashFunction]
|
||||
* @returns
|
||||
*/
|
||||
export const memoize = (functionToMemoize) => {
|
||||
/** @type {Map<Parameters<T>[0], ReturnType<T>>} */
|
||||
export const memoize = (functionToMemoize, hashFunction = pickFirstArg) => {
|
||||
/** @type {Map<string|number, ReturnType<T>>} */
|
||||
const cache = new Map()
|
||||
/**
|
||||
*
|
||||
@ -16,15 +21,14 @@ export const memoize = (functionToMemoize) => {
|
||||
* @returns {ReturnType<T>}
|
||||
*/
|
||||
const memoized = (...args) => {
|
||||
const key = args[0]
|
||||
const key = hashFunction(...args)
|
||||
if(!cache.has(key)){
|
||||
cache.set(key, functionToMemoize(...args))
|
||||
}
|
||||
//@ts-ignore
|
||||
return cache.get(key)
|
||||
}
|
||||
memoized.map = cache
|
||||
return memoized
|
||||
|
||||
return Object.assign(memoized, { cache })
|
||||
}
|
||||
|
||||
export default memoize
|
40
modules/utils/path.mjs
Normal file
40
modules/utils/path.mjs
Normal 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 }
|
Loading…
Reference in New Issue
Block a user