Skip to content

Commit

Permalink
[WIP] Stream Versioning
Browse files Browse the repository at this point in the history
  • Loading branch information
mbell697 committed Oct 9, 2024
1 parent 3163a06 commit c8811fe
Show file tree
Hide file tree
Showing 4 changed files with 151 additions and 1 deletion.
29 changes: 29 additions & 0 deletions src/core/streams/stream_actions.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,24 @@
import { session } from "../"
import { morphElements, morphChildren } from "../morphing"
import { attributeToNumber } from "../../util"

export function versionCheck(streamElement, targetElement) {
const targetVersion = attributeToNumber(targetElement.getAttribute("data-turbo-version"))
const streamVersion = streamElement.version

const streamVersionPresent = streamVersion != null
const targetVersionPresent = targetVersion != null

if (!streamVersionPresent && targetVersionPresent) {
return false // Don't allow an unversioned element to replace a versioned element
} else if (streamVersionPresent && !targetVersionPresent) {
return true // Do allow a versioned element to replace an unversioned element
} else if (streamVersionPresent && targetVersionPresent) {
return streamVersion > targetVersion
} else {
return true
}
}

export const StreamActions = {
after() {
Expand Down Expand Up @@ -28,6 +47,11 @@ export const StreamActions = {
const method = this.getAttribute("method")

this.targetElements.forEach((targetElement) => {
if (!versionCheck(this, targetElement)) {
console.debug("Skipping replace action because the version is not greater than the target element's version.")
return
}

if (method === "morph") {
morphElements(targetElement, this.templateContent)
} else {
Expand All @@ -40,6 +64,11 @@ export const StreamActions = {
const method = this.getAttribute("method")

this.targetElements.forEach((targetElement) => {
if (!versionCheck(this, targetElement)) {
console.debug("Skipping replace action because the version is not greater than the target element's version.")
return
}

if (method === "morph") {
morphChildren(targetElement, this.templateContent)
} else {
Expand Down
6 changes: 5 additions & 1 deletion src/elements/stream_element.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { StreamActions } from "../core/streams/stream_actions"
import { nextRepaint } from "../util"
import { attributeToNumber, nextRepaint } from "../util"

// <turbo-stream action=replace target=id><template>...

Expand Down Expand Up @@ -145,6 +145,10 @@ export class StreamElement extends HTMLElement {
return this.getAttribute("targets")
}

get version() {
return attributeToNumber(this.getAttribute("version"))
}

/**
* Reads the request-id attribute
*/
Expand Down
109 changes: 109 additions & 0 deletions src/tests/unit/stream_element_tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,65 @@ test("action=replace", async () => {
assert.isNull(element.parentElement)
})

test("action=replace with greater version", async () => {
subject.fixtureHTML = `<div><div id="hello" data-turbo-version="1">Hello Turbo</div></div>`

const element = createStreamElement("replace", "hello", createTemplateElement(`<h1 id="hello" data-turbo-version="2">Goodbye Turbo</h1>`), { version: "2" })
assert.equal(subject.find("#hello")?.textContent, "Hello Turbo")
assert.ok(subject.find("div#hello"))

subject.append(element)
await nextAnimationFrame()

assert.equal(subject.find("#hello")?.textContent, "Goodbye Turbo")
assert.notOk(subject.find("div#hello"))
assert.ok(subject.find("h1#hello"))
assert.isNull(element.parentElement)
})

test("action=replace with older version", async () => {
subject.fixtureHTML = `<div><div id="hello" data-turbo-version="2">Hello Turbo</div></div>`

const element = createStreamElement("replace", "hello", createTemplateElement(`<h1 id="hello" data-turbo-version="1">Goodbye Turbo</h1>`), { version: "1" })
assert.equal(subject.find("#hello")?.textContent, "Hello Turbo")
assert.ok(subject.find("div#hello"))

subject.append(element)
await nextAnimationFrame()

assert.equal(subject.find("#hello")?.textContent, "Hello Turbo")
assert.ok(subject.find("div#hello"))
})

test("action=replace with no version", async () => {
subject.fixtureHTML = `<div><div id="hello" data-turbo-version="1">Hello Turbo</div></div>`

const element = createStreamElement("replace", "hello", createTemplateElement(`<h1 id="hello">Goodbye Turbo</h1>`))
assert.equal(subject.find("#hello")?.textContent, "Hello Turbo")
assert.ok(subject.find("div#hello"))

subject.append(element)
await nextAnimationFrame()

assert.equal(subject.find("#hello")?.textContent, "Hello Turbo")
assert.notOk(subject.find("h1#hello"))
assert.isNull(element.parentElement)
})

test("action=replace with a version", async () => {
const element = createStreamElement("replace", "hello", createTemplateElement(`<h1 id="hello" data-turbo-version="1">Goodbye Turbo</h1>`), { version: "1" })
assert.equal(subject.find("#hello")?.textContent, "Hello Turbo")
assert.ok(subject.find("div#hello"))

subject.append(element)
await nextAnimationFrame()

assert.equal(subject.find("#hello")?.textContent, "Goodbye Turbo")
assert.notOk(subject.find("div#hello"))
assert.ok(subject.find("h1#hello"))
assert.isNull(element.parentElement)
})

test("action=update", async () => {
const element = createStreamElement("update", "hello", createTemplateElement("Goodbye Turbo"))
assert.equal(subject.find("#hello")?.textContent, "Hello Turbo")
Expand All @@ -145,6 +204,56 @@ test("action=update", async () => {
assert.isNull(element.parentElement)
})

test("action=update with greater version", async () => {
subject.fixtureHTML = `<div><div id="hello" data-turbo-version="1">Hello Turbo</div></div>`

const element = createStreamElement("update", "hello", createTemplateElement(`<h1 id="hello" data-turbo-version="2">Goodbye Turbo</h1>`), { version: "2" })
assert.equal(subject.find("#hello")?.textContent, "Hello Turbo")

subject.append(element)
await nextAnimationFrame()

assert.equal(subject.find("#hello")?.textContent, "Goodbye Turbo")
assert.isNull(element.parentElement)
})

test("action=update with older version", async () => {
subject.fixtureHTML = `<div><div id="hello" data-turbo-version="2">Hello Turbo</div></div>`

const element = createStreamElement("update", "hello", createTemplateElement(`<h1 id="hello" data-turbo-version="1">Goodbye Turbo</h1>`), { version: "1" })
assert.equal(subject.find("#hello")?.textContent, "Hello Turbo")

subject.append(element)
await nextAnimationFrame()

assert.equal(subject.find("#hello")?.textContent, "Hello Turbo")
assert.isNull(element.parentElement)
})

test("action=update with no version", async () => {
subject.fixtureHTML = `<div><div id="hello" data-turbo-version="1">Hello Turbo</div></div>`

const element = createStreamElement("update", "hello", createTemplateElement(`<h1 id="hello">Goodbye Turbo</h1>`))
assert.equal(subject.find("#hello")?.textContent, "Hello Turbo")

subject.append(element)
await nextAnimationFrame()

assert.equal(subject.find("#hello")?.textContent, "Hello Turbo")
assert.isNull(element.parentElement)
})

test("action=update with a version", async () => {
const element = createStreamElement("update", "hello", createTemplateElement(`<h1 id="hello" data-turbo-version="1">Goodbye Turbo</h1>`), { version: "1" })
assert.equal(subject.find("#hello")?.textContent, "Hello Turbo")

subject.append(element)
await nextAnimationFrame()

assert.equal(subject.find("#hello")?.textContent, "Goodbye Turbo")
assert.isNull(element.parentElement)
})

test("action=after", async () => {
const element = createStreamElement("after", "hello", createTemplateElement(`<h1 id="after">After Turbo</h1>`))
assert.equal(subject.find("#hello")?.textContent, "Hello Turbo")
Expand Down
8 changes: 8 additions & 0 deletions src/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -254,3 +254,11 @@ export function debounce(fn, delay) {
timeoutId = setTimeout(callback, delay)
}
}

export function isNumeric(n) {
return !isNaN(parseFloat(n)) && isFinite(n)
}

export function attributeToNumber(value) {
return isNumeric(value) ? Number(value) : null
}

0 comments on commit c8811fe

Please sign in to comment.