diff --git a/package-lock.json b/package-lock.json index 43c1197..6eedb46 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,8 +9,7 @@ "version": "3.2.2", "license": "MIT", "dependencies": { - "@swup/plugin": "^3.0.0", - "throttles": "^1.0.1" + "@swup/plugin": "^3.0.0" }, "devDependencies": { "network-information-types": "^0.1.1" @@ -5563,14 +5562,6 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" }, - "node_modules/throttles": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/throttles/-/throttles-1.0.1.tgz", - "integrity": "sha512-fab7Xg+zELr9KOv4fkaBoe/b3L0GMGLd0IBSCn16GoE/Qx6/OfCr1eGNyEcDU2pUA79qQfZ8kPQWlRuok4YwTw==", - "engines": { - "node": ">=6" - } - }, "node_modules/tiny-glob": { "version": "0.2.9", "resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.9.tgz", diff --git a/package.json b/package.json index 4ee13fb..64e6e72 100755 --- a/package.json +++ b/package.json @@ -49,8 +49,7 @@ "url": "https://github.com/swup/preload-plugin.git" }, "dependencies": { - "@swup/plugin": "^3.0.0", - "throttles": "^1.0.1" + "@swup/plugin": "^3.0.0" }, "peerDependencies": { "swup": "^4.0.0" diff --git a/src/index.ts b/src/index.ts index ebb580d..f08056a 100755 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,8 @@ import Plugin from '@swup/plugin'; import { getCurrentUrl, Handler, Location } from 'swup'; import type { DelegateEvent, DelegateEventHandler, DelegateEventUnsubscribe, PageData } from 'swup'; -import { default as throttles } from 'throttles/priority'; + +import Queue from './queue.js'; declare module 'swup' { export interface Swup { @@ -54,11 +55,6 @@ type PreloadOptions = { priority?: boolean; }; -type Queue = { - add: (fn: () => void, highPriority?: boolean) => void; - next: () => void; -}; - export default class SwupPreloadPlugin extends Plugin { name = 'SwupPreloadPlugin'; @@ -78,8 +74,7 @@ export default class SwupPreloadPlugin extends Plugin { options: PluginOptions; - protected queue: Queue; - protected preloadPromises = new Map>(); + protected preloadQueue: Queue; protected preloadObserver?: { stop: () => void; update: () => void }; protected mouseEnterDelegate?: DelegateEventUnsubscribe; @@ -106,9 +101,8 @@ export default class SwupPreloadPlugin extends Plugin { // Bind public methods this.preload = this.preload.bind(this); - // Create global priority queue - const [add, next] = throttles(this.options.throttle); - this.queue = { add, next }; + // Create priority queue + this.preloadQueue = new Queue(this.options.throttle); } mount() { @@ -158,7 +152,7 @@ export default class SwupPreloadPlugin extends Plugin { this.swup.preload = undefined; this.swup.preloadLinks = undefined; - this.preloadPromises.clear(); + this.preloadQueue.clear(); this.mouseEnterDelegate?.destroy(); this.touchStartDelegate?.destroy(); @@ -172,8 +166,8 @@ export default class SwupPreloadPlugin extends Plugin { */ protected onPageLoad: Handler<'page:load'> = (visit, args, defaultHandler) => { const { url } = visit.to; - if (url && this.preloadPromises.has(url)) { - return this.preloadPromises.get(url); + if (url && this.preloadQueue.has(url)) { + return this.preloadQueue.get(url); } return defaultHandler?.(visit, args); }; @@ -261,8 +255,8 @@ export default class SwupPreloadPlugin extends Plugin { } // Already preloading? Return existing promise - if (this.preloadPromises.has(url)) { - return this.preloadPromises.get(url); + if (this.preloadQueue.has(url)) { + return this.preloadQueue.get(url); } // Should we preload? @@ -272,21 +266,7 @@ export default class SwupPreloadPlugin extends Plugin { // Queue the preload with either low or high priority // The actual preload will happen when a spot in the queue is available - const queuedPromise = new Promise((resolve) => { - this.queue.add(() => { - this.performPreload(url) - .catch(() => {}) - .then((page) => resolve(page)) - .finally(() => { - this.queue.next(); - this.preloadPromises.delete(url); - }); - }, priority); - }); - - this.preloadPromises.set(url, queuedPromise); - - return queuedPromise; + return this.preloadQueue.add(url, () => this.performPreload(url), priority); } /** @@ -386,7 +366,7 @@ export default class SwupPreloadPlugin extends Plugin { // Already in cache? if (this.swup.cache.has(url)) return false; // Already preloading? - if (this.preloadPromises.has(url)) return false; + if (this.preloadQueue.has(url)) return false; // Should be ignored anyway? if (this.swup.shouldIgnoreVisit(href, { el })) return false; // Special condition for links: points to current page? @@ -398,10 +378,14 @@ export default class SwupPreloadPlugin extends Plugin { /** * Perform the actual preload fetch and trigger the preload hook. */ - protected async performPreload(url: string): Promise { - const page = await this.swup.fetchPage(url); - await this.swup.hooks.call('page:preload', { page }); - return page; + protected async performPreload(url: string): Promise { + try { + const page = await this.swup.fetchPage(url); + await this.swup.hooks.call('page:preload', { page }); + return page; + } catch (error) { + return; + } } /** diff --git a/src/queue.ts b/src/queue.ts new file mode 100644 index 0000000..7daae9d --- /dev/null +++ b/src/queue.ts @@ -0,0 +1,93 @@ +type QueueFunction = { (): Promise; }; + +/** + * A priority queue that runs a limited number of jobs at a time. + */ +export default class Queue { + /** The number of jobs to run at a time */ + private limit: number; + + /** The queue of low-priority jobs */ + private qlow: Map> = new Map(); + + /** The queue of high-priority jobs */ + private qhigh: Map> = new Map(); + + /** The list of currently running jobs */ + private qactive: Map> = new Map(); + + constructor(limit: number = 1) { + this.limit = limit; + } + + /** The total number of jobs in the queue */ + get total(): number { + return this.qlow.size + this.qhigh.size; + } + + /** Add a job to queue */ + async add(key: string, fn: QueueFunction, highPriority: boolean = false): Promise { + // Short-circuit if already running + if (this.qactive.has(key)) { + return this.qactive.get(key); + } + + if (this.qlow.has(key) && highPriority) { + // Promote from low to high-priority queue + this.qlow.delete(key); + } else if (this.qhigh.has(key)) { + // Skip if already in queue + return; + } + + (highPriority ? this.qhigh : this.qlow).set(key, fn); + + if (this.total <= 1) { + this.run(); + } + } + + active(key: string): boolean { + return this.qactive.has(key); + } + + queued(key: string): boolean { + return this.qlow.has(key) || this.qhigh.has(key); + } + + has(key: string): boolean { + return this.active(key) || this.queued(key); + } + + clear(): void { + this.qlow.clear(); + this.qhigh.clear(); + } + + /** Run the next available job */ + protected async run(): Promise { + if (!this.total) return; + if (this.qactive.size >= this.limit) return; + + const next = this.next(); + if (next) { + this.qactive.set(next.key); + this.run(); + await next.fn(); + this.qactive.delete(next.key); + this.run(); + } + } + + /** Get the next available job */ + protected next(): { key: string; fn: QueueFunction } | null { + return [this.qhigh, this.qlow].reduce((acc, queue) => { + if (!acc) { + const [key, fn] = queue.entries().next().value || []; + queue.delete(key); + return key ? { key, fn } : null; + } + return acc; + }, null as { key: string; fn: QueueFunction } | null); + } +}