diff --git a/javascript-modules/generate/lib/live-connector.js b/javascript-modules/generate/lib/live-connector.js index b3fd15e9..d9ff6f31 100644 --- a/javascript-modules/generate/lib/live-connector.js +++ b/javascript-modules/generate/lib/live-connector.js @@ -22,8 +22,8 @@ const getLiveEditingConnector = () => { preferBlobs: true }); const options = window.bookshopLiveOptions || {}; - await window.bookshopLive.update(frontMatter, options); - CloudCannon?.refreshInterface?.(); + const rendered = await window.bookshopLive.update(frontMatter, options); + if (rendered) CloudCannon?.refreshInterface?.(); } document.addEventListener('cloudcannon:update', updateBookshopLive); updateBookshopLive(); diff --git a/javascript-modules/integration-tests/features/eleventy-zero/eleventy_zero_bookshop_live_browser.feature b/javascript-modules/integration-tests/features/eleventy-zero/eleventy_zero_bookshop_live_browser.feature index a9225123..711780be 100644 --- a/javascript-modules/integration-tests/features/eleventy-zero/eleventy_zero_bookshop_live_browser.feature +++ b/javascript-modules/integration-tests/features/eleventy-zero/eleventy_zero_bookshop_live_browser.feature @@ -71,7 +71,7 @@ Feature: Eleventy Bookshop CloudCannon Live Editing } ] } """ - * 🌐 "window.bookshopLive?.hasRendered === true" evaluates + * 🌐 "window.bookshopLive?.renderCount > 0" evaluates Then 🌐 The selector h1 should contain "Gidday" # Testing CloudCannon data changing When 🌐 CloudCannon pushes new json: diff --git a/javascript-modules/integration-tests/features/live/bookshop_live_performance.feature b/javascript-modules/integration-tests/features/live/bookshop_live_performance.feature new file mode 100644 index 00000000..98ccca74 --- /dev/null +++ b/javascript-modules/integration-tests/features/live/bookshop_live_performance.feature @@ -0,0 +1,75 @@ +Feature: Bookshop CloudCannon Live Editing Performance + + Background: + Given the file tree: + """ + package.json from starters/generate/package.json # <-- this .json line hurts my syntax highlighting + component-lib/ + go.mod from starters/hugo/components.go.mod + config.toml from starters/hugo/components.config.toml + bookshop/ + bookshop.config.js from starters/hugo/bookshop.config.js + site/ + go.mod from starters/hugo/site.go.mod + config.toml from starters/hugo/site.config.toml + """ + * a component-lib/components/single/single.hugo.html file containing: + """ +

{{ .title }}

+ """ + * a site/layouts/index.html file containing: + """ + + + {{ partial "bookshop_bindings" `(dict "title" .Params.block.title)` }} + {{ partial "bookshop" (slice "single" (dict "title" .Params.block.title)) }} + + + """ + * [front_matter]: + """ + block: + title: "Hello There" + """ + * a site/content/_index.md file containing: + """ + --- + [front_matter] + --- + """ + * [ssg]: "hugo" + + @web + Scenario: Bookshop live renders on a throttle + Given 🌐 I have loaded my site in CloudCannon + When 🌐 CloudCannon pushes new yaml: + """ + block: + title: "Rerendered" + """ + * 🌐 CloudCannon pushes new yaml: + """ + block: + title: "Rerendered 2" + """ + * 🌐 CloudCannon pushes new yaml: + """ + block: + title: "Rerendered 3" + """ + * 🌐 CloudCannon pushes new yaml: + """ + block: + title: "Rerendered 4" + """ + * 🌐 CloudCannon pushes new yaml: + """ + block: + title: "Rerendered 5" + """ + Then 🌐 There should be no errors + * 🌐 There should be no logs + * 🌐 "window.bookshopLive?.renderCount === 2" should evaluate + * 🌐 The selector h1 should contain "Rerendered 5" + # Double check that it didn't lag another render + * 🌐 "window.bookshopLive?.renderCount === 2" should evaluate diff --git a/javascript-modules/integration-tests/support/steps.js b/javascript-modules/integration-tests/support/steps.js index 7a5a2db5..09e76d50 100644 --- a/javascript-modules/integration-tests/support/steps.js +++ b/javascript-modules/integration-tests/support/steps.js @@ -281,6 +281,15 @@ Then(/^🌐 The selector (\S+) should match "(.+)"$/i, { timeout: 60 * 1000 }, a assert.equal(outerHTML, contains ? outerHTML : `outerHTML containing \`${contents}\``); }); +Then(/^🌐 "(.+)" should evaluate$/i, { timeout: 5 * 1000 }, async function (statement) { + if (!this.page) throw Error("No page open"); + try { + await this.page.waitForFunction(statement, { timeout: 4 * 1000 }); + } catch (e) { + throw Error(`${statement} didn't evaluate within 4s`) + } +}); + Then(/^🌐 There should be no errors$/i, { timeout: 60 * 1000 }, async function () { assert.deepEqual(this.puppeteerErrors(), []); }); @@ -346,7 +355,7 @@ Given(/^🌐 I (?:have loaded|load) my site( in CloudCannon)?$/i, { timeout: 60 // Trigger cloudcannon:load await readyCloudCannon(page_data, this); try { - await this.page.waitForFunction("window.bookshopLive?.hasRendered === true", { timeout: 4 * 1000 }); + await this.page.waitForFunction("window.bookshopLive?.renderCount > 0", { timeout: 4 * 1000 }); } catch (e) { this.trackPuppeteerError(e.toString()); this.trackPuppeteerError(`Bookshop didn't do an initial render within 4s`); diff --git a/javascript-modules/live/lib/app/live.js b/javascript-modules/live/lib/app/live.js index a7d8a515..533dc84a 100644 --- a/javascript-modules/live/lib/app/live.js +++ b/javascript-modules/live/lib/app/live.js @@ -11,7 +11,11 @@ export const getLive = (engines) => class BookshopLive { this.globalData = {}; this.data = {}; this.renderOptions = {}; - this.hasRendered = false; + this.renderCount = 0; + this.renderedAt = 0; + this.shouldRenderAt = null; + this.renderFrequency = 1000; + this.renderTimeout = null; this.awaitingDataFetches = options?.remoteGlobals?.length || 0; options?.remoteGlobals?.forEach(this.fetchGlobalData.bind(this)); } @@ -61,6 +65,7 @@ export const getLive = (engines) => class BookshopLive { } async update(data, options) { + const now = Date.now(); // transformData = false means implementations like Jekyll // won't wrap the data in { page: {} } // (this is currently only used for tests) @@ -74,8 +79,19 @@ export const getLive = (engines) => class BookshopLive { while (this.awaitingDataFetches > 0) { await sleep(100); } + if (now - this.renderedAt < this.renderFrequency) { + const shouldRenderAt = this.renderedAt + this.renderFrequency; + this.shouldRenderAt = shouldRenderAt; + await sleep(shouldRenderAt - now); + if (shouldRenderAt !== this.shouldRenderAt) { + // We have a newer update() call running, so we can bail on this render. + return false; + } + } + this.shouldRenderAt = null; + this.renderedAt = Date.now(); await this.render(); - this.hasRendered = true; + return true; } async render() { @@ -113,5 +129,6 @@ export const getLive = (engines) => class BookshopLive { core.graftTrees(startNode, endNode, output); } + this.renderCount += 1; } }