From f630d2d6e0e4dab070f846ce7432e9fa050391bc Mon Sep 17 00:00:00 2001 From: Philipp Daun Date: Fri, 18 Aug 2023 21:17:17 +0200 Subject: [PATCH 1/8] Inline throttles package --- package-lock.json | 11 +-------- package.json | 3 +-- src/index.ts | 10 ++------ src/queue.ts | 59 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 63 insertions(+), 20 deletions(-) create mode 100644 src/queue.ts 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..660fc47 100755 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,7 @@ 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 createQueue, { Queue } from './queue.js'; declare module 'swup' { export interface Swup { @@ -54,11 +54,6 @@ type PreloadOptions = { priority?: boolean; }; -type Queue = { - add: (fn: () => void, highPriority?: boolean) => void; - next: () => void; -}; - export default class SwupPreloadPlugin extends Plugin { name = 'SwupPreloadPlugin'; @@ -107,8 +102,7 @@ export default class SwupPreloadPlugin extends Plugin { this.preload = this.preload.bind(this); // Create global priority queue - const [add, next] = throttles(this.options.throttle); - this.queue = { add, next }; + this.queue = createQueue(this.options.throttle); } mount() { diff --git a/src/queue.ts b/src/queue.ts new file mode 100644 index 0000000..ad4e3b6 --- /dev/null +++ b/src/queue.ts @@ -0,0 +1,59 @@ +type QueueFunction = { + (): void; + __queued?: boolean; +}; + +export type Queue = { + add: (fn: QueueFunction, highPriority?: boolean) => void; + next: () => void; +}; + +export default function createQueue(limit: number = 1): Queue { + const qlow: QueueFunction[] = []; + const qhigh: QueueFunction[] = []; + let total = 0; + let running = 0; + + function add(fn: QueueFunction, highPriority: boolean = false): void { + // Already added before? + if (fn.__queued) { + // Move from low to high-priority queue + if (highPriority) { + const idx = qlow.indexOf(fn); + if (idx >= 0) { + const removed = qlow.splice(idx, 1); + total = total - removed.length; + } + } else { + return; + } + } + + // Mark as processed + fn.__queued = true; + // Push to queue: high or low + (highPriority ? qhigh : qlow).push(fn); + // Increment total + total++; + // Initialize queue if first item + if (total <= 1) { + run(); + } + } + + function next(): void { + running--; // make room for next + run(); + } + + function run(): void { + if (running < limit && total > 0) { + const fn = qhigh.shift() || qlow.shift() || (() => {}); + fn(); + total--; + running++; // is now WIP + } + } + + return { add, next }; +} From 4c9ab77df6e71a4b1d4a7a5c9efe2d896a1a620a Mon Sep 17 00:00:00 2001 From: Philipp Daun Date: Mon, 21 Aug 2023 11:27:57 +0200 Subject: [PATCH 2/8] Rewrite Queue as class --- src/index.ts | 9 +++--- src/queue.ts | 87 ++++++++++++++++++++++++++++------------------------ 2 files changed, 51 insertions(+), 45 deletions(-) diff --git a/src/index.ts b/src/index.ts index 660fc47..b870b4b 100755 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,7 @@ import Plugin from '@swup/plugin'; import { getCurrentUrl, Handler, Location } from 'swup'; import type { DelegateEvent, DelegateEventHandler, DelegateEventUnsubscribe, PageData } from 'swup'; -import createQueue, { Queue } from './queue.js'; +import Queue from './queue.js'; declare module 'swup' { export interface Swup { @@ -102,7 +102,7 @@ export default class SwupPreloadPlugin extends Plugin { this.preload = this.preload.bind(this); // Create global priority queue - this.queue = createQueue(this.options.throttle); + this.queue = new Queue(this.options.throttle); } mount() { @@ -267,12 +267,11 @@ 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) + this.queue.add(url, () => { + return this.performPreload(url) .catch(() => {}) .then((page) => resolve(page)) .finally(() => { - this.queue.next(); this.preloadPromises.delete(url); }); }, priority); diff --git a/src/queue.ts b/src/queue.ts index ad4e3b6..bddadde 100644 --- a/src/queue.ts +++ b/src/queue.ts @@ -1,59 +1,66 @@ type QueueFunction = { - (): void; - __queued?: boolean; + (): void | Promise; }; -export type Queue = { - add: (fn: QueueFunction, highPriority?: boolean) => void; - next: () => void; -}; +export default class Queue { + private limit: number; + private qlow: Map = new Map(); + private qhigh: Map = new Map(); + private running: Set = new Set(); + + constructor(limit: number = 1) { + this.limit = limit; + } + + get total(): number { + return this.qlow.size + this.qhigh.size; + } + + has(key: string): boolean { + return this.qlow.has(key) || this.qhigh.has(key); + } -export default function createQueue(limit: number = 1): Queue { - const qlow: QueueFunction[] = []; - const qhigh: QueueFunction[] = []; - let total = 0; - let running = 0; + add(key: string, fn: QueueFunction, highPriority: boolean = false): void { + if (this.running.has(key)) { + return; + } - function add(fn: QueueFunction, highPriority: boolean = false): void { - // Already added before? - if (fn.__queued) { - // Move from low to high-priority queue + if (this.has(key)) { if (highPriority) { - const idx = qlow.indexOf(fn); - if (idx >= 0) { - const removed = qlow.splice(idx, 1); - total = total - removed.length; - } + // Promote from low to high-priority queue + this.qlow.delete(key); } else { return; } } - // Mark as processed - fn.__queued = true; - // Push to queue: high or low - (highPriority ? qhigh : qlow).push(fn); - // Increment total - total++; - // Initialize queue if first item - if (total <= 1) { - run(); + (highPriority ? this.qhigh : this.qlow).set(key, fn); + + if (!this.running.size) { + this.run(); } } - function next(): void { - running--; // make room for next - run(); - } + protected async run(): Promise { + if (!this.total) return; + if (this.running.size >= this.limit) return; - function run(): void { - if (running < limit && total > 0) { - const fn = qhigh.shift() || qlow.shift() || (() => {}); - fn(); - total--; - running++; // is now WIP + const next = this.next(); + if (next) { + this.running.add(next.key); + await next.fn(); + this.running.delete(next.key); + this.run(); } } - return { add, next }; + protected next(): { key: string; fn: QueueFunction } | null { + return [this.qhigh, this.qlow].reduce((acc, queue) => { + if (!acc) { + const [key, fn] = queue.entries().next().value || []; + return key ? { key, fn } : acc; + } + return acc; + }, null as { key: string; fn: QueueFunction } | null); + } } From a9e3c342bb4406d77a66ee635e08352e5fde742f Mon Sep 17 00:00:00 2001 From: Philipp Daun Date: Mon, 21 Aug 2023 11:31:58 +0200 Subject: [PATCH 3/8] Remove item from queue --- src/queue.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/queue.ts b/src/queue.ts index bddadde..fbcf11e 100644 --- a/src/queue.ts +++ b/src/queue.ts @@ -58,7 +58,10 @@ export default class Queue { return [this.qhigh, this.qlow].reduce((acc, queue) => { if (!acc) { const [key, fn] = queue.entries().next().value || []; - return key ? { key, fn } : acc; + if (key) { + queue.delete(key); + return { key, fn }; + } } return acc; }, null as { key: string; fn: QueueFunction } | null); From ea7464d9aa1b2c1b643c22ec2e96003f29baaf84 Mon Sep 17 00:00:00 2001 From: Philipp Daun Date: Mon, 21 Aug 2023 12:11:14 +0200 Subject: [PATCH 4/8] Fix queue running order --- src/queue.ts | 32 ++++++++++++-------------------- 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/src/queue.ts b/src/queue.ts index fbcf11e..bf035f6 100644 --- a/src/queue.ts +++ b/src/queue.ts @@ -1,5 +1,5 @@ type QueueFunction = { - (): void | Promise; + (): unknown | Promise; }; export default class Queue { @@ -16,27 +16,20 @@ export default class Queue { return this.qlow.size + this.qhigh.size; } - has(key: string): boolean { - return this.qlow.has(key) || this.qhigh.has(key); - } - add(key: string, fn: QueueFunction, highPriority: boolean = false): void { - if (this.running.has(key)) { - return; - } + if (this.running.has(key)) return; - if (this.has(key)) { - if (highPriority) { - // Promote from low to high-priority queue - this.qlow.delete(key); - } else { - return; - } + 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.running.size) { + if (this.total <= 1) { this.run(); } } @@ -48,6 +41,7 @@ export default class Queue { const next = this.next(); if (next) { this.running.add(next.key); + this.run(); await next.fn(); this.running.delete(next.key); this.run(); @@ -58,10 +52,8 @@ export default class Queue { return [this.qhigh, this.qlow].reduce((acc, queue) => { if (!acc) { const [key, fn] = queue.entries().next().value || []; - if (key) { - queue.delete(key); - return { key, fn }; - } + queue.delete(key); + return key ? { key, fn } : null; } return acc; }, null as { key: string; fn: QueueFunction } | null); From ebc50605dca335d4600ad050e2b6a440af00b877 Mon Sep 17 00:00:00 2001 From: Philipp Daun Date: Mon, 21 Aug 2023 12:12:07 +0200 Subject: [PATCH 5/8] Switch queued preload promise to async method --- src/index.ts | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/index.ts b/src/index.ts index b870b4b..2773443 100755 --- a/src/index.ts +++ b/src/index.ts @@ -267,13 +267,10 @@ 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(url, () => { - return this.performPreload(url) - .catch(() => {}) - .then((page) => resolve(page)) - .finally(() => { - this.preloadPromises.delete(url); - }); + this.queue.add(url, async () => { + const page = await this.performPreload(url); + this.preloadPromises.delete(url); + resolve(page); }, priority); }); @@ -391,10 +388,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; + } } /** From dc2ac4328e3c5ca07edd40dd7752b0236cb287aa Mon Sep 17 00:00:00 2001 From: Philipp Daun Date: Mon, 21 Aug 2023 12:26:10 +0200 Subject: [PATCH 6/8] Add comments --- src/queue.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/queue.ts b/src/queue.ts index bf035f6..bbe852a 100644 --- a/src/queue.ts +++ b/src/queue.ts @@ -2,20 +2,29 @@ type QueueFunction = { (): unknown | 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 running: Set = new Set(); 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 */ add(key: string, fn: QueueFunction, highPriority: boolean = false): void { if (this.running.has(key)) return; @@ -34,6 +43,7 @@ export default class Queue { } } + /** Run the next available job */ protected async run(): Promise { if (!this.total) return; if (this.running.size >= this.limit) return; @@ -48,6 +58,7 @@ export default class Queue { } } + /** Get the next available job */ protected next(): { key: string; fn: QueueFunction } | null { return [this.qhigh, this.qlow].reduce((acc, queue) => { if (!acc) { From e3837688a4621771e6ee7175b0e77b16ee7c8ba1 Mon Sep 17 00:00:00 2001 From: Philipp Daun Date: Tue, 29 Aug 2023 10:55:18 +0200 Subject: [PATCH 7/8] Key jobs by url (WIP) --- src/index.ts | 31 ++++++++++--------------------- src/queue.ts | 19 +++++++++++++++---- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/src/index.ts b/src/index.ts index 2773443..494f327 100755 --- a/src/index.ts +++ b/src/index.ts @@ -73,8 +73,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; @@ -101,8 +100,8 @@ export default class SwupPreloadPlugin extends Plugin { // Bind public methods this.preload = this.preload.bind(this); - // Create global priority queue - this.queue = new Queue(this.options.throttle); + // Create priority queue + this.preloadQueue = new Queue(this.options.throttle); } mount() { @@ -152,7 +151,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(); @@ -166,8 +165,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); }; @@ -255,8 +254,8 @@ export default class SwupPreloadPlugin extends Plugin { } // Already preloading? Return existing promise - if (this.preloadPromises.has(url)) { - return this.preloadPromises.get(url); + if (this.queue.has(url)) { + return this.queue.get(url); } // Should we preload? @@ -266,17 +265,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(url, async () => { - const page = await this.performPreload(url); - this.preloadPromises.delete(url); - resolve(page); - }, priority); - }); - - this.preloadPromises.set(url, queuedPromise); - - return queuedPromise; + return this.preloadQueue.add(url, () => this.performPreload(url), priority); } /** @@ -376,7 +365,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? diff --git a/src/queue.ts b/src/queue.ts index bbe852a..ffa2aba 100644 --- a/src/queue.ts +++ b/src/queue.ts @@ -5,7 +5,7 @@ type QueueFunction = { /** * A priority queue that runs a limited number of jobs at a time. */ -export default class Queue { +export default class Queue { /** The number of jobs to run at a time */ private limit: number; /** The queue of low-priority jobs */ @@ -13,7 +13,7 @@ export default class Queue { /** The queue of high-priority jobs */ private qhigh: Map = new Map(); /** The list of currently running jobs */ - private running: Set = new Set(); + private running: Map> = new Map(); constructor(limit: number = 1) { this.limit = limit; @@ -25,8 +25,10 @@ export default class Queue { } /** Add a job to queue */ - add(key: string, fn: QueueFunction, highPriority: boolean = false): void { - if (this.running.has(key)) return; + async add(key: string, fn: QueueFunction, highPriority: boolean = false): Promise { + if (this.running.has(key)) { + return this.running.get(key) as Promise; + } if (this.qlow.has(key) && highPriority) { // Promote from low to high-priority queue @@ -43,6 +45,15 @@ export default class Queue { } } + has(key: string): boolean { + return this.running.has(key) || this.qlow.has(key) || this.qhigh.has(key); + } + + clear(): void { + this.qlow.clear(); + this.qhigh.clear(); + } + /** Run the next available job */ protected async run(): Promise { if (!this.total) return; From 0739b4fa6fb7669f491400f5916344fc76190a5c Mon Sep 17 00:00:00 2001 From: Philipp Daun Date: Tue, 29 Aug 2023 18:17:40 +0200 Subject: [PATCH 8/8] Rename queues --- src/index.ts | 5 +++-- src/queue.ts | 36 +++++++++++++++++++++++------------- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/src/index.ts b/src/index.ts index 494f327..f08056a 100755 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,7 @@ import Plugin from '@swup/plugin'; import { getCurrentUrl, Handler, Location } from 'swup'; import type { DelegateEvent, DelegateEventHandler, DelegateEventUnsubscribe, PageData } from 'swup'; + import Queue from './queue.js'; declare module 'swup' { @@ -254,8 +255,8 @@ export default class SwupPreloadPlugin extends Plugin { } // Already preloading? Return existing promise - if (this.queue.has(url)) { - return this.queue.get(url); + if (this.preloadQueue.has(url)) { + return this.preloadQueue.get(url); } // Should we preload? diff --git a/src/queue.ts b/src/queue.ts index ffa2aba..7daae9d 100644 --- a/src/queue.ts +++ b/src/queue.ts @@ -1,6 +1,4 @@ -type QueueFunction = { - (): unknown | Promise; -}; +type QueueFunction = { (): Promise; }; /** * A priority queue that runs a limited number of jobs at a time. @@ -8,12 +6,15 @@ type QueueFunction = { 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(); + private qlow: Map> = new Map(); + /** The queue of high-priority jobs */ - private qhigh: Map = new Map(); + private qhigh: Map> = new Map(); + /** The list of currently running jobs */ - private running: Map> = new Map(); + private qactive: Map> = new Map(); constructor(limit: number = 1) { this.limit = limit; @@ -25,9 +26,10 @@ export default class Queue { } /** Add a job to queue */ - async add(key: string, fn: QueueFunction, highPriority: boolean = false): Promise { - if (this.running.has(key)) { - return this.running.get(key) as Promise; + 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) { @@ -45,8 +47,16 @@ export default class Queue { } } + 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.running.has(key) || this.qlow.has(key) || this.qhigh.has(key); + return this.active(key) || this.queued(key); } clear(): void { @@ -57,14 +67,14 @@ export default class Queue { /** Run the next available job */ protected async run(): Promise { if (!this.total) return; - if (this.running.size >= this.limit) return; + if (this.qactive.size >= this.limit) return; const next = this.next(); if (next) { - this.running.add(next.key); + this.qactive.set(next.key); this.run(); await next.fn(); - this.running.delete(next.key); + this.qactive.delete(next.key); this.run(); } }