UNPKG

dog

Version:
446 lines (440 loc) 12.4 kB
// src/internal/headers.ts var GROUPID = "x-dog-group-identifier"; var CLIENTID = "x-dog-client-identifier"; var OBJECTID = "x-dog-replica-identifier"; var NEIGHBORID = "x-dog-neighbor-identifier"; var TARGETID = "x-dog-target-identifier"; var ISEMPTY = "x-dog-client-empty"; // src/internal/routes.ts var CLOSE = "/~$~/close"; var NEIGHBOR = "/~$~/jello"; var BROADCAST = "/~$~/message"; var WHISPER = "/~$~/whisper"; var GOSSIP = "/~$~/gossip"; var IDENTIFY = "/~$~/identify"; // src/internal/utils.ts var Encoder = /* @__PURE__ */ new TextEncoder(); var STATUS_CODES = { "400": "Bad Request", "401": "Unauthorized", "403": "Forbidden", "404": "Not Found", "405": "Method Not Allowed", "411": "Length Required", "413": "Payload Too Large", "422": "Unprocessable Entity", "426": "Upgrade Required" }; function abort(code, message) { message = message || STATUS_CODES[code]; let length = Encoder.encode(message).byteLength; return new Response(message, { status: code, statusText: STATUS_CODES[code], headers: { "Connection": "close", "Content-Type": "text/plain", "Content-Length": String(length) } }); } function validate(req, replicaid) { let oid = req.headers.get(OBJECTID); if (oid == null) throw new Error("Missing: Replica ID"); if (replicaid && oid !== replicaid) throw new Error("Mismatch: Replica ID"); let gid = req.headers.get(GROUPID); if (gid == null) throw new Error("Missing: Group ID"); let nid = req.headers.get(NEIGHBORID); let rid = req.headers.get(CLIENTID) || nid; if (rid == null) throw new Error("Missing: Request ID"); let tid = req.headers.get(TARGETID); return { gid, rid, oid, tid }; } function load(ns, uid) { let doid = ns.idFromString(uid); return ns.get(doid); } // src/group.ts var Group = class { #child; #mapping; #kids; #sorted; #current; constructor(state, env) { this.uid = state.id.toString(); this.#mapping = new Map(); this.#kids = new Map(); this.#sorted = []; let refs = this.link(env); this.#child = refs.child; } receive(req) { return abort(404); } clusterize(req, target) { return target.newUniqueId(); } async fetch(input, init) { let request = new Request(input, init); let { pathname } = new URL(request.url, "http://dog"); if (pathname === CLOSE) { try { return await this.#close(request); } catch (err) { return abort(400, err.message); } } if (pathname === IDENTIFY) { try { request.headers.set(OBJECTID, ""); let { rid } = validate(request); return this.#identify(request, rid); } catch (err) { return abort(400, err.message); } } return this.receive(request); } async #identify(request, rid) { let alive; let sid = this.#mapping.get(rid) || this.#current || this.#sorted[0]; if (sid != null) alive = this.#kids.get(sid); if (alive != null && this.limit >= ++alive) { } else { let pair = this.#sorted.length > 0 && await this.#sort(); if (pair) { sid = pair[0]; alive = pair[1] + 1; } else { sid = (await this.clusterize(request, this.#child)).toString(); this.#welcome(sid); alive = 1; } } this.#current = alive < this.limit ? sid : void 0; this.#mapping.set(rid, sid); this.#kids.set(sid, alive); return new Response(sid); } async #welcome(nid) { let items = [...this.#kids.keys()]; this.#sorted.unshift(nid); this.#kids.set(nid, 1); if (items.length > 0) { await Promise.all(items.map((sid) => Promise.all([ this.#introduce(nid, sid), this.#introduce(sid, nid) ]))); } } #introduce(stranger, target) { let headers = new Headers(); headers.set(OBJECTID, target); headers.set(NEIGHBORID, stranger); headers.set(GROUPID, this.uid); let url = new URL(NEIGHBOR, "http://dog"); let stub = load(this.#child, target); return stub.fetch(url.href, { headers }); } async #sort() { let tuples = [...this.#kids]; if (tuples.length > 1) { tuples.sort((a, b) => a[1] - b[1]); } let bucket; let i = 0, list = []; for (; i < tuples.length; i++) { if (tuples[i][1] < this.limit) { if (!bucket) bucket = tuples[i]; list.push(tuples[i][0]); } } this.#sorted = list; return bucket; } async #close(req) { var { rid, oid, gid } = validate(req); if (gid !== this.uid) throw new Error("Mismatch: Group ID"); let alive = this.#kids.get(oid); if (alive == null) throw new Error("Unknown: Replica ID"); alive = Math.max(0, --alive); this.#kids.set(oid, alive); if (req.headers.get(ISEMPTY) === "1") { this.#mapping.delete(rid); } let bucket = await this.#sort(); this.#current = bucket ? bucket[0] : void 0; return new Response("OK"); } }; // src/replica.ts function send(conns, msg) { for (let ws of conns) ws.send(msg); } var Replica = class { #pool; #neighbors; #parent; #self; #gid; constructor(state, env) { this.uid = state.id.toString(); this.#neighbors = new Set(); this.#pool = new Map(); let refs = this.link(env); this.#parent = refs.parent; this.#self = refs.self; } async connect(req) { if (req.method !== "GET") return abort(405); let value = req.headers.get("upgrade"); if (value !== "websocket") return abort(426); value = (req.headers.get("sec-websocket-key") || "").trim(); if (!/^[+/0-9A-Za-z]{22}==$/.test(value)) return abort(400); value = req.headers.get("sec-websocket-version"); if (value !== "13") return abort(400); try { var { rid, gid } = validate(req, this.uid); } catch (err) { return abort(400, err.message); } let { 0: client, 1: server } = new WebSocketPair(); server.accept(); let socket = { uid: rid, send: server.send.bind(server), close: server.close.bind(server), broadcast: this.#broadcast.bind(this, gid, rid), whisper: this.#whisper.bind(this, gid, rid), emit: this.#emit.bind(this, rid) }; let closer = async (evt) => { try { if (evt.type === "error" && this.onerror) await this.onerror(socket); else if (this.onclose) await this.onclose(socket); } finally { let state2 = this.#pool.get(rid); let isEmpty; if (!state2 || state2.socket.size < 2) { this.#pool.delete(rid); isEmpty = true; } else { state2.socket.delete(server); this.#pool.set(rid, state2); isEmpty = false; } await this.#close(rid, gid, isEmpty); server.close(); } }; server.addEventListener("close", closer); server.addEventListener("error", closer); if (this.onmessage) { server.addEventListener("message", (evt) => { this.onmessage(socket, evt.data); }); } if (this.onopen) { await this.onopen(socket); } let state = this.#pool.get(rid) || { group: gid, socket: new Set() }; state.socket.add(server); this.#pool.set(rid, state); return new Response(null, { status: 101, statusText: "Switching Protocols", webSocket: client }); } async gossip(msg) { if (this.#neighbors.size < 1) return []; let list = await this.#dispatch({ group: this.#gid, sender: this.uid, route: GOSSIP, body: msg == null ? msg : JSON.stringify(msg) }); return Promise.all(list.map((r) => r.json())); } async fetch(input, init) { let request = new Request(input, init); try { var { pathname } = new URL(request.url, "foo://"); var { rid, gid, tid } = validate(request, this.uid); } catch (err) { return abort(400, err.message); } if (pathname === NEIGHBOR) { this.#gid = this.#gid || gid; this.#neighbors.add(rid); return new Response(); } if (pathname === BROADCAST) { try { this.#emit(rid, await request.text()); return new Response(); } catch (err) { let msg = err.stack; return abort(400, msg || "Error parsing broadcast message"); } } if (pathname === WHISPER) { try { if (!tid) throw new Error("Missing: Target ID"); let state = this.#pool.get(tid); if (state) send(state.socket, await request.text()); return new Response(); } catch (err) { let msg = err.stack; return abort(400, msg || "Error parsing whisper message"); } } if (pathname === GOSSIP) { try { if (!this.ongossip) throw new Error("Missing: `ongossip` handler"); let payload = await this.ongossip(await request.json()); let body = payload == null ? null : JSON.stringify(payload); let headers = { "Content-Type": "application/json" }; return new Response(body, { headers }); } catch (err) { let msg = err.stack; return abort(400, msg || "Error while gossiping"); } } let res; try { return res = await this.receive(request); } catch (err) { let stack = err.stack; return res = abort(400, stack || "Error in `receive` method"); } finally { if (res.status !== 101) { await this.#close(rid, gid, true); } } } emit(msg) { this.#emit(this.#gid, msg, true); } broadcast(msg) { return this.#broadcast(this.#gid, this.uid, msg, true); } whisper(target, msg) { return this.#whisper(this.#gid, this.uid, target, msg); } #emit(sender, msg, self) { if (typeof msg === "object") { msg = JSON.stringify(msg); } for (let [rid, state] of this.#pool) { if (self || rid !== sender) send(state.socket, msg); } } async #broadcast(group, sender, msg, self) { let body = typeof msg === "object" ? JSON.stringify(msg) : msg; this.#emit(sender, body, self); await this.#dispatch({ group, sender, body, route: BROADCAST }); } async #dispatch(params) { let list = [...this.#neighbors]; if (list.length < 1) return; let commons = { [NEIGHBORID]: this.uid, [GROUPID]: params.group, [CLIENTID]: params.sender }; if (params.target) { commons[TARGETID] = params.target; } let url = new URL(params.route, "http://dog"); return Promise.all(list.map((sid) => { let stub = load(this.#self, sid); let headers = new Headers(commons); headers.set(OBJECTID, sid); return stub.fetch(url.href, { method: "POST", headers, body: params.body }); })); } async #whisper(group, sender, target, msg) { if (sender === target) return; let body = typeof msg === "object" ? JSON.stringify(msg) : msg; let state = this.#pool.get(target); if (state) return send(state.socket, body); await this.#dispatch({ group, sender, target, body, route: WHISPER }); } async #close(rid, gid, isEmpty) { let headers = new Headers(); headers.set(GROUPID, gid); headers.set(OBJECTID, this.uid); headers.set(CLIENTID, rid); headers.set(ISEMPTY, isEmpty ? "1" : "0"); let url = new URL(CLOSE, "http://dog"); let group = load(this.#parent, gid); await group.fetch(url.href, { headers }); } }; // src/glue.ts var identify = async function(gid, rid, family) { let group = family.parent.get(gid); let request = new Request("http://dog" + IDENTIFY); request.headers.set(GROUPID, gid.toString()); request.headers.set(CLIENTID, rid); let text = await group.fetch(request).then((r) => r.text()); let sid = family.child.idFromString(text); let stub = family.child.get(sid); let prev = stub.fetch.bind(stub); stub.fetch = function(input, init) { let request2 = new Request(input, init); request2.headers.set(CLIENTID, rid); request2.headers.set(OBJECTID, stub.id.toString()); request2.headers.set(GROUPID, gid.toString()); return prev(request2); }; return stub; }; export { Group, Replica, identify };