From 4c9ab77df6e71a4b1d4a7a5c9efe2d896a1a620a Mon Sep 17 00:00:00 2001 From: Philipp Daun Date: Mon, 21 Aug 2023 11:27:57 +0200 Subject: [PATCH 1/7] 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 2/7] 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 3/7] 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 4/7] 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 5/7] 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 6/7] 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 7/7] 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(); } }