smol update
This commit is contained in:
parent
eadca78080
commit
cde1e58a47
@ -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;
|
||||||
|
@ -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
|
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 { 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,
|
||||||
|
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 { 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 };
|
||||||
};
|
};
|
||||||
|
@ -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>>} */
|
||||||
|
@ -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
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