+26
src/helpers.js
+26
src/helpers.js
···
1
+
export function joinurl(...segments) {
2
+
let url = segments[0];
3
+
for (const segment of segments.slice(1)) {
4
+
if (url.endsWith("/") && segment.startsWith("/")) {
5
+
url = url.slice(0, -1) + segment;
6
+
} else if (!url.endsWith("/") && !segment.startsWith("/")) {
7
+
url = url + "/" + segment;
8
+
} else {
9
+
url = url + segment;
10
+
}
11
+
}
12
+
return url;
13
+
}
14
+
15
+
export function extname(filename) {
16
+
if (!filename.includes(".")) {
17
+
return "";
18
+
}
19
+
return "." + filename.split(".").pop();
20
+
}
21
+
22
+
export function getContentTypeForFilename(filename) {
23
+
const extension = extname(filename).toLowerCase();
24
+
return getContentTypeForExtension(extension);
25
+
}
26
+
1
27
export function getContentTypeForExtension(extension, fallback = "text/plain") {
2
28
switch (extension) {
3
29
case ".html":
+4
wrangler.toml
+4
wrangler.toml
+8
-12
config.worker.example.json
+8
-12
config.worker.example.json
···
1
1
{
2
-
"sites": [
3
-
{
4
-
"subdomain": "example",
5
-
"knotDomain": "knot.gracekind.net",
6
-
"ownerDid": "did:plc:p572wxnsuoogcrhlfrlizlrb",
7
-
"repoName": "tangled-pages-example",
8
-
"branch": "main",
9
-
"baseDir": "/public",
10
-
"notFoundFilepath": "/404.html"
11
-
}
12
-
],
13
-
"subdomainOffset": 3
2
+
"site": {
3
+
"ownerDid": "did:plc:p572wxnsuoogcrhlfrlizlrb",
4
+
"knotDomain": "knot.gracekind.net",
5
+
"repoName": "tangled-pages-example",
6
+
"branch": "main",
7
+
"baseDir": "/public",
8
+
"notFoundFilepath": "/404.html"
9
+
}
14
10
}
+6
-2
src/worker.js
+6
-2
src/worker.js
···
2
2
import { Config } from "./config.js";
3
3
import configObj from "../config.worker.example.json"; // must be set at build time
4
4
5
+
const config = new Config(configObj);
6
+
if (config.cache) {
7
+
throw new Error("Cache is not supported in worker mode");
8
+
}
9
+
5
10
export default {
6
11
async fetch(request, env, ctx) {
7
-
const config = new Config(configObj);
8
-
const handler = await Handler.fromConfig(config);
9
12
const url = new URL(request.url);
10
13
const host = url.host;
11
14
const path = url.pathname;
15
+
const handler = await Handler.fromConfig(config);
12
16
const { status, content, contentType } = await handler.handleRequest({
13
17
host,
14
18
path,
+24
-4
src/knot-event-listener.js
+24
-4
src/knot-event-listener.js
···
1
1
import EventEmitter from "node:events";
2
2
3
3
export class KnotEventListener extends EventEmitter {
4
-
constructor({ knotDomain }) {
4
+
constructor({ knotDomain, reconnectTimeout = 10000 }) {
5
5
super();
6
6
this.knotDomain = knotDomain;
7
+
this.reconnectTimeout = reconnectTimeout;
8
+
this.connection = null;
7
9
}
8
10
9
11
async start() {
10
-
const ws = new WebSocket(`wss://${this.knotDomain}/events`);
11
-
ws.onmessage = (event) => this.handleMessage(event);
12
+
this.connection = new WebSocket(`wss://${this.knotDomain}/events`);
13
+
this.connection.onmessage = (event) => this.handleMessage(event);
14
+
this.connection.onerror = (event) => this.handleError(event);
15
+
this.connection.onclose = () => this.handleClose();
12
16
return new Promise((resolve) => {
13
-
ws.onopen = () => {
17
+
this.connection.onopen = () => {
14
18
console.log("Knot event listener connected to:", this.knotDomain);
15
19
resolve();
16
20
};
···
29
33
this.emit("refUpdate", event);
30
34
}
31
35
}
36
+
37
+
handleError(event) {
38
+
console.error("Knot event listener error:", event);
39
+
this.emit("error", event);
40
+
}
41
+
42
+
handleClose() {
43
+
console.log("Knot event listener closed");
44
+
this.emit("close");
45
+
if (this.reconnectTimeout) {
46
+
setTimeout(() => {
47
+
console.log("Knot event listener reconnecting...");
48
+
this.start();
49
+
}, this.reconnectTimeout).unref();
50
+
}
51
+
}
32
52
}
+30
-2
src/config.js
+30
-2
src/config.js
···
1
+
class SiteConfig {
2
+
constructor({
3
+
tangledUrl,
4
+
knotDomain,
5
+
ownerDid,
6
+
repoName,
7
+
branch,
8
+
baseDir,
9
+
notFoundFilepath,
10
+
}) {
11
+
if (tangledUrl) {
12
+
if ([ownerDid, repoName].some((v) => !!v)) {
13
+
throw new Error("Cannot use ownerDid and repoName with url");
14
+
}
15
+
}
16
+
this.tangledUrl = tangledUrl;
17
+
this.ownerDid = ownerDid;
18
+
this.repoName = repoName;
19
+
this.knotDomain = knotDomain;
20
+
this.branch = branch;
21
+
this.baseDir = baseDir;
22
+
this.notFoundFilepath = notFoundFilepath;
23
+
}
24
+
}
25
+
1
26
export class Config {
2
27
constructor({ site, sites, subdomainOffset, cache = false }) {
3
-
this.site = site;
4
-
this.sites = sites;
28
+
if (site && sites) {
29
+
throw new Error("Cannot use both site and sites in config");
30
+
}
31
+
this.site = site ? new SiteConfig(site) : null;
32
+
this.sites = sites ? sites.map((site) => new SiteConfig(site)) : null;
5
33
this.subdomainOffset = subdomainOffset;
6
34
this.cache = cache;
7
35
}