smol update
This commit is contained in:
		@@ -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 }
 | 
			
		||||
		Reference in New Issue
	
	Block a user