diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 77c0da8a..f2ab1152 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -23,7 +23,7 @@ jobs: with: fetch-depth: 0 - name: Lint Code Base - uses: super-linter/super-linter/slim@v7 + uses: super-linter/super-linter/slim@v6 env: DEFAULT_BRANCH: main GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -34,6 +34,6 @@ jobs: VALIDATE_GO_MODULES: false VALIDATE_PHP_PHPCS: false VALIDATE_KUBERNETES_KUBECONFORM: false - VALIDATE_JAVASCRIPT_PRETTIER: false - VALIDATE_TYPESCRIPT_PRETTIER: false + VALIDATE_JAVASCRIPT_STANDARD: false + VALIDATE_TYPESCRIPT_STANDARD: false VALIDATE_PYTHON_PYLINT: false diff --git a/.prettierignore b/.prettierignore index da1b690f..a85af267 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,7 +1,3 @@ -# We use standardjs for JavaScript and TypeScript files -**/*.js -**/*.ts - # The spec is in the IETF flavor of Markdown spec/mercure.md diff --git a/conformance-tests/mercure.spec.ts b/conformance-tests/mercure.spec.ts index d62b76d4..411a85d0 100644 --- a/conformance-tests/mercure.spec.ts +++ b/conformance-tests/mercure.spec.ts @@ -1,124 +1,204 @@ -import { test, expect } from '@playwright/test'; +import { test, expect } from "@playwright/test"; import { randomBytes } from "crypto"; function randomString() { - return randomBytes(20).toString('hex'); + return randomBytes(20).toString("hex"); } -test.beforeEach(async ({ page }) => await page.goto('/')); +test.beforeEach(async ({ page }) => await page.goto("/")); -test.describe('Publish update', () => { +test.describe("Publish update", () => { const randomStrings: string[] = Array.from({ length: 6 }, randomString); - type Data = { name: string, updateTopics: string[], topicSelectors: string[], mustBeReceived: boolean, updateID?: string, private?: true }; + type Data = { + name: string; + updateTopics: string[]; + topicSelectors: string[]; + mustBeReceived: boolean; + updateID?: string; + private?: true; + }; const dataset: Data[] = [ - { name: 'raw string', mustBeReceived: true, updateTopics: [randomStrings[0]], topicSelectors: [randomStrings[0]] }, - { name: 'multiple topics', mustBeReceived: true, updateTopics: [randomString(), randomStrings[1]], topicSelectors: [randomStrings[1]] }, - { name: 'multiple topic selectors', mustBeReceived: true, updateTopics: [randomStrings[2]], topicSelectors: ['foo', randomStrings[2]] }, - { name: 'URI', mustBeReceived: true, updateTopics: [`https://example.net/foo/${randomStrings[3]}`], topicSelectors: [`https://example.net/foo/${randomStrings[3]}`] }, - { name: 'URI template', mustBeReceived: true, updateTopics: [`https://example.net/foo/${randomStrings[4]}`], topicSelectors: ['https://example.net/foo/{random}'] }, - { name: 'nonmatching raw string', mustBeReceived: false, updateTopics: [`will-not-match}`], topicSelectors: ['another-name'] }, - { name: 'nonmatching URI', mustBeReceived: false, updateTopics: [`https://example.net/foo/will-not-match}`], topicSelectors: ['https://example.net/foo/another-name'] }, - { name: 'nonmatching URI template', mustBeReceived: false, updateTopics: [`https://example.net/foo/will-not-match}`], topicSelectors: ['https://example.net/bar/{var}'] }, - { name: 'private raw string', mustBeReceived: false, private: true, updateTopics: [randomStrings[0]], topicSelectors: [randomStrings[0]] }, - { name: 'private URI', mustBeReceived: false, private: true, updateTopics: [`https://example.net/foo/${randomStrings[3]}`], topicSelectors: [`https://example.net/foo/${randomStrings[3]}`] }, - { name: 'private URI template', mustBeReceived: false, private: true, updateTopics: [`https://example.net/foo/${randomStrings[4]}`], topicSelectors: ['https://example.net/foo/{random}'] }, + { + name: "raw string", + mustBeReceived: true, + updateTopics: [randomStrings[0]], + topicSelectors: [randomStrings[0]], + }, + { + name: "multiple topics", + mustBeReceived: true, + updateTopics: [randomString(), randomStrings[1]], + topicSelectors: [randomStrings[1]], + }, + { + name: "multiple topic selectors", + mustBeReceived: true, + updateTopics: [randomStrings[2]], + topicSelectors: ["foo", randomStrings[2]], + }, + { + name: "URI", + mustBeReceived: true, + updateTopics: [`https://example.net/foo/${randomStrings[3]}`], + topicSelectors: [`https://example.net/foo/${randomStrings[3]}`], + }, + { + name: "URI template", + mustBeReceived: true, + updateTopics: [`https://example.net/foo/${randomStrings[4]}`], + topicSelectors: ["https://example.net/foo/{random}"], + }, + { + name: "nonmatching raw string", + mustBeReceived: false, + updateTopics: [`will-not-match}`], + topicSelectors: ["another-name"], + }, + { + name: "nonmatching URI", + mustBeReceived: false, + updateTopics: [`https://example.net/foo/will-not-match}`], + topicSelectors: ["https://example.net/foo/another-name"], + }, + { + name: "nonmatching URI template", + mustBeReceived: false, + updateTopics: [`https://example.net/foo/will-not-match}`], + topicSelectors: ["https://example.net/bar/{var}"], + }, + { + name: "private raw string", + mustBeReceived: false, + private: true, + updateTopics: [randomStrings[0]], + topicSelectors: [randomStrings[0]], + }, + { + name: "private URI", + mustBeReceived: false, + private: true, + updateTopics: [`https://example.net/foo/${randomStrings[3]}`], + topicSelectors: [`https://example.net/foo/${randomStrings[3]}`], + }, + { + name: "private URI template", + mustBeReceived: false, + private: true, + updateTopics: [`https://example.net/foo/${randomStrings[4]}`], + topicSelectors: ["https://example.net/foo/{random}"], + }, ]; for (const data of dataset) { test(data.name, async ({ page }) => { - page.on('console', msg => console.log(msg.text())); + page.on("console", (msg) => console.log(msg.text())); data.updateID = `id-${JSON.stringify(data.updateTopics)}`; - const { received, contentType, status, body } = await page.evaluate(async (data) => { - const receivedResult = Symbol('received'); - const notReceivedResult = Symbol('not received'); - - let resolveReady: () => void; - const ready = new Promise((resolve) => { - resolveReady = () => resolve(true); - }); - - let resolveReceived: () => void; - const received = new Promise((resolve) => { - resolveReceived = () => resolve(receivedResult); - }); - - const timeout = new Promise((resolve) => setTimeout(resolve, 2000, notReceivedResult)); - - const url = new window.URL('/.well-known/mercure', window.origin); - data.topicSelectors.forEach(topicSelector => url.searchParams.append('topic', topicSelector)); - - const event = new window.URLSearchParams(); - data.updateTopics.forEach(updateTopic => event.append('topic', updateTopic)); - event.set('id', data.updateID); - event.set('data', `data for - - ${data.name}`); - if (data.private) event.set('private', 'on'); - - console.log(`data: ${JSON.stringify(data)}`); - - console.log(`creating EventSource: ${url}`); - const es = new EventSource(url); - console.log('EventSource created'); - - es.onopen = () => { - console.log('EventSource opened'); - resolveReady(); - } - - let id: string; - es.onmessage = (e) => { - console.log(`EventSource event received: ${e.data}`); - if ( - e.type === 'message' && - e.lastEventId === event.get('id') && - e.data === event.get('data') - ) { - es.close(); - resolveReceived(); - } - }; - - await ready; // Wait for the EventSource to be ready - - console.log(`Creating POST request: ${event.toString()}`); - const resp = await fetch(`/.well-known/mercure`, { - method: 'POST', - headers: { 'Authorization': 'Bearer eyJhbGciOiJIUzI1NiJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiKiJdLCJzdWJzY3JpYmUiOlsiKiJdfX0.bVXdlWXwfw9ySx7-iV5OpUSHo34RkjUdVzDLBcc6l_g' }, - body: event, - }); - - id = await resp.text(); - console.log(`POST request done: ${JSON.stringify({ status: resp.status, id, event: event.toString() })}`); - - switch (await Promise.race([received, timeout])) { - case receivedResult: - return { - received: true, - contentType: resp.headers.get('Content-Type'), - status: resp.status, - body: id, - }; - - case notReceivedResult: - return { - received: false, + const { received, contentType, status, body } = await page.evaluate( + async (data) => { + const receivedResult = Symbol("received"); + const notReceivedResult = Symbol("not received"); + + let resolveReady: () => void; + const ready = new Promise((resolve) => { + resolveReady = () => resolve(true); + }); + + let resolveReceived: () => void; + const received = new Promise((resolve) => { + resolveReceived = () => resolve(receivedResult); + }); + + const timeout = new Promise((resolve) => + setTimeout(resolve, 2000, notReceivedResult), + ); + + const url = new window.URL("/.well-known/mercure", window.origin); + data.topicSelectors.forEach((topicSelector) => + url.searchParams.append("topic", topicSelector), + ); + + const event = new window.URLSearchParams(); + data.updateTopics.forEach((updateTopic) => + event.append("topic", updateTopic), + ); + event.set("id", data.updateID); + event.set( + "data", + `data for + + ${data.name}`, + ); + if (data.private) event.set("private", "on"); + + console.log(`data: ${JSON.stringify(data)}`); + + console.log(`creating EventSource: ${url}`); + const es = new EventSource(url); + console.log("EventSource created"); + + es.onopen = () => { + console.log("EventSource opened"); + resolveReady(); + }; + + let id: string; + es.onmessage = (e) => { + console.log(`EventSource event received: ${e.data}`); + if ( + e.type === "message" && + e.lastEventId === event.get("id") && + e.data === event.get("data") + ) { + es.close(); + resolveReceived(); } - } - - }, data); + }; + + await ready; // Wait for the EventSource to be ready + + console.log(`Creating POST request: ${event.toString()}`); + const resp = await fetch(`/.well-known/mercure`, { + method: "POST", + headers: { + Authorization: + "Bearer eyJhbGciOiJIUzI1NiJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiKiJdLCJzdWJzY3JpYmUiOlsiKiJdfX0.bVXdlWXwfw9ySx7-iV5OpUSHo34RkjUdVzDLBcc6l_g", + }, + body: event, + }); + + id = await resp.text(); + console.log( + `POST request done: ${JSON.stringify({ status: resp.status, id, event: event.toString() })}`, + ); + + switch (await Promise.race([received, timeout])) { + case receivedResult: + return { + received: true, + contentType: resp.headers.get("Content-Type"), + status: resp.status, + body: id, + }; + + case notReceivedResult: + return { + received: false, + }; + } + }, + data, + ); expect(received).toBe(data.mustBeReceived); if (data.mustBeReceived) { expect(contentType).toMatch(/^text\/plain(?:$|;.*)/); expect(status).toBe(200); - if (process.env.CUSTOM_ID) - expect(body).toBe(data.updateID); + if (process.env.CUSTOM_ID) expect(body).toBe(data.updateID); } }); } diff --git a/conformance-tests/playwright.config.ts b/conformance-tests/playwright.config.ts index 8a410c57..0168b611 100644 --- a/conformance-tests/playwright.config.ts +++ b/conformance-tests/playwright.config.ts @@ -1,17 +1,17 @@ -import type { PlaywrightTestConfig } from '@playwright/test'; -import { devices } from '@playwright/test'; +import type { PlaywrightTestConfig } from "@playwright/test"; +import { devices } from "@playwright/test"; /** * Read environment variables from file. * https://github.com/motdotla/dotenv */ -require('dotenv').config(); +require("dotenv").config(); /** * See https://playwright.dev/docs/test-configuration. */ const config: PlaywrightTestConfig = { - testDir: './.', + testDir: "./.", /* Maximum time one test can run for. */ timeout: 10 * 1000, expect: { @@ -19,7 +19,7 @@ const config: PlaywrightTestConfig = { * Maximum time expect() should wait for the condition to be met. * For example in `await expect(locator).toHaveText();` */ - timeout: 5000 + timeout: 5000, }, /* Fail the build on CI if you accidentally left test.only in the source code. */ forbidOnly: !!process.env.CI, @@ -28,15 +28,15 @@ const config: PlaywrightTestConfig = { /* Opt out of parallel tests on CI. */ workers: process.env.CI ? 1 : undefined, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: 'html', + reporter: "html", /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ actionTimeout: 0, /* Base URL to use in actions like `await page.goto('/')`. */ - baseURL: process.env.BASE_URL ?? 'https://localhost/.well-known/mercure', + baseURL: process.env.BASE_URL ?? "https://localhost/.well-known/mercure", /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ - trace: 'on-first-retry', + trace: "on-first-retry", /* ignore HTTPS errors */ ignoreHTTPSErrors: true, }, @@ -44,23 +44,23 @@ const config: PlaywrightTestConfig = { /* Configure projects for major browsers */ projects: [ { - name: 'chromium', + name: "chromium", use: { - ...devices['Desktop Chrome'], + ...devices["Desktop Chrome"], }, }, { - name: 'firefox', + name: "firefox", use: { - ...devices['Desktop Firefox'], + ...devices["Desktop Firefox"], }, }, { - name: 'webkit', + name: "webkit", use: { - ...devices['Desktop Safari'], + ...devices["Desktop Safari"], }, }, diff --git a/examples/chat/static/chat.js b/examples/chat/static/chat.js index fb9488ba..9e6cf46d 100644 --- a/examples/chat/static/chat.js +++ b/examples/chat/static/chat.js @@ -1,107 +1,109 @@ /* eslint-env browser */ -const type = 'https://chat.example.com/Message' +const type = "https://chat.example.com/Message"; const { hubURL, messageURITemplate, subscriptionsTopic, username } = JSON.parse( - document.getElementById('config').textContent -) + document.getElementById("config").textContent, +); -document.getElementById('username').textContent = username +document.getElementById("username").textContent = username; -const $messages = document.getElementById('messages') -const $messageTemplate = document.getElementById('message') -const $userList = document.getElementById('user-list') -const $onlineUserTemplate = document.getElementById('online-user') +const $messages = document.getElementById("messages"); +const $messageTemplate = document.getElementById("message"); +const $userList = document.getElementById("user-list"); +const $onlineUserTemplate = document.getElementById("online-user"); let userList; (async () => { const resp = await fetch(new URL(subscriptionsTopic, hubURL), { - credentials: 'include' - }) - const subscriptionCollection = await resp.json() + credentials: "include", + }); + const subscriptionCollection = await resp.json(); userList = new Map( subscriptionCollection.subscriptions .reduce((acc, { payload }) => { - if (payload.username !== username) acc.push([payload.username, true]) - return acc + if (payload.username !== username) acc.push([payload.username, true]); + return acc; }, []) - .sort() - ) - updateUserListView() + .sort(), + ); + updateUserListView(); - const subscribeURL = new URL(hubURL) + const subscribeURL = new URL(hubURL); subscribeURL.searchParams.append( - 'lastEventID', - subscriptionCollection.lastEventID - ) - subscribeURL.searchParams.append('topic', messageURITemplate) + "lastEventID", + subscriptionCollection.lastEventID, + ); + subscribeURL.searchParams.append("topic", messageURITemplate); subscribeURL.searchParams.append( - 'topic', - `${subscriptionsTopic}{/subscriber}` - ) + "topic", + `${subscriptionsTopic}{/subscriber}`, + ); - const es = new EventSource(subscribeURL, { withCredentials: true }) + const es = new EventSource(subscribeURL, { withCredentials: true }); es.onmessage = ({ data }) => { - const update = JSON.parse(data) + const update = JSON.parse(data); - if (update['@type'] === type) { - displayMessage(update) - return + if (update["@type"] === type) { + displayMessage(update); + return; } - if (update.type === 'Subscription') { - updateUserList(update) - return + if (update.type === "Subscription") { + updateUserList(update); + return; } - console.warn('Received an unsupported update type', update) - } -})() + console.warn("Received an unsupported update type", update); + }; +})(); const updateUserListView = () => { - $userList.textContent = '' + $userList.textContent = ""; userList.forEach((_, username) => { - const el = document.importNode($onlineUserTemplate.content, true) - el.querySelector('.username').textContent = username - $userList.append(el) - }) -} + const el = document.importNode($onlineUserTemplate.content, true); + el.querySelector(".username").textContent = username; + $userList.append(el); + }); +}; const displayMessage = ({ username, message }) => { - const el = document.importNode($messageTemplate.content, true) - el.querySelector('.username').textContent = username - el.querySelector('.msg').textContent = message - $messages.append(el) + const el = document.importNode($messageTemplate.content, true); + el.querySelector(".username").textContent = username; + el.querySelector(".msg").textContent = message; + $messages.append(el); // scroll at the bottom when a new message is received - $messages.scrollTop = $messages.scrollHeight -} + $messages.scrollTop = $messages.scrollHeight; +}; const updateUserList = ({ active, payload }) => { - if (username === payload.username) return + if (username === payload.username) return; - active ? userList.set(payload.username, true) : userList.delete(payload.username) - userList = new Map([...userList.entries()].sort()) + active + ? userList.set(payload.username, true) + : userList.delete(payload.username); + userList = new Map([...userList.entries()].sort()); - updateUserListView() -} + updateUserListView(); +}; -document.querySelector('form').onsubmit = function (e) { - e.preventDefault() +document.querySelector("form").onsubmit = function (e) { + e.preventDefault(); - const uid = window.crypto.getRandomValues(new Uint8Array(10)).join('') - const messageTopic = messageURITemplate.replace('{id}', uid) + const uid = window.crypto.getRandomValues(new Uint8Array(10)).join(""); + const messageTopic = messageURITemplate.replace("{id}", uid); const body = new URLSearchParams({ data: JSON.stringify({ - '@type': type, - '@id': messageTopic, + "@type": type, + "@id": messageTopic, username, - message: this.elements.message.value + message: this.elements.message.value, }), topic: messageTopic, - private: true - }) - fetch(hubURL, { method: 'POST', body, credentials: 'include' }) - this.elements.message.value = '' - this.elements.message.focus() -} + private: true, + }); + fetch(hubURL, { method: "POST", body, credentials: "include" }); + this.elements.message.value = ""; + this.elements.message.focus(); +}; diff --git a/examples/publish/node.js b/examples/publish/node.js index f3e0e677..f046d2d9 100644 --- a/examples/publish/node.js +++ b/examples/publish/node.js @@ -1,35 +1,35 @@ -const http = require('http') -const querystring = require('querystring') +const http = require("http"); +const querystring = require("querystring"); const demoJwt = - 'eyJhbGciOiJIUzI1NiJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiKiJdLCJzdWJzY3JpYmUiOlsiaHR0cHM6Ly9leGFtcGxlLmNvbS9teS1wcml2YXRlLXRvcGljIiwie3NjaGVtZX06Ly97K2hvc3R9L2RlbW8vYm9va3Mve2lkfS5qc29ubGQiLCIvLndlbGwta25vd24vbWVyY3VyZS9zdWJzY3JpcHRpb25zey90b3BpY317L3N1YnNjcmliZXJ9Il0sInBheWxvYWQiOnsidXNlciI6Imh0dHBzOi8vZXhhbXBsZS5jb20vdXNlcnMvZHVuZ2xhcyIsInJlbW90ZUFkZHIiOiIxMjcuMC4wLjEifX19.KKPIikwUzRuB3DTpVw6ajzwSChwFw5omBMmMcWKiDcM' + "eyJhbGciOiJIUzI1NiJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiKiJdLCJzdWJzY3JpYmUiOlsiaHR0cHM6Ly9leGFtcGxlLmNvbS9teS1wcml2YXRlLXRvcGljIiwie3NjaGVtZX06Ly97K2hvc3R9L2RlbW8vYm9va3Mve2lkfS5qc29ubGQiLCIvLndlbGwta25vd24vbWVyY3VyZS9zdWJzY3JpcHRpb25zey90b3BpY317L3N1YnNjcmliZXJ9Il0sInBheWxvYWQiOnsidXNlciI6Imh0dHBzOi8vZXhhbXBsZS5jb20vdXNlcnMvZHVuZ2xhcyIsInJlbW90ZUFkZHIiOiIxMjcuMC4wLjEifX19.KKPIikwUzRuB3DTpVw6ajzwSChwFw5omBMmMcWKiDcM"; const postData = querystring.stringify({ - topic: 'https://localhost/demo/books/1.jsonld', - data: JSON.stringify({ key: 'updated value' }) -}) + topic: "https://localhost/demo/books/1.jsonld", + data: JSON.stringify({ key: "updated value" }), +}); const req = http.request( { - hostname: 'localhost', - port: '3000', - path: '/.well-known/mercure', - method: 'POST', + hostname: "localhost", + port: "3000", + path: "/.well-known/mercure", + method: "POST", headers: { Authorization: `Bearer ${demoJwt}`, - 'Content-Type': 'application/x-www-form-urlencoded', - 'Content-Length': Buffer.byteLength(postData) - } + "Content-Type": "application/x-www-form-urlencoded", + "Content-Length": Buffer.byteLength(postData), + }, }, (res) => { - console.log(`Status: ${res.statusCode}`) - console.log(`Headers: ${JSON.stringify(res.headers)}`) - } -) + console.log(`Status: ${res.statusCode}`); + console.log(`Headers: ${JSON.stringify(res.headers)}`); + }, +); -req.on('error', (e) => { - console.error(`An error occurred: ${e.message}`) -}) +req.on("error", (e) => { + console.error(`An error occurred: ${e.message}`); +}); -req.write(postData) -req.end() +req.write(postData); +req.end(); diff --git a/public/app.js b/public/app.js index 7ecd8709..439b8f67 100644 --- a/public/app.js +++ b/public/app.js @@ -1,278 +1,285 @@ -'use strict'; +"use strict"; /* eslint-env browser */ /* global EventSourcePolyfill */ (function () { - const origin = window.location.origin - const defaultTopic = document.URL + 'demo/books/1.jsonld' - const placeholderTopic = 'https://example.com/my-private-topic' + const origin = window.location.origin; + const defaultTopic = document.URL + "demo/books/1.jsonld"; + const placeholderTopic = "https://example.com/my-private-topic"; const defaultJwt = - 'eyJhbGciOiJIUzI1NiJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiKiJdLCJzdWJzY3JpYmUiOlsiaHR0cHM6Ly9leGFtcGxlLmNvbS9teS1wcml2YXRlLXRvcGljIiwie3NjaGVtZX06Ly97K2hvc3R9L2RlbW8vYm9va3Mve2lkfS5qc29ubGQiLCIvLndlbGwta25vd24vbWVyY3VyZS9zdWJzY3JpcHRpb25zey90b3BpY317L3N1YnNjcmliZXJ9Il0sInBheWxvYWQiOnsidXNlciI6Imh0dHBzOi8vZXhhbXBsZS5jb20vdXNlcnMvZHVuZ2xhcyIsInJlbW90ZUFkZHIiOiIxMjcuMC4wLjEifX19.KKPIikwUzRuB3DTpVw6ajzwSChwFw5omBMmMcWKiDcM' + "eyJhbGciOiJIUzI1NiJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiKiJdLCJzdWJzY3JpYmUiOlsiaHR0cHM6Ly9leGFtcGxlLmNvbS9teS1wcml2YXRlLXRvcGljIiwie3NjaGVtZX06Ly97K2hvc3R9L2RlbW8vYm9va3Mve2lkfS5qc29ubGQiLCIvLndlbGwta25vd24vbWVyY3VyZS9zdWJzY3JpcHRpb25zey90b3BpY317L3N1YnNjcmliZXJ9Il0sInBheWxvYWQiOnsidXNlciI6Imh0dHBzOi8vZXhhbXBsZS5jb20vdXNlcnMvZHVuZ2xhcyIsInJlbW90ZUFkZHIiOiIxMjcuMC4wLjEifX19.KKPIikwUzRuB3DTpVw6ajzwSChwFw5omBMmMcWKiDcM"; - const $updates = document.getElementById('updates') - const $subscriptions = document.getElementById('subscriptions') - const $settingsForm = document.forms.settings - const $discoverForm = document.forms.discover - const $subscribeForm = document.forms.subscribe - const $publishForm = document.forms.publish - const $subscriptionsForm = document.forms.subscriptions + const $updates = document.getElementById("updates"); + const $subscriptions = document.getElementById("subscriptions"); + const $settingsForm = document.forms.settings; + const $discoverForm = document.forms.discover; + const $subscribeForm = document.forms.subscribe; + const $publishForm = document.forms.publish; + const $subscriptionsForm = document.forms.subscriptions; const error = (e) => { - if (!e.error || e.error.message?.includes?.('Reconnecting')) { + if (!e.error || e.error.message?.includes?.("Reconnecting")) { // Silent reconnecting messages from the polyfill - console.log('Connection closed, reconnecting...', e) + console.log("Connection closed, reconnecting...", e); - return + return; } - console.log(e) + console.log(e); if (e.toString !== Object.prototype.toString) { // Display relevant error message - alert(e.toString()) + alert(e.toString()); - return + return; } if (e.statusText) { // Special handling of errors from the polyfill - alert(e.statusText) + alert(e.statusText); - return + return; } - alert('An error occured, details have been logged.') - } + alert("An error occured, details have been logged."); + }; const getHubUrl = (resp) => { - const link = resp.headers.get('Link') + const link = resp.headers.get("Link"); if (!link) { - error('No rel="mercure" Link header provided.') + error('No rel="mercure" Link header provided.'); } - const match = link.match(/<(.*)>.*rel="mercure".*/) - if (match && match[1]) return match[1] - } + const match = link.match(/<(.*)>.*rel="mercure".*/); + if (match && match[1]) return match[1]; + }; // Set default values - document.addEventListener('DOMContentLoaded', () => { - $settingsForm.hubUrl.value = origin + '/.well-known/mercure' - $settingsForm.jwt.value = defaultJwt + document.addEventListener("DOMContentLoaded", () => { + $settingsForm.hubUrl.value = origin + "/.well-known/mercure"; + $settingsForm.jwt.value = defaultJwt; - $discoverForm.topic.value = defaultTopic + $discoverForm.topic.value = defaultTopic; $discoverForm.body.value = JSON.stringify( { - '@id': defaultTopic, - availability: 'https://schema.org/InStock' + "@id": defaultTopic, + availability: "https://schema.org/InStock", }, null, - 2 - ) + 2, + ); $publishForm.data.value = JSON.stringify( { - '@id': defaultTopic, - availability: 'https://schema.org/OutOfStock' + "@id": defaultTopic, + availability: "https://schema.org/OutOfStock", }, null, - 2 - ) + 2, + ); - document.getElementById( - 'subscribeTopicsExamples' - ).textContent = `${document.URL}demo/novels/{id}.jsonld + document.getElementById("subscribeTopicsExamples").textContent = + `${document.URL}demo/novels/{id}.jsonld ${defaultTopic} -foo` - }) +foo`; + }); // Discover $discoverForm.onsubmit = async function (e) { - e.preventDefault() + e.preventDefault(); const { - elements: { topic, body } - } = this - const jwt = $settingsForm.jwt.value + elements: { topic, body }, + } = this; + const jwt = $settingsForm.jwt.value; - const url = new URL(topic.value) - if (body.value) url.searchParams.append('body', body.value) - if (jwt) url.searchParams.append('jwt', jwt) + const url = new URL(topic.value); + if (body.value) url.searchParams.append("body", body.value); + if (jwt) url.searchParams.append("jwt", jwt); try { - const resp = await fetch(url) - if (!resp.ok) throw new Error(resp.statusText) + const resp = await fetch(url); + if (!resp.ok) throw new Error(resp.statusText); // Set hub default - const hubUrl = getHubUrl(resp) - if (hubUrl) $settingsForm.hubUrl.value = new URL(hubUrl, topic.value) + const hubUrl = getHubUrl(resp); + if (hubUrl) $settingsForm.hubUrl.value = new URL(hubUrl, topic.value); - const subscribeTopics = $subscribeForm.topics - if (subscribeTopics.value === placeholderTopic) { subscribeTopics.value = topic.value } + const subscribeTopics = $subscribeForm.topics; + if (subscribeTopics.value === placeholderTopic) { + subscribeTopics.value = topic.value; + } // Set publish default values - const publishTopics = $publishForm.topics - if (publishTopics.value === placeholderTopic) { publishTopics.value = topic.value } + const publishTopics = $publishForm.topics; + if (publishTopics.value === placeholderTopic) { + publishTopics.value = topic.value; + } - const text = await resp.text() - body.value = text + const text = await resp.text(); + body.value = text; } catch (e) { - error(e) + error(e); } - } + }; // Subscribe - const $updateTemplate = document.getElementById('update') - let updateEventSource + const $updateTemplate = document.getElementById("update"); + let updateEventSource; $subscribeForm.onsubmit = function (e) { - e.preventDefault() + e.preventDefault(); - updateEventSource && updateEventSource.close() - $updates.textContent = 'No updates pushed yet.' + updateEventSource && updateEventSource.close(); + $updates.textContent = "No updates pushed yet."; const { - elements: { topics, lastEventId } - } = this - - const topicList = topics.value.split('\n') - const u = new URL($settingsForm.hubUrl.value) - topicList.forEach((topic) => u.searchParams.append('topic', topic)) - if (lastEventId.value) { u.searchParams.append('lastEventID', lastEventId.value) } + elements: { topics, lastEventId }, + } = this; + + const topicList = topics.value.split("\n"); + const u = new URL($settingsForm.hubUrl.value); + topicList.forEach((topic) => u.searchParams.append("topic", topic)); + if (lastEventId.value) { + u.searchParams.append("lastEventID", lastEventId.value); + } - let ol = null - if ($settingsForm.authorization.value === 'header') { + let ol = null; + if ($settingsForm.authorization.value === "header") { updateEventSource = new EventSourcePolyfill(u, { headers: { - Authorization: `Bearer ${$settingsForm.jwt.value}` - } - }) - } else updateEventSource = new EventSource(u) + Authorization: `Bearer ${$settingsForm.jwt.value}`, + }, + }); + } else updateEventSource = new EventSource(u); updateEventSource.onmessage = function (e) { if (!ol) { - ol = document.createElement('ol') - ol.reversed = true + ol = document.createElement("ol"); + ol.reversed = true; - $updates.textContent = '' - $updates.appendChild(ol) + $updates.textContent = ""; + $updates.appendChild(ol); } - const li = document.importNode($updateTemplate.content, true) - li.querySelector('h2').textContent = e.lastEventId - li.querySelector('pre').textContent = e.data - ol.firstChild ? ol.insertBefore(li, ol.firstChild) : ol.appendChild(li) - } - updateEventSource.onerror = error - this.elements.unsubscribe.disabled = false - } + const li = document.importNode($updateTemplate.content, true); + li.querySelector("h2").textContent = e.lastEventId; + li.querySelector("pre").textContent = e.data; + ol.firstChild ? ol.insertBefore(li, ol.firstChild) : ol.appendChild(li); + }; + updateEventSource.onerror = error; + this.elements.unsubscribe.disabled = false; + }; $subscribeForm.elements.unsubscribe.onclick = function (e) { - e.preventDefault() + e.preventDefault(); - updateEventSource && updateEventSource.close() - this.disabled = true - $updates.textContent = 'Unsubscribed.' - } + updateEventSource && updateEventSource.close(); + this.disabled = true; + $updates.textContent = "Unsubscribed."; + }; // Publish $publishForm.onsubmit = async function (e) { - e.preventDefault() + e.preventDefault(); const { - elements: { topics, data, priv, id, type, retry } - } = this + elements: { topics, data, priv, id, type, retry }, + } = this; const body = new URLSearchParams({ data: data.value, id: id.value, type: type.value, - retry: retry.value - }) + retry: retry.value, + }); - topics.value.split('\n').forEach((topic) => body.append('topic', topic)) - priv.checked && body.append('private', 'on') + topics.value.split("\n").forEach((topic) => body.append("topic", topic)); + priv.checked && body.append("private", "on"); - const opt = { method: 'POST', body } - if ($settingsForm.authorization.value === 'header') { opt.headers = { Authorization: `Bearer ${$settingsForm.jwt.value}` } } + const opt = { method: "POST", body }; + if ($settingsForm.authorization.value === "header") { + opt.headers = { Authorization: `Bearer ${$settingsForm.jwt.value}` }; + } try { - const resp = await fetch($settingsForm.hubUrl.value, opt) - if (!resp.ok) throw new Error(resp.statusText) + const resp = await fetch($settingsForm.hubUrl.value, opt); + if (!resp.ok) throw new Error(resp.statusText); } catch (e) { - error(e) + error(e); } - } + }; // Subscriptions - const $subscriptionTemplate = document.getElementById('subscription') - let subscriptionEventSource + const $subscriptionTemplate = document.getElementById("subscription"); + let subscriptionEventSource; const addSubscription = (s) => { const subscription = document.importNode( $subscriptionTemplate.content, - true - ) - subscription.querySelector('div').setAttribute('id', s.id) - subscription.querySelector('.card-header-title').textContent = s.id - subscription.querySelector('.topic').textContent = s.topic - subscription.querySelector('.subscriber').textContent = s.subscriber - subscription.querySelector('code').textContent = JSON.stringify( + true, + ); + subscription.querySelector("div").setAttribute("id", s.id); + subscription.querySelector(".card-header-title").textContent = s.id; + subscription.querySelector(".topic").textContent = s.topic; + subscription.querySelector(".subscriber").textContent = s.subscriber; + subscription.querySelector("code").textContent = JSON.stringify( s.payload, null, - 2 - ) - $subscriptions.appendChild(subscription) - } + 2, + ); + $subscriptions.appendChild(subscription); + }; $subscriptionsForm.onsubmit = async (e) => { - e.preventDefault() + e.preventDefault(); - subscriptionEventSource && subscriptionEventSource.close() - $subscriptions.textContent = '' + subscriptionEventSource && subscriptionEventSource.close(); + $subscriptions.textContent = ""; try { const opt = - $settingsForm.authorization.value === 'header' + $settingsForm.authorization.value === "header" ? { headers: { Authorization: `Bearer ${$settingsForm.jwt.value}` } } - : undefined + : undefined; const resp = await fetch( `${$settingsForm.hubUrl.value}/subscriptions`, - opt - ) - if (!resp.ok) throw new Error(resp.statusText) - const json = await resp.json() + opt, + ); + if (!resp.ok) throw new Error(resp.statusText); + const json = await resp.json(); - json.subscriptions.forEach(addSubscription) + json.subscriptions.forEach(addSubscription); - const u = new URL($settingsForm.hubUrl.value) + const u = new URL($settingsForm.hubUrl.value); u.searchParams.append( - 'topic', - '/.well-known/mercure/subscriptions{/topic}{/subscriber}' - ) - u.searchParams.append('lastEventID', json.lastEventID) + "topic", + "/.well-known/mercure/subscriptions{/topic}{/subscriber}", + ); + u.searchParams.append("lastEventID", json.lastEventID); - if (opt) subscriptionEventSource = new EventSourcePolyfill(u, opt) - else subscriptionEventSource = new EventSource(u) + if (opt) subscriptionEventSource = new EventSourcePolyfill(u, opt); + else subscriptionEventSource = new EventSource(u); subscriptionEventSource.onmessage = function (e) { - const s = JSON.parse(e.data) + const s = JSON.parse(e.data); if (s.active) { - addSubscription(s) - return + addSubscription(s); + return; } - document.getElementById(s.id).remove() - } - subscriptionEventSource.onerror = error + document.getElementById(s.id).remove(); + }; + subscriptionEventSource.onerror = error; - $subscriptionsForm.elements.unsubscribe.disabled = false + $subscriptionsForm.elements.unsubscribe.disabled = false; } catch (e) { - error(e) + error(e); } - } + }; $subscriptionsForm.elements.unsubscribe.onclick = function (e) { - e.preventDefault() + e.preventDefault(); - subscriptionEventSource.close() - this.disabled = true - $subscriptions.textContent = '' - } -})() + subscriptionEventSource.close(); + this.disabled = true; + $subscriptions.textContent = ""; + }; +})();