From 73766d77551ed370272cf20d12d8c343621c180f Mon Sep 17 00:00:00 2001 From: Juliette Wang immjs Date: Thu, 20 Nov 2025 23:09:08 +0100 Subject: [PATCH 01/10] strut-feat: Begin work to exit from PoC --- .gitignore | 3 + package.json | 5 +- src/codeToScancode.ts | 164 -------------- src/index.ts | 459 ---------------------------------------- src/{ => main}/main.ts | 8 +- src/main/permissions.ts | 5 + src/main/protocols.ts | 47 ++++ src/renderer/index.html | 12 ++ src/renderer/index.ts | 0 src/utils.ts | 8 - 10 files changed, 76 insertions(+), 635 deletions(-) delete mode 100644 src/codeToScancode.ts delete mode 100644 src/index.ts rename src/{ => main}/main.ts (87%) create mode 100644 src/main/permissions.ts create mode 100644 src/main/protocols.ts create mode 100644 src/renderer/index.html create mode 100644 src/renderer/index.ts delete mode 100644 src/utils.ts diff --git a/.gitignore b/.gitignore index b947077..eb4cb9b 100755 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ node_modules/ dist/ + +src/assets +src/config.json diff --git a/package.json b/package.json index 9d0c2b6..d41ccb3 100755 --- a/package.json +++ b/package.json @@ -2,11 +2,12 @@ "name": "@cathodique/de", "version": "1.0.1", "description": "Cathodique Desktop Environment", - "main": "dist/main.js", + "main": "dist/main/main.js", + "type": "module", "scripts": { "start": "tsc && LOGGERS=wl_serv_i8a56qb0lm3ox1ng1cqai0e1 electron .", "test": "echo \"Error: no test specified\" && exit 1", - "build": "tsc && cp -frfr assets/* dist/" + "build": "rm -frfr dist && mkdir dist && cp -frfr src/* dist/ && find dist/ -name \"*.ts\" -type f -delete && npx tsc" }, "repository": { "type": "git", diff --git a/src/codeToScancode.ts b/src/codeToScancode.ts deleted file mode 100644 index 0fbc0d3..0000000 --- a/src/codeToScancode.ts +++ /dev/null @@ -1,164 +0,0 @@ -export const codeToScan: Record = { - "Escape": 0x0009, - "Digit1": 0x000A, - "Digit2": 0x000B, - "Digit3": 0x000C, - "Digit4": 0x000D, - "Digit5": 0x000E, - "Digit6": 0x000F, - "Digit7": 0x0010, - "Digit8": 0x0011, - "Digit9": 0x0012, - "Digit0": 0x0013, - "Minus": 0x0014, - "Equal": 0x0015, - "Backspace": 0x0016, - "Tab": 0x0017, - "KeyQ": 0x0018, - "KeyW": 0x0019, - "KeyE": 0x001A, - "KeyR": 0x001B, - "KeyT": 0x001C, - "KeyY": 0x001D, - "KeyU": 0x001E, - "KeyI": 0x001F, - "KeyO": 0x0020, - "KeyP": 0x0021, - "BracketLeft": 0x0022, - "BracketRight": 0x0023, - "Enter": 0x0024, - "ControlLeft": 0x0025, - "KeyA": 0x0026, - "KeyS": 0x0027, - "KeyD": 0x0028, - "KeyF": 0x0029, - "KeyG": 0x002A, - "KeyH": 0x002B, - "KeyJ": 0x002C, - "KeyK": 0x002D, - "KeyL": 0x002E, - "Semicolon": 0x002F, - "Quote": 0x0030, - "Backquote": 0x0031, - "ShiftLeft": 0x0032, - "Backslash": 0x0033, - "KeyZ": 0x0034, - "KeyX": 0x0035, - "KeyC": 0x0036, - "KeyV": 0x0037, - "KeyB": 0x0038, - "KeyN": 0x0039, - "KeyM": 0x003A, - "Comma": 0x003B, - "Period": 0x003C, - "Slash": 0x003D, - "ShiftRight": 0x003E, - "NumpadMultiply": 0x003F, - "AltLeft": 0x0040, - "Space": 0x0041, - "CapsLock": 0x0042, - "F1": 0x0043, - "F2": 0x0044, - "F3": 0x0045, - "F4": 0x0046, - "F5": 0x0047, - "F6": 0x0048, - "F7": 0x0049, - "F8": 0x004A, - "F9": 0x004B, - "F10": 0x004C, - "NumLock": 0x004D, - "ScrollLock": 0x004E, - "Numpad7": 0x004F, - "Numpad8": 0x0050, - "Numpad9": 0x0051, - "NumpadSubtract": 0x0052, - "Numpad4": 0x0053, - "Numpad5": 0x0054, - "Numpad6": 0x0055, - "NumpadAdd": 0x0056, - "Numpad1": 0x0057, - "Numpad2": 0x0058, - "Numpad3": 0x0059, - "Numpad0": 0x005A, - "NumpadDecimal": 0x005B, - "Lang5": 0x005D, - "IntlBackslash": 0x005E, - "F11": 0x005F, - "F12": 0x0060, - "IntlRo": 0x0061, - "Lang3": 0x0062, - "Lang4": 0x0063, - "Convert": 0x0064, - "KanaMode": 0x0065, - "NonConvert": 0x0066, - "NumpadEnter": 0x0068, - "ControlRight": 0x0069, - "NumpadDivide": 0x006A, - "PrintScreen": 0x006B, - "AltRight": 0x006C, - "Home": 0x006E, - "ArrowUp": 0x006F, - "PageUp": 0x0070, - "ArrowLeft": 0x0071, - "ArrowRight": 0x0072, - "End": 0x0073, - "ArrowDown": 0x0074, - "PageDown": 0x0075, - "Insert": 0x0076, - "Delete": 0x0077, - "AudioVolumeMute": 0x0079, - "AudioVolumeDown": 0x007A, - "AudioVolumeUp": 0x007B, - "Power": 0x007C, - "NumpadEqual": 0x007D, - "Pause": 0x007F, - "NumpadComma": 0x0081, - "Lang1": 0x0082, - "Lang2": 0x0083, - "IntlYen": 0x0084, - "MetaLeft": 0x0085, - "MetaRight": 0x0086, - "ContextMenu": 0x0087, - "BrowserStop": 0x0088, - "Again": 0x0089, - "Undo": 0x008B, - "Select": 0x008C, - "Copy": 0x008D, - "Open": 0x008E, - "Paste": 0x008F, - "Find": 0x0090, - "Cut": 0x0091, - "Help": 0x0092, - "LaunchApp2": 0x0094, - "Sleep": 0x0096, - "WakeUp": 0x0097, - "LaunchApp1": 0x0098, - "LaunchMail": 0x00A3, - "BrowserFavorites": 0x00A4, - "BrowserBack": 0x00A6, - "BrowserForward": 0x00A7, - "Eject": 0x00A9, - "MediaTrackNext": 0x00AB, - "MediaPlayPause": 0x00AC, - "MediaTrackPrevious": 0x00AD, - "MediaStop": 0x00AE, - "MediaSelect": 0x00B3, - "BrowserHome": 0x00B4, - "BrowserRefresh": 0x00B5, - "NumpadParenLeft": 0x00BB, - "NumpadParenRight": 0x00BC, - "F13": 0x00BF, - "F14": 0x00C0, - "F15": 0x00C1, - "F16": 0x00C2, - "F17": 0x00C3, - "F18": 0x00C4, - "F19": 0x00C5, - "F20": 0x00C6, - "F21": 0x00C7, - "F22": 0x00C8, - "F23": 0x00C9, - "F24": 0x00CA, - "BrowserSearch": 0x00E1, -} diff --git a/src/index.ts b/src/index.ts deleted file mode 100644 index b6f5130..0000000 --- a/src/index.ts +++ /dev/null @@ -1,459 +0,0 @@ -import { HLCompositor, HLConnection } from "@cathodique/wl-serv-high"; -import { - InstructionType, - RegRectangle, -} from "@cathodique/wl-serv-high/dist/objects/wl_region"; -import { SeatConfiguration, WlSeat } from "@cathodique/wl-serv-high/dist/objects/wl_seat"; -import { KeyboardRegistry } from "@cathodique/wl-serv-high/dist/objects/wl_keyboard"; -import { BaseObject } from "@cathodique/wl-serv-high/dist/objects/base_object"; -import { WlSurface } from "@cathodique/wl-serv-high/dist/objects/wl_surface"; -import { XdgWmBase } from "@cathodique/wl-serv-high/dist/objects/xdg_wm_base"; -import { ipcRenderer } from "electron"; - -import { codeToScan } from "./codeToScancode"; -import { WlSubsurface } from "@cathodique/wl-serv-high/dist/objects/wl_subsurface"; -import { XdgToplevel } from "@cathodique/wl-serv-high/dist/objects/xdg_toplevel"; -import { WindowGeometry, XdgSurface } from "@cathodique/wl-serv-high/dist/objects/xdg_surface"; -import { ZxdgToplevelDecorationV1 } from "@cathodique/wl-serv-high/dist/objects/zxdg_decoration_manager_v1"; -import { WlBuffer } from "@cathodique/wl-serv-high/dist/objects/wl_buffer"; -import { SeatAuthority, SeatInstances, SeatRegistry } from "@cathodique/wl-serv-high/dist/registries/seat"; -import { OutputRegistry } from "@cathodique/wl-serv-high/dist/registries/output"; -// import { WlPointer } from "@cathodique/wl-serv-high/dist/objects/wl_pointer"; - -// HERE -// TODO::: -// Direct events towards their respective authorities -// for both Seat and Output - -export function isInRegion(reg: RegRectangle[], y: number, x: number, defaultValue: boolean = false) { - if (reg.length === 0) return defaultValue; - - return ( - reg.reduce((a, v) => { - if (v.hasCoordinate(y, x)) return v.type; - return a; - }, null) === InstructionType.Add - ); -} - -const knownMods = ["Shift", "Lock", "Control", "Mod1", "Mod2", "Mod3", "Mod4", "Mod5"] as const; -class Modifiers { - depressed = Object.fromEntries(knownMods.map((v) => [v, false])) as Record; - depressedBitmask = 0; - latched = Object.fromEntries(knownMods.map((v) => [v, false])) as Record; - latchedBitmask = 0; - locked = Object.fromEntries(knownMods.map((v) => [v, false])) as Record; - lockedBitmask = 0; - - group = 0; - - seatConfig: SeatConfiguration; - - constructor(seatConfig: SeatConfiguration) { - this.seatConfig = seatConfig; - } - - updateAccordingly(evt: KeyboardEvent | MouseEvent) { - let changed = { depressed: false, latched: false, locked: false }; - function checkIfChangedAndUpdate(origin: Record, modifier: typeof knownMods[number], value: boolean) { - if (origin[modifier] === value) return false; - origin[modifier] = value; - return true; - } - // Shift: "Shift" - changed.depressed ||= checkIfChangedAndUpdate(this.depressed, "Shift", evt.getModifierState("Shift")); - // Lock: "CapsLock" - changed.locked ||= checkIfChangedAndUpdate(this.locked, "Lock", evt.getModifierState("CapsLock")); - if (evt instanceof KeyboardEvent) changed.depressed ||= checkIfChangedAndUpdate(this.depressed, "Lock", evt.type === "keydown" && evt.key === "CapsLock"); - // Control: "Control" - changed.depressed ||= checkIfChangedAndUpdate(this.depressed, "Control", evt.getModifierState("Control")); - // Mod1: "Alt" - changed.depressed ||= checkIfChangedAndUpdate(this.depressed, "Mod1", evt.getModifierState("Alt")); - // Mod2: "NumLock" - changed.depressed ||= checkIfChangedAndUpdate(this.depressed, "Mod2", evt.getModifierState("NumLock")); - // Mod3: "Hyper" (No Level 5 in browser spec) - changed.depressed ||= checkIfChangedAndUpdate(this.depressed, "Mod3", evt.getModifierState("Hyper")); - // Mod4: "Meta" - changed.depressed ||= checkIfChangedAndUpdate(this.depressed, "Mod4", evt.getModifierState("Meta")); - // Mod5: "AltGraph" - changed.depressed ||= checkIfChangedAndUpdate(this.depressed, "Mod5", evt.getModifierState("AltGraph")); - - return changed; - } - - static createMask(object: Record) { - let result = 0; - for (let modIdx = 0; modIdx < knownMods.length; modIdx += 1) { - const mask = 2 ** modIdx; - if (object[knownMods[modIdx]]) result += mask; - } - - return result; - } - - update(connection: HLConnection, serial?: number) { - const authority = connection.display.seatRegistry.get(this.seatConfig)!.get(connection)!; - - authority.modifiers(this.depressedBitmask, this.latchedBitmask, this.lockedBitmask, this.group, serial); - } - - ifUpdateThenEmit(evt: KeyboardEvent | MouseEvent, connection: HLConnection) { - const xWasUpdated = this.updateAccordingly(evt); - if (xWasUpdated.depressed || xWasUpdated.latched || xWasUpdated.locked) { - if (xWasUpdated.depressed) this.depressedBitmask = Modifiers.createMask(this.depressed); - if (xWasUpdated.latched) this.latchedBitmask = Modifiers.createMask(this.latched); - if (xWasUpdated.locked) this.lockedBitmask = Modifiers.createMask(this.locked); - - this.update(connection); - } - } -} - -const mySeatConfig = { - name: "seat0", - capabilities: 3, - modifiers: null as unknown as Modifiers, -}; -mySeatConfig.modifiers = new Modifiers(mySeatConfig); - -const seatReg = new SeatRegistry(); -seatReg.addAuthority(mySeatConfig); - -const myOutput = { - x: 0, - y: 0, - w: 1920, - h: 1080, - effectiveW: 1920, - effectiveH: 1080, -}; -const outputReg = new OutputRegistry(); -outputReg.addAuthority(myOutput); - -// const outputMap = new Map(); - -const compo = new HLCompositor({ - wl_registry: { - outputs: outputReg, - seats: seatReg, - }, - wl_keyboard: new KeyboardRegistry({ keymap: "us" }), -}); - -const tickAnimationFrame = () => { - compo.ticks.emit("tick"); - requestAnimationFrame(tickAnimationFrame); -}; -tickAnimationFrame(); - -const surfaceToDom = new Map(); - -let currentSeat: SeatInstances | undefined = undefined; -// WTF!! -// let currentKeyboards: Map = new Map(); - -document.body.addEventListener("keydown", (v) => { - if (!currentSeat) { - mySeatConfig.modifiers.updateAccordingly(v); - return; - } - v.preventDefault(); - mySeatConfig.modifiers.ifUpdateThenEmit(v, currentSeat.connection); - - const isInMap = (code: string): code is keyof typeof codeToScan => - code in codeToScan; - if (!isInMap(v.code)) return; - - const scancode = codeToScan[v.code]; - - // if (currentSeat) surf.modifiers(currentSeat, 0, 0, 0, 0); - currentSeat.keyDown(scancode); -}); - -document.body.addEventListener("keyup", (v) => { - if (!currentSeat) { - mySeatConfig.modifiers.updateAccordingly(v); - return; - } - v.preventDefault(); - mySeatConfig.modifiers.ifUpdateThenEmit(v, currentSeat.connection); - - const isInMap = (code: string): code is keyof typeof codeToScan => - code in codeToScan; - if (!isInMap(v.code)) return; - - const scancode = codeToScan[v.code]; - - // if (currentSeat) surf.modifiers(currentSeat, 0, 0, 0, 0); - currentSeat.keyUp(scancode); -}); - -const buffers = new Map(); - -compo.on("connection", (c) => { - // console.log(c); - - // let currentKeyboard: WlKeyboard | undefined; - // let currentPointer: WlPointer | undefined; - // const myOutputTransport = outputReg.transports.get(c)!.get(myOutput)!; - - c.on("new_obj", async (obj: BaseObject) => { - // TODO: Separate buffer logic up here! - if (obj instanceof ZxdgToplevelDecorationV1) { - obj.on('wlSetMode', () => { - obj.sendToplevelDecoration('server_side'); - }); - obj.sendToplevelDecoration('server_side'); - } - if (obj instanceof WlSubsurface) { - const parentDom = surfaceToDom.get(obj.meta.parent)!; - - parentDom.append(surfaceToDom.get(obj.meta.surface)!); - - // Subsurface shenanigans - // TODO: Apply on commit - obj.on("wlPlaceAbove", function (this: WlSubsurface, { sibling: other }: { sibling: WlSurface }) { - switch (this.getRelationWith(other)) { - case "sibling": { - const siblingDom = surfaceToDom.get(other)!; - const parentDom = siblingDom.parentElement!; - - parentDom.insertBefore(surfaceToDom.get(this.meta.surface)!, siblingDom); - break; - } - case "parent": { - const parentDom = surfaceToDom.get(other)!; - const parentCanvas = Array.from(parentDom.children).find((v) => v.tagName === 'canvas')!; - - parentDom.insertBefore(surfaceToDom.get(this.meta.surface)!, parentCanvas); - break; - } - default: - // Already handled by wl-serv-high - } - }); - - obj.on("wlPlaceBelow", function (this: WlSubsurface, { sibling: other }: { sibling: WlSurface }) { - switch (this.getRelationWith(other)) { - case "sibling": { - const siblingDom = surfaceToDom.get(other)!; - const parentDom = siblingDom.parentElement!; - - parentDom.insertBefore(surfaceToDom.get(this.meta.surface)!, siblingDom.nextSibling); - break; - } - case "parent": { - const parentDom = surfaceToDom.get(other)!; - const parentCanvas = Array.from(parentDom.children).find((v) => v.tagName === 'canvas')!; - - parentDom.insertBefore(surfaceToDom.get(this.meta.surface)!, parentCanvas.nextSibling); - break; - } - default: - // Already handled by wl-serv-high - } - }); - obj.on('wlSetPosition', function (this: WlSubsurface, { y, x }: { y: number, x: number }) { - const thisDom = surfaceToDom.get(this.meta.surface)!; - - thisDom.style.top = `${y}px`; - thisDom.style.left = `${x}px`; - }); - } - - if (!(obj instanceof WlSurface)) return; - - // console.log(surf); - // const awaitCommit = () => new Promise((r) => surf.once('wlCommit', () => r())); - const container = document.createElement("div") as HTMLDivElement; - container.classList.add("surface-container"); - - container.style.display = "none"; - - const canvas = document.createElement("canvas") as HTMLCanvasElement; - canvas.classList.add("surface-contents"); - - // FPS element - // const fpsEl = document.createElement('p'); - // let lastFrameTimes = [1]; - // let lastNow = Date.now(); - - container.append(canvas); - - surfaceToDom.set(obj, container); - - const ctx = canvas.getContext("2d"); - if (!ctx) - throw new Error( - "Failed to derive 2d context from canvas element; is anything disabled?", - ); - - // console.log(surf); - - let wasInSurface = false; - - obj.on("updateRole", () => { - switch (obj.role) { - case "cursor": - break; - case "toplevel": - const titleTextNode = document.createTextNode('Window'); - - const toplevel = obj.xdgSurface!.toplevel!; - - const windowTemplate = document.querySelector('template#window')! as HTMLTemplateElement; - const clone = windowTemplate.content.cloneNode(true) as DocumentFragment; - - const cropContainer = document.createElement('div'); - cropContainer.classList.add('crop-container'); - cropContainer.append(container); - - const windowGeometryDoubleBuff = (toplevel.parent as XdgSurface).geometry; - function applyWindowGeometry(windowGeometry: WindowGeometry) { - cropContainer.style.height = `${windowGeometry.height}px`; - cropContainer.style.width = `${windowGeometry.width}px`; - container.style.top = `-${windowGeometry.y}px`; - container.style.left = `-${windowGeometry.x}px`; - } - applyWindowGeometry(windowGeometryDoubleBuff.current); - windowGeometryDoubleBuff.on('current', applyWindowGeometry); - - clone.querySelector('slot[name=window_title]')!.replaceWith(titleTextNode); - clone.querySelector('slot[name=window_contents]')!.replaceWith(cropContainer); - - titleTextNode.textContent = toplevel.title || 'Untitled window'; - toplevel.on('wlSetTitle', (v) => titleTextNode.textContent = toplevel.title || 'Untitled window'); - - document.body.append(clone); - container.classList.add("xdg-toplevel"); - break; - case "popup": - document.body.append(container); - container.classList.add("xdg-popup"); - break; - case "subsurface": - container.classList.add("subsurface"); - break; - } - }); - - const move = function (evt: MouseEvent, forceLeave?: boolean) { - (obj.xdgSurface?.parent as XdgWmBase)?.addCommand("ping", { - serial: obj.connection.time.getTime(), - }); - - const containerPos = container.getBoundingClientRect(); - - const mouseY = evt.clientY - containerPos.top; - const mouseX = evt.clientX - containerPos.left; - - console.log("Something ok?"); - - console.log(obj, obj.inputRegions, obj.inputRegions.current, mouseY, mouseX); - - evt.stopPropagation(); - - if ( - !forceLeave && - isInRegion(obj.inputRegions.current, mouseY, mouseX, true) - ) { - if (!wasInSurface) { - wasInSurface = true; - currentSeat = obj.connection.display.seatRegistry.get(mySeatConfig)!.get(c)!; - console.log('enter'); - const enterSerial = currentSeat.focus(obj, []); - mySeatConfig.modifiers.update(currentSeat.connection, enterSerial); - currentSeat.enter(obj, mouseX, mouseY); - } - currentSeat!.moveTo(mouseX, mouseY); - } else { - if (wasInSurface) { - wasInSurface = false; - if (currentSeat) currentSeat.blur(obj); - if (currentSeat) currentSeat.leave(obj); - currentSeat = undefined; - console.log('leave'); - } - } - }; - - container.addEventListener("mouseenter", move); - container.addEventListener("mousemove", move); - container.addEventListener("mouseleave", (v) => move(v, true)); - const webToButtonMap: Record = { - 0: 0x110, - 1: 0x112, - 2: 0x111, - 3: 0x116, - 4: 0x115, - }; - container.addEventListener("mousedown", (evt) => { - if (wasInSurface && currentSeat) - currentSeat.buttonDown(webToButtonMap[evt.button]); - }); - container.addEventListener("mouseup", (evt) => { - if (wasInSurface && currentSeat) - currentSeat.buttonUp(webToButtonMap[evt.button]); - }); - - let wasShown = false; - - let lastDimensions: [number, number] = [-Infinity, -Infinity]; - const commitHandler = async function () { - - const b = obj.buffer.current; - - if (b === null) container.style.display = "none"; - if (b == null) return; - - if (!wasShown) { - wasShown = true; - obj.shown(myOutput); - } - - container.style.display = "block"; - if (lastDimensions[0] !== b.meta.height || lastDimensions[1] !== b.meta.width) { - container.style.width = `${b.meta.width}px`; - container.style.height = `${b.meta.height}px`; - canvas.width = b.meta.width; - canvas.height = b.meta.height; - lastDimensions = [b.meta.height, b.meta.width]; - } - - container.style.transform = ``; - - const currlyDamagedBuffer = obj.getCurrlyDammagedBuffer(); - - for (const rect of currlyDamagedBuffer) { - b.updateBufferArea(rect.y, rect.x, rect.h, rect.w) - } - const arr = new Uint8ClampedArray( - b.buffer.buffer, - 0, - b.meta.width * b.meta.height * 4, - ); - if (arr.length > 0) { - let imageData = new ImageData(arr, b.meta.width, b.meta.height); - - for (const rect of currlyDamagedBuffer) { - ctx!.putImageData(imageData, 0, 0, rect.x, rect.y, rect.w, rect.h); - } - } - }; - - commitHandler(); - obj.on("update", () => commitHandler()); - - obj.once("beforeWlDestroy", () => { - // Unsure vvv - container.remove(); - }); - }); -}); -compo.start(); - -compo.on("ready", () => { - document.body.append(`Ready at ${compo.params.socketPath}`); - ipcRenderer.send("addToDeleteQueue", compo.params.socketPath); - ipcRenderer.send(`Ready at ${compo.params.socketPath}.lock`); -}); diff --git a/src/main.ts b/src/main/main.ts similarity index 87% rename from src/main.ts rename to src/main/main.ts index d7cd640..bf3f545 100755 --- a/src/main.ts +++ b/src/main/main.ts @@ -1,6 +1,7 @@ import { app, BrowserWindow, ipcMain } from "electron"; import { rmSync } from "node:fs"; -import { join } from "node:path"; + +import { registerProtocols } from "./protocols.js"; // app.allowRendererProcessReuse = false; @@ -13,12 +14,15 @@ const createWindow = () => { // fullscreen: true, webPreferences: { nodeIntegration: true, + nodeIntegrationInSubFrames: false, contextIsolation: false, }, }); + registerProtocols(); + // win.webContents.openDevTools(); - win.loadFile(join(__dirname, "../dist/index.html")); + win.loadURL("app://top/index.html"); }; const deleteQueue: string[] = []; diff --git a/src/main/permissions.ts b/src/main/permissions.ts new file mode 100644 index 0000000..a718d4f --- /dev/null +++ b/src/main/permissions.ts @@ -0,0 +1,5 @@ + + +export const processPermissions = () => { + +}; diff --git a/src/main/protocols.ts b/src/main/protocols.ts new file mode 100644 index 0000000..66ca7ed --- /dev/null +++ b/src/main/protocols.ts @@ -0,0 +1,47 @@ +import { net, OnBeforeRequestListenerDetails, protocol, session } from "electron"; +import { join } from "node:path"; + +import { pathToFileURL } from "node:url"; + +protocol.registerSchemesAsPrivileged([{ + scheme: "app", + privileges: { + standard: true, + secure: true, + bypassCSP: false, + allowServiceWorkers: false, + corsEnabled: true, + stream: true, + codeCache: true, + }, +}]); + +export const registerProtocols = () => { + protocol.handle('app', (request) => { + const reqUrl = new URL(request.url); + + switch (reqUrl.host) { + case 'top': + console.log(pathToFileURL(join(import.meta.dirname, '../renderer', reqUrl.pathname)).toString()); + return net.fetch(pathToFileURL(join(import.meta.dirname, '../renderer', reqUrl.pathname)).toString()); + default: + return new Response("Not found", { status: 404 }); + } + }); + + session.defaultSession.webRequest.onBeforeRequest((request: OnBeforeRequestListenerDetails, callback) => { + if (['http', 'https', 'file', 'ftp'].some((v) => request.url.startsWith(v))) { + const { frame } = request; + if (frame == null) { + return callback({ cancel: true }); + } + console.log(frame.url); + + + + // TODO: Handle permissions of each module. For now though... + callback({ cancel: true }); + } + callback({}); + }); +}; diff --git a/src/renderer/index.html b/src/renderer/index.html new file mode 100644 index 0000000..1733837 --- /dev/null +++ b/src/renderer/index.html @@ -0,0 +1,12 @@ + + + + + + Cathodique + + + + + + diff --git a/src/renderer/index.ts b/src/renderer/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/utils.ts b/src/utils.ts deleted file mode 100644 index 908820b..0000000 --- a/src/utils.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { InstructionType, RegRectangle } from "@cathodique/wl-serv-high/dist/objects/wl_region"; - -export function isInRegion(reg: RegRectangle[], y: number, x: number) { - return reg.reduce((a, v) => { - if (!v.hasCoordinate(y, x)) return a; - return v.type; - }, null) === InstructionType.Add; -} From 503dc5bfe5f9caec2cb833ba90e764a7605a54cd Mon Sep 17 00:00:00 2001 From: Juliette Wang immjs Date: Sat, 20 Dec 2025 11:27:58 +0100 Subject: [PATCH 02/10] feat: works, no output --- package-lock.json | 599 +++++++++++++++++- package.json | 11 +- src/main/main.ts | 4 - src/main/protocols.ts | 36 +- src/modules/.common/classes/component.ts | 31 + src/modules/.common/classes/componentList.ts | 23 + src/modules/.common/classes/latch.ts | 147 +++++ src/modules/.common/classes/orderedPeer.ts | 130 ++++ .../.common/classes/sharedDomRemote.ts | 174 +++++ src/modules/.common/classes/withTransfer.ts | 12 + src/modules/.common/index.ts | 14 + .../.common/ipcHandlers/cathodiqueConsumer.ts | 13 + .../.common/ipcHandlers/cathodiqueProvider.ts | 59 ++ .../.common/ipcHandlers/cathodiqueRemote.ts | 7 + src/modules/.common/ipcHandlers/domRemote.ts | 33 + src/modules/.common/parentIpc.ts | 14 + src/modules/.common/setup.ts | 2 + .../.common/utils/nodeEventListener.ts | 205 ++++++ src/modules/.common/utils/types.ts | 30 + src/modules/.common/utils/utils.ts | 16 + .../immjs.macos-aqua-windowframe/module.html | 35 + src/renderer/.common/classes/component.ts | 31 + src/renderer/.common/classes/componentList.ts | 23 + src/renderer/.common/classes/latch.ts | 147 +++++ src/renderer/.common/classes/orderedPeer.ts | 130 ++++ .../.common/classes/sharedDomRemote.ts | 174 +++++ src/renderer/.common/classes/withTransfer.ts | 12 + src/renderer/.common/index.ts | 14 + .../.common/ipcHandlers/cathodiqueConsumer.ts | 13 + .../.common/ipcHandlers/cathodiqueProvider.ts | 59 ++ .../.common/ipcHandlers/cathodiqueRemote.ts | 7 + src/renderer/.common/ipcHandlers/domRemote.ts | 33 + src/renderer/.common/parentIpc.ts | 14 + src/renderer/.common/setup.ts | 2 + .../.common/utils/nodeEventListener.ts | 205 ++++++ src/renderer/.common/utils/types.ts | 30 + src/renderer/.common/utils/utils.ts | 16 + src/renderer/classes/handlers/dom/base.ts | 21 + src/renderer/classes/handlers/dom/popup.ts | 18 + .../classes/handlers/dom/subsurface.ts | 81 +++ src/renderer/classes/handlers/dom/surface.ts | 94 +++ src/renderer/classes/handlers/dom/toplevel.ts | 30 + src/renderer/classes/handlers/handlers.ts | 10 + .../lib/toplevel_decoration_manager.ts | 13 + src/renderer/classes/wayland/output/output.ts | 30 + .../classes/wayland/seat/codeToScancode.ts | 166 +++++ .../classes/wayland/seat/modifiers.ts | 75 +++ src/renderer/classes/wayland/seat/seat.ts | 109 ++++ src/renderer/host/classes/latch.ts | 1 + src/renderer/host/classes/module.ts | 95 +++ src/renderer/host/classes/orchestrator.ts | 25 + src/renderer/host/classes/orderedPeer.ts | 1 + src/renderer/host/classes/sharedDomHost.ts | 163 +++++ src/renderer/host/index.ts | 6 + .../host/ipcHandlers/cathodiqueConsumer.ts | 1 + .../host/ipcHandlers/cathodiqueHost.ts | 19 + src/renderer/host/ipcHandlers/domHost.ts | 63 ++ src/renderer/host/utils.ts | 9 + src/renderer/host/utils/componentProxy.ts | 35 + src/renderer/host/utils/types.ts | 1 + src/renderer/index.html | 14 +- src/renderer/index.ts | 0 src/renderer/wayland/index.ts | 87 +++ tsconfig.json | 9 +- tsconfig.modules.json | 113 ++++ 65 files changed, 3774 insertions(+), 20 deletions(-) create mode 100644 src/modules/.common/classes/component.ts create mode 100644 src/modules/.common/classes/componentList.ts create mode 100644 src/modules/.common/classes/latch.ts create mode 100644 src/modules/.common/classes/orderedPeer.ts create mode 100644 src/modules/.common/classes/sharedDomRemote.ts create mode 100644 src/modules/.common/classes/withTransfer.ts create mode 100644 src/modules/.common/index.ts create mode 100644 src/modules/.common/ipcHandlers/cathodiqueConsumer.ts create mode 100644 src/modules/.common/ipcHandlers/cathodiqueProvider.ts create mode 100644 src/modules/.common/ipcHandlers/cathodiqueRemote.ts create mode 100644 src/modules/.common/ipcHandlers/domRemote.ts create mode 100644 src/modules/.common/parentIpc.ts create mode 100644 src/modules/.common/setup.ts create mode 100644 src/modules/.common/utils/nodeEventListener.ts create mode 100644 src/modules/.common/utils/types.ts create mode 100644 src/modules/.common/utils/utils.ts create mode 100644 src/modules/immjs.macos-aqua-windowframe/module.html create mode 100644 src/renderer/.common/classes/component.ts create mode 100644 src/renderer/.common/classes/componentList.ts create mode 100644 src/renderer/.common/classes/latch.ts create mode 100644 src/renderer/.common/classes/orderedPeer.ts create mode 100644 src/renderer/.common/classes/sharedDomRemote.ts create mode 100644 src/renderer/.common/classes/withTransfer.ts create mode 100644 src/renderer/.common/index.ts create mode 100644 src/renderer/.common/ipcHandlers/cathodiqueConsumer.ts create mode 100644 src/renderer/.common/ipcHandlers/cathodiqueProvider.ts create mode 100644 src/renderer/.common/ipcHandlers/cathodiqueRemote.ts create mode 100644 src/renderer/.common/ipcHandlers/domRemote.ts create mode 100644 src/renderer/.common/parentIpc.ts create mode 100644 src/renderer/.common/setup.ts create mode 100644 src/renderer/.common/utils/nodeEventListener.ts create mode 100644 src/renderer/.common/utils/types.ts create mode 100644 src/renderer/.common/utils/utils.ts create mode 100644 src/renderer/classes/handlers/dom/base.ts create mode 100644 src/renderer/classes/handlers/dom/popup.ts create mode 100644 src/renderer/classes/handlers/dom/subsurface.ts create mode 100644 src/renderer/classes/handlers/dom/surface.ts create mode 100644 src/renderer/classes/handlers/dom/toplevel.ts create mode 100644 src/renderer/classes/handlers/handlers.ts create mode 100644 src/renderer/classes/handlers/lib/toplevel_decoration_manager.ts create mode 100644 src/renderer/classes/wayland/output/output.ts create mode 100644 src/renderer/classes/wayland/seat/codeToScancode.ts create mode 100644 src/renderer/classes/wayland/seat/modifiers.ts create mode 100644 src/renderer/classes/wayland/seat/seat.ts create mode 100644 src/renderer/host/classes/latch.ts create mode 100644 src/renderer/host/classes/module.ts create mode 100644 src/renderer/host/classes/orchestrator.ts create mode 100644 src/renderer/host/classes/orderedPeer.ts create mode 100644 src/renderer/host/classes/sharedDomHost.ts create mode 100644 src/renderer/host/index.ts create mode 100644 src/renderer/host/ipcHandlers/cathodiqueConsumer.ts create mode 100644 src/renderer/host/ipcHandlers/cathodiqueHost.ts create mode 100644 src/renderer/host/ipcHandlers/domHost.ts create mode 100644 src/renderer/host/utils.ts create mode 100644 src/renderer/host/utils/componentProxy.ts create mode 100644 src/renderer/host/utils/types.ts delete mode 100644 src/renderer/index.ts create mode 100644 src/renderer/wayland/index.ts create mode 100644 tsconfig.modules.json diff --git a/package-lock.json b/package-lock.json index e3298bb..ac5365e 100755 --- a/package-lock.json +++ b/package-lock.json @@ -10,9 +10,12 @@ "license": "MIT", "dependencies": { "@cathodique/mmap-io": "file:../mmap-io", + "@cathodique/usocket": "file:../usocket", "@cathodique/wl-serv-high": "file:../wl-serv-high", "@paralleldrive/cuid2": "^2.2.2", - "electron": "35.7", + "@typescript/native-preview": "^7.0.0-dev.20251220.1", + "electron": "35.6", + "esbuild": "^0.27.2", "koffi": "^2.14.0", "node-abi": "^4.12.0", "xml-parser": "^1.2.1" @@ -43,6 +46,24 @@ "typescript": "^3.5.3" } }, + "../usocket": { + "name": "@cathodique/usocket", + "version": "1.0.7", + "hasInstallScript": true, + "license": "ISC", + "dependencies": { + "bindings": "^1.5.0", + "debug": "^4.3.4", + "nan": "^2.23.0", + "node-gyp": "^11.3.0" + }, + "devDependencies": { + "@types/debug": "^4.1.12", + "@types/node": "^22.10.2", + "mocha": "^9.2.2", + "typescript": "^5.7.2" + } + }, "../wayland-server-js-impl": { "name": "@cathodique/wayland-server-js-impl", "version": "1.0.13", @@ -82,6 +103,10 @@ "resolved": "../mmap-io", "link": true }, + "node_modules/@cathodique/usocket": { + "resolved": "../usocket", + "link": true + }, "node_modules/@cathodique/wl-serv-high": { "resolved": "../wl-serv-high", "link": true @@ -238,6 +263,422 @@ "node": ">= 10.0.0" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@gar/promisify": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", @@ -430,6 +871,115 @@ "@types/node": "*" } }, + "node_modules/@typescript/native-preview": { + "version": "7.0.0-dev.20251220.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview/-/native-preview-7.0.0-dev.20251220.1.tgz", + "integrity": "sha512-PmKa/JV9oVC+34VDVDj8fCnjtJKbcFXzPOOUtebsQhudnJN2L7cUvSUAvsPA36W3MwHA030rNUHaelcKG9bY3w==", + "license": "Apache-2.0", + "bin": { + "tsgo": "bin/tsgo.js" + }, + "optionalDependencies": { + "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20251220.1", + "@typescript/native-preview-darwin-x64": "7.0.0-dev.20251220.1", + "@typescript/native-preview-linux-arm": "7.0.0-dev.20251220.1", + "@typescript/native-preview-linux-arm64": "7.0.0-dev.20251220.1", + "@typescript/native-preview-linux-x64": "7.0.0-dev.20251220.1", + "@typescript/native-preview-win32-arm64": "7.0.0-dev.20251220.1", + "@typescript/native-preview-win32-x64": "7.0.0-dev.20251220.1" + } + }, + "node_modules/@typescript/native-preview-darwin-arm64": { + "version": "7.0.0-dev.20251220.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-darwin-arm64/-/native-preview-darwin-arm64-7.0.0-dev.20251220.1.tgz", + "integrity": "sha512-kFdUHBL0f6tzZfgviBJm7SpX7NBMUIJvS7Gp0SsFbV72Lc/W5k7aFYG5cJScpdlNzG64dC0A5GBl3C/WkPe9Rg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@typescript/native-preview-darwin-x64": { + "version": "7.0.0-dev.20251220.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-darwin-x64/-/native-preview-darwin-x64-7.0.0-dev.20251220.1.tgz", + "integrity": "sha512-i2RNLjZaiskvqeNt9XBN/FdssB+i/PURqLkDP6mY6cLSOVClygBtha0qqBAmj+huTvpa64Nwb740a7uFMpVudw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@typescript/native-preview-linux-arm": { + "version": "7.0.0-dev.20251220.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-arm/-/native-preview-linux-arm-7.0.0-dev.20251220.1.tgz", + "integrity": "sha512-KRLhiLNEjWfWX9cu8/iXtsebQdfH43QVSmkwcnQJCD2lVodw9bAJRL6o7jVXJM4tofDP3i8dCk85SAiwaNiC+A==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@typescript/native-preview-linux-arm64": { + "version": "7.0.0-dev.20251220.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-arm64/-/native-preview-linux-arm64-7.0.0-dev.20251220.1.tgz", + "integrity": "sha512-iiRl8pG4tfImt0LM+M4sYnsdf39eFMGdK2ThgBhVWRUSKZfrtvkqM5odwwVuw9xPKF5hFbx3k9lx2s4mTSM6Gg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@typescript/native-preview-linux-x64": { + "version": "7.0.0-dev.20251220.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-x64/-/native-preview-linux-x64-7.0.0-dev.20251220.1.tgz", + "integrity": "sha512-Gq+YxQWFV5+gBuGv9J939Vw5vYB/ux+q2DLyTGXrgLcXrSCiNGAhf9j2F4DGs0aJOJZIsZN+emp2GTRCUXqdXg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@typescript/native-preview-win32-arm64": { + "version": "7.0.0-dev.20251220.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-win32-arm64/-/native-preview-win32-arm64-7.0.0-dev.20251220.1.tgz", + "integrity": "sha512-7oBfrT5DalZPhmm4SMS0DzUxw5VEG+cq3Qh6Zgr09+QrAuKBHcuwyZNvbcWhHN7ERMY5xNAIMPILmXOpiarTKQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@typescript/native-preview-win32-x64": { + "version": "7.0.0-dev.20251220.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-win32-x64/-/native-preview-win32-x64-7.0.0-dev.20251220.1.tgz", + "integrity": "sha512-Jvg2hAotYaRTp4z/6gJWDfvTZXAPOHQ4/81PsZC68asms8mUBrZT/xBy3rxTpWTKmebsGGRg4cUKHMZCEKNq1Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -914,9 +1464,9 @@ "optional": true }, "node_modules/electron": { - "version": "35.7.5", - "resolved": "https://registry.npmjs.org/electron/-/electron-35.7.5.tgz", - "integrity": "sha512-dnL+JvLraKZl7iusXTVTGYs10TKfzUi30uEDTqsmTm0guN9V2tbOjTzyIZbh9n3ygUjgEYyo+igAwMRXIi3IPw==", + "version": "35.6.0", + "resolved": "https://registry.npmjs.org/electron/-/electron-35.6.0.tgz", + "integrity": "sha512-C+fzUIVkF6HzJjVeCjd6efL5Y01DwuQAnU6yqwh09grYvSQnlMJDC1qsQ8lSWl+sNYwtwUHAdZd7JtUi6Uyo/A==", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -1001,6 +1551,47 @@ "license": "MIT", "optional": true }, + "node_modules/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", diff --git a/package.json b/package.json index d41ccb3..4f52b5f 100755 --- a/package.json +++ b/package.json @@ -3,11 +3,11 @@ "version": "1.0.1", "description": "Cathodique Desktop Environment", "main": "dist/main/main.js", - "type": "module", "scripts": { - "start": "tsc && LOGGERS=wl_serv_i8a56qb0lm3ox1ng1cqai0e1 electron .", + "start": "npm run build && LOGGERS=wl_serv_i8a56qb0lm3ox1ng1cqai0e1 electron .", "test": "echo \"Error: no test specified\" && exit 1", - "build": "rm -frfr dist && mkdir dist && cp -frfr src/* dist/ && find dist/ -name \"*.ts\" -type f -delete && npx tsc" + "build-browser": "esbuild dist/renderer/wayland/index.js --bundle --platform=node --target=node22.4 --external:electron --external:@cathodique/wl-serv-high --outfile=dist/renderer/index.js", + "build": "rm -frfr dist && mkdir dist && cp -frfr src/* dist/ && find dist/ -name \"*.ts\" -type f -delete && npx tsgo && npx tsc -p tsconfig.modules.json && npm run build-browser" }, "repository": { "type": "git", @@ -26,10 +26,13 @@ "typescript": "^5.7.2" }, "dependencies": { + "@cathodique/usocket": "file:../usocket", "@cathodique/mmap-io": "file:../mmap-io", "@cathodique/wl-serv-high": "file:../wl-serv-high", "@paralleldrive/cuid2": "^2.2.2", - "electron": "35.7", + "@typescript/native-preview": "^7.0.0-dev.20251220.1", + "electron": "35.6", + "esbuild": "^0.27.2", "koffi": "^2.14.0", "node-abi": "^4.12.0", "xml-parser": "^1.2.1" diff --git a/src/main/main.ts b/src/main/main.ts index bf3f545..e3b8314 100755 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -3,10 +3,6 @@ import { rmSync } from "node:fs"; import { registerProtocols } from "./protocols.js"; -// app.allowRendererProcessReuse = false; - -// app.commandLine.appendSwitch("disable-hid-blocklist"); - const createWindow = () => { const win = new BrowserWindow({ width: 800, diff --git a/src/main/protocols.ts b/src/main/protocols.ts index 66ca7ed..782817f 100644 --- a/src/main/protocols.ts +++ b/src/main/protocols.ts @@ -22,22 +22,50 @@ export const registerProtocols = () => { switch (reqUrl.host) { case 'top': - console.log(pathToFileURL(join(import.meta.dirname, '../renderer', reqUrl.pathname)).toString()); - return net.fetch(pathToFileURL(join(import.meta.dirname, '../renderer', reqUrl.pathname)).toString()); + if (reqUrl.pathname.split('/').some((v) => v === '.' || v === '..')) { // Path accesses + return new Response("Forbidden", { status: 403 }); + } + return net.fetch(pathToFileURL(join(__dirname, '../renderer', reqUrl.pathname)).toString()); default: return new Response("Not found", { status: 404 }); } }); + protocol.handle('https', (request) => { + const reqUrl = new URL(request.url); + + if (reqUrl.pathname.split('/').some((v) => v === '.' || v === '..')) { // Path accesses + return new Response("Forbidden", { status: 403 }); + } + + const [tld, sld, ...rest] = reqUrl.host.split('.').toReversed(); + const domain = `${sld}.${tld}`; + + switch (domain) { + case "raytu.be": { + console.log(reqUrl.pathname); + if (reqUrl.pathname.startsWith('/.common/')) { + return net.fetch(pathToFileURL(join(__dirname, '../modules', reqUrl.pathname)).toString()); + } + + return net.fetch(pathToFileURL(join(__dirname, '../modules', rest.join('.'), reqUrl.pathname)).toString()); + } + } + + return new Response("Not found", { status: 404 }); + }); + session.defaultSession.webRequest.onBeforeRequest((request: OnBeforeRequestListenerDetails, callback) => { + const url = new URL(request.url); if (['http', 'https', 'file', 'ftp'].some((v) => request.url.startsWith(v))) { const { frame } = request; if (frame == null) { return callback({ cancel: true }); } - console.log(frame.url); - + if (url.host.endsWith(".raytu.be") || url.host === "raytu.be") { + return callback({ cancel: false }); + } // TODO: Handle permissions of each module. For now though... callback({ cancel: true }); diff --git a/src/modules/.common/classes/component.ts b/src/modules/.common/classes/component.ts new file mode 100644 index 0000000..4202273 --- /dev/null +++ b/src/modules/.common/classes/component.ts @@ -0,0 +1,31 @@ +import { nanoid } from "../utils/utils.js"; +import { parentIpc } from "../parentIpc.js"; + +export class Component extends EventTarget { + componentHandle: string; + + constructor() { + super(); + const handle = nanoid(); + this.componentHandle = handle; + } + + init() {} + + post(obj: Record) { + return parentIpc.post({ ...obj, componentHandle: this.componentHandle }); + } + rpc(type: string, data: Record, obj: Record = {}) { + return parentIpc.rpc(type, data, { ...obj, componentHandle: this.componentHandle }); + } + + async getDependency(dependency: string) { + return await this.rpc("getDependency", { dependency }); + } + async getOptionalDependency(dependency: string) { + return await this.rpc("getOptionalDependency", { dependency }); + } + async getAllDependency(dependency: string) { + return await this.rpc("getAllDependency", { dependency }); + } +} diff --git a/src/modules/.common/classes/componentList.ts b/src/modules/.common/classes/componentList.ts new file mode 100644 index 0000000..bbbd1d5 --- /dev/null +++ b/src/modules/.common/classes/componentList.ts @@ -0,0 +1,23 @@ +import { parentIpc } from "../parentIpc.js"; +import type { Component } from "./component.js"; + +export class ComponentList extends EventTarget { + componentClasses = new Map(); + + register(componentName: string, componentClass: new (...a: any[]) => Component) { + if (this.componentClasses.has(componentName)) + throw new Error("This component already exists"); + this.componentClasses.set(componentName, componentClass); + + parentIpc.post({ + type: "componentRegistered", + data: { componentName }, + }); + } + + get(componentName: string) { + return this.componentClasses.get(componentName); + } +} + +export const componentList = new ComponentList(); diff --git a/src/modules/.common/classes/latch.ts b/src/modules/.common/classes/latch.ts new file mode 100644 index 0000000..666fe03 --- /dev/null +++ b/src/modules/.common/classes/latch.ts @@ -0,0 +1,147 @@ +enum LatchState { + Pending, + Fulfilled, +} + +export class Latch { + promise: Promise; + resolve: ((v: T) => void) | undefined; + constructor(value?: T) { + if (value) { + this.resolve = undefined; + this.promise = Promise.resolve(value); + } else { + let resultingResolve: (r: T) => void; + this.promise = new Promise((r) => { resultingResolve = r }); + this.resolve = function (this: Latch, r: T) { + resultingResolve(r); + this.resolve = undefined; + }.bind(this); + } + } + + getState() { + if (this.resolve) return LatchState.Pending; + return LatchState.Fulfilled; + } +} + +export class KeyedLatch { + map = new Map>(); + + getStateOf(key: T) { + return this.map.get(key)?.getState() ?? LatchState.Pending; + } + get(key: T) { + if (this.map.has(key)) return this.map.get(key)!.promise; + const latch = new Latch(); + this.map.set(key, latch); + return latch.promise; + } + resolve(key: T, value: U) { + if (!this.map.has(key)) { + this.map.set(key, new Latch(value)); + } + this.map.get(key)!.resolve?.(value); + return value; + } + getOptional(key: T) { + if (this.map.has(key)) return this.get(key); + return undefined; + } + delete(key: T) { + this.map.delete(key); + } +} + +export class WeakKeyedLatch { + map = new WeakMap>(); + + getStateOf(key: T) { + return this.map.get(key)?.getState() ?? LatchState.Pending; + } + get(key: T) { + if (this.map.has(key)) return this.map.get(key)!.promise; + const latch = new Latch(); + this.map.set(key, latch); + return latch.promise; + } + resolve(key: T, value: U) { + if (!this.map.has(key)) { + this.map.set(key, new Latch(value)); + } + this.map.get(key)!.resolve?.(value); + return value; + } + getOptional(key: T) { + if (this.map.has(key)) return this.get(key); + return undefined; + } + delete(key: T) { + this.map.delete(key); + } +} + +export class ConsumableKeyedLatch extends KeyedLatch { + consumed = new Set(); + + consume(key: T) { + const result = super.get(key); + switch (this.getStateOf(key)) { + case LatchState.Pending: + this.consumed.add(key); + break; + case LatchState.Fulfilled: + this.delete(key); + break; + } + return result; + } + /** + * @deprecated For semantic reasons, use consume instead + * Method kept for inheritance + */ + get(key: T) { + return this.consume(key); + } + resolve(key: T, value: U) { + const result = super.resolve(key, value); + if (this.consumed.has(key)) { + this.consumed.delete(key); + this.delete(key); + } + return result; + } +} + +export class ConsumableWeakKeyedLatch extends WeakKeyedLatch { + consumed = new WeakSet(); + + consume(key: T) { + const result = super.get(key); + switch (this.getStateOf(key)) { + case LatchState.Pending: + this.consumed.add(key); + break; + case LatchState.Fulfilled: + this.delete(key); + break; + } + return result; + } + /** + * @deprecated For semantic reasons, use consume instead + * Method kept for inheritance + */ + get(key: T) { + return this.consume(key); + } + resolve(key: T, value: U) { + const result = super.resolve(key, value); + if (this.consumed.has(key)) { + this.consumed.delete(key); + this.delete(key); + } + return result; + } +} diff --git a/src/modules/.common/classes/orderedPeer.ts b/src/modules/.common/classes/orderedPeer.ts new file mode 100644 index 0000000..5a0b4d0 --- /dev/null +++ b/src/modules/.common/classes/orderedPeer.ts @@ -0,0 +1,130 @@ +import { ConsumableKeyedLatch } from "./latch.js"; + +import { nanoid } from "../utils/utils.js"; +import { WithTransfer } from "./withTransfer.js"; + +export class OrderedPeer { + handlers: Record) => any>[]; + + static actualHandlers = new WeakMap void>(); + private static registered = false; + static registerIpcListener() { + if (this.registered) return; + window.addEventListener( + "message", + (evt) => { + const actualHandler = this.actualHandlers.get(evt.source as WindowProxy); + if (!actualHandler) return; + actualHandler(evt); + }, + ); + this.registered = true; + } + + currentOrderSubmission: bigint = 0n; + pendingMessages: any[] = []; + pendingTransfer: Transferable[] = []; + origin: string; + + promiseMap = new ConsumableKeyedLatch(); + + win: WindowProxy; + postMessage: typeof window["postMessage"]; + constructor(win: WindowProxy, origin = "*", handlers: Record any>[]) { + if (OrderedPeer.actualHandlers.has(win)) throw new Error("A window may only admit a single OrderedPeer"); + + this.win = win; + this.postMessage = win.postMessage.bind(win); + this.origin = origin; + this.handlers = handlers; + + OrderedPeer.actualHandlers.set(win, this.orderedDecoder.bind(this)); + } + + post(data: any) { + let transfer = []; + if (data instanceof WithTransfer) { + transfer = data.transfer; + data = data.data; + } + + this.pendingMessages.push(data); + this.pendingTransfer.push(...transfer); + + if (this.pendingMessages.length === 1) { + queueMicrotask(() => { + this.postMessage( + { + messages: this.pendingMessages, + currentOrder: this.currentOrderSubmission, + }, + this.origin, + this.pendingTransfer, + ); + + this.pendingMessages = []; + this.pendingTransfer = []; + this.currentOrderSubmission += 1n; + }); + } + } + + async rpc(type: string, data: any | WithTransfer, obj: Record = {}) { + const promiseId = nanoid(); + this.post({ type, data, promiseId, ...obj }); + const result = await this.promiseMap.consume(promiseId); + + if (!result.error) return result.reply; + throw result.error; + } + + remainingMessages = new Map(); + currentOrderReception: bigint = 0n; + + originMatch(origin: string) { + if (this.origin === '*') return true; + if (this.origin === '/') return origin === window.origin; + return origin === this.origin; + } + + async orderedDecoder(evt: MessageEvent) { + if (!this.originMatch(evt.origin)) return; + + console.log(); + + const { data: { messages, currentOrder } } = evt; + this.remainingMessages.set(currentOrder, messages); + if (this.currentOrderReception === currentOrder) { + do { + await Promise.all( + evt.data.messages.map(async (message: { data: any, type: string, promiseId?: string, componentHandle?: string }) => { + const { type, promiseId } = message; + + if (type === "reply") { + return this.promiseMap.resolve(promiseId!, message); + } + + const handler = this.handlers.find((v) => type in v); + + if (!handler) { + if (promiseId) this.post({ type: "reply", error: new Error("No such function"), promiseId }); + return; + } + + try { + const result = await handler[type](message); + + if (promiseId) { + this.post({ type: "reply", reply: result, promiseId }); + } + } catch (e) { + this.post({ type: "reply", error: e, promiseId }); + } + }) + ); + this.remainingMessages.delete(this.currentOrderReception); + this.currentOrderReception += 1n; + } while (this.remainingMessages.has(this.currentOrderReception)); + } + } +} diff --git a/src/modules/.common/classes/sharedDomRemote.ts b/src/modules/.common/classes/sharedDomRemote.ts new file mode 100644 index 0000000..fc7d0ac --- /dev/null +++ b/src/modules/.common/classes/sharedDomRemote.ts @@ -0,0 +1,174 @@ +import { parentIpc } from "../parentIpc.js"; +import { handlersMap } from "../utils/nodeEventListener.js"; +import { nanoid } from "../utils/utils.js"; + +export class NodeRegistry { + static nodeToId = new WeakMap(); + static idToNode = new Map>(); + + static getNode(id: string) { + return this.idToNode.get(id)?.deref(); + } + + static hasNode(node: Node) { + return this.nodeToId.has(node); + } + + static getId(node: Node): string { + let id = this.nodeToId.get(node); + if (!id) { + id = nanoid(); + this.nodeToId.set(node, id); + this.idToNode.set(id, new WeakRef(node)); + } + return id; + } +} + +function serializeEvents(node: Node) { + return [...(handlersMap.get(node)?.keys() ?? [])]; +} + +function serializeNode(node: Node) { + switch (node.nodeType) { + case Node.ELEMENT_NODE: { + const el = node as Element; + + return { + kind: "element", + tagName: el.tagName, + attributes: Array.from(el.attributes).map(a => [ + a.namespaceURI, + a.name, + a.value, + ]), + children: Array.from(el.childNodes) + .map(function (this: typeof NodeRegistry, v: Node) { + return NodeRegistry.getId(v); + }.bind(NodeRegistry)), + content: (el as HTMLTemplateElement).content && NodeRegistry.getId((el as HTMLTemplateElement).content), + }; + } + + case Node.TEXT_NODE: + return { + kind: "text", + content: node.nodeValue, + }; + + case Node.DOCUMENT_FRAGMENT_NODE: + const el = node as DocumentFragment; + return { + kind: "document_fragment", + children: Array.from(el.childNodes) + .map(function (this: typeof NodeRegistry, v: Node) { + return NodeRegistry.getId(v); + }.bind(NodeRegistry)), + }; + + default: + return { + kind: "arbitrary", + nodeType: node.nodeType, + }; + } +} + +class MutationDispatcher { + private static observer = new MutationObserver(MutationDispatcher.handle); + + static observe(root: Node) { + this.observer.observe(root, { + subtree: true, + attributes: true, + childList: true, + characterData: true, + }); + } + + private static handle(mutations: MutationRecord[]) { + for (const m of mutations) { + SharedDOM.handleMutation(m); + } + } +} + +export class SharedDOM { + static initOrGet(root: Node) { + if (NodeRegistry.hasNode(root)) return NodeRegistry.getId(root); + this.init(root); + return NodeRegistry.getId(root); + } + + static init(root: Node) { + this.registerSubtree(root); + MutationDispatcher.observe(root); + } + + static registerSubtree(node: Node) { + NodeRegistry.getId(node); + if (node instanceof HTMLTemplateElement) this.registerSubtree(node.content); + node.childNodes.forEach(function (this: typeof SharedDOM, n: Node) { this.registerSubtree(n) }.bind(this)); + + parentIpc.post({ + type: "createNode", + data: { + id: NodeRegistry.getId(node), + payload: serializeNode(node), + events: serializeEvents(node), + }, + }); + } + + static handleMutation(m: MutationRecord) { + const targetId = NodeRegistry.getId(m.target); + + switch (m.type) { + case "attributes": + parentIpc.post({ + type: "changeAttribute", + data: { + target: targetId, + name: m.attributeName, + namespace: m.attributeNamespace, + }, + }); + break; + + case "childList": + if (m.addedNodes.length) { + const ids = Array.from(m.addedNodes, NodeRegistry.getId); + parentIpc.post({ + type: "addNodes", + data: { + target: targetId, + added: ids, + before: m.nextSibling && NodeRegistry.getId(m.nextSibling), + }, + }); + } + + if (m.removedNodes.length) { + const ids = Array.from(m.removedNodes, NodeRegistry.getId); + parentIpc.post({ + type: "removeNodes", + data: { + target: targetId, + removed: ids, + }, + }); + } + break; + + case "characterData": + parentIpc.post({ + type: "characterData", + data: { + target: targetId, + value: m.target.nodeValue, + }, + }); + break; + } + } +} diff --git a/src/modules/.common/classes/withTransfer.ts b/src/modules/.common/classes/withTransfer.ts new file mode 100644 index 0000000..099794f --- /dev/null +++ b/src/modules/.common/classes/withTransfer.ts @@ -0,0 +1,12 @@ + +export class WithTransfer { + data: any; + transfer: any[]; + constructor(data: any, transfer: any[] = []) { + this.data = data; + this.transfer = transfer; + } + clone() { + return new WithTransfer(this.data, this.transfer); + } +} diff --git a/src/modules/.common/index.ts b/src/modules/.common/index.ts new file mode 100644 index 0000000..3265069 --- /dev/null +++ b/src/modules/.common/index.ts @@ -0,0 +1,14 @@ +// NOTES FOR FUTURE USE +// TODO: TURN INTO README +// - Element lifetimes are handled by the client (this, here!) +// Because, them being dereferenced implied it's not ref'able +// through the DOM tree + + +import { OrderedPeer } from "./classes/orderedPeer.js"; +import { patchAllEvents } from "./utils/nodeEventListener.js"; +patchAllEvents(); +OrderedPeer.registerIpcListener(); + +export { Component } from "./classes/component.js"; +export { componentList } from "./classes/componentList.js" diff --git a/src/modules/.common/ipcHandlers/cathodiqueConsumer.ts b/src/modules/.common/ipcHandlers/cathodiqueConsumer.ts new file mode 100644 index 0000000..4f266db --- /dev/null +++ b/src/modules/.common/ipcHandlers/cathodiqueConsumer.ts @@ -0,0 +1,13 @@ +import { KeyedLatch } from "../classes/latch.js"; + +export class CathodiqueConsumerHandler { + instanceReady: KeyedLatch; + + constructor(instanceReady: KeyedLatch) { + this.instanceReady = instanceReady; + } + + async componentRegistered({ data }: { data: { componentName: string } }) { + this.instanceReady.resolve(data.componentName, undefined); + } +}; diff --git a/src/modules/.common/ipcHandlers/cathodiqueProvider.ts b/src/modules/.common/ipcHandlers/cathodiqueProvider.ts new file mode 100644 index 0000000..c44d0f1 --- /dev/null +++ b/src/modules/.common/ipcHandlers/cathodiqueProvider.ts @@ -0,0 +1,59 @@ +import { ComponentList } from "../classes/componentList.js"; +import { SharedDOM } from "../classes/sharedDomRemote.js"; + +export class CathodiqueProviderHandler { + componentList: ComponentList; + + constructor(componentList: ComponentList) { + this.componentList = componentList; + } + + componentInstances = new Map(); + + async createInstance({ data }: { data: { className: string; componentId: string } }) { + // TODO Obj verification + const ClassObj = this.componentList.get(data.className); + if (!ClassObj) return; // Quiet fail + + const componentInstance = new ClassObj(); + + await componentInstance.init(); + + this.componentInstances.set(data.componentId, componentInstance); + return; + } + + getProperty({ data }: { data: { propertyName: string; componentId: string } }) { + const component = this.componentInstances.get(data.componentId); + + const value = component[data.propertyName]; + + if (value instanceof Node) { + const nodeId = SharedDOM.initOrGet(value); + + return { nodeId }; + } + + return { value }; + } + + async callProperty({ data }: { + data: { + methodName: string; + arguments: string[]; + componentId: string; + }; + }) { + const component = this.componentInstances.get(data.componentId); + + const value = await component?.[data.methodName]?.(...data.arguments); + + if (value instanceof Node) { + const nodeId = SharedDOM.initOrGet(value); + + return { nodeId }; + } + + return { value }; + } +}; diff --git a/src/modules/.common/ipcHandlers/cathodiqueRemote.ts b/src/modules/.common/ipcHandlers/cathodiqueRemote.ts new file mode 100644 index 0000000..bb49a2b --- /dev/null +++ b/src/modules/.common/ipcHandlers/cathodiqueRemote.ts @@ -0,0 +1,7 @@ +export class CathodiqueRemoteHandler { + win: WindowProxy; + + constructor(win: WindowProxy) { + this.win = win; + } +} diff --git a/src/modules/.common/ipcHandlers/domRemote.ts b/src/modules/.common/ipcHandlers/domRemote.ts new file mode 100644 index 0000000..cba8af0 --- /dev/null +++ b/src/modules/.common/ipcHandlers/domRemote.ts @@ -0,0 +1,33 @@ +import { NodeRegistry } from "../classes/sharedDomRemote.js"; +import { EventFromIpc } from "../utils/types.js"; + +export class DOMRemoteHandler { + deserializeEvent(evtData: EventFromIpc): Event { + if (!evtData.className.endsWith("Event")) throw new Error("Constructor name must be an event"); + const EventClassObj = globalThis[evtData.className as keyof typeof globalThis] as typeof Event; + + const newValues: Record = {}; + for (const [key, value] of Object.entries(evtData.values)) { + if (value === undefined || ("nodeId" in value && value.nodeId === undefined)) { + continue; + } + + if ("nodeId" in value) { + newValues[key] = NodeRegistry.getNode(value.nodeId); + continue; + } + + newValues[key] = value.value; + } + + return new EventClassObj(evtData.type, newValues); + } + + domEmitEvent({ data }: { data: { id: string, event: EventFromIpc } }) { + const element = NodeRegistry.getNode(data.id); + + if (!element) return console.error(`Tried to emit event ${data.event} to inexistent element ${data.id}`); + + element.dispatchEvent(this.deserializeEvent(data.event)); + } +}; diff --git a/src/modules/.common/parentIpc.ts b/src/modules/.common/parentIpc.ts new file mode 100644 index 0000000..f3eeed6 --- /dev/null +++ b/src/modules/.common/parentIpc.ts @@ -0,0 +1,14 @@ +import { OrderedPeer } from "./classes/orderedPeer.js"; +import { CathodiqueConsumerHandler } from "./ipcHandlers/cathodiqueConsumer.js"; +import { CathodiqueProviderHandler } from "./ipcHandlers/cathodiqueProvider.js"; +import { CathodiqueRemoteHandler } from "./ipcHandlers/cathodiqueRemote.js"; +import { DOMRemoteHandler } from "./ipcHandlers/domRemote.js"; +import { KeyedLatch } from "./classes/latch.js"; +import { componentList } from "./classes/componentList.js"; + +export const parentIpc = new OrderedPeer(window.parent, "*", [ + new CathodiqueConsumerHandler(new KeyedLatch()) as any, + new CathodiqueProviderHandler(componentList) as any, + new CathodiqueRemoteHandler(window.parent) as any, + new DOMRemoteHandler() as any, +]); diff --git a/src/modules/.common/setup.ts b/src/modules/.common/setup.ts new file mode 100644 index 0000000..0ef4d1e --- /dev/null +++ b/src/modules/.common/setup.ts @@ -0,0 +1,2 @@ +import { patchAllEvents } from "./utils/nodeEventListener.js"; +patchAllEvents(); diff --git a/src/modules/.common/utils/nodeEventListener.ts b/src/modules/.common/utils/nodeEventListener.ts new file mode 100644 index 0000000..b972ec8 --- /dev/null +++ b/src/modules/.common/utils/nodeEventListener.ts @@ -0,0 +1,205 @@ +// This file tracks lifetimes of event listeners so the host only forwards the relevant events. +// AI-gen'd. Sorry, too much stuff (capture, once), couldn't be bothered. + +export const handlersMap = new WeakMap< + Node, + Map< + string, + Map<((...args: any[]) => any) | { handleEvent: (...args: any[]) => any }, number> + > +>(); + +export const nodeEventEvents = new EventTarget(); + +class EventAddedEvent extends Event { + addedEvent: string; + target: Node; + + constructor(addedEvent: string, target: Node) { + super('eventAdded'); + this.addedEvent = addedEvent; + this.target = target; + } +} +class EventRemovedEvent extends Event { + removedEvent: string; + target: Node; + + constructor(removedEvent: string, target: Node) { + super('eventRemoved'); + this.removedEvent = removedEvent; + this.target = target; + } +} + +export const getEventsRegisteredOnNode = (node: Node) => [...(handlersMap.get(node)?.keys() ?? [])]; + +function patchNodeEventListeners() { + // Map original listener → wrapped once listener per capture + const onceWrappers = new WeakMap< + ((...args: any[]) => any) | { handleEvent: (...args: any[]) => any }, + [(((...a: any[]) => any) | undefined), (((...a: any[]) => any) | undefined)] + >(); + + /** + * Bitfield explanation: + * 1 → registered with capture: false + * 2 → registered with capture: true + * 3 → registered with both capture values + */ + Node.prototype.addEventListener = function ( + type: string, + listener: ((...args: any[]) => any) | { handleEvent: (...args: any[]) => any }, + options?: boolean | AddEventListenerOptions + ) { + const capture = typeof options === "boolean" ? options : !!options?.capture; + const once = typeof options === "object" && !!options?.once; + + let typeMap = handlersMap.get(this); + if (!typeMap) { + typeMap = new Map(); + handlersMap.set(this, typeMap); + } + + let listeners = typeMap.get(type); + if (!listeners) { + listeners = new Map(); + typeMap.set(type, listeners); + nodeEventEvents.dispatchEvent(new EventAddedEvent(type, this)); + } + + const existingBit = listeners.get(listener) ?? 0; + const bit = capture ? 2 : 1; + + // Update bitfield + listeners.set(listener, existingBit | bit); + + // Once priority: first registration for this capture wins + let effectiveOnce = once; + if (existingBit & bit) effectiveOnce = false; + + let actualListener = listener; + + if (effectiveOnce) { + let captureMap = onceWrappers.get(listener); + if (!captureMap) { + captureMap = [undefined, undefined]; + onceWrappers.set(listener, captureMap); + } + + if (!captureMap[+capture]) { + actualListener = (...args: any[]) => { + try { + if (typeof listener === "function") listener.apply(this, args); + else listener.handleEvent.apply(listener, args); + } finally { + Node.prototype.removeEventListener.call(this, type, listener, capture); + captureMap[+capture] = undefined; + } + }; + captureMap[+capture] = actualListener; + } else { + actualListener = captureMap[+capture]!; // ... + } + } + + return EventTarget.prototype.addEventListener.call(this, type, actualListener as any, options); + }; + + Node.prototype.removeEventListener = function ( + type: string, + listener: ((...args: any[]) => any) | { handleEvent: (...args: any[]) => any }, + options?: boolean | EventListenerOptions + ) { + const capture = typeof options === "boolean" ? options : !!options?.capture; + + let typeMap = handlersMap.get(this); + if (typeMap) { + let listeners = typeMap.get(type); + if (listeners) { + const existingBit = listeners.get(listener); + + if (existingBit) { + const bit = capture ? 2 : 1; + const newBit = existingBit & ~bit; // Remove capture from bitfield + + if (newBit === 0) { + listeners.delete(listener); + if (listeners.size === 0) { + typeMap.delete(type); + + nodeEventEvents.dispatchEvent(new EventRemovedEvent(type, this)); + + if (typeMap.size === 0) { + handlersMap.delete(this); + } + } + } else { + listeners.set(listener, newBit); + } + } + } + } + + const captureMap = onceWrappers.get(listener); + const actualListener = captureMap?.[+capture] ?? listener; + if (captureMap) captureMap[+capture] = undefined; + + return EventTarget.prototype.removeEventListener.call(this, type, actualListener as any, options); + }; +} + +function patchOnEvents() { + // Collect all global constructors inheriting from Node + const globalKeys = Reflect.ownKeys(window) + .filter((v) => typeof v === "string" && v[0] === v[0].toUpperCase()) // class + .filter((v) => window[v as keyof Window]) // from window + .filter((v) => Node.isPrototypeOf(window[v as keyof Window])); // extends Node + const nodeConstructors: Function[] = globalKeys.map((key) => (globalThis as any)[key]); + + for (const ctor of nodeConstructors) { + const proto = ctor.prototype; + const propNames = Object.getOwnPropertyNames(proto); + + for (const prop of propNames) { + if (!prop.startsWith("on")) continue; + + const desc = Object.getOwnPropertyDescriptor(proto, prop); + if (!desc || !desc.configurable) continue; // skip unconfigurable + + let currentListener: ((...args: any[]) => any) | null = null; + + Object.defineProperty(proto, prop, { + configurable: true, + enumerable: true, + get() { + return currentListener; + }, + set(fn: ((...args: any[]) => any) | null) { + console.log(fn) + + const node = this as Node; + + // Remove previous listener + if (currentListener) { + node.removeEventListener(prop.slice(2), currentListener); + const typeMap = handlersMap.get(node)?.get(prop.slice(2)); + if (typeMap) typeMap.delete(currentListener); + } + + currentListener = fn; + + if (fn) { + // Add new listener through patched addEventListener + node.addEventListener(prop.slice(2), fn); + } + } + }); + } + } +} + +export function patchAllEvents() { + patchNodeEventListeners(); + patchOnEvents(); +} diff --git a/src/modules/.common/utils/types.ts b/src/modules/.common/utils/types.ts new file mode 100644 index 0000000..bec276d --- /dev/null +++ b/src/modules/.common/utils/types.ts @@ -0,0 +1,30 @@ +export interface ElementFromIpc { + kind: "element"; + tagName: string; + attributes: [string, string, string][]; + children: string[]; + content?: string; +} + +export interface TextNodeFromIpc { + kind: "text"; + content: string; +} + +export interface DocumentFragmentFromIpc { + kind: "document_fragment"; + children: string[]; +} + +export interface ArbitraryNodeFromIpc { + kind: "arbitrary"; + nodeType: string; +} + +export type NodeFromIpc = ElementFromIpc | TextNodeFromIpc | DocumentFragmentFromIpc | ArbitraryNodeFromIpc; + +export interface EventFromIpc { + className: string; + type: string; + values: Record; +} diff --git a/src/modules/.common/utils/utils.ts b/src/modules/.common/utils/utils.ts new file mode 100644 index 0000000..6d152f0 --- /dev/null +++ b/src/modules/.common/utils/utils.ts @@ -0,0 +1,16 @@ +let alphabet = + "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict"; + +export function nanoid(e = 21) { + let t = "", + r = crypto.getRandomValues(new Uint8Array(e)); + for (let n = 0; n < e; n++) t += alphabet[63 & r[n]]; + return t; +} + +const [projectSubdomain, userSubdomain, ...hostList] = + window.location.hostname.split("."); +export { projectSubdomain, userSubdomain }; +export const host = hostList.join('.') + +export const hostWithoutSubdomain = `${location.protocol}//${hostList.join(".")}:${location.port}`; diff --git a/src/modules/immjs.macos-aqua-windowframe/module.html b/src/modules/immjs.macos-aqua-windowframe/module.html new file mode 100644 index 0000000..6631710 --- /dev/null +++ b/src/modules/immjs.macos-aqua-windowframe/module.html @@ -0,0 +1,35 @@ + + + + + + Test + + + + + + + diff --git a/src/renderer/.common/classes/component.ts b/src/renderer/.common/classes/component.ts new file mode 100644 index 0000000..4202273 --- /dev/null +++ b/src/renderer/.common/classes/component.ts @@ -0,0 +1,31 @@ +import { nanoid } from "../utils/utils.js"; +import { parentIpc } from "../parentIpc.js"; + +export class Component extends EventTarget { + componentHandle: string; + + constructor() { + super(); + const handle = nanoid(); + this.componentHandle = handle; + } + + init() {} + + post(obj: Record) { + return parentIpc.post({ ...obj, componentHandle: this.componentHandle }); + } + rpc(type: string, data: Record, obj: Record = {}) { + return parentIpc.rpc(type, data, { ...obj, componentHandle: this.componentHandle }); + } + + async getDependency(dependency: string) { + return await this.rpc("getDependency", { dependency }); + } + async getOptionalDependency(dependency: string) { + return await this.rpc("getOptionalDependency", { dependency }); + } + async getAllDependency(dependency: string) { + return await this.rpc("getAllDependency", { dependency }); + } +} diff --git a/src/renderer/.common/classes/componentList.ts b/src/renderer/.common/classes/componentList.ts new file mode 100644 index 0000000..bbbd1d5 --- /dev/null +++ b/src/renderer/.common/classes/componentList.ts @@ -0,0 +1,23 @@ +import { parentIpc } from "../parentIpc.js"; +import type { Component } from "./component.js"; + +export class ComponentList extends EventTarget { + componentClasses = new Map(); + + register(componentName: string, componentClass: new (...a: any[]) => Component) { + if (this.componentClasses.has(componentName)) + throw new Error("This component already exists"); + this.componentClasses.set(componentName, componentClass); + + parentIpc.post({ + type: "componentRegistered", + data: { componentName }, + }); + } + + get(componentName: string) { + return this.componentClasses.get(componentName); + } +} + +export const componentList = new ComponentList(); diff --git a/src/renderer/.common/classes/latch.ts b/src/renderer/.common/classes/latch.ts new file mode 100644 index 0000000..666fe03 --- /dev/null +++ b/src/renderer/.common/classes/latch.ts @@ -0,0 +1,147 @@ +enum LatchState { + Pending, + Fulfilled, +} + +export class Latch { + promise: Promise; + resolve: ((v: T) => void) | undefined; + constructor(value?: T) { + if (value) { + this.resolve = undefined; + this.promise = Promise.resolve(value); + } else { + let resultingResolve: (r: T) => void; + this.promise = new Promise((r) => { resultingResolve = r }); + this.resolve = function (this: Latch, r: T) { + resultingResolve(r); + this.resolve = undefined; + }.bind(this); + } + } + + getState() { + if (this.resolve) return LatchState.Pending; + return LatchState.Fulfilled; + } +} + +export class KeyedLatch { + map = new Map>(); + + getStateOf(key: T) { + return this.map.get(key)?.getState() ?? LatchState.Pending; + } + get(key: T) { + if (this.map.has(key)) return this.map.get(key)!.promise; + const latch = new Latch(); + this.map.set(key, latch); + return latch.promise; + } + resolve(key: T, value: U) { + if (!this.map.has(key)) { + this.map.set(key, new Latch(value)); + } + this.map.get(key)!.resolve?.(value); + return value; + } + getOptional(key: T) { + if (this.map.has(key)) return this.get(key); + return undefined; + } + delete(key: T) { + this.map.delete(key); + } +} + +export class WeakKeyedLatch { + map = new WeakMap>(); + + getStateOf(key: T) { + return this.map.get(key)?.getState() ?? LatchState.Pending; + } + get(key: T) { + if (this.map.has(key)) return this.map.get(key)!.promise; + const latch = new Latch(); + this.map.set(key, latch); + return latch.promise; + } + resolve(key: T, value: U) { + if (!this.map.has(key)) { + this.map.set(key, new Latch(value)); + } + this.map.get(key)!.resolve?.(value); + return value; + } + getOptional(key: T) { + if (this.map.has(key)) return this.get(key); + return undefined; + } + delete(key: T) { + this.map.delete(key); + } +} + +export class ConsumableKeyedLatch extends KeyedLatch { + consumed = new Set(); + + consume(key: T) { + const result = super.get(key); + switch (this.getStateOf(key)) { + case LatchState.Pending: + this.consumed.add(key); + break; + case LatchState.Fulfilled: + this.delete(key); + break; + } + return result; + } + /** + * @deprecated For semantic reasons, use consume instead + * Method kept for inheritance + */ + get(key: T) { + return this.consume(key); + } + resolve(key: T, value: U) { + const result = super.resolve(key, value); + if (this.consumed.has(key)) { + this.consumed.delete(key); + this.delete(key); + } + return result; + } +} + +export class ConsumableWeakKeyedLatch extends WeakKeyedLatch { + consumed = new WeakSet(); + + consume(key: T) { + const result = super.get(key); + switch (this.getStateOf(key)) { + case LatchState.Pending: + this.consumed.add(key); + break; + case LatchState.Fulfilled: + this.delete(key); + break; + } + return result; + } + /** + * @deprecated For semantic reasons, use consume instead + * Method kept for inheritance + */ + get(key: T) { + return this.consume(key); + } + resolve(key: T, value: U) { + const result = super.resolve(key, value); + if (this.consumed.has(key)) { + this.consumed.delete(key); + this.delete(key); + } + return result; + } +} diff --git a/src/renderer/.common/classes/orderedPeer.ts b/src/renderer/.common/classes/orderedPeer.ts new file mode 100644 index 0000000..5a0b4d0 --- /dev/null +++ b/src/renderer/.common/classes/orderedPeer.ts @@ -0,0 +1,130 @@ +import { ConsumableKeyedLatch } from "./latch.js"; + +import { nanoid } from "../utils/utils.js"; +import { WithTransfer } from "./withTransfer.js"; + +export class OrderedPeer { + handlers: Record) => any>[]; + + static actualHandlers = new WeakMap void>(); + private static registered = false; + static registerIpcListener() { + if (this.registered) return; + window.addEventListener( + "message", + (evt) => { + const actualHandler = this.actualHandlers.get(evt.source as WindowProxy); + if (!actualHandler) return; + actualHandler(evt); + }, + ); + this.registered = true; + } + + currentOrderSubmission: bigint = 0n; + pendingMessages: any[] = []; + pendingTransfer: Transferable[] = []; + origin: string; + + promiseMap = new ConsumableKeyedLatch(); + + win: WindowProxy; + postMessage: typeof window["postMessage"]; + constructor(win: WindowProxy, origin = "*", handlers: Record any>[]) { + if (OrderedPeer.actualHandlers.has(win)) throw new Error("A window may only admit a single OrderedPeer"); + + this.win = win; + this.postMessage = win.postMessage.bind(win); + this.origin = origin; + this.handlers = handlers; + + OrderedPeer.actualHandlers.set(win, this.orderedDecoder.bind(this)); + } + + post(data: any) { + let transfer = []; + if (data instanceof WithTransfer) { + transfer = data.transfer; + data = data.data; + } + + this.pendingMessages.push(data); + this.pendingTransfer.push(...transfer); + + if (this.pendingMessages.length === 1) { + queueMicrotask(() => { + this.postMessage( + { + messages: this.pendingMessages, + currentOrder: this.currentOrderSubmission, + }, + this.origin, + this.pendingTransfer, + ); + + this.pendingMessages = []; + this.pendingTransfer = []; + this.currentOrderSubmission += 1n; + }); + } + } + + async rpc(type: string, data: any | WithTransfer, obj: Record = {}) { + const promiseId = nanoid(); + this.post({ type, data, promiseId, ...obj }); + const result = await this.promiseMap.consume(promiseId); + + if (!result.error) return result.reply; + throw result.error; + } + + remainingMessages = new Map(); + currentOrderReception: bigint = 0n; + + originMatch(origin: string) { + if (this.origin === '*') return true; + if (this.origin === '/') return origin === window.origin; + return origin === this.origin; + } + + async orderedDecoder(evt: MessageEvent) { + if (!this.originMatch(evt.origin)) return; + + console.log(); + + const { data: { messages, currentOrder } } = evt; + this.remainingMessages.set(currentOrder, messages); + if (this.currentOrderReception === currentOrder) { + do { + await Promise.all( + evt.data.messages.map(async (message: { data: any, type: string, promiseId?: string, componentHandle?: string }) => { + const { type, promiseId } = message; + + if (type === "reply") { + return this.promiseMap.resolve(promiseId!, message); + } + + const handler = this.handlers.find((v) => type in v); + + if (!handler) { + if (promiseId) this.post({ type: "reply", error: new Error("No such function"), promiseId }); + return; + } + + try { + const result = await handler[type](message); + + if (promiseId) { + this.post({ type: "reply", reply: result, promiseId }); + } + } catch (e) { + this.post({ type: "reply", error: e, promiseId }); + } + }) + ); + this.remainingMessages.delete(this.currentOrderReception); + this.currentOrderReception += 1n; + } while (this.remainingMessages.has(this.currentOrderReception)); + } + } +} diff --git a/src/renderer/.common/classes/sharedDomRemote.ts b/src/renderer/.common/classes/sharedDomRemote.ts new file mode 100644 index 0000000..fc7d0ac --- /dev/null +++ b/src/renderer/.common/classes/sharedDomRemote.ts @@ -0,0 +1,174 @@ +import { parentIpc } from "../parentIpc.js"; +import { handlersMap } from "../utils/nodeEventListener.js"; +import { nanoid } from "../utils/utils.js"; + +export class NodeRegistry { + static nodeToId = new WeakMap(); + static idToNode = new Map>(); + + static getNode(id: string) { + return this.idToNode.get(id)?.deref(); + } + + static hasNode(node: Node) { + return this.nodeToId.has(node); + } + + static getId(node: Node): string { + let id = this.nodeToId.get(node); + if (!id) { + id = nanoid(); + this.nodeToId.set(node, id); + this.idToNode.set(id, new WeakRef(node)); + } + return id; + } +} + +function serializeEvents(node: Node) { + return [...(handlersMap.get(node)?.keys() ?? [])]; +} + +function serializeNode(node: Node) { + switch (node.nodeType) { + case Node.ELEMENT_NODE: { + const el = node as Element; + + return { + kind: "element", + tagName: el.tagName, + attributes: Array.from(el.attributes).map(a => [ + a.namespaceURI, + a.name, + a.value, + ]), + children: Array.from(el.childNodes) + .map(function (this: typeof NodeRegistry, v: Node) { + return NodeRegistry.getId(v); + }.bind(NodeRegistry)), + content: (el as HTMLTemplateElement).content && NodeRegistry.getId((el as HTMLTemplateElement).content), + }; + } + + case Node.TEXT_NODE: + return { + kind: "text", + content: node.nodeValue, + }; + + case Node.DOCUMENT_FRAGMENT_NODE: + const el = node as DocumentFragment; + return { + kind: "document_fragment", + children: Array.from(el.childNodes) + .map(function (this: typeof NodeRegistry, v: Node) { + return NodeRegistry.getId(v); + }.bind(NodeRegistry)), + }; + + default: + return { + kind: "arbitrary", + nodeType: node.nodeType, + }; + } +} + +class MutationDispatcher { + private static observer = new MutationObserver(MutationDispatcher.handle); + + static observe(root: Node) { + this.observer.observe(root, { + subtree: true, + attributes: true, + childList: true, + characterData: true, + }); + } + + private static handle(mutations: MutationRecord[]) { + for (const m of mutations) { + SharedDOM.handleMutation(m); + } + } +} + +export class SharedDOM { + static initOrGet(root: Node) { + if (NodeRegistry.hasNode(root)) return NodeRegistry.getId(root); + this.init(root); + return NodeRegistry.getId(root); + } + + static init(root: Node) { + this.registerSubtree(root); + MutationDispatcher.observe(root); + } + + static registerSubtree(node: Node) { + NodeRegistry.getId(node); + if (node instanceof HTMLTemplateElement) this.registerSubtree(node.content); + node.childNodes.forEach(function (this: typeof SharedDOM, n: Node) { this.registerSubtree(n) }.bind(this)); + + parentIpc.post({ + type: "createNode", + data: { + id: NodeRegistry.getId(node), + payload: serializeNode(node), + events: serializeEvents(node), + }, + }); + } + + static handleMutation(m: MutationRecord) { + const targetId = NodeRegistry.getId(m.target); + + switch (m.type) { + case "attributes": + parentIpc.post({ + type: "changeAttribute", + data: { + target: targetId, + name: m.attributeName, + namespace: m.attributeNamespace, + }, + }); + break; + + case "childList": + if (m.addedNodes.length) { + const ids = Array.from(m.addedNodes, NodeRegistry.getId); + parentIpc.post({ + type: "addNodes", + data: { + target: targetId, + added: ids, + before: m.nextSibling && NodeRegistry.getId(m.nextSibling), + }, + }); + } + + if (m.removedNodes.length) { + const ids = Array.from(m.removedNodes, NodeRegistry.getId); + parentIpc.post({ + type: "removeNodes", + data: { + target: targetId, + removed: ids, + }, + }); + } + break; + + case "characterData": + parentIpc.post({ + type: "characterData", + data: { + target: targetId, + value: m.target.nodeValue, + }, + }); + break; + } + } +} diff --git a/src/renderer/.common/classes/withTransfer.ts b/src/renderer/.common/classes/withTransfer.ts new file mode 100644 index 0000000..099794f --- /dev/null +++ b/src/renderer/.common/classes/withTransfer.ts @@ -0,0 +1,12 @@ + +export class WithTransfer { + data: any; + transfer: any[]; + constructor(data: any, transfer: any[] = []) { + this.data = data; + this.transfer = transfer; + } + clone() { + return new WithTransfer(this.data, this.transfer); + } +} diff --git a/src/renderer/.common/index.ts b/src/renderer/.common/index.ts new file mode 100644 index 0000000..3265069 --- /dev/null +++ b/src/renderer/.common/index.ts @@ -0,0 +1,14 @@ +// NOTES FOR FUTURE USE +// TODO: TURN INTO README +// - Element lifetimes are handled by the client (this, here!) +// Because, them being dereferenced implied it's not ref'able +// through the DOM tree + + +import { OrderedPeer } from "./classes/orderedPeer.js"; +import { patchAllEvents } from "./utils/nodeEventListener.js"; +patchAllEvents(); +OrderedPeer.registerIpcListener(); + +export { Component } from "./classes/component.js"; +export { componentList } from "./classes/componentList.js" diff --git a/src/renderer/.common/ipcHandlers/cathodiqueConsumer.ts b/src/renderer/.common/ipcHandlers/cathodiqueConsumer.ts new file mode 100644 index 0000000..4f266db --- /dev/null +++ b/src/renderer/.common/ipcHandlers/cathodiqueConsumer.ts @@ -0,0 +1,13 @@ +import { KeyedLatch } from "../classes/latch.js"; + +export class CathodiqueConsumerHandler { + instanceReady: KeyedLatch; + + constructor(instanceReady: KeyedLatch) { + this.instanceReady = instanceReady; + } + + async componentRegistered({ data }: { data: { componentName: string } }) { + this.instanceReady.resolve(data.componentName, undefined); + } +}; diff --git a/src/renderer/.common/ipcHandlers/cathodiqueProvider.ts b/src/renderer/.common/ipcHandlers/cathodiqueProvider.ts new file mode 100644 index 0000000..c44d0f1 --- /dev/null +++ b/src/renderer/.common/ipcHandlers/cathodiqueProvider.ts @@ -0,0 +1,59 @@ +import { ComponentList } from "../classes/componentList.js"; +import { SharedDOM } from "../classes/sharedDomRemote.js"; + +export class CathodiqueProviderHandler { + componentList: ComponentList; + + constructor(componentList: ComponentList) { + this.componentList = componentList; + } + + componentInstances = new Map(); + + async createInstance({ data }: { data: { className: string; componentId: string } }) { + // TODO Obj verification + const ClassObj = this.componentList.get(data.className); + if (!ClassObj) return; // Quiet fail + + const componentInstance = new ClassObj(); + + await componentInstance.init(); + + this.componentInstances.set(data.componentId, componentInstance); + return; + } + + getProperty({ data }: { data: { propertyName: string; componentId: string } }) { + const component = this.componentInstances.get(data.componentId); + + const value = component[data.propertyName]; + + if (value instanceof Node) { + const nodeId = SharedDOM.initOrGet(value); + + return { nodeId }; + } + + return { value }; + } + + async callProperty({ data }: { + data: { + methodName: string; + arguments: string[]; + componentId: string; + }; + }) { + const component = this.componentInstances.get(data.componentId); + + const value = await component?.[data.methodName]?.(...data.arguments); + + if (value instanceof Node) { + const nodeId = SharedDOM.initOrGet(value); + + return { nodeId }; + } + + return { value }; + } +}; diff --git a/src/renderer/.common/ipcHandlers/cathodiqueRemote.ts b/src/renderer/.common/ipcHandlers/cathodiqueRemote.ts new file mode 100644 index 0000000..bb49a2b --- /dev/null +++ b/src/renderer/.common/ipcHandlers/cathodiqueRemote.ts @@ -0,0 +1,7 @@ +export class CathodiqueRemoteHandler { + win: WindowProxy; + + constructor(win: WindowProxy) { + this.win = win; + } +} diff --git a/src/renderer/.common/ipcHandlers/domRemote.ts b/src/renderer/.common/ipcHandlers/domRemote.ts new file mode 100644 index 0000000..cba8af0 --- /dev/null +++ b/src/renderer/.common/ipcHandlers/domRemote.ts @@ -0,0 +1,33 @@ +import { NodeRegistry } from "../classes/sharedDomRemote.js"; +import { EventFromIpc } from "../utils/types.js"; + +export class DOMRemoteHandler { + deserializeEvent(evtData: EventFromIpc): Event { + if (!evtData.className.endsWith("Event")) throw new Error("Constructor name must be an event"); + const EventClassObj = globalThis[evtData.className as keyof typeof globalThis] as typeof Event; + + const newValues: Record = {}; + for (const [key, value] of Object.entries(evtData.values)) { + if (value === undefined || ("nodeId" in value && value.nodeId === undefined)) { + continue; + } + + if ("nodeId" in value) { + newValues[key] = NodeRegistry.getNode(value.nodeId); + continue; + } + + newValues[key] = value.value; + } + + return new EventClassObj(evtData.type, newValues); + } + + domEmitEvent({ data }: { data: { id: string, event: EventFromIpc } }) { + const element = NodeRegistry.getNode(data.id); + + if (!element) return console.error(`Tried to emit event ${data.event} to inexistent element ${data.id}`); + + element.dispatchEvent(this.deserializeEvent(data.event)); + } +}; diff --git a/src/renderer/.common/parentIpc.ts b/src/renderer/.common/parentIpc.ts new file mode 100644 index 0000000..f3eeed6 --- /dev/null +++ b/src/renderer/.common/parentIpc.ts @@ -0,0 +1,14 @@ +import { OrderedPeer } from "./classes/orderedPeer.js"; +import { CathodiqueConsumerHandler } from "./ipcHandlers/cathodiqueConsumer.js"; +import { CathodiqueProviderHandler } from "./ipcHandlers/cathodiqueProvider.js"; +import { CathodiqueRemoteHandler } from "./ipcHandlers/cathodiqueRemote.js"; +import { DOMRemoteHandler } from "./ipcHandlers/domRemote.js"; +import { KeyedLatch } from "./classes/latch.js"; +import { componentList } from "./classes/componentList.js"; + +export const parentIpc = new OrderedPeer(window.parent, "*", [ + new CathodiqueConsumerHandler(new KeyedLatch()) as any, + new CathodiqueProviderHandler(componentList) as any, + new CathodiqueRemoteHandler(window.parent) as any, + new DOMRemoteHandler() as any, +]); diff --git a/src/renderer/.common/setup.ts b/src/renderer/.common/setup.ts new file mode 100644 index 0000000..0ef4d1e --- /dev/null +++ b/src/renderer/.common/setup.ts @@ -0,0 +1,2 @@ +import { patchAllEvents } from "./utils/nodeEventListener.js"; +patchAllEvents(); diff --git a/src/renderer/.common/utils/nodeEventListener.ts b/src/renderer/.common/utils/nodeEventListener.ts new file mode 100644 index 0000000..b972ec8 --- /dev/null +++ b/src/renderer/.common/utils/nodeEventListener.ts @@ -0,0 +1,205 @@ +// This file tracks lifetimes of event listeners so the host only forwards the relevant events. +// AI-gen'd. Sorry, too much stuff (capture, once), couldn't be bothered. + +export const handlersMap = new WeakMap< + Node, + Map< + string, + Map<((...args: any[]) => any) | { handleEvent: (...args: any[]) => any }, number> + > +>(); + +export const nodeEventEvents = new EventTarget(); + +class EventAddedEvent extends Event { + addedEvent: string; + target: Node; + + constructor(addedEvent: string, target: Node) { + super('eventAdded'); + this.addedEvent = addedEvent; + this.target = target; + } +} +class EventRemovedEvent extends Event { + removedEvent: string; + target: Node; + + constructor(removedEvent: string, target: Node) { + super('eventRemoved'); + this.removedEvent = removedEvent; + this.target = target; + } +} + +export const getEventsRegisteredOnNode = (node: Node) => [...(handlersMap.get(node)?.keys() ?? [])]; + +function patchNodeEventListeners() { + // Map original listener → wrapped once listener per capture + const onceWrappers = new WeakMap< + ((...args: any[]) => any) | { handleEvent: (...args: any[]) => any }, + [(((...a: any[]) => any) | undefined), (((...a: any[]) => any) | undefined)] + >(); + + /** + * Bitfield explanation: + * 1 → registered with capture: false + * 2 → registered with capture: true + * 3 → registered with both capture values + */ + Node.prototype.addEventListener = function ( + type: string, + listener: ((...args: any[]) => any) | { handleEvent: (...args: any[]) => any }, + options?: boolean | AddEventListenerOptions + ) { + const capture = typeof options === "boolean" ? options : !!options?.capture; + const once = typeof options === "object" && !!options?.once; + + let typeMap = handlersMap.get(this); + if (!typeMap) { + typeMap = new Map(); + handlersMap.set(this, typeMap); + } + + let listeners = typeMap.get(type); + if (!listeners) { + listeners = new Map(); + typeMap.set(type, listeners); + nodeEventEvents.dispatchEvent(new EventAddedEvent(type, this)); + } + + const existingBit = listeners.get(listener) ?? 0; + const bit = capture ? 2 : 1; + + // Update bitfield + listeners.set(listener, existingBit | bit); + + // Once priority: first registration for this capture wins + let effectiveOnce = once; + if (existingBit & bit) effectiveOnce = false; + + let actualListener = listener; + + if (effectiveOnce) { + let captureMap = onceWrappers.get(listener); + if (!captureMap) { + captureMap = [undefined, undefined]; + onceWrappers.set(listener, captureMap); + } + + if (!captureMap[+capture]) { + actualListener = (...args: any[]) => { + try { + if (typeof listener === "function") listener.apply(this, args); + else listener.handleEvent.apply(listener, args); + } finally { + Node.prototype.removeEventListener.call(this, type, listener, capture); + captureMap[+capture] = undefined; + } + }; + captureMap[+capture] = actualListener; + } else { + actualListener = captureMap[+capture]!; // ... + } + } + + return EventTarget.prototype.addEventListener.call(this, type, actualListener as any, options); + }; + + Node.prototype.removeEventListener = function ( + type: string, + listener: ((...args: any[]) => any) | { handleEvent: (...args: any[]) => any }, + options?: boolean | EventListenerOptions + ) { + const capture = typeof options === "boolean" ? options : !!options?.capture; + + let typeMap = handlersMap.get(this); + if (typeMap) { + let listeners = typeMap.get(type); + if (listeners) { + const existingBit = listeners.get(listener); + + if (existingBit) { + const bit = capture ? 2 : 1; + const newBit = existingBit & ~bit; // Remove capture from bitfield + + if (newBit === 0) { + listeners.delete(listener); + if (listeners.size === 0) { + typeMap.delete(type); + + nodeEventEvents.dispatchEvent(new EventRemovedEvent(type, this)); + + if (typeMap.size === 0) { + handlersMap.delete(this); + } + } + } else { + listeners.set(listener, newBit); + } + } + } + } + + const captureMap = onceWrappers.get(listener); + const actualListener = captureMap?.[+capture] ?? listener; + if (captureMap) captureMap[+capture] = undefined; + + return EventTarget.prototype.removeEventListener.call(this, type, actualListener as any, options); + }; +} + +function patchOnEvents() { + // Collect all global constructors inheriting from Node + const globalKeys = Reflect.ownKeys(window) + .filter((v) => typeof v === "string" && v[0] === v[0].toUpperCase()) // class + .filter((v) => window[v as keyof Window]) // from window + .filter((v) => Node.isPrototypeOf(window[v as keyof Window])); // extends Node + const nodeConstructors: Function[] = globalKeys.map((key) => (globalThis as any)[key]); + + for (const ctor of nodeConstructors) { + const proto = ctor.prototype; + const propNames = Object.getOwnPropertyNames(proto); + + for (const prop of propNames) { + if (!prop.startsWith("on")) continue; + + const desc = Object.getOwnPropertyDescriptor(proto, prop); + if (!desc || !desc.configurable) continue; // skip unconfigurable + + let currentListener: ((...args: any[]) => any) | null = null; + + Object.defineProperty(proto, prop, { + configurable: true, + enumerable: true, + get() { + return currentListener; + }, + set(fn: ((...args: any[]) => any) | null) { + console.log(fn) + + const node = this as Node; + + // Remove previous listener + if (currentListener) { + node.removeEventListener(prop.slice(2), currentListener); + const typeMap = handlersMap.get(node)?.get(prop.slice(2)); + if (typeMap) typeMap.delete(currentListener); + } + + currentListener = fn; + + if (fn) { + // Add new listener through patched addEventListener + node.addEventListener(prop.slice(2), fn); + } + } + }); + } + } +} + +export function patchAllEvents() { + patchNodeEventListeners(); + patchOnEvents(); +} diff --git a/src/renderer/.common/utils/types.ts b/src/renderer/.common/utils/types.ts new file mode 100644 index 0000000..bec276d --- /dev/null +++ b/src/renderer/.common/utils/types.ts @@ -0,0 +1,30 @@ +export interface ElementFromIpc { + kind: "element"; + tagName: string; + attributes: [string, string, string][]; + children: string[]; + content?: string; +} + +export interface TextNodeFromIpc { + kind: "text"; + content: string; +} + +export interface DocumentFragmentFromIpc { + kind: "document_fragment"; + children: string[]; +} + +export interface ArbitraryNodeFromIpc { + kind: "arbitrary"; + nodeType: string; +} + +export type NodeFromIpc = ElementFromIpc | TextNodeFromIpc | DocumentFragmentFromIpc | ArbitraryNodeFromIpc; + +export interface EventFromIpc { + className: string; + type: string; + values: Record; +} diff --git a/src/renderer/.common/utils/utils.ts b/src/renderer/.common/utils/utils.ts new file mode 100644 index 0000000..6d152f0 --- /dev/null +++ b/src/renderer/.common/utils/utils.ts @@ -0,0 +1,16 @@ +let alphabet = + "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict"; + +export function nanoid(e = 21) { + let t = "", + r = crypto.getRandomValues(new Uint8Array(e)); + for (let n = 0; n < e; n++) t += alphabet[63 & r[n]]; + return t; +} + +const [projectSubdomain, userSubdomain, ...hostList] = + window.location.hostname.split("."); +export { projectSubdomain, userSubdomain }; +export const host = hostList.join('.') + +export const hostWithoutSubdomain = `${location.protocol}//${hostList.join(".")}:${location.port}`; diff --git a/src/renderer/classes/handlers/dom/base.ts b/src/renderer/classes/handlers/dom/base.ts new file mode 100644 index 0000000..cf46c0f --- /dev/null +++ b/src/renderer/classes/handlers/dom/base.ts @@ -0,0 +1,21 @@ +import { BaseObject } from "@cathodique/wl-serv-high/dist/objects/base_object.js"; + +export class BaseDom { + wl: From; + dom: To; + constructor(wl: From, dom: To) { + this.wl = wl; + this.dom = dom; + } + + unmount: (() => any)[] = []; + onUnmount(f: (this: this) => any) { + this.unmount.push(f.bind(this)); + } + + destroy() { + type yo = this; + + this.unmount.forEach(function (this: yo, v: typeof this.unmount[number]) { v.bind(this)(); }.bind(this)); + } +} diff --git a/src/renderer/classes/handlers/dom/popup.ts b/src/renderer/classes/handlers/dom/popup.ts new file mode 100644 index 0000000..bc1e588 --- /dev/null +++ b/src/renderer/classes/handlers/dom/popup.ts @@ -0,0 +1,18 @@ +import { BaseDom } from "./base.js"; +import { XdgPopup } from "@cathodique/wl-serv-high/dist/objects/xdg_popup.js"; + +// Toplevels: context for other subsurfaces to appear in +// Popups are the same +// TODO: (to reconsider) Should Toplevels and Popups have a mother class? +export class PopupDom extends BaseDom { + static wlToPopupDom = new Map(); + + constructor(wl: XdgPopup) { + super(wl, document.createElement('div')); + PopupDom.wlToPopupDom.set(wl, this); + } + + async init() { + + } +} diff --git a/src/renderer/classes/handlers/dom/subsurface.ts b/src/renderer/classes/handlers/dom/subsurface.ts new file mode 100644 index 0000000..9f1bfa2 --- /dev/null +++ b/src/renderer/classes/handlers/dom/subsurface.ts @@ -0,0 +1,81 @@ +import { WlSubsurface } from "@cathodique/wl-serv-high/dist/objects/wl_subsurface.js"; +import { BaseDom } from "./base.js"; +import { SurfaceDom } from "./surface.js"; +import { WlSurface } from "@cathodique/wl-serv-high/dist/objects/wl_surface.js"; + +export class SubsurfaceDom extends BaseDom { + static wlToSubsurfaceDom = new Map(); + + constructor(wl: WlSubsurface) { + super(wl, document.createElement("div")); + SubsurfaceDom.wlToSubsurfaceDom.set(wl, this); + this.init(); + } + + get kid() { + const kidWl = this.wl.meta.surface; + const kidDom = SurfaceDom.wlToSurfaceDom.get(kidWl); + + if (!kidDom) throw new Error("DOM of surface does not exist"); + return kidDom; + } + + init () { + this.dom.append(this.kid.dom); + + // Subsurface shenanigans + // TODO: Apply on commit + this.wl.on("wlPlaceAbove", function (this: SubsurfaceDom, { sibling: other }: { sibling: WlSurface }) { + switch (this.wl.getRelationWith(other)) { + case "sibling": { + const sibling = SurfaceDom.wlToSurfaceDom.get(other)!; + const commonParentWl = other.subsurface!.meta.parent; + const commonParent = SurfaceDom.wlToSurfaceDom.get(commonParentWl)!; + + commonParent.dom.insertBefore(this.dom, sibling.dom); + break; + } + case "parent": { + const parent = SurfaceDom.wlToSurfaceDom.get(other)!; + + const parentSubsurface = SubsurfaceDom.wlToSubsurfaceDom.get(other.subsurface!)!; + + parentSubsurface.dom.insertBefore(this.dom, parent.dom); + break; + } + default: + // Already handled by wl-serv-high + } + }.bind(this)); + + this.wl.on("wlPlaceBelow", function (this: SubsurfaceDom, { sibling: other }: { sibling: WlSurface }) { + switch (this.wl.getRelationWith(other)) { + case "sibling": { + const sibling = SurfaceDom.wlToSurfaceDom.get(other)!; + const commonParentWl = other.subsurface!.meta.parent; + const commonParent = SurfaceDom.wlToSurfaceDom.get(commonParentWl)!; + + commonParent.dom.insertBefore(this.dom, sibling.dom.nextSibling); + break; + } + case "parent": { + const parent = SurfaceDom.wlToSurfaceDom.get(other)!; + + const parentSubsurface = SubsurfaceDom.wlToSubsurfaceDom.get(other.subsurface!)!; + + parentSubsurface.dom.insertBefore(this.dom, parent.dom.nextSibling); + break; + } + default: + // Already handled by wl-serv-high + } + }.bind(this)); + + this.wl.on('wlSetPosition', function (this: SubsurfaceDom, { y, x }: { y: number, x: number }) { + const surface = SurfaceDom.wlToSurfaceDom.get(this.wl.meta.surface)!; + + surface.dom.style.top = `${y}px`; + surface.dom.style.left = `${x}px`; + }.bind(this)); + } +} diff --git a/src/renderer/classes/handlers/dom/surface.ts b/src/renderer/classes/handlers/dom/surface.ts new file mode 100644 index 0000000..b0efa01 --- /dev/null +++ b/src/renderer/classes/handlers/dom/surface.ts @@ -0,0 +1,94 @@ +import { WlSurface } from "@cathodique/wl-serv-high/dist/objects/wl_surface.js"; +import { Seat } from "../../wayland/seat/seat.js"; +import { BaseDom } from "./base.js"; +import { Output } from "../../wayland/output/output.js"; + +export class SurfaceDom extends BaseDom { + static wlToSurfaceDom = new Map(); + + ctx: CanvasRenderingContext2D; + constructor(wl: WlSurface) { + super(wl, document.createElement("canvas")); + SurfaceDom.wlToSurfaceDom.set(wl, this); + + const ctx = this.dom.getContext("2d"); + if (!ctx) + throw new Error( + "Failed to derive 2d context from canvas element; is anything disabled?", + ); + this.ctx = ctx; + } + + shownOnOutputs = new Set(); + + init() { + let lastDimensions: [number, number] = [-Infinity, -Infinity]; + const commitHandler = async function (this: SurfaceDom) { + const b = this.wl.buffer.current; + + if (b === null) this.dom.style.display = "none"; + if (b == null) return; + + if (lastDimensions[0] !== b.meta.height || lastDimensions[1] !== b.meta.width) { + this.dom.width = b.meta.width; + this.dom.height = b.meta.height; + lastDimensions = [b.meta.height, b.meta.width]; + } + + const currlyDamagedBuffer = this.wl.getCurrlyDammagedBuffer(); + + for (const rect of currlyDamagedBuffer) { + b.updateBufferArea(rect.y, rect.x, rect.h, rect.w) + } + const arr = new Uint8ClampedArray( + b.buffer.buffer, + 0, + b.meta.width * b.meta.height * 4, + ); + if (arr.length > 0) { + // Idk what the f*** tsgo is doing here.... anyways + // @ts-ignore + let imageData = new ImageData(arr, b.meta.width, b.meta.height); + + for (const rect of currlyDamagedBuffer) { + this.ctx.putImageData(imageData, 0, 0, rect.x, rect.y, rect.w, rect.h); + } + } + }.bind(this); + + commitHandler(); + this.wl.on("update", () => commitHandler()); + + this.wl.once("beforeWlDestroy", () => { + // Unsure vvv + this.dom.remove(); + }); + } + + initSeatMouse(seat: Seat) { + // TODO(multiparty): Single E.L. for each seat; + + const noForceLeave = (e: MouseEvent) => seat.move(e, this); + this.dom.addEventListener("mouseenter", noForceLeave); + this.onUnmount(function () { this.dom.removeEventListener("mouseenter", noForceLeave) }); + this.dom.addEventListener("mousemove", noForceLeave); + this.onUnmount(function () { this.dom.removeEventListener("mousemove", noForceLeave) }); + + const forceLeave = (e: MouseEvent) => seat.move(e, this, true); + this.onUnmount(function () { this.dom.removeEventListener("mouseleave", forceLeave) }); + + const mouseDown = (evt: MouseEvent) => { + if (seat.mouseFocus) + seat.mouseFocus.instances.buttonDown(Seat.mouseWebToButtonMap[evt.button]); + }; + this.dom.addEventListener("mousedown", mouseDown); + this.onUnmount(function () { this.dom.removeEventListener("mousedown", mouseDown) }); + + const mouseUp = (evt: MouseEvent) => { + if (seat.mouseFocus) + seat.mouseFocus.instances.buttonUp(Seat.mouseWebToButtonMap[evt.button]); + } + this.dom.addEventListener("mouseup", mouseUp); + this.onUnmount(function () { this.dom.removeEventListener("mouseup", mouseUp) }); + } +} diff --git a/src/renderer/classes/handlers/dom/toplevel.ts b/src/renderer/classes/handlers/dom/toplevel.ts new file mode 100644 index 0000000..20b3322 --- /dev/null +++ b/src/renderer/classes/handlers/dom/toplevel.ts @@ -0,0 +1,30 @@ +import { XdgToplevel } from "@cathodique/wl-serv-high/dist/objects/xdg_toplevel.js"; +import { BaseDom } from "./base.js"; +import { orchestrator } from "../../../host/index.js"; +// import { Module } from "../../../host/classes/module.js"; + +export class ToplevelDom extends BaseDom { + static wlToToplevelDom = new Map(); + + instance: Record & ((...args: any[]) => Promise)>; + constructor(wl: XdgToplevel) { + super(wl, document.createElement("div")); + ToplevelDom.wlToToplevelDom.set(wl, this); + + this.instance = orchestrator.load("WindowFrame").createInstance("WindowFrame"); + + this.init(); + } + + async init () { + this.instance.rpcSetGeometry(this.wl.parent.geometry.current); + this.wl.parent.geometry.on('current', function (this: ToplevelDom) { + this.instance.rpcSetGeometry(this.wl.parent.geometry.current); + }.bind(this)); + + this.instance.rpcSetTitle(this.wl.title); + this.wl.on('wlSetTitle', function (this: ToplevelDom) { + this.instance.rpcSetTitle(this.wl.title); + }.bind(this)); + } +} diff --git a/src/renderer/classes/handlers/handlers.ts b/src/renderer/classes/handlers/handlers.ts new file mode 100644 index 0000000..42f453e --- /dev/null +++ b/src/renderer/classes/handlers/handlers.ts @@ -0,0 +1,10 @@ +import { WlSurface } from "@cathodique/wl-serv-high/dist/objects/wl_surface.js"; +import { PopupDom } from "./dom/popup.js"; +import { ToplevelDom } from "./dom/toplevel.js"; + +export const objectHandlers = { + 'xdg_popup': PopupDom, + 'xdg_toplevel': ToplevelDom, + 'wl_surface': WlSurface, + 'wl_output': WlSurface, +}; diff --git a/src/renderer/classes/handlers/lib/toplevel_decoration_manager.ts b/src/renderer/classes/handlers/lib/toplevel_decoration_manager.ts new file mode 100644 index 0000000..088fdb7 --- /dev/null +++ b/src/renderer/classes/handlers/lib/toplevel_decoration_manager.ts @@ -0,0 +1,13 @@ +import { ZxdgToplevelDecorationV1 } from "@cathodique/wl-serv-high/dist/objects/zxdg_decoration_manager_v1.js"; + +export class ZxdgToplevelDecorationManager { + wl: ZxdgToplevelDecorationV1; + constructor(wl: ZxdgToplevelDecorationV1) { + this.wl = wl; + + wl.on('wlSetMode', () => { + wl.sendToplevelDecoration('server_side'); + }); + wl.sendToplevelDecoration('server_side'); + } +} diff --git a/src/renderer/classes/wayland/output/output.ts b/src/renderer/classes/wayland/output/output.ts new file mode 100644 index 0000000..aebd27a --- /dev/null +++ b/src/renderer/classes/wayland/output/output.ts @@ -0,0 +1,30 @@ +import { OutputConfiguration } from "@cathodique/wl-serv-high/dist/objects/wl_output.js"; +import { OutputRegistry } from "@cathodique/wl-serv-high/dist/registries/output.js"; + +export class Output { + configToOutput = new Map(); + + wlOutputReg: OutputRegistry; + config: OutputConfiguration; + dom = document.createElement("div"); + get wlOutputAuth() { + const result = this.wlOutputReg.get(this.config); + if (!result) throw new Error(); + return result; + } + + constructor(config: OutputConfiguration, seatReg: OutputRegistry) { + this.wlOutputReg = seatReg; + this.config = config; + + this.initOutput(); + } + + initOutput() { + this.dom.style.position = "absolute"; + this.dom.style.top = `${this.config.x}px`; + this.dom.style.left = `${this.config.y}px`; + this.dom.style.width = `${this.config.w}px`; + this.dom.style.height = `${this.config.h}px`; + } +} diff --git a/src/renderer/classes/wayland/seat/codeToScancode.ts b/src/renderer/classes/wayland/seat/codeToScancode.ts new file mode 100644 index 0000000..22ee4e5 --- /dev/null +++ b/src/renderer/classes/wayland/seat/codeToScancode.ts @@ -0,0 +1,166 @@ +// TODO: Find a much better way.... + +export const codeToScan: Record = { + "Escape": 0x0009, + "Digit1": 0x000A, + "Digit2": 0x000B, + "Digit3": 0x000C, + "Digit4": 0x000D, + "Digit5": 0x000E, + "Digit6": 0x000F, + "Digit7": 0x0010, + "Digit8": 0x0011, + "Digit9": 0x0012, + "Digit0": 0x0013, + "Minus": 0x0014, + "Equal": 0x0015, + "Backspace": 0x0016, + "Tab": 0x0017, + "KeyQ": 0x0018, + "KeyW": 0x0019, + "KeyE": 0x001A, + "KeyR": 0x001B, + "KeyT": 0x001C, + "KeyY": 0x001D, + "KeyU": 0x001E, + "KeyI": 0x001F, + "KeyO": 0x0020, + "KeyP": 0x0021, + "BracketLeft": 0x0022, + "BracketRight": 0x0023, + "Enter": 0x0024, + "ControlLeft": 0x0025, + "KeyA": 0x0026, + "KeyS": 0x0027, + "KeyD": 0x0028, + "KeyF": 0x0029, + "KeyG": 0x002A, + "KeyH": 0x002B, + "KeyJ": 0x002C, + "KeyK": 0x002D, + "KeyL": 0x002E, + "Semicolon": 0x002F, + "Quote": 0x0030, + "Backquote": 0x0031, + "ShiftLeft": 0x0032, + "Backslash": 0x0033, + "KeyZ": 0x0034, + "KeyX": 0x0035, + "KeyC": 0x0036, + "KeyV": 0x0037, + "KeyB": 0x0038, + "KeyN": 0x0039, + "KeyM": 0x003A, + "Comma": 0x003B, + "Period": 0x003C, + "Slash": 0x003D, + "ShiftRight": 0x003E, + "NumpadMultiply": 0x003F, + "AltLeft": 0x0040, + "Space": 0x0041, + "CapsLock": 0x0042, + "F1": 0x0043, + "F2": 0x0044, + "F3": 0x0045, + "F4": 0x0046, + "F5": 0x0047, + "F6": 0x0048, + "F7": 0x0049, + "F8": 0x004A, + "F9": 0x004B, + "F10": 0x004C, + "NumLock": 0x004D, + "ScrollLock": 0x004E, + "Numpad7": 0x004F, + "Numpad8": 0x0050, + "Numpad9": 0x0051, + "NumpadSubtract": 0x0052, + "Numpad4": 0x0053, + "Numpad5": 0x0054, + "Numpad6": 0x0055, + "NumpadAdd": 0x0056, + "Numpad1": 0x0057, + "Numpad2": 0x0058, + "Numpad3": 0x0059, + "Numpad0": 0x005A, + "NumpadDecimal": 0x005B, + "Lang5": 0x005D, + "IntlBackslash": 0x005E, + "F11": 0x005F, + "F12": 0x0060, + "IntlRo": 0x0061, + "Lang3": 0x0062, + "Lang4": 0x0063, + "Convert": 0x0064, + "KanaMode": 0x0065, + "NonConvert": 0x0066, + "NumpadEnter": 0x0068, + "ControlRight": 0x0069, + "NumpadDivide": 0x006A, + "PrintScreen": 0x006B, + "AltRight": 0x006C, + "Home": 0x006E, + "ArrowUp": 0x006F, + "PageUp": 0x0070, + "ArrowLeft": 0x0071, + "ArrowRight": 0x0072, + "End": 0x0073, + "ArrowDown": 0x0074, + "PageDown": 0x0075, + "Insert": 0x0076, + "Delete": 0x0077, + "AudioVolumeMute": 0x0079, + "AudioVolumeDown": 0x007A, + "AudioVolumeUp": 0x007B, + "Power": 0x007C, + "NumpadEqual": 0x007D, + "Pause": 0x007F, + "NumpadComma": 0x0081, + "Lang1": 0x0082, + "Lang2": 0x0083, + "IntlYen": 0x0084, + "MetaLeft": 0x0085, + "MetaRight": 0x0086, + "ContextMenu": 0x0087, + "BrowserStop": 0x0088, + "Again": 0x0089, + "Undo": 0x008B, + "Select": 0x008C, + "Copy": 0x008D, + "Open": 0x008E, + "Paste": 0x008F, + "Find": 0x0090, + "Cut": 0x0091, + "Help": 0x0092, + "LaunchApp2": 0x0094, + "Sleep": 0x0096, + "WakeUp": 0x0097, + "LaunchApp1": 0x0098, + "LaunchMail": 0x00A3, + "BrowserFavorites": 0x00A4, + "BrowserBack": 0x00A6, + "BrowserForward": 0x00A7, + "Eject": 0x00A9, + "MediaTrackNext": 0x00AB, + "MediaPlayPause": 0x00AC, + "MediaTrackPrevious": 0x00AD, + "MediaStop": 0x00AE, + "MediaSelect": 0x00B3, + "BrowserHome": 0x00B4, + "BrowserRefresh": 0x00B5, + "NumpadParenLeft": 0x00BB, + "NumpadParenRight": 0x00BC, + "F13": 0x00BF, + "F14": 0x00C0, + "F15": 0x00C1, + "F16": 0x00C2, + "F17": 0x00C3, + "F18": 0x00C4, + "F19": 0x00C5, + "F20": 0x00C6, + "F21": 0x00C7, + "F22": 0x00C8, + "F23": 0x00C9, + "F24": 0x00CA, + "BrowserSearch": 0x00E1, +}; diff --git a/src/renderer/classes/wayland/seat/modifiers.ts b/src/renderer/classes/wayland/seat/modifiers.ts new file mode 100644 index 0000000..6fd17aa --- /dev/null +++ b/src/renderer/classes/wayland/seat/modifiers.ts @@ -0,0 +1,75 @@ +import { HLConnection } from "@cathodique/wl-serv-high"; +import { Seat } from "./seat.js"; + +const knownMods = ["Shift", "Lock", "Control", "Mod1", "Mod2", "Mod3", "Mod4", "Mod5"] as const; +export class Modifiers { + depressed = Object.fromEntries(knownMods.map((v) => [v, false])) as Record; + depressedBitmask = 0; + latched = Object.fromEntries(knownMods.map((v) => [v, false])) as Record; + latchedBitmask = 0; + locked = Object.fromEntries(knownMods.map((v) => [v, false])) as Record; + lockedBitmask = 0; + + group = 0; + + seat: Seat; + + constructor(seat: Seat) { + this.seat = seat; + } + + updateAccordingly(evt: KeyboardEvent | MouseEvent) { + let changed = { depressed: false, latched: false, locked: false }; + function checkIfChangedAndUpdate(origin: Record, modifier: typeof knownMods[number], value: boolean) { + if (origin[modifier] === value) return false; + origin[modifier] = value; + return true; + } + // Shift: "Shift" + changed.depressed ||= checkIfChangedAndUpdate(this.depressed, "Shift", evt.getModifierState("Shift")); + // Lock: "CapsLock" + changed.locked ||= checkIfChangedAndUpdate(this.locked, "Lock", evt.getModifierState("CapsLock")); + if (evt instanceof KeyboardEvent) changed.depressed ||= checkIfChangedAndUpdate(this.depressed, "Lock", evt.type === "keydown" && evt.key === "CapsLock"); + // Control: "Control" + changed.depressed ||= checkIfChangedAndUpdate(this.depressed, "Control", evt.getModifierState("Control")); + // Mod1: "Alt" + changed.depressed ||= checkIfChangedAndUpdate(this.depressed, "Mod1", evt.getModifierState("Alt")); + // Mod2: "NumLock" + changed.depressed ||= checkIfChangedAndUpdate(this.depressed, "Mod2", evt.getModifierState("NumLock")); + // Mod3: "Hyper" (No Level 5 in browser spec) + changed.depressed ||= checkIfChangedAndUpdate(this.depressed, "Mod3", evt.getModifierState("Hyper")); + // Mod4: "Meta" + changed.depressed ||= checkIfChangedAndUpdate(this.depressed, "Mod4", evt.getModifierState("Meta")); + // Mod5: "AltGraph" + changed.depressed ||= checkIfChangedAndUpdate(this.depressed, "Mod5", evt.getModifierState("AltGraph")); + + return changed; + } + + static createMask(object: Record) { + let result = 0; + for (let modIdx = 0; modIdx < knownMods.length; modIdx += 1) { + const mask = 2 ** modIdx; + if (object[knownMods[modIdx]]) result += mask; + } + + return result; + } + + update(connection: HLConnection, serial?: number) { + const authority = this.seat.wlSeatAuth.get(connection)!; + + authority.modifiers(this.depressedBitmask, this.latchedBitmask, this.lockedBitmask, this.group, serial); + } + + ifUpdateThenEmit(evt: KeyboardEvent | MouseEvent, connection: HLConnection) { + const xWasUpdated = this.updateAccordingly(evt); + if (xWasUpdated.depressed || xWasUpdated.latched || xWasUpdated.locked) { + if (xWasUpdated.depressed) this.depressedBitmask = Modifiers.createMask(this.depressed); + if (xWasUpdated.latched) this.latchedBitmask = Modifiers.createMask(this.latched); + if (xWasUpdated.locked) this.lockedBitmask = Modifiers.createMask(this.locked); + + this.update(connection); + } + } +} diff --git a/src/renderer/classes/wayland/seat/seat.ts b/src/renderer/classes/wayland/seat/seat.ts new file mode 100644 index 0000000..9ebfc69 --- /dev/null +++ b/src/renderer/classes/wayland/seat/seat.ts @@ -0,0 +1,109 @@ +import { SeatAuthority, SeatInstances, SeatRegistry } from "@cathodique/wl-serv-high/dist/registries/seat.js"; +import { Modifiers } from "./modifiers.js"; +import { codeToScan } from "./codeToScancode.js"; +import { SurfaceDom } from "../../handlers/dom/surface.js"; +import { isInRegion } from "../../../wayland/index.js"; +import { SeatConfiguration } from "@cathodique/wl-serv-high/dist/objects/wl_seat.js"; + +export class Seat { + static mouseWebToButtonMap: Record = { + 0: 0x110, + 1: 0x112, + 2: 0x111, + 3: 0x116, + 4: 0x115, + }; + + wlSeatReg: SeatRegistry; + config: SeatConfiguration; + get wlSeatAuth() { + const result = this.wlSeatReg.get(this.config); + if (!result) throw new Error(); + return result; + } + + keyboardFocus?: { instances: SeatInstances, surface: SurfaceDom }; + + mouseFocus?: { instances: SeatInstances, surface: SurfaceDom }; + + modifiers: Modifiers; + constructor(config: SeatConfiguration, seatReg: SeatRegistry) { + this.wlSeatReg = seatReg; + this.config = config; + this.modifiers = new Modifiers(this); + + this.initKeydown(); + this.initKeyup(); + } + + initKeyup() { + document.body.addEventListener("keyup", function (this: Seat, v: KeyboardEvent) { + if (!this.keyboardFocus) { + this.modifiers.updateAccordingly(v); + return; + } + v.preventDefault(); + this.modifiers.ifUpdateThenEmit(v, this.keyboardFocus.instances.connection); + + const isInMap = (code: string): code is keyof typeof codeToScan => + code in codeToScan; + if (!isInMap(v.code)) return; + + const scancode = codeToScan[v.code]; + + this.keyboardFocus.instances.keyUp(scancode); + }.bind(this)); + } + + initKeydown() { + document.body.addEventListener("keydown", (v) => { + if (!this.keyboardFocus) { + this.modifiers.updateAccordingly(v); + return; + } + v.preventDefault(); + this.modifiers.ifUpdateThenEmit(v, this.keyboardFocus.instances.connection); + + const isInMap = (code: string): code is keyof typeof codeToScan => + code in codeToScan; + if (!isInMap(v.code)) return; + + const scancode = codeToScan[v.code]; + + this.keyboardFocus.instances.keyDown(scancode); + }); + } + + move (evt: MouseEvent, surface: SurfaceDom, forceLeave?: boolean) { + // (obj.xdgSurface?.parent as XdgWmBase)?.addCommand("ping", { + // serial: obj.connection.time.getTime(), + // }); + // We'll see abt that later + + const containerPos = surface.dom.getBoundingClientRect(); + + const mouseY = evt.clientY - containerPos.top; + const mouseX = evt.clientX - containerPos.left; + + evt.stopPropagation(); + + if ( + !forceLeave && + isInRegion(surface.wl.inputRegions.current, mouseY, mouseX, true) + ) { + if (this.mouseFocus?.surface !== surface) { + this.mouseFocus!.instances = this.wlSeatAuth.get(surface.wl.connection)!; + const enterSerial = this.mouseFocus!.instances.focus(surface.wl, []); + this.modifiers.update(this.mouseFocus!.instances.connection, enterSerial); + this.mouseFocus!.instances.enter(surface.wl, mouseX, mouseY); + } + this.mouseFocus!.instances.moveTo(mouseX, mouseY); + } else { + if (this.mouseFocus?.surface === surface) { + this.mouseFocus!.instances.blur(surface.wl); + this.mouseFocus!.instances.leave(surface.wl); + this.mouseFocus = undefined; + } + } + } +} diff --git a/src/renderer/host/classes/latch.ts b/src/renderer/host/classes/latch.ts new file mode 100644 index 0000000..b2e10d5 --- /dev/null +++ b/src/renderer/host/classes/latch.ts @@ -0,0 +1 @@ +export * from "../../.common/classes/latch.js"; diff --git a/src/renderer/host/classes/module.ts b/src/renderer/host/classes/module.ts new file mode 100644 index 0000000..8010931 --- /dev/null +++ b/src/renderer/host/classes/module.ts @@ -0,0 +1,95 @@ +import { KeyedLatch, Latch } from "./latch.js"; +import { OrderedPeer } from "./orderedPeer.js"; +import { CathodiqueConsumerHandler } from "../ipcHandlers/cathodiqueConsumer.js"; +import { CathodiqueHostHandler } from "../ipcHandlers/cathodiqueHost.js"; +import { DOMHostHandler } from "../ipcHandlers/domHost.js"; +import { nanoid } from "../../.common/utils/utils.js"; +import { makeComponentProxy } from "../utils/componentProxy.js"; +import { OtherNodeRegistry } from "./sharedDomHost.js"; + +export class Module { + static summonnedModules = new Map(); + + static getModule(moduleName: string) { + return this.summonnedModules.get(moduleName) ?? new Module(moduleName); + } + + iframe: HTMLIFrameElement; + moduleName: string; + #ipcLatch: Latch; + get peer() { return this.#ipcLatch.promise } + #winLatch: Latch; + get win() { return this.#winLatch.promise } + + constructor(moduleName: string) { + if (Module.summonnedModules.has(moduleName)) throw new Error("Module already initialized"); + + this.moduleName = moduleName; + + const moduleSubdomain = moduleName.split('.').toReversed().join('.'); + + this.iframe = document.createElement("iframe"); + this.iframe.src = `https://${moduleSubdomain}.raytu.be/module.html`; + this.iframe.hidden = true; + document.body.append(this.iframe); + + this.#ipcLatch = new Latch(); + this.#winLatch = new Latch(); + + this.#init(); + } + + #iframeLoaded = false + get iframeLoad() { + if (this.#iframeLoaded) return Promise.resolve(); + return new Promise((r) => { + this.iframe.addEventListener("load", () => r(), { once: true }); + }) + .then(function (this: Module) { this.#iframeLoaded = true; }.bind(this)) + } + + componentReady = new KeyedLatch(); + + async #init() { + await this.iframeLoad; + + const win = this.iframe.contentWindow!; + this.#winLatch.resolve!(win); + + const moduleSubdomain = this.moduleName.split('.').toReversed().join('.'); + + console.log(`https://${moduleSubdomain}.raytu.be`); + + OtherNodeRegistry.setRegistry(win, new OtherNodeRegistry(win)); + + const ipc = new OrderedPeer( + this.iframe.contentWindow!, + `https://${moduleSubdomain}.raytu.be`, + [ + new CathodiqueConsumerHandler(this.componentReady) as any, + new CathodiqueHostHandler(this) as any, + new DOMHostHandler(win, this) as any, + ] + ); + this.#ipcLatch.resolve!(ipc); + } + + async waitForComponent(componentName: string) { + await this.componentReady.get(componentName); + } + + createInstance(componentName: string) { + const componentId = nanoid(); + + const peerPromise = this.waitForComponent(componentName).then(async function (this: Module) { + const peer = await this.peer; + await peer.rpc("createInstance", { + className: componentName, + componentId, + }) + return peer; + }.bind(this)); + + return makeComponentProxy(componentId, peerPromise); + } +} diff --git a/src/renderer/host/classes/orchestrator.ts b/src/renderer/host/classes/orchestrator.ts new file mode 100644 index 0000000..8f0c4c5 --- /dev/null +++ b/src/renderer/host/classes/orchestrator.ts @@ -0,0 +1,25 @@ +import { Module } from "./module.js"; + +interface OrchestratorData { + defaults: Record; +} + +export class Orchestrator { + data: OrchestratorData; + + constructor() { + const script = document.querySelector("script[type=\"application/vnd.raytube.orchestrator-data\"]"); + + if (!script || !script.textContent) throw new Error("Cannot orchestrate: No script"); + + this.data = JSON.parse(script.textContent); + } + + loaded = new Map(); + load(schemaName: string) { + if (this.loaded.has(schemaName)) return this.loaded.get(schemaName)!; + const module = Module.getModule(this.data.defaults[schemaName]); + this.loaded.set(schemaName, module); + return module; + } +} diff --git a/src/renderer/host/classes/orderedPeer.ts b/src/renderer/host/classes/orderedPeer.ts new file mode 100644 index 0000000..2751911 --- /dev/null +++ b/src/renderer/host/classes/orderedPeer.ts @@ -0,0 +1 @@ +export * from "../../.common/classes/orderedPeer.js"; diff --git a/src/renderer/host/classes/sharedDomHost.ts b/src/renderer/host/classes/sharedDomHost.ts new file mode 100644 index 0000000..c57e717 --- /dev/null +++ b/src/renderer/host/classes/sharedDomHost.ts @@ -0,0 +1,163 @@ +import { NodeFromIpc } from "../utils/types.js"; + +class NodeData { + node: Node; + eventMap = new Map void>(); + registry: OtherNodeRegistry; + + constructor(node: Node, reg: OtherNodeRegistry) { + this.node = node; + this.registry = reg; + } + + registerEventListener(event: string, fn: (v: Event) => void) { + this.eventMap.set(event, fn); + return fn; + } + deregisterEventListener(event: string) { + const temp = this.eventMap.get(event); + if (!temp) throw new Error("Event not been registered through NodeData"); + this.eventMap.delete(event); + return temp; + } +} + +export class OtherNodeRegistry { + static registryPerWindow = new WeakMap(); + + static registryOf(win: WindowProxy) { + return this.registryPerWindow.get(win); + } + static setRegistry(win: WindowProxy, nr: OtherNodeRegistry) { + if (OtherNodeRegistry.registryPerWindow.has(win)) throw new Error("Only one NodeRegistry per window may exist"); + return this.registryPerWindow.set(win, nr); + } + + nodeToId = new WeakMap(); + idToNode = new Map>(); + nodeData = new WeakMap(); + nodeDataOf(node: Node) { + return this.nodeData.get(node); + } + + win: WindowProxy; + + constructor(win: WindowProxy) { + this.win = win; + } + + hasNode(node: Node) { + return this.nodeToId.has(node); + } + getNode(id: string) { + return this.idToNode.get(id)?.deref(); + } + + setNodeId(node: Node, id: string) { + this.nodeToId.set(node, id); + this.idToNode.set(id, new WeakRef(node)); + + this.nodeData.set(node, new NodeData(node, this)); + + return node; + } + + changeOwnership(id: string, node: Node) { + const originalNode = this.getNode(id); + if (!originalNode) throw new Error(); + + switch (node.nodeType) { + case Node.TEXT_NODE: { + if (!(originalNode instanceof Text)) throw new Error("Node type mismatch"); + + node.nodeValue = originalNode.nodeValue; + + break; + } + case Node.ELEMENT_NODE: { + if (!(originalNode instanceof Element)) throw new Error("Node type mismatch"); + + const el = node as Element; + if (originalNode.tagName !== el.tagName) throw new Error("Tag name mismatch"); + + for (const attr of Array.from(originalNode.attributes)) { + el.setAttributeNode(attr); + } + + for (const child of Array.from(originalNode.childNodes)) { + el.appendChild(child); + } + + break; + } + case Node.DOCUMENT_FRAGMENT_NODE: { + if (!(originalNode instanceof DocumentFragment)) throw new Error("Node type mismatch"); + + const docFrag = node as DocumentFragment; + for (const child of Array.from(originalNode.childNodes)) { + docFrag.appendChild(child); + } + + break; + } + default: + break; + } + this.setNodeId(node, id); + this.nodeToId.delete(originalNode); + } + + getId(node: Node) { + return this.nodeToId.get(node); + } + + deserializeNode(serializedNode: NodeFromIpc): Node { + switch (serializedNode.kind) { + case "element": { + const newEl = document.createElement(serializedNode.tagName); + + for (const [namespaceURI, name, value] of serializedNode.attributes) { + if (name === "id") continue; + newEl.setAttributeNS(namespaceURI, name, value); + } + + for (const kidId of serializedNode.children) { + newEl.append(this.getNode(kidId)!); + } + + if (newEl instanceof HTMLTemplateElement && serializedNode.content) { + this.changeOwnership(serializedNode.content, newEl.content); + } + + return newEl; + } + + case "text": { + return new Text(serializedNode.content); + } + + case "document_fragment": { + const newEl = document.createDocumentFragment(); + + for (const kidId of serializedNode.children) { + newEl.appendChild(this.getNode(kidId)!); + } + + return newEl; + } + + default: { + return new Comment(`Node of type ${serializedNode.nodeType}`); + } + } + } + + registerEvent (node: Node, event: string, evtHandler: (v: Event) => any) { + this.nodeDataOf(node)!.registerEventListener(event, evtHandler); + node.addEventListener(event, evtHandler); + } + unregisterEvent (node: Node, event: string) { + const eventListener = this.nodeDataOf(node)!.deregisterEventListener(event); + node.removeEventListener(event, eventListener); + } +} diff --git a/src/renderer/host/index.ts b/src/renderer/host/index.ts new file mode 100644 index 0000000..3a4bb49 --- /dev/null +++ b/src/renderer/host/index.ts @@ -0,0 +1,6 @@ +import { Orchestrator } from "./classes/orchestrator.js"; +import { OrderedPeer } from "./classes/orderedPeer.js"; + +OrderedPeer.registerIpcListener(); + +export const orchestrator = new Orchestrator(); diff --git a/src/renderer/host/ipcHandlers/cathodiqueConsumer.ts b/src/renderer/host/ipcHandlers/cathodiqueConsumer.ts new file mode 100644 index 0000000..cdb496a --- /dev/null +++ b/src/renderer/host/ipcHandlers/cathodiqueConsumer.ts @@ -0,0 +1 @@ +export * from "../../.common/ipcHandlers/cathodiqueConsumer.js"; diff --git a/src/renderer/host/ipcHandlers/cathodiqueHost.ts b/src/renderer/host/ipcHandlers/cathodiqueHost.ts new file mode 100644 index 0000000..85aff8e --- /dev/null +++ b/src/renderer/host/ipcHandlers/cathodiqueHost.ts @@ -0,0 +1,19 @@ +import { Module } from "../classes/module.js"; + +export class CathodiqueHostHandler { + mod: Module; + + constructor (mod: Module) { + this.mod = mod; + } + + async getDependency({ data }: { data: { dependency: string } }) { + + } + async getOptionalDependency({ data }: { data: { dependency: string } }) { + + } + async getAllDependency({ data }: { data: { dependency: string } }) { + + } +}; diff --git a/src/renderer/host/ipcHandlers/domHost.ts b/src/renderer/host/ipcHandlers/domHost.ts new file mode 100644 index 0000000..3692f81 --- /dev/null +++ b/src/renderer/host/ipcHandlers/domHost.ts @@ -0,0 +1,63 @@ +import { EventFromIpc, NodeFromIpc } from "../../.common/utils/types.js"; +import { Module } from "../classes/module.js"; +import { OtherNodeRegistry } from "../classes/sharedDomHost.js"; + +function allProperties(obj: any) { + const result = []; + for (const prop in obj) { + result.push(prop); + } + return result; +} + +export class DOMHostHandler { + win: WindowProxy; + module: Module; + constructor(win: WindowProxy, module: Module) { + this.win = win; + this.module = module; + } + + get nodeReg() { + return OtherNodeRegistry.registryOf(this.win)!; + } + + serializeEvent(evt: Event): EventFromIpc { + return { + type: evt.type, + className: evt.constructor.name, + values: Object.fromEntries( + allProperties(evt.constructor.prototype) + .map((v) => [v, evt[v as keyof typeof evt]]) + .filter(([k, v]) => !["function"].includes(typeof v)) + .map(([k, v]) => { + if (v instanceof Node) { + if (this.nodeReg.hasNode(v)) { + return [k, { nodeId: this.nodeReg.getId(v) }]; + } + return [k, undefined]; + } + + try { + structuredClone(v); + return [k, { value: v }]; + } catch { + return [k, undefined]; + } + }) + ), + }; + } + createNode({ data }: { data: { id: string, payload: NodeFromIpc, events: string[] } }) { + console.log(data); + const node = this.nodeReg.deserializeNode(data.payload); + this.nodeReg.setNodeId(node, data.id); + for (const event of data.events) { + this.nodeReg.registerEvent(node, event, async function (this: DOMHostHandler, v: Event) { + const ipc = await this.module.peer; + + await ipc.rpc("domEmitEvent", { id: data.id, event: this.serializeEvent(v) }); + }.bind(this)); + } + } +}; diff --git a/src/renderer/host/utils.ts b/src/renderer/host/utils.ts new file mode 100644 index 0000000..18a28fc --- /dev/null +++ b/src/renderer/host/utils.ts @@ -0,0 +1,9 @@ +let alphabet = + "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict"; + +export function nanoid(e = 21) { + let t = "", + r = crypto.getRandomValues(new Uint8Array(e)); + for (let n = 0; n < e; n++) t += alphabet[63 & r[n]]; + return t; +} diff --git a/src/renderer/host/utils/componentProxy.ts b/src/renderer/host/utils/componentProxy.ts new file mode 100644 index 0000000..1ed138e --- /dev/null +++ b/src/renderer/host/utils/componentProxy.ts @@ -0,0 +1,35 @@ +import { OrderedPeer } from "../classes/orderedPeer.js"; +import { OtherNodeRegistry } from "../classes/sharedDomHost.js"; + +export function makeComponentProxy(componentId: string, peer: Promise | OrderedPeer) { + return new Proxy({} as Record, { + get(_, prop) { + if (prop === "then") return undefined; + + const calledOrGotten = async function (...args: any[]) { + console.log(args); + console.trace(); + const v = await (await peer).rpc("callProperty", { + methodName: prop, + arguments: args, + componentId, + }); + if ("nodeId" in v) { + return OtherNodeRegistry.registryOf((await peer).win)!.getNode(v.nodeId); + } + return v.value; + }; + calledOrGotten.then = async (resolve: (a: any) => void) => (await peer).rpc("getProperty", { + propertyName: prop, + componentId, + }).then(async (v) => { + if ("nodeId" in v) { + return resolve(OtherNodeRegistry.registryOf((await peer).win)!.getNode(v.nodeId)); + } + return resolve(v.value); + }); + + return calledOrGotten; + }, + }); +} diff --git a/src/renderer/host/utils/types.ts b/src/renderer/host/utils/types.ts new file mode 100644 index 0000000..8d3c499 --- /dev/null +++ b/src/renderer/host/utils/types.ts @@ -0,0 +1 @@ +export * from "../../.common/utils/types.js"; diff --git a/src/renderer/index.html b/src/renderer/index.html index 1733837..118f15e 100644 --- a/src/renderer/index.html +++ b/src/renderer/index.html @@ -3,10 +3,18 @@ - Cathodique - + Document + + + + - diff --git a/src/renderer/index.ts b/src/renderer/index.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/renderer/wayland/index.ts b/src/renderer/wayland/index.ts new file mode 100644 index 0000000..4322492 --- /dev/null +++ b/src/renderer/wayland/index.ts @@ -0,0 +1,87 @@ +import "../host/index.js"; + +import { HLCompositor } from "@cathodique/wl-serv-high"; +import { InstructionType, RegRectangle } from "@cathodique/wl-serv-high/dist/objects/wl_region.js"; +import { Modifiers } from "../classes/wayland/seat/modifiers.js"; +import { SeatRegistry } from "@cathodique/wl-serv-high/dist/registries/seat.js"; +import { OutputRegistry } from "@cathodique/wl-serv-high/dist/registries/output.js"; +import { KeyboardRegistry } from "@cathodique/wl-serv-high/dist/objects/wl_keyboard.js"; +import { ipcRenderer } from "electron/renderer"; +import { Seat } from "../classes/wayland/seat/seat.js"; +import { BaseObject } from "@cathodique/wl-serv-high/dist/objects/base_object.js"; +import { objectHandlers } from "../classes/handlers/handlers.js"; +import { Output } from "../classes/wayland/output/output.js"; + +// HERE +// TODO::: +// Direct events towards their respective authorities +// for both Seat and Output + +export function isInRegion(reg: RegRectangle[], y: number, x: number, defaultValue: boolean = false) { + if (reg.length === 0) return defaultValue; + + return ( + reg.reduce((a, v) => { + if (v.hasCoordinate(y, x)) return v.type; + return a; + }, null) === InstructionType.Add + ); +} + +const mySeatConfig = { + name: "seat0", + capabilities: 3, + modifiers: null as unknown as Modifiers, +}; +const seatReg = new SeatRegistry(); + +new Seat(mySeatConfig, seatReg); + +seatReg.addAuthority(mySeatConfig); + +const myOutputConfig = { + x: 0, + y: 0, + w: 1920, + h: 1080, + effectiveW: 1920, + effectiveH: 1080, +}; +const outputReg = new OutputRegistry(); +outputReg.addAuthority(myOutputConfig); + +new Output(myOutputConfig, outputReg); + +new Seat(mySeatConfig, seatReg); + +const compo = new HLCompositor({ + wl_registry: { + outputs: outputReg, + seats: seatReg, + }, + wl_keyboard: new KeyboardRegistry({ keymap: "us" }), +}); + +const tickAnimationFrame = () => { + compo.ticks.emit("tick"); + requestAnimationFrame(tickAnimationFrame); +}; +tickAnimationFrame(); + +compo.on("connection", (c) => { + c.on("new_obj", async (obj: BaseObject) => { + const matching = objectHandlers[obj.iface as keyof typeof objectHandlers]; + + if (!matching) return; + + // @ts-ignore + new matching(obj); + }); +}); +compo.start(); + +compo.on("ready", () => { + document.body.append(`Ready at ${compo.params.socketPath}`); + ipcRenderer.send("addToDeleteQueue", compo.params.socketPath); + ipcRenderer.send(`Ready at ${compo.params.socketPath}.lock`); +}); diff --git a/tsconfig.json b/tsconfig.json index 38d57f7..92c4d44 100755 --- a/tsconfig.json +++ b/tsconfig.json @@ -105,5 +105,12 @@ /* Completeness */ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ "skipLibCheck": true /* Skip type checking all .d.ts files. */ - } + }, + "include": [ + "src/**/*", + "src/renderer/.common/**/*.ts" + ], + "exclude": [ + "modules" + ] } diff --git a/tsconfig.modules.json b/tsconfig.modules.json new file mode 100644 index 0000000..bbccf41 --- /dev/null +++ b/tsconfig.modules.json @@ -0,0 +1,113 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "ESNext", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + "lib": ["ESNext", "DOM"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + "jsx": "react-jsx", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "preserve", /* Specify what module code is generated. */ + "rootDir": "./src", /* Specify the root folder within your source files. */ + // "moduleResolution": "Node16", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + "outDir": "./dist", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + + /* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */ + "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + }, + "include": [ + "src/modules/**/*", + "src/modules/.common/**/*" + ] +} From 80bd297ced8c879779d614b0cc9fd82027719e18 Mon Sep 17 00:00:00 2001 From: Juliette Wang immjs Date: Sat, 3 Jan 2026 19:23:33 +0100 Subject: [PATCH 03/10] feat: Finish module system --- .prettierrc | 1 + TODO.md | 5 + package-lock.json | 29 ++- package.json | 12 +- src/main/main.ts | 3 +- src/main/protocols.ts | 4 + src/modules/.common/classes/component.ts | 60 +++-- src/modules/.common/classes/componentList.ts | 37 +++- src/modules/.common/classes/latch.ts | 42 +++- src/modules/.common/classes/module.ts | 66 ++++++ src/modules/.common/classes/orderedPeer.ts | 31 +-- src/modules/.common/classes/sharedDomDummy.ts | 41 ++++ .../.common/classes/sharedDomRemote.ts | 9 +- src/modules/.common/index.ts | 1 - .../.common/ipcHandlers/cathodiqueConsumer.ts | 17 +- .../.common/ipcHandlers/cathodiqueProvider.ts | 109 +++++++--- .../.common/ipcHandlers/cathodiqueRemote.ts | 42 +++- src/modules/.common/ipcHandlers/domRemote.ts | 19 +- src/modules/.common/parentIpc.ts | 19 +- .../.common/utils/remoteToLocalAdapter.ts | 104 +++++++++ src/modules/.common/utils/types.ts | 17 +- src/modules/.common/utils/utils.ts | 3 +- src/modules/.common/utils/wrap.ts | 68 ++++++ .../cathodique.windowmanager/module.html | 45 ++++ .../immjs.macos-aqua-windowframe/module.html | 10 +- src/renderer/.common/classes/component.ts | 31 --- src/renderer/.common/classes/componentList.ts | 23 -- src/renderer/.common/classes/latch.ts | 147 ------------- src/renderer/.common/classes/orderedPeer.ts | 130 ----------- .../.common/classes/sharedDomRemote.ts | 174 --------------- src/renderer/.common/index.ts | 14 -- .../.common/ipcHandlers/cathodiqueConsumer.ts | 13 -- .../.common/ipcHandlers/cathodiqueProvider.ts | 59 ----- .../.common/ipcHandlers/cathodiqueRemote.ts | 7 - src/renderer/.common/ipcHandlers/domRemote.ts | 33 --- src/renderer/.common/parentIpc.ts | 14 -- src/renderer/.common/setup.ts | 2 - .../.common/utils/nodeEventListener.ts | 205 ------------------ src/renderer/.common/utils/types.ts | 30 --- src/renderer/classes/handlers/dom/toplevel.ts | 14 +- src/renderer/classes/wayland/output/output.ts | 2 + src/renderer/host/classes/component.ts | 41 ++++ src/renderer/host/classes/componentList.ts | 52 +++++ src/renderer/host/classes/latch.ts | 176 ++++++++++++++- src/renderer/host/classes/module.ts | 194 ++++++++++++++--- src/renderer/host/classes/orchestrator.ts | 46 +++- src/renderer/host/classes/orderedPeer.ts | 136 +++++++++++- .../host/classes/semanticMessageChannel.ts | 8 + src/renderer/host/classes/sharedDomHost.ts | 54 +++-- .../{.common => host}/classes/withTransfer.ts | 0 src/renderer/host/index.ts | 5 +- .../host/ipcHandlers/cathodiqueConsumer.ts | 21 +- .../host/ipcHandlers/cathodiqueHost.ts | 69 +++++- .../host/ipcHandlers/cathodiqueProvider.ts | 88 ++++++++ src/renderer/host/ipcHandlers/domHost.ts | 64 ++++-- .../host/localModules/loadAllLocalModules.ts | 8 + src/renderer/host/localModules/window.ts | 11 + src/renderer/host/utils.ts | 9 - src/renderer/host/utils/componentProxy.ts | 35 --- .../host/utils/remoteToLocalAdapter.ts | 105 +++++++++ src/renderer/host/utils/types.ts | 49 ++++- src/renderer/{.common => host}/utils/utils.ts | 8 + src/renderer/host/utils/wrap.ts | 67 ++++++ src/renderer/index.html | 9 +- tsconfig.modules.json | 2 +- 65 files changed, 1815 insertions(+), 1134 deletions(-) create mode 100644 .prettierrc create mode 100644 TODO.md create mode 100644 src/modules/.common/classes/module.ts create mode 100644 src/modules/.common/classes/sharedDomDummy.ts create mode 100644 src/modules/.common/utils/remoteToLocalAdapter.ts create mode 100644 src/modules/.common/utils/wrap.ts create mode 100644 src/modules/cathodique.windowmanager/module.html delete mode 100644 src/renderer/.common/classes/component.ts delete mode 100644 src/renderer/.common/classes/componentList.ts delete mode 100644 src/renderer/.common/classes/latch.ts delete mode 100644 src/renderer/.common/classes/orderedPeer.ts delete mode 100644 src/renderer/.common/classes/sharedDomRemote.ts delete mode 100644 src/renderer/.common/index.ts delete mode 100644 src/renderer/.common/ipcHandlers/cathodiqueConsumer.ts delete mode 100644 src/renderer/.common/ipcHandlers/cathodiqueProvider.ts delete mode 100644 src/renderer/.common/ipcHandlers/cathodiqueRemote.ts delete mode 100644 src/renderer/.common/ipcHandlers/domRemote.ts delete mode 100644 src/renderer/.common/parentIpc.ts delete mode 100644 src/renderer/.common/setup.ts delete mode 100644 src/renderer/.common/utils/nodeEventListener.ts delete mode 100644 src/renderer/.common/utils/types.ts create mode 100644 src/renderer/host/classes/component.ts create mode 100644 src/renderer/host/classes/componentList.ts create mode 100644 src/renderer/host/classes/semanticMessageChannel.ts rename src/renderer/{.common => host}/classes/withTransfer.ts (100%) create mode 100644 src/renderer/host/ipcHandlers/cathodiqueProvider.ts create mode 100644 src/renderer/host/localModules/loadAllLocalModules.ts create mode 100644 src/renderer/host/localModules/window.ts delete mode 100644 src/renderer/host/utils.ts delete mode 100644 src/renderer/host/utils/componentProxy.ts create mode 100644 src/renderer/host/utils/remoteToLocalAdapter.ts rename src/renderer/{.common => host}/utils/utils.ts (77%) create mode 100644 src/renderer/host/utils/wrap.ts diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/.prettierrc @@ -0,0 +1 @@ +{} diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..49a9e9d --- /dev/null +++ b/TODO.md @@ -0,0 +1,5 @@ +# TODO +- Mutation observer thingy +- Component lifecycle, managed through a FinReg in every place a handle of a component was provided +- Prevent scripts, meta tag +- Protect styling with shadowRoot diff --git a/package-lock.json b/package-lock.json index ac5365e..53b9964 100755 --- a/package-lock.json +++ b/package-lock.json @@ -18,12 +18,14 @@ "esbuild": "^0.27.2", "koffi": "^2.14.0", "node-abi": "^4.12.0", - "xml-parser": "^1.2.1" + "xml-parser": "^1.2.1", + "zod": "^4.2.1" }, "devDependencies": { "@electron/rebuild": "^3.7.1", "@types/electron": "^1.4.38", "@types/node": "^22.10.2", + "prettier": "3.7.4", "typescript": "^5.7.2" } }, @@ -2511,6 +2513,22 @@ "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", "license": "MIT" }, + "node_modules/prettier": { + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.4.tgz", + "integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/proc-log": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-2.0.1.tgz", @@ -3181,6 +3199,15 @@ "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } + }, + "node_modules/zod": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.2.1.tgz", + "integrity": "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index 4f52b5f..19c3ecd 100755 --- a/package.json +++ b/package.json @@ -6,8 +6,10 @@ "scripts": { "start": "npm run build && LOGGERS=wl_serv_i8a56qb0lm3ox1ng1cqai0e1 electron .", "test": "echo \"Error: no test specified\" && exit 1", - "build-browser": "esbuild dist/renderer/wayland/index.js --bundle --platform=node --target=node22.4 --external:electron --external:@cathodique/wl-serv-high --outfile=dist/renderer/index.js", - "build": "rm -frfr dist && mkdir dist && cp -frfr src/* dist/ && find dist/ -name \"*.ts\" -type f -delete && npx tsgo && npx tsc -p tsconfig.modules.json && npm run build-browser" + "build-asset-homomorphic": "rm -frfr dist && mkdir dist && cp -frfr src/* dist/ && find dist/ -name \"*.ts\" -type f -delete", + "build-browser-host": "esbuild dist/renderer/wayland/index.js --bundle --platform=node --target=node22.4 --external:electron --external:@cathodique/wl-serv-high --outfile=dist/renderer/index.js", + "build-browser-common": "esbuild dist/modules/.common/index.js --bundle --outfile=dist/modules/.common/bundle.js --format=esm", + "build": "npm run build-asset-homomorphic && npx tsgo && npx tsc -p tsconfig.modules.json && npm run build-browser-host && npm run build-browser-common" }, "repository": { "type": "git", @@ -23,11 +25,12 @@ "@electron/rebuild": "^3.7.1", "@types/electron": "^1.4.38", "@types/node": "^22.10.2", + "prettier": "3.7.4", "typescript": "^5.7.2" }, "dependencies": { - "@cathodique/usocket": "file:../usocket", "@cathodique/mmap-io": "file:../mmap-io", + "@cathodique/usocket": "file:../usocket", "@cathodique/wl-serv-high": "file:../wl-serv-high", "@paralleldrive/cuid2": "^2.2.2", "@typescript/native-preview": "^7.0.0-dev.20251220.1", @@ -35,7 +38,8 @@ "esbuild": "^0.27.2", "koffi": "^2.14.0", "node-abi": "^4.12.0", - "xml-parser": "^1.2.1" + "xml-parser": "^1.2.1", + "zod": "^4.2.1" }, "overrides": { "nan": "^2.23.0" diff --git a/src/main/main.ts b/src/main/main.ts index e3b8314..7ea34df 100755 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -7,7 +7,8 @@ const createWindow = () => { const win = new BrowserWindow({ width: 800, height: 600, - // fullscreen: true, + fullscreen: true, + resizable: false, webPreferences: { nodeIntegration: true, nodeIntegrationInSubFrames: false, diff --git a/src/main/protocols.ts b/src/main/protocols.ts index 782817f..e1116d4 100644 --- a/src/main/protocols.ts +++ b/src/main/protocols.ts @@ -67,6 +67,10 @@ export const registerProtocols = () => { return callback({ cancel: false }); } + if (url.host.endsWith(".raytu.be") || url.host === "raytu.be") { + return callback({ cancel: false }); + } + // TODO: Handle permissions of each module. For now though... callback({ cancel: true }); } diff --git a/src/modules/.common/classes/component.ts b/src/modules/.common/classes/component.ts index 4202273..313cccf 100644 --- a/src/modules/.common/classes/component.ts +++ b/src/modules/.common/classes/component.ts @@ -1,31 +1,65 @@ -import { nanoid } from "../utils/utils.js"; import { parentIpc } from "../parentIpc.js"; +import { nanoid } from "../utils/utils.js"; +import { RemoteModule } from "./module.js"; +import { OrderedPeer } from "./orderedPeer.js"; +import z from "zod"; + +export interface ComponentContext { + ipc: OrderedPeer; +} +export type ComponentHandle = { + ipc: OrderedPeer; + componentId: string | Promise; -export class Component extends EventTarget { - componentHandle: string; + init(): any; +} & { [k in `$${string}`]: any; }; - constructor() { +const isComponentSymbol = Symbol(); +export abstract class Component extends EventTarget { + [k: `$${string}`]: any; + + static isComponentSymbol: typeof isComponentSymbol = isComponentSymbol; + componentId: string; + ipc: OrderedPeer; + + [isComponentSymbol] = true; + constructor(ctx: ComponentContext) { super(); - const handle = nanoid(); - this.componentHandle = handle; + this.ipc = ctx.ipc; + this.componentId = nanoid(); } - init() {} + abstract init(): any; post(obj: Record) { - return parentIpc.post({ ...obj, componentHandle: this.componentHandle }); + return this.ipc.post({ ...obj, componentHandle: this.componentId }); } rpc(type: string, data: Record, obj: Record = {}) { - return parentIpc.rpc(type, data, { ...obj, componentHandle: this.componentHandle }); + return this.ipc.rpc(type, data, { ...obj, componentHandle: this.componentId }); } + async #getDependencyRpc(dependency: string) { + const result = await parentIpc.rpc("getDependency", { dependency }); + return z.object({ + port: z.instanceof(MessagePort), + id: z.string(), + }).parse(result); + } async getDependency(dependency: string) { - return await this.rpc("getDependency", { dependency }); + const v = await this.#getDependencyRpc(dependency); + const remoteModule = RemoteModule.getOrCreate(v.port, v.id); + return remoteModule.localHandle; } - async getOptionalDependency(dependency: string) { - return await this.rpc("getOptionalDependency", { dependency }); + + async #getAllDependencyRpc(dependency: string) { + const result = await parentIpc.rpc("getAllDependency", { dependency }); + return z.array(z.object({ + port: z.instanceof(MessagePort), + id: z.string(), + })).parse(result); } async getAllDependency(dependency: string) { - return await this.rpc("getAllDependency", { dependency }); + const handles = await this.#getAllDependencyRpc(dependency); + return handles.map((v) => RemoteModule.getOrCreate(v.port, v.id).localHandle); } } diff --git a/src/modules/.common/classes/componentList.ts b/src/modules/.common/classes/componentList.ts index bbbd1d5..164d27b 100644 --- a/src/modules/.common/classes/componentList.ts +++ b/src/modules/.common/classes/componentList.ts @@ -1,18 +1,32 @@ -import { parentIpc } from "../parentIpc.js"; -import type { Component } from "./component.js"; +import { Component, ComponentHandle } from "./component.js"; + +export class InvalidComponentError extends Error {} export class ComponentList extends EventTarget { - componentClasses = new Map(); + componentClasses = new Map ComponentHandle>(); + componentClassToClassName = new Map ComponentHandle, string>() + + componentTypeOf(component: Component) { + // Traversing prototype chain (from most specific to least specific) + // will take less time than traversing all the possible components. + let currentPrototype: any = Object.getPrototypeOf(component); + while (currentPrototype !== null) { + if (this.componentClassToClassName.has(currentPrototype.constructor)) { + return this.componentClassToClassName.get(currentPrototype.constructor)!; + } + currentPrototype = Object.getPrototypeOf(currentPrototype); + } + + throw new InvalidComponentError(); + // Implications: The object has had [Symbol(Component.isComponentSymbol)] set to true but was not a component + } - register(componentName: string, componentClass: new (...a: any[]) => Component) { + register(componentName: string, componentClass: new (...args: any[]) => ComponentHandle) { if (this.componentClasses.has(componentName)) throw new Error("This component already exists"); - this.componentClasses.set(componentName, componentClass); - parentIpc.post({ - type: "componentRegistered", - data: { componentName }, - }); + this.componentClasses.set(componentName, componentClass); + this.componentClassToClassName.set(componentClass, componentName); } get(componentName: string) { @@ -20,4 +34,9 @@ export class ComponentList extends EventTarget { } } +export type ComponentListHandle = { + get(componentName: string): undefined + | (new (...args: any[]) => ComponentHandle); +}; + export const componentList = new ComponentList(); diff --git a/src/modules/.common/classes/latch.ts b/src/modules/.common/classes/latch.ts index 666fe03..a60bfd4 100644 --- a/src/modules/.common/classes/latch.ts +++ b/src/modules/.common/classes/latch.ts @@ -1,4 +1,4 @@ -enum LatchState { +export enum LatchState { Pending, Fulfilled, } @@ -6,13 +6,17 @@ enum LatchState { export class Latch { promise: Promise; resolve: ((v: T) => void) | undefined; + constructor(value?: T) { - if (value) { + if (value != null) { this.resolve = undefined; this.promise = Promise.resolve(value); } else { let resultingResolve: (r: T) => void; + + // Assignment with side effect onto resultingResolve this.promise = new Promise((r) => { resultingResolve = r }); + this.resolve = function (this: Latch, r: T) { resultingResolve(r); this.resolve = undefined; @@ -29,26 +33,36 @@ export class Latch { export class KeyedLatch { map = new Map>(); - getStateOf(key: T) { + getStateOf(key: T): LatchState { return this.map.get(key)?.getState() ?? LatchState.Pending; } - get(key: T) { + + get(key: T): Promise { if (this.map.has(key)) return this.map.get(key)!.promise; + const latch = new Latch(); this.map.set(key, latch); + return latch.promise; } - resolve(key: T, value: U) { + + resolve(key: T, value: U): typeof value { if (!this.map.has(key)) { this.map.set(key, new Latch(value)); + } else { + const { resolve } = this.map.get(key)!; + if (!resolve) throw new Error('Double resolution is unacceptable'); + + resolve(value); } - this.map.get(key)!.resolve?.(value); return value; } + getOptional(key: T) { if (this.map.has(key)) return this.get(key); return undefined; } + delete(key: T) { this.map.delete(key); } @@ -60,23 +74,33 @@ export class WeakKeyedLatch { getStateOf(key: T) { return this.map.get(key)?.getState() ?? LatchState.Pending; } + get(key: T) { if (this.map.has(key)) return this.map.get(key)!.promise; + const latch = new Latch(); this.map.set(key, latch); + return latch.promise; } + resolve(key: T, value: U) { if (!this.map.has(key)) { this.map.set(key, new Latch(value)); + } else { + const { resolve } = this.map.get(key)!; + if (!resolve) throw new Error('Double resolution is unacceptable'); + + resolve(value); } - this.map.get(key)!.resolve?.(value); return value; } + getOptional(key: T) { if (this.map.has(key)) return this.get(key); return undefined; } + delete(key: T) { this.map.delete(key); } @@ -97,6 +121,7 @@ export class ConsumableKeyedLatch extends KeyedLatch { } return result; } + /** * @deprecated For semantic reasons, use consume instead * Method kept for inheritance @@ -104,6 +129,7 @@ export class ConsumableKeyedLatch extends KeyedLatch { get(key: T) { return this.consume(key); } + resolve(key: T, value: U) { const result = super.resolve(key, value); if (this.consumed.has(key)) { @@ -129,6 +155,7 @@ export class ConsumableWeakKeyedLatch extends WeakKeyedLat } return result; } + /** * @deprecated For semantic reasons, use consume instead * Method kept for inheritance @@ -136,6 +163,7 @@ export class ConsumableWeakKeyedLatch extends WeakKeyedLat get(key: T) { return this.consume(key); } + resolve(key: T, value: U) { const result = super.resolve(key, value); if (this.consumed.has(key)) { diff --git a/src/modules/.common/classes/module.ts b/src/modules/.common/classes/module.ts new file mode 100644 index 0000000..f706693 --- /dev/null +++ b/src/modules/.common/classes/module.ts @@ -0,0 +1,66 @@ +import { CathodiqueConsumerHandler } from "../ipcHandlers/cathodiqueConsumer.js"; +import { ComponentInstanceProxy, makeComponentProxy } from "../utils/remoteToLocalAdapter.js"; +import { KeyedLatch } from "./latch.js"; +import { OrderedPeer } from "./orderedPeer.js"; +import { DummyNodeRegistry } from "./sharedDomDummy.js"; + +// The RemoteModule class manages the lifecycle of a RemoteModule +// Mainly, the components made available (through keyed latch componentReady) +export class RemoteModule { + static summonnedModulesByPort = new Map(); + static summonnedModulesById = new Map(); + + static getOrCreate(port: MessagePort, id: string) { + if (this.summonnedModulesByPort.has(port)) return this.summonnedModulesByPort.get(port)!; + + return this.createModule(port, id); + } + + static createModule(port: MessagePort, id: string) { + const mod = new RemoteModule(port, id); + this.summonnedModulesByPort.set(port, mod); + this.summonnedModulesById.set(id, mod) + + return mod; + } + + static moduleById(id: string) { + return this.summonnedModulesById.get(id); + } + static moduleOfPort(port: MessagePort) { + return this.summonnedModulesByPort.get(port); + } + + id: string; + + port: MessagePort; + peer: OrderedPeer; + constructor(port: MessagePort, id: string) { + this.port = port; + this.id = id; + + // "*" is fine because we can trust the origin who passed the messageport onto us + this.peer = new OrderedPeer(port, "*"); + this.peer.addHandler(new CathodiqueConsumerHandler(this.componentReady)); + + DummyNodeRegistry.setRegistry(port, new DummyNodeRegistry(port)); + } + + componentReady = new KeyedLatch(); + + async waitForComponent(componentName: string) { + await this.componentReady.get(componentName); + } + + localHandle = { + get: function (this: RemoteModule, componentName: string) { + return function (this: RemoteModule, ...args: any[]) { + return makeComponentProxy(this, componentName, { args }); + }.bind(this) as unknown as new () => ComponentInstanceProxy; + }.bind(this), + } + + async componentExists(id: string) { + return await this.peer.rpc("componentExists", { componentId: id }); + } +} diff --git a/src/modules/.common/classes/orderedPeer.ts b/src/modules/.common/classes/orderedPeer.ts index 5a0b4d0..013cf44 100644 --- a/src/modules/.common/classes/orderedPeer.ts +++ b/src/modules/.common/classes/orderedPeer.ts @@ -2,18 +2,19 @@ import { ConsumableKeyedLatch } from "./latch.js"; import { nanoid } from "../utils/utils.js"; import { WithTransfer } from "./withTransfer.js"; +import { HandlerContext } from "../utils/types.js"; export class OrderedPeer { - handlers: Record) => any>[]; + handlers: Record, ctx: HandlerContext) => any>[] = []; - static actualHandlers = new WeakMap void>(); + static actualHandlers = new WeakMap void>(); private static registered = false; static registerIpcListener() { if (this.registered) return; window.addEventListener( "message", (evt) => { - const actualHandler = this.actualHandlers.get(evt.source as WindowProxy); + const actualHandler = this.actualHandlers.get(evt.source as MessageEventSource); if (!actualHandler) return; actualHandler(evt); }, @@ -28,21 +29,25 @@ export class OrderedPeer { promiseMap = new ConsumableKeyedLatch(); - win: WindowProxy; + source: MessageEventSource; postMessage: typeof window["postMessage"]; - constructor(win: WindowProxy, origin = "*", handlers: Record any>[]) { - if (OrderedPeer.actualHandlers.has(win)) throw new Error("A window may only admit a single OrderedPeer"); + constructor(source: MessageEventSource, origin = "*") { + if (OrderedPeer.actualHandlers.has(source)) throw new Error("A window may only admit a single OrderedPeer"); - this.win = win; - this.postMessage = win.postMessage.bind(win); + this.source = source; + this.postMessage = source.postMessage.bind(source); this.origin = origin; - this.handlers = handlers; - OrderedPeer.actualHandlers.set(win, this.orderedDecoder.bind(this)); + OrderedPeer.actualHandlers.set(source, this.orderedDecoder.bind(this)); + } + + addHandler(handler: (typeof this.handlers)[number]) { + this.handlers.push(handler); + return this; // Builder } post(data: any) { - let transfer = []; + let transfer: WithTransfer[] = []; if (data instanceof WithTransfer) { transfer = data.transfer; data = data.data; @@ -90,8 +95,6 @@ export class OrderedPeer { async orderedDecoder(evt: MessageEvent) { if (!this.originMatch(evt.origin)) return; - console.log(); - const { data: { messages, currentOrder } } = evt; this.remainingMessages.set(currentOrder, messages); if (this.currentOrderReception === currentOrder) { @@ -112,7 +115,7 @@ export class OrderedPeer { } try { - const result = await handler[type](message); + const result = await handler[type](message, { ipc: this, event: evt }); if (promiseId) { this.post({ type: "reply", reply: result, promiseId }); diff --git a/src/modules/.common/classes/sharedDomDummy.ts b/src/modules/.common/classes/sharedDomDummy.ts new file mode 100644 index 0000000..601f5eb --- /dev/null +++ b/src/modules/.common/classes/sharedDomDummy.ts @@ -0,0 +1,41 @@ +export class DummyNodeRegistry { + static registryPerSource = new WeakMap(); + + static registryOf(source: MessageEventSource) { + return this.registryPerSource.get(source); + } + static setRegistry(source: MessageEventSource, nr: DummyNodeRegistry) { + if (DummyNodeRegistry.registryPerSource.has(source)) throw new Error("Only one NodeRegistry per window may exist"); + return this.registryPerSource.set(source, nr); + } + + nodeToId = new WeakMap(); + idToNode = new Map>(); + + source: MessageEventSource; + + constructor(source: MessageEventSource) { + this.source = source; + DummyNodeRegistry.setRegistry(source, this); + } + + hasNode(node: Node) { + return this.nodeToId.has(node); + } + getNode(id: string) { + if (this.idToNode.has(id)) { + const result = this.idToNode.get(id)!.deref(); + if (result) return result; + } + + const node = document.createElement("div"); + + this.nodeToId.set(node, id); + this.idToNode.set(id, new WeakRef(node)); + return node; + } + + getId(node: Node) { + return this.nodeToId.get(node); + } +} diff --git a/src/modules/.common/classes/sharedDomRemote.ts b/src/modules/.common/classes/sharedDomRemote.ts index fc7d0ac..0878d15 100644 --- a/src/modules/.common/classes/sharedDomRemote.ts +++ b/src/modules/.common/classes/sharedDomRemote.ts @@ -94,6 +94,10 @@ class MutationDispatcher { } export class SharedDOM { + static finReg = new FinalizationRegistry((heldValue) => { + parentIpc.rpc("deleteNode", { id: heldValue }); + }); + static initOrGet(root: Node) { if (NodeRegistry.hasNode(root)) return NodeRegistry.getId(root); this.init(root); @@ -106,18 +110,19 @@ export class SharedDOM { } static registerSubtree(node: Node) { - NodeRegistry.getId(node); + const id = NodeRegistry.getId(node); if (node instanceof HTMLTemplateElement) this.registerSubtree(node.content); node.childNodes.forEach(function (this: typeof SharedDOM, n: Node) { this.registerSubtree(n) }.bind(this)); parentIpc.post({ type: "createNode", data: { - id: NodeRegistry.getId(node), + id: id, payload: serializeNode(node), events: serializeEvents(node), }, }); + this.finReg.register(node, id); } static handleMutation(m: MutationRecord) { diff --git a/src/modules/.common/index.ts b/src/modules/.common/index.ts index 3265069..d2b9d9c 100644 --- a/src/modules/.common/index.ts +++ b/src/modules/.common/index.ts @@ -4,7 +4,6 @@ // Because, them being dereferenced implied it's not ref'able // through the DOM tree - import { OrderedPeer } from "./classes/orderedPeer.js"; import { patchAllEvents } from "./utils/nodeEventListener.js"; patchAllEvents(); diff --git a/src/modules/.common/ipcHandlers/cathodiqueConsumer.ts b/src/modules/.common/ipcHandlers/cathodiqueConsumer.ts index 4f266db..761998a 100644 --- a/src/modules/.common/ipcHandlers/cathodiqueConsumer.ts +++ b/src/modules/.common/ipcHandlers/cathodiqueConsumer.ts @@ -1,13 +1,22 @@ +import z from "zod"; import { KeyedLatch } from "../classes/latch.js"; +import { HandlerContext } from "../utils/types.js"; export class CathodiqueConsumerHandler { - instanceReady: KeyedLatch; + [k: string]: (arg: Record, ctx: HandlerContext) => any; + + #instanceReady: KeyedLatch; constructor(instanceReady: KeyedLatch) { - this.instanceReady = instanceReady; + this.#instanceReady = instanceReady; } - async componentRegistered({ data }: { data: { componentName: string } }) { - this.instanceReady.resolve(data.componentName, undefined); + componentRegistered(args: Record) { + return this.#componentRegistered(z.object({ + data: z.object({ componentName: z.string() }), + }).parse(args)); + } + async #componentRegistered({ data }: { data: { componentName: string } }) { + this.#instanceReady.resolve(data.componentName, undefined); } }; diff --git a/src/modules/.common/ipcHandlers/cathodiqueProvider.ts b/src/modules/.common/ipcHandlers/cathodiqueProvider.ts index c44d0f1..d4de855 100644 --- a/src/modules/.common/ipcHandlers/cathodiqueProvider.ts +++ b/src/modules/.common/ipcHandlers/cathodiqueProvider.ts @@ -1,59 +1,102 @@ -import { ComponentList } from "../classes/componentList.js"; -import { SharedDOM } from "../classes/sharedDomRemote.js"; +import z from "zod"; +import { componentList, ComponentList } from "../classes/componentList.js"; +import { HandlerContext } from "../utils/types.js"; +import { unwrapValue, WrappedValue, wrapValue, zodWrappedValue } from "../utils/wrap.js"; +import { Component } from "../classes/component.js"; +import { RemoteModule } from "../classes/module.js"; + +// DISTINCTIONS WITH HOST +// A single module has a single componentList +// So componentList need be static + +// A single module determines its own IDs thus makes ID attacks impossible +// So componentInstances need be static export class CathodiqueProviderHandler { - componentList: ComponentList; + [k: string]: (arg: Record, ctx: HandlerContext) => any; - constructor(componentList: ComponentList) { - this.componentList = componentList; - } + static #componentList: ComponentList = componentList; + + static #componentInstances = new Map(); - componentInstances = new Map(); + #fromModule: RemoteModule | undefined; + constructor(fromModule: RemoteModule | undefined) { + this.#fromModule = fromModule; + } - async createInstance({ data }: { data: { className: string; componentId: string } }) { - // TODO Obj verification - const ClassObj = this.componentList.get(data.className); - if (!ClassObj) return; // Quiet fail + createInstance(arg: Record) { + return this.#createInstance(z.object({ + data: z.object({ + className: z.string(), + args: z.array(zodWrappedValue), + }), + }).parse(arg)); + } + async #createInstance({ data }: { data: { className: string, args: WrappedValue[] } }) { + const ClassObj = CathodiqueProviderHandler.#componentList.get(data.className); + if (!ClassObj) return; // TODO error here - const componentInstance = new ClassObj(); + const unwrapped = data.args.map(function (this: CathodiqueProviderHandler, v: WrappedValue) { + return unwrapValue(v, this.#fromModule) + }.bind(this)); + const componentInstance = new ClassObj(...unwrapped) as Component; await componentInstance.init(); - this.componentInstances.set(data.componentId, componentInstance); - return; + CathodiqueProviderHandler.#componentInstances.set( + componentInstance.componentId, + componentInstance, + ); + return componentInstance.componentId; } - getProperty({ data }: { data: { propertyName: string; componentId: string } }) { - const component = this.componentInstances.get(data.componentId); - - const value = component[data.propertyName]; + instanceExists(arg: Record) { + return this.#instanceExists(z.object({ + data: z.object({ + componentId: z.string(), + }), + }).parse(arg)); + } + #instanceExists({ data }: { data: { componentId: string } }) { + return CathodiqueProviderHandler.#componentInstances.has(data.componentId); + } - if (value instanceof Node) { - const nodeId = SharedDOM.initOrGet(value); + getProperty(arg: Record) { + return this.#getProperty(z.object({ + data: z.object({ + propertyName: z.string(), + componentId: z.string(), + }), + }).parse(arg)); + } + async #getProperty({ data }: { data: { propertyName: string; componentId: string } }) { + const component = CathodiqueProviderHandler.#componentInstances.get(data.componentId); - return { nodeId }; - } + const value = component[data.propertyName]; - return { value }; + return wrapValue(value); } - async callProperty({ data }: { + callProperty(arg: Record) { + return this.#callProperty(z.object({ + data: z.object({ + methodName: z.string(), + arguments: z.array(z.any()), + componentId: z.string(), + }), + }).parse(arg)); + } + async #callProperty({ data }: { data: { methodName: string; - arguments: string[]; + arguments: any[]; componentId: string; }; }) { - const component = this.componentInstances.get(data.componentId); + const component = CathodiqueProviderHandler.#componentInstances.get(data.componentId); const value = await component?.[data.methodName]?.(...data.arguments); - if (value instanceof Node) { - const nodeId = SharedDOM.initOrGet(value); - - return { nodeId }; - } - - return { value }; + return wrapValue(value); } }; diff --git a/src/modules/.common/ipcHandlers/cathodiqueRemote.ts b/src/modules/.common/ipcHandlers/cathodiqueRemote.ts index bb49a2b..abe9882 100644 --- a/src/modules/.common/ipcHandlers/cathodiqueRemote.ts +++ b/src/modules/.common/ipcHandlers/cathodiqueRemote.ts @@ -1,7 +1,43 @@ +import z from "zod"; +import { RemoteModule } from "../classes/module.js"; +import { OrderedPeer } from "../classes/orderedPeer.js"; +import { HandlerContext } from "../utils/types.js"; +import { CathodiqueProviderHandler } from "./cathodiqueProvider.js"; + export class CathodiqueRemoteHandler { - win: WindowProxy; + [k: string]: (arg: Record, ctx: HandlerContext) => any; + + constructor() {} + + connectAsProvider(arg: Record) { + return this.#connectAsProvider(z.object({ + data: z.object({ + port: z.instanceof(MessagePort), + moduleToken: z.string(), + }), + }).parse(arg)); + } + #connectAsProvider({ data }: { data: { port: MessagePort, moduleToken: string } }) { + const module = RemoteModule.getOrCreate(data.port, data.moduleToken); + // TODO Expose self as provider + + // Note: We can trust this messageport (unless vuln) because the parent is trusted to be raytube + const peer = new OrderedPeer(data.port, "*"); + peer.addHandler(new CathodiqueProviderHandler(module) as any); + } + + connectAsConsumer(arg: Record) { + return this.#connectAsConsumer(z.object({ + data: z.object({ + port: z.instanceof(MessagePort), + moduleToken: z.string(), + }), + }).parse(arg)); + } + #connectAsConsumer({ data }: { data: { port: MessagePort, moduleToken: string } }) { + // This is for, if *this* module wishes to connect to another module. - constructor(win: WindowProxy) { - this.win = win; + RemoteModule.createModule(data.port, data.moduleToken); + // The rest will be handled by remoteToLocalAdapter.ts } } diff --git a/src/modules/.common/ipcHandlers/domRemote.ts b/src/modules/.common/ipcHandlers/domRemote.ts index cba8af0..92535d8 100644 --- a/src/modules/.common/ipcHandlers/domRemote.ts +++ b/src/modules/.common/ipcHandlers/domRemote.ts @@ -1,8 +1,11 @@ +import z from "zod"; import { NodeRegistry } from "../classes/sharedDomRemote.js"; -import { EventFromIpc } from "../utils/types.js"; +import { EventFromIpc, HandlerContext, zodEventFromIpc } from "../utils/types.js"; export class DOMRemoteHandler { - deserializeEvent(evtData: EventFromIpc): Event { + [k: string]: (arg: Record, ctx: HandlerContext) => any; + + #deserializeEvent(evtData: EventFromIpc): Event { if (!evtData.className.endsWith("Event")) throw new Error("Constructor name must be an event"); const EventClassObj = globalThis[evtData.className as keyof typeof globalThis] as typeof Event; @@ -23,11 +26,19 @@ export class DOMRemoteHandler { return new EventClassObj(evtData.type, newValues); } - domEmitEvent({ data }: { data: { id: string, event: EventFromIpc } }) { + domEmitEvent(arg: Record) { + return this.#domEmitEvent(z.object({ + data: z.object({ + id: z.string(), + event: zodEventFromIpc, + }), + }).parse(arg)); + } + #domEmitEvent({ data }: { data: { id: string, event: EventFromIpc } }) { const element = NodeRegistry.getNode(data.id); if (!element) return console.error(`Tried to emit event ${data.event} to inexistent element ${data.id}`); - element.dispatchEvent(this.deserializeEvent(data.event)); + element.dispatchEvent(this.#deserializeEvent(data.event)); } }; diff --git a/src/modules/.common/parentIpc.ts b/src/modules/.common/parentIpc.ts index f3eeed6..a8deb83 100644 --- a/src/modules/.common/parentIpc.ts +++ b/src/modules/.common/parentIpc.ts @@ -1,14 +1,15 @@ import { OrderedPeer } from "./classes/orderedPeer.js"; -import { CathodiqueConsumerHandler } from "./ipcHandlers/cathodiqueConsumer.js"; import { CathodiqueProviderHandler } from "./ipcHandlers/cathodiqueProvider.js"; import { CathodiqueRemoteHandler } from "./ipcHandlers/cathodiqueRemote.js"; import { DOMRemoteHandler } from "./ipcHandlers/domRemote.js"; -import { KeyedLatch } from "./classes/latch.js"; -import { componentList } from "./classes/componentList.js"; +import { canonicalHost } from "./utils/utils.js"; -export const parentIpc = new OrderedPeer(window.parent, "*", [ - new CathodiqueConsumerHandler(new KeyedLatch()) as any, - new CathodiqueProviderHandler(componentList) as any, - new CathodiqueRemoteHandler(window.parent) as any, - new DOMRemoteHandler() as any, -]); +// We are removing the * for now because +// we would rather trust the parent. +// TODO expose public handle with API. +const parentIpc = new OrderedPeer(window.parent, canonicalHost); +parentIpc.addHandler(new CathodiqueRemoteHandler()); +parentIpc.addHandler(new CathodiqueProviderHandler(undefined)); +parentIpc.addHandler(new DOMRemoteHandler()); + +export { parentIpc }; diff --git a/src/modules/.common/utils/remoteToLocalAdapter.ts b/src/modules/.common/utils/remoteToLocalAdapter.ts new file mode 100644 index 0000000..74f1676 --- /dev/null +++ b/src/modules/.common/utils/remoteToLocalAdapter.ts @@ -0,0 +1,104 @@ +import { Component } from "../classes/component.js"; +import { ComponentListHandle } from "../classes/componentList.js"; +import { Latch, LatchState } from "../classes/latch.js"; +import { RemoteModule } from "../classes/module.js"; +import { OrderedPeer } from "../classes/orderedPeer.js"; +import { unwrapValue, WrappedValue, wrapValue } from "./wrap.js"; + +export class ComponentListProxy implements ComponentListHandle { + componentClasses = new Map ComponentInstanceProxy>(); + + get(componentName: string) { + return this.componentClasses.get(componentName); + } +} + +export class ComponentInstance { + ipc: OrderedPeer; + + module: RemoteModule; + + #args: any[] = []; + + componentName: string; + constructor(module: RemoteModule, componentName: string, options: { componentId: string } | { args: any[] }) { + this.module = module; + + this.ipc = this.module.peer; + this.componentName = componentName; + + if ("componentId" in options) this.#cidLatch.resolve!(options.componentId); + if ("args" in options) this.#args = options.args; + } + + #cidLatch = new Latch(); + get componentId() { return this.#cidLatch.promise; } + + ready = new Latch(); + + async init() { + if (this.#cidLatch.getState() === LatchState.Pending) { + await this.module.waitForComponent(this.componentName); + + await this.module.componentReady.get(this.componentName); + const componentId = await this.ipc.rpc("createInstance", { + className: this.componentName, + args: this.#args.map((v) => wrapValue(v)), + }); + this.#cidLatch.resolve!(componentId); + this.ready.resolve!(); + } + } +} +export type ComponentInstanceProxy = ComponentInstance + & Partial> + & { [Component.isComponentSymbol]: true }; + +function generateCalledOrAwaited({ called, awaited }: { called: (...args: any[]) => any, awaited: () => any }) { + const result = async function (...args: any[]) { + return called(...args); + }; + result.then = async (resolve: (a: any) => void) => { + resolve(await awaited()); + }; + return result; +} + +export function makeComponentProxy(module: RemoteModule, componentName: string, options: { componentId: string } | { args: any[] }): ComponentInstanceProxy { + const compInst = new ComponentInstance(module, componentName, options); + compInst.init(); + + return new Proxy(compInst, { + get(target, prop) { + if (prop === Component.isComponentSymbol) return true; + + if (target[prop as keyof typeof target]) return target[prop as keyof typeof target]; + + if (prop === "then") return undefined; + + return generateCalledOrAwaited({ + called: async function (...args: any[]) { + const id = await compInst.componentId; + + const val = await module.peer.rpc("callProperty", { + methodName: prop, + arguments: args.map((v) => wrapValue(v)), + componentId: id, + }); + + return unwrapValue(val, module); + }, + awaited: async () => { + const id = await compInst.componentId; + + const val = await module.peer.rpc("getProperty", { + propertyName: prop, + componentId: id, + }); + + return unwrapValue(val, module); + }, + }); + }, + }) as ComponentInstanceProxy; +} diff --git a/src/modules/.common/utils/types.ts b/src/modules/.common/utils/types.ts index bec276d..f26c6cb 100644 --- a/src/modules/.common/utils/types.ts +++ b/src/modules/.common/utils/types.ts @@ -1,3 +1,6 @@ +import z from "zod"; +import { OrderedPeer } from "../classes/orderedPeer"; + export interface ElementFromIpc { kind: "element"; tagName: string; @@ -23,8 +26,14 @@ export interface ArbitraryNodeFromIpc { export type NodeFromIpc = ElementFromIpc | TextNodeFromIpc | DocumentFragmentFromIpc | ArbitraryNodeFromIpc; -export interface EventFromIpc { - className: string; - type: string; - values: Record; +export const zodEventFromIpc = z.object({ + className: z.string(), + type: z.string(), + values: z.record(z.string(), z.any()), +}); +export type EventFromIpc = z.output; + +export interface HandlerContext { + ipc: OrderedPeer; + event: MessageEvent; } diff --git a/src/modules/.common/utils/utils.ts b/src/modules/.common/utils/utils.ts index 6d152f0..08e51f5 100644 --- a/src/modules/.common/utils/utils.ts +++ b/src/modules/.common/utils/utils.ts @@ -13,4 +13,5 @@ const [projectSubdomain, userSubdomain, ...hostList] = export { projectSubdomain, userSubdomain }; export const host = hostList.join('.') -export const hostWithoutSubdomain = `${location.protocol}//${hostList.join(".")}:${location.port}`; +export const canonicalHost = `${location.protocol}//${hostList.join(".")}:${location.port}`; +export const parentOrigin = await fetch("/.common/hostOverride").then((v) => v.text(), () => canonicalHost); diff --git a/src/modules/.common/utils/wrap.ts b/src/modules/.common/utils/wrap.ts new file mode 100644 index 0000000..9660609 --- /dev/null +++ b/src/modules/.common/utils/wrap.ts @@ -0,0 +1,68 @@ +import z from "zod"; +import { Component } from "../classes/component"; +import { componentList } from "../classes/componentList"; +import { SharedDOM } from "../classes/sharedDomRemote"; +import { ComponentInstanceProxy, makeComponentProxy } from "./remoteToLocalAdapter"; +import { RemoteModule } from "../classes/module"; +import { DummyNodeRegistry } from "../classes/sharedDomDummy"; + +export const zodWrappedValue = z.union([ + z.object({ + value: z.any(), + }), + z.object({ + type: z.literal("component"), + componentName: z.string(), + componentId: z.string(), + moduleId: z.string().optional(), // undefined => This module comes from myself + }), + z.object({ + type: z.literal("node"), + nodeId: z.string(), + }), +]); +export type WrappedValue = z.output; + +export async function wrapValue(value: any): Promise> { + const isComponent = (v: any): v is (Component | ComponentInstanceProxy) => v[Component.isComponentSymbol]; + if (isComponent(value)) { + const isRemote = "module" in value; + + return { + type: "component", + componentName: isRemote ? value.componentName : componentList.componentTypeOf(value), + componentId: isRemote ? await value.componentId : value.componentId, + moduleId: isRemote ? value.module.id : undefined, + }; + } + + if (value instanceof Node) { + const nodeId = SharedDOM.initOrGet(value); + + return { + type: "node", + nodeId, + }; + } + + return { value }; +} + +export function unwrapValue(value: any, fromModule: RemoteModule | undefined) { + const wrapped = zodWrappedValue.parse(value); + + if (!("type" in wrapped)) return wrapped.value; + + switch (wrapped.type) { + case "component": + const moduleId = wrapped.moduleId || fromModule?.id; + if (!moduleId) return undefined; + const module = RemoteModule.moduleById(moduleId); + if (!module?.componentExists(wrapped.componentId)) { + return undefined; + } + return makeComponentProxy(module, wrapped.componentName, { componentId: wrapped.componentId }); + case "node": + return DummyNodeRegistry.registryOf(fromModule?.peer.source || window.parent)!.getNode(value.nodeId); + } +} diff --git a/src/modules/cathodique.windowmanager/module.html b/src/modules/cathodique.windowmanager/module.html new file mode 100644 index 0000000..4abce79 --- /dev/null +++ b/src/modules/cathodique.windowmanager/module.html @@ -0,0 +1,45 @@ + + + + + + Test + + + + + + + diff --git a/src/modules/immjs.macos-aqua-windowframe/module.html b/src/modules/immjs.macos-aqua-windowframe/module.html index 6631710..08f2da6 100644 --- a/src/modules/immjs.macos-aqua-windowframe/module.html +++ b/src/modules/immjs.macos-aqua-windowframe/module.html @@ -13,20 +13,24 @@

+
diff --git a/tsconfig.modules.json b/tsconfig.modules.json index bbccf41..468edce 100644 --- a/tsconfig.modules.json +++ b/tsconfig.modules.json @@ -31,7 +31,7 @@ // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ - // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + "typeRoots": ["./src/types"], /* Specify multiple folders that act like './node_modules/@types'. */ // "types": [], /* Specify type package names to be included without being referenced in a source file. */ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ From 779448cb30a7233e5458c81ab0e35f0ea07bf362 Mon Sep 17 00:00:00 2001 From: Juliette Wang immjs Date: Sun, 4 Jan 2026 12:54:49 +0100 Subject: [PATCH 04/10] feat: Implement event emitters --- TODO.md | 3 + package-lock.json | 18 +++ package.json | 2 + src/main/protocols.ts | 4 - src/modules/.common/classes/component.ts | 45 +++++- src/modules/.common/classes/componentList.ts | 13 +- src/modules/.common/classes/latch.ts | 4 +- src/modules/.common/classes/module.ts | 35 +++-- .../.common/classes/sharedDomRemote.ts | 2 +- .../.common/ipcHandlers/cathodiqueConsumer.ts | 25 ++- .../.common/ipcHandlers/cathodiqueProvider.ts | 94 +++++++++-- .../.common/utils/remoteToLocalAdapter.ts | 42 ++++- src/modules/.common/utils/types.ts | 4 + src/modules/.common/utils/utils.ts | 8 + src/modules/.common/utils/wrap.ts | 2 +- src/renderer/classes/handlers/dom/base.ts | 2 +- src/renderer/classes/handlers/dom/popup.ts | 2 +- .../classes/handlers/dom/subsurface.ts | 4 +- src/renderer/classes/handlers/dom/surface.ts | 11 +- src/renderer/classes/handlers/dom/toplevel.ts | 14 +- src/renderer/classes/handlers/handlers.ts | 2 +- .../lib/toplevel_decoration_manager.ts | 2 +- src/renderer/classes/wayland/output/output.ts | 4 +- src/renderer/classes/wayland/seat/seat.ts | 4 +- src/renderer/host/classes/component.ts | 39 ++++- src/renderer/host/classes/componentList.ts | 12 +- src/renderer/host/classes/module.ts | 148 ++++++++++-------- src/renderer/host/classes/orchestrator.ts | 4 +- .../host/ipcHandlers/cathodiqueConsumer.ts | 25 ++- .../host/ipcHandlers/cathodiqueHost.ts | 4 +- .../host/ipcHandlers/cathodiqueProvider.ts | 72 +++++++-- .../host/utils/remoteToLocalAdapter.ts | 21 ++- src/renderer/host/utils/types.ts | 3 + src/renderer/host/utils/utils.ts | 14 +- src/renderer/host/utils/wrap.ts | 2 +- src/renderer/wayland/index.ts | 30 ++-- .../wayland/overlays/outputRegistryOverlay.ts | 32 ++++ .../wayland/overlays/seatRegistryOverlay.ts | 32 ++++ tsconfig.json | 2 +- tsconfig.modules.json | 2 +- 40 files changed, 589 insertions(+), 199 deletions(-) create mode 100644 src/renderer/wayland/overlays/outputRegistryOverlay.ts create mode 100644 src/renderer/wayland/overlays/seatRegistryOverlay.ts diff --git a/TODO.md b/TODO.md index 49a9e9d..80280f1 100644 --- a/TODO.md +++ b/TODO.md @@ -3,3 +3,6 @@ - Component lifecycle, managed through a FinReg in every place a handle of a component was provided - Prevent scripts, meta tag - Protect styling with shadowRoot + +# To consider +- Should cathodique events follow $ also? diff --git a/package-lock.json b/package-lock.json index 53b9964..2f07500 100755 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@typescript/native-preview": "^7.0.0-dev.20251220.1", "electron": "35.6", "esbuild": "^0.27.2", + "events": "^3.3.0", "koffi": "^2.14.0", "node-abi": "^4.12.0", "xml-parser": "^1.2.1", @@ -24,6 +25,7 @@ "devDependencies": { "@electron/rebuild": "^3.7.1", "@types/electron": "^1.4.38", + "@types/events": "^3.0.3", "@types/node": "^22.10.2", "prettier": "3.7.4", "typescript": "^5.7.2" @@ -830,6 +832,13 @@ "@types/node": "*" } }, + "node_modules/@types/events": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.3.tgz", + "integrity": "sha512-trOc4AAUThEz9hapPtSd7wf5tiQKvTtu5b371UxXdTuqzIh0ArcRspRP0i0Viu+LXstIQ1z96t1nsPxT9ol01g==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/http-cache-semantics": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", @@ -1617,6 +1626,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/exponential-backoff": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.1.tgz", diff --git a/package.json b/package.json index 19c3ecd..3d6d3fb 100755 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "devDependencies": { "@electron/rebuild": "^3.7.1", "@types/electron": "^1.4.38", + "@types/events": "^3.0.3", "@types/node": "^22.10.2", "prettier": "3.7.4", "typescript": "^5.7.2" @@ -36,6 +37,7 @@ "@typescript/native-preview": "^7.0.0-dev.20251220.1", "electron": "35.6", "esbuild": "^0.27.2", + "events": "^3.3.0", "koffi": "^2.14.0", "node-abi": "^4.12.0", "xml-parser": "^1.2.1", diff --git a/src/main/protocols.ts b/src/main/protocols.ts index e1116d4..782817f 100644 --- a/src/main/protocols.ts +++ b/src/main/protocols.ts @@ -67,10 +67,6 @@ export const registerProtocols = () => { return callback({ cancel: false }); } - if (url.host.endsWith(".raytu.be") || url.host === "raytu.be") { - return callback({ cancel: false }); - } - // TODO: Handle permissions of each module. For now though... callback({ cancel: true }); } diff --git a/src/modules/.common/classes/component.ts b/src/modules/.common/classes/component.ts index 313cccf..c0ae098 100644 --- a/src/modules/.common/classes/component.ts +++ b/src/modules/.common/classes/component.ts @@ -1,14 +1,15 @@ import { parentIpc } from "../parentIpc.js"; import { nanoid } from "../utils/utils.js"; +import { wrapValue } from "../utils/wrap.js"; import { RemoteModule } from "./module.js"; import { OrderedPeer } from "./orderedPeer.js"; import z from "zod"; export interface ComponentContext { - ipc: OrderedPeer; + peer: OrderedPeer; } export type ComponentHandle = { - ipc: OrderedPeer; + peer: OrderedPeer; componentId: string | Promise; init(): any; @@ -20,22 +21,22 @@ export abstract class Component extends EventTarget { static isComponentSymbol: typeof isComponentSymbol = isComponentSymbol; componentId: string; - ipc: OrderedPeer; + peer: OrderedPeer; [isComponentSymbol] = true; constructor(ctx: ComponentContext) { super(); - this.ipc = ctx.ipc; + this.peer = ctx.peer; this.componentId = nanoid(); } abstract init(): any; post(obj: Record) { - return this.ipc.post({ ...obj, componentHandle: this.componentId }); + return this.peer.post({ ...obj, componentHandle: this.componentId }); } rpc(type: string, data: Record, obj: Record = {}) { - return this.ipc.rpc(type, data, { ...obj, componentHandle: this.componentId }); + return this.peer.rpc(type, data, { ...obj, componentHandle: this.componentId }); } async #getDependencyRpc(dependency: string) { @@ -62,4 +63,36 @@ export abstract class Component extends EventTarget { const handles = await this.#getAllDependencyRpc(dependency); return handles.map((v) => RemoteModule.getOrCreate(v.port, v.id).localHandle); } + + #listenersFromRemote = new Map>(); + listenFor(eventName: string, peer: OrderedPeer) { + const innerSet = this.#listenersFromRemote.get(eventName) || new Set(); + if (!this.#listenersFromRemote.has(eventName)) this.#listenersFromRemote.set(eventName, innerSet); + + innerSet.add(peer); + } + unlistenFor(eventName: string, peer: OrderedPeer) { + const innerSet = this.#listenersFromRemote.get(eventName) || new Set(); + + innerSet.delete(peer); + + if (innerSet.size === 0) this.#listenersFromRemote.delete(eventName); + } + async emit(eventName: string, args: any[]) { + const wrapped = args.map((v) => wrapValue(v)) + + const innerSet = this.#listenersFromRemote.get(eventName); + if (!innerSet) return; + + await Promise.all( + [...innerSet] + .map((peer) => + peer.rpc("emitEvent", { + componentId: this.componentId, + eventName: eventName, + args: wrapped, + }) + ), + ); + } } diff --git a/src/modules/.common/classes/componentList.ts b/src/modules/.common/classes/componentList.ts index 164d27b..499eff2 100644 --- a/src/modules/.common/classes/componentList.ts +++ b/src/modules/.common/classes/componentList.ts @@ -1,10 +1,11 @@ +import { ComponentHandleClass } from "../utils/types.js"; import { Component, ComponentHandle } from "./component.js"; export class InvalidComponentError extends Error {} export class ComponentList extends EventTarget { - componentClasses = new Map ComponentHandle>(); - componentClassToClassName = new Map ComponentHandle, string>() + componentClasses = new Map(); + componentClassToClassName = new Map() componentTypeOf(component: Component) { // Traversing prototype chain (from most specific to least specific) @@ -21,7 +22,7 @@ export class ComponentList extends EventTarget { // Implications: The object has had [Symbol(Component.isComponentSymbol)] set to true but was not a component } - register(componentName: string, componentClass: new (...args: any[]) => ComponentHandle) { + register(componentName: string, componentClass: ComponentHandleClass) { if (this.componentClasses.has(componentName)) throw new Error("This component already exists"); @@ -32,11 +33,15 @@ export class ComponentList extends EventTarget { get(componentName: string) { return this.componentClasses.get(componentName); } + + has(componentName: string) { + return this.componentClasses.has(componentName); + } } export type ComponentListHandle = { get(componentName: string): undefined - | (new (...args: any[]) => ComponentHandle); + | ComponentHandleClass; }; export const componentList = new ComponentList(); diff --git a/src/modules/.common/classes/latch.ts b/src/modules/.common/classes/latch.ts index a60bfd4..fc055a7 100644 --- a/src/modules/.common/classes/latch.ts +++ b/src/modules/.common/classes/latch.ts @@ -17,10 +17,10 @@ export class Latch { // Assignment with side effect onto resultingResolve this.promise = new Promise((r) => { resultingResolve = r }); - this.resolve = function (this: Latch, r: T) { + this.resolve = (r: T) => { resultingResolve(r); this.resolve = undefined; - }.bind(this); + }; } } diff --git a/src/modules/.common/classes/module.ts b/src/modules/.common/classes/module.ts index f706693..14e7fc4 100644 --- a/src/modules/.common/classes/module.ts +++ b/src/modules/.common/classes/module.ts @@ -8,7 +8,7 @@ import { DummyNodeRegistry } from "./sharedDomDummy.js"; // Mainly, the components made available (through keyed latch componentReady) export class RemoteModule { static summonnedModulesByPort = new Map(); - static summonnedModulesById = new Map(); + static summonnedModulesByOpaqueToken = new Map(); static getOrCreate(port: MessagePort, id: string) { if (this.summonnedModulesByPort.has(port)) return this.summonnedModulesByPort.get(port)!; @@ -16,21 +16,32 @@ export class RemoteModule { return this.createModule(port, id); } - static createModule(port: MessagePort, id: string) { - const mod = new RemoteModule(port, id); + static createModule(port: MessagePort, opaqueToken: string) { + const mod = new RemoteModule(port, opaqueToken); this.summonnedModulesByPort.set(port, mod); - this.summonnedModulesById.set(id, mod) + this.summonnedModulesByOpaqueToken.set(opaqueToken, mod) return mod; } - static moduleById(id: string) { - return this.summonnedModulesById.get(id); + static moduleByOpaqueToken(opaqueToken: string) { + return this.summonnedModulesByOpaqueToken.get(opaqueToken); } static moduleOfPort(port: MessagePort) { return this.summonnedModulesByPort.get(port); } + #componentInstances = new Map(); // TODO Memory management + registerInstanceProxy(id: string, comp: ComponentInstanceProxy) { + this.#componentInstances.set(id, comp); + } + instanceProxyExists(componentId: string) { + return this.#componentInstances.get(componentId); + } + getInstanceProxy(componentId: string) { + return this.#componentInstances.get(componentId); + } + id: string; port: MessagePort; @@ -41,7 +52,7 @@ export class RemoteModule { // "*" is fine because we can trust the origin who passed the messageport onto us this.peer = new OrderedPeer(port, "*"); - this.peer.addHandler(new CathodiqueConsumerHandler(this.componentReady)); + this.peer.addHandler(new CathodiqueConsumerHandler(this)); DummyNodeRegistry.setRegistry(port, new DummyNodeRegistry(port)); } @@ -53,14 +64,14 @@ export class RemoteModule { } localHandle = { - get: function (this: RemoteModule, componentName: string) { + get: (componentName: string) => { return function (this: RemoteModule, ...args: any[]) { return makeComponentProxy(this, componentName, { args }); }.bind(this) as unknown as new () => ComponentInstanceProxy; - }.bind(this), - } + }, + }; - async componentExists(id: string) { - return await this.peer.rpc("componentExists", { componentId: id }); + async componentExists(componentId: string) { + return await this.peer.rpc("componentExists", { componentId }); } } diff --git a/src/modules/.common/classes/sharedDomRemote.ts b/src/modules/.common/classes/sharedDomRemote.ts index 0878d15..2cb873b 100644 --- a/src/modules/.common/classes/sharedDomRemote.ts +++ b/src/modules/.common/classes/sharedDomRemote.ts @@ -112,7 +112,7 @@ export class SharedDOM { static registerSubtree(node: Node) { const id = NodeRegistry.getId(node); if (node instanceof HTMLTemplateElement) this.registerSubtree(node.content); - node.childNodes.forEach(function (this: typeof SharedDOM, n: Node) { this.registerSubtree(n) }.bind(this)); + node.childNodes.forEach((n: Node) => this.registerSubtree(n)); parentIpc.post({ type: "createNode", diff --git a/src/modules/.common/ipcHandlers/cathodiqueConsumer.ts b/src/modules/.common/ipcHandlers/cathodiqueConsumer.ts index 761998a..8f7fcc6 100644 --- a/src/modules/.common/ipcHandlers/cathodiqueConsumer.ts +++ b/src/modules/.common/ipcHandlers/cathodiqueConsumer.ts @@ -1,14 +1,15 @@ import z from "zod"; -import { KeyedLatch } from "../classes/latch.js"; import { HandlerContext } from "../utils/types.js"; +import { RemoteModule } from "../classes/module.js"; +import { unwrapValue, WrappedValue, zodWrappedValue } from "../utils/wrap.js"; export class CathodiqueConsumerHandler { [k: string]: (arg: Record, ctx: HandlerContext) => any; - #instanceReady: KeyedLatch; + #module: RemoteModule; - constructor(instanceReady: KeyedLatch) { - this.#instanceReady = instanceReady; + constructor(module: RemoteModule) { + this.#module = module; } componentRegistered(args: Record) { @@ -17,6 +18,20 @@ export class CathodiqueConsumerHandler { }).parse(args)); } async #componentRegistered({ data }: { data: { componentName: string } }) { - this.#instanceReady.resolve(data.componentName, undefined); + this.#module.componentReady.resolve(data.componentName, undefined); + } + + emitEvent(arg: Record) { + return this.#emitEvent(z.object({ + data: z.object({ + eventName: z.string(), + componentId: z.string(), + args: z.array(zodWrappedValue), + }), + }).parse(arg)); + } + #emitEvent({ data }: { data: { eventName: string, componentId: string, args: WrappedValue[] } }) { + const unwrappedArgs = data.args.map((v) => unwrapValue(v, this.#module)); + this.#module.getInstanceProxy(data.componentId)?.emit(data.eventName, ...unwrappedArgs); } }; diff --git a/src/modules/.common/ipcHandlers/cathodiqueProvider.ts b/src/modules/.common/ipcHandlers/cathodiqueProvider.ts index d4de855..1a2a86c 100644 --- a/src/modules/.common/ipcHandlers/cathodiqueProvider.ts +++ b/src/modules/.common/ipcHandlers/cathodiqueProvider.ts @@ -4,6 +4,8 @@ import { HandlerContext } from "../utils/types.js"; import { unwrapValue, WrappedValue, wrapValue, zodWrappedValue } from "../utils/wrap.js"; import { Component } from "../classes/component.js"; import { RemoteModule } from "../classes/module.js"; +import { ShouldHaveBeenZodError, stringStartsWithDollar } from "../utils/utils.js"; +import { parentIpc } from "../parentIpc.js"; // DISTINCTIONS WITH HOST // A single module has a single componentList @@ -12,12 +14,17 @@ import { RemoteModule } from "../classes/module.js"; // A single module determines its own IDs thus makes ID attacks impossible // So componentInstances need be static +// TODO: Manage lifecycle of component + export class CathodiqueProviderHandler { [k: string]: (arg: Record, ctx: HandlerContext) => any; static #componentList: ComponentList = componentList; - static #componentInstances = new Map(); + static #componentInstances = new Map(); + static componentExists(id: string) { + return this.#componentInstances.has(id); + } #fromModule: RemoteModule | undefined; constructor(fromModule: RemoteModule | undefined) { @@ -27,18 +34,16 @@ export class CathodiqueProviderHandler { createInstance(arg: Record) { return this.#createInstance(z.object({ data: z.object({ - className: z.string(), + className: z.string().refine(CathodiqueProviderHandler.#componentList.has), args: z.array(zodWrappedValue), }), }).parse(arg)); } async #createInstance({ data }: { data: { className: string, args: WrappedValue[] } }) { const ClassObj = CathodiqueProviderHandler.#componentList.get(data.className); - if (!ClassObj) return; // TODO error here + if (!ClassObj) throw new ShouldHaveBeenZodError(); - const unwrapped = data.args.map(function (this: CathodiqueProviderHandler, v: WrappedValue) { - return unwrapValue(v, this.#fromModule) - }.bind(this)); + const unwrapped = data.args.map((v: WrappedValue) => unwrapValue(v, this.#fromModule)); const componentInstance = new ClassObj(...unwrapped) as Component; await componentInstance.init(); @@ -61,18 +66,37 @@ export class CathodiqueProviderHandler { return CathodiqueProviderHandler.#componentInstances.has(data.componentId); } + getInstanceData(arg: Record) { + return this.#getInstanceData(z.object({ + data: z.object({ + componentId: z.string(), + }), + }).parse(arg)); + } + #getInstanceData({ data }: { data: { componentId: string } }) { + const instance = CathodiqueProviderHandler.#componentInstances.get(data.componentId); + return instance && { + componentName: componentList.componentTypeOf(instance), + }; + } + getProperty(arg: Record) { return this.#getProperty(z.object({ data: z.object({ - propertyName: z.string(), - componentId: z.string(), + propertyName: z.string().startsWith("$"), + componentId: z.string().refine(CathodiqueProviderHandler.componentExists), }), }).parse(arg)); } async #getProperty({ data }: { data: { propertyName: string; componentId: string } }) { const component = CathodiqueProviderHandler.#componentInstances.get(data.componentId); + if (!component) throw new ShouldHaveBeenZodError(); + + // Zod... + const propertyName = data.propertyName; + if (!stringStartsWithDollar(propertyName)) throw new ShouldHaveBeenZodError(); - const value = component[data.propertyName]; + const value = component[propertyName]; return wrapValue(value); } @@ -80,9 +104,9 @@ export class CathodiqueProviderHandler { callProperty(arg: Record) { return this.#callProperty(z.object({ data: z.object({ - methodName: z.string(), + methodName: z.string().startsWith('$'), arguments: z.array(z.any()), - componentId: z.string(), + componentId: z.string().refine(CathodiqueProviderHandler.componentExists), }), }).parse(arg)); } @@ -93,10 +117,54 @@ export class CathodiqueProviderHandler { componentId: string; }; }) { - const component = CathodiqueProviderHandler.#componentInstances.get(data.componentId); + const component = CathodiqueProviderHandler.#componentInstances.get(data.componentId)!; + + // Zod... + const methodName = data.methodName; + if (!stringStartsWithDollar(methodName)) throw new ShouldHaveBeenZodError(); - const value = await component?.[data.methodName]?.(...data.arguments); + const value = await component?.[methodName]?.(...data.arguments); return wrapValue(value); } + + listenToEvent(arg: Record) { + return this.#listenToEvent(z.object({ + data: z.object({ + eventName: z.string(), + componentId: z.string(), + }), + }).parse(arg)); + } + async #listenToEvent({ data }: { + data: { + eventName: string; + componentId: string; + }; + }) { + const component = CathodiqueProviderHandler.#componentInstances.get(data.componentId); + if (!component) throw new ShouldHaveBeenZodError(); + + component.listenFor(data.eventName, this.#fromModule?.peer ?? parentIpc); + } + + unlistenToEvent(arg: Record) { + return this.#unlistenToEvent(z.object({ + data: z.object({ + eventName: z.string(), + componentId: z.string().refine(CathodiqueProviderHandler.componentExists), + }), + }).parse(arg)); + } + async #unlistenToEvent({ data }: { + data: { + eventName: string; + componentId: string; + }; + }) { + const component = CathodiqueProviderHandler.#componentInstances.get(data.componentId); + if (!component) throw new ShouldHaveBeenZodError(); + + component.unlistenFor(data.eventName, this.#fromModule?.peer ?? parentIpc); + } }; diff --git a/src/modules/.common/utils/remoteToLocalAdapter.ts b/src/modules/.common/utils/remoteToLocalAdapter.ts index 74f1676..e8dd71f 100644 --- a/src/modules/.common/utils/remoteToLocalAdapter.ts +++ b/src/modules/.common/utils/remoteToLocalAdapter.ts @@ -3,7 +3,8 @@ import { ComponentListHandle } from "../classes/componentList.js"; import { Latch, LatchState } from "../classes/latch.js"; import { RemoteModule } from "../classes/module.js"; import { OrderedPeer } from "../classes/orderedPeer.js"; -import { unwrapValue, WrappedValue, wrapValue } from "./wrap.js"; +import { unwrapValue, wrapValue } from "./wrap.js"; +import { EventEmitter } from "events"; export class ComponentListProxy implements ComponentListHandle { componentClasses = new Map ComponentInstanceProxy>(); @@ -13,8 +14,8 @@ export class ComponentListProxy implements ComponentListHandle { } } -export class ComponentInstance { - ipc: OrderedPeer; +export class ComponentInstance extends EventEmitter { + peer: OrderedPeer; module: RemoteModule; @@ -22,13 +23,31 @@ export class ComponentInstance { componentName: string; constructor(module: RemoteModule, componentName: string, options: { componentId: string } | { args: any[] }) { + super(); this.module = module; - this.ipc = this.module.peer; + this.peer = this.module.peer; this.componentName = componentName; if ("componentId" in options) this.#cidLatch.resolve!(options.componentId); if ("args" in options) this.#args = options.args; + + // TODO How to clean? + // Will it undo itself? Since there is no way it can emit and it's not ref'd + this.on("newListener", async function (this: ComponentInstance, evtName: string) { + if (evtName === "newListener" || evtName === "removeListener") return; + + if (this.listenerCount(evtName) === 0) { + this.peer.rpc("listenToEvent", { componentId: await this.componentId, eventName: evtName }); + } + }.bind(this)); + this.on("removeListener", async function (this: ComponentInstance, evtName: string) { + if (evtName === "newListener" || evtName === "removeListener") return; + + if (this.listenerCount(evtName) === 0) { + this.peer.rpc("unlistenToEvent", { componentId: await this.componentId, eventName: evtName }); + } + }.bind(this)); } #cidLatch = new Latch(); @@ -41,7 +60,7 @@ export class ComponentInstance { await this.module.waitForComponent(this.componentName); await this.module.componentReady.get(this.componentName); - const componentId = await this.ipc.rpc("createInstance", { + const componentId = await this.peer.rpc("createInstance", { className: this.componentName, args: this.#args.map((v) => wrapValue(v)), }); @@ -65,10 +84,14 @@ function generateCalledOrAwaited({ called, awaited }: { called: (...args: any[]) } export function makeComponentProxy(module: RemoteModule, componentName: string, options: { componentId: string } | { args: any[] }): ComponentInstanceProxy { + if ("componentId" in options && module.instanceProxyExists(options.componentId)) { + return module.getInstanceProxy(options.componentId)!; + } + const compInst = new ComponentInstance(module, componentName, options); compInst.init(); - return new Proxy(compInst, { + const compInstProxy = new Proxy(compInst, { get(target, prop) { if (prop === Component.isComponentSymbol) return true; @@ -101,4 +124,11 @@ export function makeComponentProxy(module: RemoteModule, componentName: string, }); }, }) as ComponentInstanceProxy; + + (async () => { + const cid = await compInst.componentId; + module.registerInstanceProxy(cid, compInstProxy); + })(); + + return compInstProxy; } diff --git a/src/modules/.common/utils/types.ts b/src/modules/.common/utils/types.ts index f26c6cb..df1abdd 100644 --- a/src/modules/.common/utils/types.ts +++ b/src/modules/.common/utils/types.ts @@ -1,5 +1,6 @@ import z from "zod"; import { OrderedPeer } from "../classes/orderedPeer"; +import { Component, ComponentHandle } from "../classes/component"; export interface ElementFromIpc { kind: "element"; @@ -37,3 +38,6 @@ export interface HandlerContext { ipc: OrderedPeer; event: MessageEvent; } + +export type ComponentClass = new (...a: any[]) => Component; +export type ComponentHandleClass = new (...a: any[]) => ComponentHandle; diff --git a/src/modules/.common/utils/utils.ts b/src/modules/.common/utils/utils.ts index 08e51f5..a055ab5 100644 --- a/src/modules/.common/utils/utils.ts +++ b/src/modules/.common/utils/utils.ts @@ -1,6 +1,12 @@ let alphabet = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict"; +export class ShouldHaveBeenZodError extends Error { + constructor(message?: string) { + super(message || "This error should have been caught by zod"); + } +} + export function nanoid(e = 21) { let t = "", r = crypto.getRandomValues(new Uint8Array(e)); @@ -15,3 +21,5 @@ export const host = hostList.join('.') export const canonicalHost = `${location.protocol}//${hostList.join(".")}:${location.port}`; export const parentOrigin = await fetch("/.common/hostOverride").then((v) => v.text(), () => canonicalHost); + +export const stringStartsWithDollar = (v: string): v is `$${string}` => v.startsWith("$"); diff --git a/src/modules/.common/utils/wrap.ts b/src/modules/.common/utils/wrap.ts index 9660609..6252d83 100644 --- a/src/modules/.common/utils/wrap.ts +++ b/src/modules/.common/utils/wrap.ts @@ -57,7 +57,7 @@ export function unwrapValue(value: any, fromModule: RemoteModule | undefined) { case "component": const moduleId = wrapped.moduleId || fromModule?.id; if (!moduleId) return undefined; - const module = RemoteModule.moduleById(moduleId); + const module = RemoteModule.moduleByOpaqueToken(moduleId); if (!module?.componentExists(wrapped.componentId)) { return undefined; } diff --git a/src/renderer/classes/handlers/dom/base.ts b/src/renderer/classes/handlers/dom/base.ts index cf46c0f..d5a7f4b 100644 --- a/src/renderer/classes/handlers/dom/base.ts +++ b/src/renderer/classes/handlers/dom/base.ts @@ -1,4 +1,4 @@ -import { BaseObject } from "@cathodique/wl-serv-high/dist/objects/base_object.js"; +import { BaseObject } from "@cathodique/wl-serv-high/objects"; export class BaseDom { wl: From; diff --git a/src/renderer/classes/handlers/dom/popup.ts b/src/renderer/classes/handlers/dom/popup.ts index bc1e588..e2ed8fa 100644 --- a/src/renderer/classes/handlers/dom/popup.ts +++ b/src/renderer/classes/handlers/dom/popup.ts @@ -1,5 +1,5 @@ import { BaseDom } from "./base.js"; -import { XdgPopup } from "@cathodique/wl-serv-high/dist/objects/xdg_popup.js"; +import { XdgPopup } from "@cathodique/wl-serv-high/objects"; // Toplevels: context for other subsurfaces to appear in // Popups are the same diff --git a/src/renderer/classes/handlers/dom/subsurface.ts b/src/renderer/classes/handlers/dom/subsurface.ts index 9f1bfa2..1037488 100644 --- a/src/renderer/classes/handlers/dom/subsurface.ts +++ b/src/renderer/classes/handlers/dom/subsurface.ts @@ -1,7 +1,7 @@ -import { WlSubsurface } from "@cathodique/wl-serv-high/dist/objects/wl_subsurface.js"; +import { WlSubsurface } from "@cathodique/wl-serv-high/objects"; import { BaseDom } from "./base.js"; import { SurfaceDom } from "./surface.js"; -import { WlSurface } from "@cathodique/wl-serv-high/dist/objects/wl_surface.js"; +import { WlSurface } from "@cathodique/wl-serv-high/objects"; export class SubsurfaceDom extends BaseDom { static wlToSubsurfaceDom = new Map(); diff --git a/src/renderer/classes/handlers/dom/surface.ts b/src/renderer/classes/handlers/dom/surface.ts index b0efa01..0549cc7 100644 --- a/src/renderer/classes/handlers/dom/surface.ts +++ b/src/renderer/classes/handlers/dom/surface.ts @@ -1,8 +1,11 @@ -import { WlSurface } from "@cathodique/wl-serv-high/dist/objects/wl_surface.js"; +import { WlSurface } from "@cathodique/wl-serv-high/objects"; import { Seat } from "../../wayland/seat/seat.js"; import { BaseDom } from "./base.js"; import { Output } from "../../wayland/output/output.js"; +// HEY JULIETTE!! THIS IS WHERE WE AT +// WE NEED TO CREATE THE ACTUAL COMPONENT, THEN HOOK IT UP!! + export class SurfaceDom extends BaseDom { static wlToSurfaceDom = new Map(); @@ -41,13 +44,11 @@ export class SurfaceDom extends BaseDom { b.updateBufferArea(rect.y, rect.x, rect.h, rect.w) } const arr = new Uint8ClampedArray( - b.buffer.buffer, + b.buffer.buffer as ArrayBuffer, 0, b.meta.width * b.meta.height * 4, ); if (arr.length > 0) { - // Idk what the f*** tsgo is doing here.... anyways - // @ts-ignore let imageData = new ImageData(arr, b.meta.width, b.meta.height); for (const rect of currlyDamagedBuffer) { @@ -63,6 +64,8 @@ export class SurfaceDom extends BaseDom { // Unsure vvv this.dom.remove(); }); + + // this.initSeatMouse(); } initSeatMouse(seat: Seat) { diff --git a/src/renderer/classes/handlers/dom/toplevel.ts b/src/renderer/classes/handlers/dom/toplevel.ts index 559eec7..ebbb888 100644 --- a/src/renderer/classes/handlers/dom/toplevel.ts +++ b/src/renderer/classes/handlers/dom/toplevel.ts @@ -1,17 +1,25 @@ -import { XdgToplevel } from "@cathodique/wl-serv-high/dist/objects/xdg_toplevel.js"; +import { XdgToplevel } from "@cathodique/wl-serv-high/objects"; import { BaseDom } from "./base.js"; import { orchestrator } from "../../../host/index.js"; import { ComponentHandle } from "../../../host/classes/component.js"; +import { BaseModule, LocalModule } from "../../../host/classes/module.js"; export class ToplevelDom extends BaseDom { static wlToToplevelDom = new Map(); + static async create(wl: XdgToplevel) { + const windowFrame = await orchestrator.load("WindowFrame"); + if (!windowFrame) throw new Error("Expected existing WindowFrame"); + + return new this(wl, windowFrame); + } + instance: ComponentHandle; - constructor(wl: XdgToplevel) { + private constructor(wl: XdgToplevel, windowFrameModule: BaseModule) { super(wl, document.createElement("div")); ToplevelDom.wlToToplevelDom.set(wl, this); - this.instance = new (orchestrator.load("WindowFrame")!.localHandle.get("WindowFrame")!)(); + this.instance = new (windowFrameModule.localHandle.get("WindowFrame")!)(); this.init(); } diff --git a/src/renderer/classes/handlers/handlers.ts b/src/renderer/classes/handlers/handlers.ts index 42f453e..b76b841 100644 --- a/src/renderer/classes/handlers/handlers.ts +++ b/src/renderer/classes/handlers/handlers.ts @@ -1,4 +1,4 @@ -import { WlSurface } from "@cathodique/wl-serv-high/dist/objects/wl_surface.js"; +import { WlSurface } from "@cathodique/wl-serv-high/objects"; import { PopupDom } from "./dom/popup.js"; import { ToplevelDom } from "./dom/toplevel.js"; diff --git a/src/renderer/classes/handlers/lib/toplevel_decoration_manager.ts b/src/renderer/classes/handlers/lib/toplevel_decoration_manager.ts index 088fdb7..b8bc7c0 100644 --- a/src/renderer/classes/handlers/lib/toplevel_decoration_manager.ts +++ b/src/renderer/classes/handlers/lib/toplevel_decoration_manager.ts @@ -1,4 +1,4 @@ -import { ZxdgToplevelDecorationV1 } from "@cathodique/wl-serv-high/dist/objects/zxdg_decoration_manager_v1.js"; +import { ZxdgToplevelDecorationV1 } from "@cathodique/wl-serv-high/objects"; export class ZxdgToplevelDecorationManager { wl: ZxdgToplevelDecorationV1; diff --git a/src/renderer/classes/wayland/output/output.ts b/src/renderer/classes/wayland/output/output.ts index d94096f..deef64c 100644 --- a/src/renderer/classes/wayland/output/output.ts +++ b/src/renderer/classes/wayland/output/output.ts @@ -1,5 +1,5 @@ -import { OutputConfiguration } from "@cathodique/wl-serv-high/dist/objects/wl_output.js"; -import { OutputRegistry } from "@cathodique/wl-serv-high/dist/registries/output.js"; +import { OutputConfiguration } from "@cathodique/wl-serv-high/objects"; +import { OutputRegistry } from "@cathodique/wl-serv-high/registries"; export class Output { configToOutput = new Map(); diff --git a/src/renderer/classes/wayland/seat/seat.ts b/src/renderer/classes/wayland/seat/seat.ts index 9ebfc69..584d3e4 100644 --- a/src/renderer/classes/wayland/seat/seat.ts +++ b/src/renderer/classes/wayland/seat/seat.ts @@ -1,9 +1,9 @@ -import { SeatAuthority, SeatInstances, SeatRegistry } from "@cathodique/wl-serv-high/dist/registries/seat.js"; +import { SeatAuthority, SeatInstances, SeatRegistry } from "@cathodique/wl-serv-high/registries"; import { Modifiers } from "./modifiers.js"; import { codeToScan } from "./codeToScancode.js"; import { SurfaceDom } from "../../handlers/dom/surface.js"; import { isInRegion } from "../../../wayland/index.js"; -import { SeatConfiguration } from "@cathodique/wl-serv-high/dist/objects/wl_seat.js"; +import { SeatConfiguration } from "@cathodique/wl-serv-high/objects"; export class Seat { static mouseWebToButtonMap: Record = { diff --git a/src/renderer/host/classes/component.ts b/src/renderer/host/classes/component.ts index 42ffb2f..216198c 100644 --- a/src/renderer/host/classes/component.ts +++ b/src/renderer/host/classes/component.ts @@ -1,6 +1,8 @@ import { nanoid } from "../utils/utils.js"; +import { wrapValue } from "../utils/wrap.js"; import { BaseModule, LocalModule } from "./module.js"; import { orchestrator } from "./orchestrator.js"; +import { OrderedPeer } from "./orderedPeer.js"; export interface ComponentContext { module: BaseModule; @@ -28,14 +30,41 @@ export class Component extends EventTarget { this.module = module; } - init() {} + async init() {} - getDependency(dependency: string) { - const newMod = orchestrator.load(dependency); + async getDependency(dependency: string) { + const newMod = await orchestrator.load(dependency); return newMod?.localHandle; } - getAllDependency(dependency: string) { - const newMod = orchestrator.loadAll(dependency); + async getAllDependency(dependency: string) { + const newMod = await orchestrator.loadAll(dependency); return newMod?.map((v) => v.localHandle); } + #listenersFromRemote = new Map>(); + listenFor(eventName: string, peer: OrderedPeer) { + const innerSet = this.#listenersFromRemote.get(eventName) || new Set(); + if (!this.#listenersFromRemote.has(eventName)) this.#listenersFromRemote.set(eventName, innerSet); + + innerSet.add(peer); + } + unlistenFor(eventName: string, peer: OrderedPeer) { + const innerSet = this.#listenersFromRemote.get(eventName) || new Set(); + + innerSet.delete(peer); + + if (innerSet.size === 0) this.#listenersFromRemote.delete(eventName); + } + emit(eventName: string, args: any[]) { + const wrapped = args.map((v) => wrapValue(v)) + + const innerSet = this.#listenersFromRemote.get(eventName); + if (!innerSet) return; + for (const peer of innerSet) { + peer.rpc("emitEvent", { + componentId: this.componentId, + eventName: eventName, + args: wrapped, + }); + } + } }; diff --git a/src/renderer/host/classes/componentList.ts b/src/renderer/host/classes/componentList.ts index 813bc91..3ca09b6 100644 --- a/src/renderer/host/classes/componentList.ts +++ b/src/renderer/host/classes/componentList.ts @@ -1,8 +1,10 @@ +import { ComponentClass, ComponentHandleClass } from "../utils/types.js"; import { Component, ComponentHandle } from "./component.js"; import { LocalModule } from "./module.js"; export class InvalidComponentError extends Error {} +// ASSUMPTION: All components will return themselves. export class ComponentList extends EventTarget { componentClasses = new Map ComponentHandle>(); componentClassToClassName = new Map ComponentHandle, string>() @@ -28,7 +30,7 @@ export class ComponentList extends EventTarget { // Implications: The object has had [Symbol(Component.isComponentSymbol)] set to true but was not a component } - register(componentName: string, componentClass: new (mod: LocalModule) => ComponentHandle) { + register(componentName: string, componentClass: new (mod: LocalModule) => Component) { if (this.componentClasses.has(componentName)) throw new Error("This component already exists"); @@ -42,11 +44,15 @@ export class ComponentList extends EventTarget { return function (this: ComponentList, ...args: any[]) { return new InnerClass(this.module, ...args); - }.bind(this) as unknown as new (...args: any[]) => ComponentHandle; + }.bind(this) as unknown as ComponentClass; + } + + has(componentName: string) { + return this.componentClasses.has(componentName); } } export type ComponentListHandle = { get(componentName: string): undefined - | (new (...args: any[]) => ComponentHandle); + | ComponentHandleClass; }; diff --git a/src/renderer/host/classes/module.ts b/src/renderer/host/classes/module.ts index 75d3128..8746c49 100644 --- a/src/renderer/host/classes/module.ts +++ b/src/renderer/host/classes/module.ts @@ -10,7 +10,8 @@ import { CathodiqueProviderHandler } from "../ipcHandlers/cathodiqueProvider.js" import { ComponentInstanceProxy, ComponentListProxy, makeComponentProxy } from "../utils/remoteToLocalAdapter.js"; import { WithTransfer } from "./withTransfer.js"; import { orchestrator } from "./orchestrator.js"; -import { Component } from "./component.js"; +import { Component, ComponentHandle } from "./component.js"; +import z from "zod"; /* Four cases. @@ -43,12 +44,12 @@ export abstract class BaseModule { return window.origin; } - static getModule(moduleName: string) { + static async getModule(moduleName: string) { if (this.summonnedModules.has(moduleName)) { return this.summonnedModules.get(moduleName)!; } - return new RemoteModule(moduleName); + return RemoteModule.create(moduleName); } opaqueToken: string; @@ -65,7 +66,8 @@ export abstract class BaseModule { abstract localHandle: ComponentListHandle; abstract getRemoteHandle(to: RemoteModule): MessagePort | Promise; - abstract componentExists(id: string): boolean | Promise; + abstract instanceExists(id: string): boolean | Promise; + abstract getComponent(id: string): ComponentHandle | undefined | Promise; } export class LocalModule extends BaseModule { @@ -100,85 +102,73 @@ export class LocalModule extends BaseModule { return messageChannel.consumerPort; } - componentInstances = new Map(); - componentExists(id: string) { - return this.componentInstances.has(id); + #componentInstances = new Map(); + instanceExists(id: string) { + return this.#componentInstances.has(id); } register(id: string, comp: Component) { - this.componentInstances.set(id, comp); + this.#componentInstances.set(id, comp); + } + getComponent(id: string) { + return this.#componentInstances.get(id); } } export class RemoteModule extends BaseModule { + static iframeLoad(iframe: HTMLIFrameElement) { + return new Promise((r) => { + iframe.addEventListener("load", () => r(), { once: true }); + }); + } + static moduleSubdomainOf(moduleName: string) { + return moduleName.split('.').toReversed().join('.'); + } + // Init resolves latches: It's why it's hidden + static async create(moduleName: string) { + const moduleSubdomain = this.moduleSubdomainOf(moduleName); + + const iframe = document.createElement("iframe"); + iframe.src = `https://${moduleSubdomain}.raytu.be/module.html`; + iframe.hidden = true; + + document.body.append(iframe); + + const win = iframe.contentWindow!; + OtherNodeRegistry.setRegistry(win, new OtherNodeRegistry(win)); + + return new RemoteModule(moduleName, iframe); + } + + peer: OrderedPeer; iframe: HTMLIFrameElement; - #ipcLatch: Latch; - get peer() { return this.#ipcLatch.promise } - #winLatch: Latch; - get win() { return this.#winLatch.promise } + get win() { return this.iframe.contentWindow! }; componentList: ComponentListHandle; - constructor(moduleName: string) { + private constructor(moduleName: string, iframe: HTMLIFrameElement) { super(moduleName); - this.iframe = this.#createIframe(); - document.body.append(this.iframe); - - this.#ipcLatch = new Latch(); - this.#winLatch = new Latch(); - - this.#init(); + this.iframe = iframe; this.componentList = new ComponentListProxy(); + this.peer = new OrderedPeer( + iframe.contentWindow!, + `https://${this.#moduleSubdomain}.raytu.be`, + ); + this.peer.addHandler(new CathodiqueConsumerHandler(this)); + this.peer.addHandler(new CathodiqueHostHandler(this)); + this.peer.addHandler(new DOMHostHandler(this.win, this)); } get #moduleSubdomain() { - return this.moduleName.split('.').toReversed().join('.'); + return RemoteModule.moduleSubdomainOf(this.moduleName); } get origin() { return `https://${this.#moduleSubdomain}.raytu.be`; } - #createIframe() { - const iframe = document.createElement("iframe"); - iframe.src = `https://${this.#moduleSubdomain}.raytu.be/module.html`; - iframe.hidden = true; - - return iframe; - } - #iframeLoaded = false - get iframeLoad() { - if (this.#iframeLoaded) return Promise.resolve(); - return new Promise((r) => { - this.iframe.addEventListener("load", () => r(), { once: true }); - }) - .then(function (this: RemoteModule) { this.#iframeLoaded = true; }.bind(this)) - } - componentReady = new KeyedLatch(); - // Init resolves latches: It's why it's hidden - async #init() { - await this.iframeLoad; - - const win = this.iframe.contentWindow!; - this.#winLatch.resolve!(win); - - const moduleSubdomain = this.moduleName.split('.').toReversed().join('.'); - - OtherNodeRegistry.setRegistry(win, new OtherNodeRegistry(win)); - - const ipc = new OrderedPeer( - this.iframe.contentWindow!, - `https://${moduleSubdomain}.raytu.be`, - ); - ipc.addHandler(new CathodiqueConsumerHandler(this.componentReady)); - ipc.addHandler(new CathodiqueHostHandler(this)); - ipc.addHandler(new DOMHostHandler(win, this)); - - this.#ipcLatch.resolve!(ipc); - } - async waitForComponent(componentName: string) { await this.componentReady.get(componentName); } @@ -197,8 +187,7 @@ export class RemoteModule extends BaseModule { const messageChannel = new SemanticMessageChannel(); - const ipc = await this.#ipcLatch.promise; - await ipc.rpc("connectAsProvider", + await this.peer.rpc("connectAsProvider", new WithTransfer( { data: { port: messageChannel.providerPort, moduleToken: undefined } }, [messageChannel.providerPort], @@ -209,15 +198,44 @@ export class RemoteModule extends BaseModule { } async submitRemoteHandle(port: MessagePort, from: RemoteModule) { - const ipc = await this.#ipcLatch.promise; - await ipc.rpc("connectAsProvider", + await this.peer.rpc("connectAsProvider", new WithTransfer( { data: { port: port, moduleToken: from.opaqueToken } }, [port], )); } - async componentExists(id: string) { - return await (await this.peer).rpc("componentExists", { componentId: id }); + async instanceExists(id: string) { + return await this.peer.rpc("instanceExists", { componentId: id }); + } + async getInstanceData(id: string) { + return await this.peer.rpc("getInstanceData", { componentId: id }); + } + + #componentInstances = new Map(); + instanceProxyExists(id: string) { + return this.#componentInstances.has(id); + } + getInstanceProxy(id: string) { + return this.#componentInstances.get(id); + } + + registerInstanceProxy(id: string, comp: ComponentInstanceProxy) { + this.#componentInstances.set(id, comp); + } + async getComponent(id: string): Promise { + if (this.#componentInstances.has(id)) return this.#componentInstances.get(id)!; + + const componentData = z.union( + [ + z.undefined(), + z.object({ + componentName: z.string(), + }), + ] + ).parse(await this.getInstanceData(id)); + if (componentData) { + return makeComponentProxy(this, componentData.componentName, { componentId: id }); + } } } diff --git a/src/renderer/host/classes/orchestrator.ts b/src/renderer/host/classes/orchestrator.ts index 428ff86..6f2fad2 100644 --- a/src/renderer/host/classes/orchestrator.ts +++ b/src/renderer/host/classes/orchestrator.ts @@ -50,9 +50,9 @@ export class Orchestrator { const moduleNames = overriddenBy ?? this.data.defaultsAll[schemaName]; if (!moduleNames) return undefined; - return moduleNames.map(function (this: Orchestrator, moduleName: string) { + return Promise.all(moduleNames.map(function (this: Orchestrator, moduleName: string) { return BaseModule.getModule(moduleName)!; - }); + })); } } diff --git a/src/renderer/host/ipcHandlers/cathodiqueConsumer.ts b/src/renderer/host/ipcHandlers/cathodiqueConsumer.ts index 6d5ea3f..ff0c2f3 100644 --- a/src/renderer/host/ipcHandlers/cathodiqueConsumer.ts +++ b/src/renderer/host/ipcHandlers/cathodiqueConsumer.ts @@ -1,20 +1,35 @@ import z from "zod"; -import { KeyedLatch } from "../classes/latch.js"; import { HandlerContext } from "../utils/types.js"; +import { unwrapValue, WrappedValue, zodWrappedValue } from "../utils/wrap.js"; +import { RemoteModule } from "../classes/module.js"; export class CathodiqueConsumerHandler { [k: string]: (arg: Record, ctx: HandlerContext) => any; - #instanceReady: KeyedLatch; + #module: RemoteModule; - constructor(instanceReady: KeyedLatch) { - this.#instanceReady = instanceReady; + constructor(module: RemoteModule ) { + this.#module = module; } componentRegistered(arg: Record) { return this.#componentRegistered(z.object({ data: z.object({ componentName: z.string() }) }).parse(arg)); } async #componentRegistered({ data }: { data: { componentName: string } }) { - this.#instanceReady.resolve(data.componentName, undefined); + this.#module.componentReady.resolve(data.componentName, undefined); + } + + emitEvent(arg: Record) { + return this.#emitEvent(z.object({ + data: z.object({ + eventName: z.string(), + componentId: z.string(), + args: z.array(zodWrappedValue), + }), + }).parse(arg)); + } + async #emitEvent({ data }: { data: { eventName: string, componentId: string, args: WrappedValue[] } }) { + const unwrappedArgs = data.args.map((v) => unwrapValue(v, this.#module)); + (await this.#module.getComponent(data.componentId))?.emit(data.eventName, ...unwrappedArgs); } }; diff --git a/src/renderer/host/ipcHandlers/cathodiqueHost.ts b/src/renderer/host/ipcHandlers/cathodiqueHost.ts index 48e0239..2712cfd 100644 --- a/src/renderer/host/ipcHandlers/cathodiqueHost.ts +++ b/src/renderer/host/ipcHandlers/cathodiqueHost.ts @@ -21,7 +21,7 @@ export class CathodiqueHostHandler { }).parse(arg)); } async #getDependency({ data }: { data: { dependency: string } }) { - const module = orchestrator.load(data.dependency); + const module = await orchestrator.load(data.dependency); if (!module) return undefined; @@ -39,7 +39,7 @@ export class CathodiqueHostHandler { }).parse(arg)); } async #getAllDependency({ data }: { data: { dependency: string } }) { - const modules = orchestrator.loadAll(data.dependency); + const modules = await orchestrator.loadAll(data.dependency); if (!modules) return []; diff --git a/src/renderer/host/ipcHandlers/cathodiqueProvider.ts b/src/renderer/host/ipcHandlers/cathodiqueProvider.ts index 6c5ee56..57ba829 100644 --- a/src/renderer/host/ipcHandlers/cathodiqueProvider.ts +++ b/src/renderer/host/ipcHandlers/cathodiqueProvider.ts @@ -2,7 +2,8 @@ import z from "zod"; import { LocalModule, RemoteModule } from "../classes/module.js"; import { HandlerContext } from "../utils/types.js"; import { unwrapValue, WrappedValue, wrapValue, zodWrappedValue } from "../utils/wrap.js"; -import { ComponentHandle } from "../classes/component.js"; +import { Component, ComponentHandle } from "../classes/component.js"; +import { ShouldHaveBeenZodError, stringStartsWithDollar } from "../utils/utils.js"; export class CathodiqueProviderHandler { [k: string]: (arg: Record, ctx: HandlerContext) => any; @@ -14,19 +15,22 @@ export class CathodiqueProviderHandler { this.#toModule = toModule; } - static #componentInstancesByModule = new Map>(); + static #componentInstancesByModule = new Map>(); get #componentInstances() { - const result = CathodiqueProviderHandler.#componentInstancesByModule.get(this.#toModule) || new Map(); + const result = CathodiqueProviderHandler.#componentInstancesByModule.get(this.#toModule) || new Map(); if (!CathodiqueProviderHandler.#componentInstancesByModule.has(this.#toModule)) { CathodiqueProviderHandler.#componentInstancesByModule.set(this.#toModule, result); } return result; } + #componentExists(id: string) { + this.#componentInstances.has(id); + } createInstance(arg: Record) { return this.#createInstance(z.object({ data: z.object({ - className: z.string(), + className: z.string().refine(this.#toModule.localHandle.has), args: z.array(zodWrappedValue), }), }).parse(arg)); @@ -39,26 +43,29 @@ export class CathodiqueProviderHandler { const unwrapped = await Promise.all(data.args.map(async function (this: CathodiqueProviderHandler, v: WrappedValue) { return unwrapValue(v, this.#fromModule) }.bind(this))); - const componentInstance = new ClassObj(...unwrapped) as ComponentHandle; + const componentInstance = new ClassObj(...unwrapped); await componentInstance.init(); - this.#componentInstances.set(await componentInstance.componentId, componentInstance); + this.#componentInstances.set(componentInstance.componentId, componentInstance); return; } getProperty(arg: Record) { return this.#getProperty(z.object({ data: z.object({ - propertyName: z.string(), - componentId: z.string(), + propertyName: z.string().startsWith("$"), + componentId: z.string().refine(this.#componentExists), }), }).parse(arg)); } async #getProperty({ data }: { data: { propertyName: string; componentId: string } }) { const component = this.#componentInstances.get(data.componentId); + if (!component) throw new ShouldHaveBeenZodError(); - const value = component[data.propertyName]; + const propertyName = data.propertyName; + if (!stringStartsWithDollar(propertyName)) throw new ShouldHaveBeenZodError(); + const value = component[propertyName]; return wrapValue(value); } @@ -68,7 +75,7 @@ export class CathodiqueProviderHandler { data: z.object({ methodName: z.string(), arguments: z.array(z.any()), - componentId: z.string(), + componentId: z.string().refine(this.#componentExists), }), }).parse(arg)); } @@ -80,9 +87,52 @@ export class CathodiqueProviderHandler { }; }) { const component = this.#componentInstances.get(data.componentId); + if (!component) throw new ShouldHaveBeenZodError(); - const value = await component?.[data.methodName]?.(...data.arguments); + const methodName = data.methodName; + if (!stringStartsWithDollar(methodName)) throw new ShouldHaveBeenZodError(); + const value = await component[methodName](...data.arguments); return wrapValue(value); } + + listenToEvent(arg: Record) { + return this.#listenToEvent(z.object({ + data: z.object({ + eventName: z.string().startsWith("$"), + componentId: z.string().refine(this.#componentExists), + }), + }).parse(arg)); + } + async #listenToEvent({ data }: { + data: { + eventName: string; + componentId: string; + }; + }) { + const component = this.#componentInstances.get(data.componentId); + if (!component) throw new ShouldHaveBeenZodError(); + + component.listenFor(data.eventName, await this.#fromModule.peer); + } + + unlistenToEvent(arg: Record) { + return this.#unlistenToEvent(z.object({ + data: z.object({ + eventName: z.string(), + componentId: z.string().refine(this.#componentExists), + }), + }).parse(arg)); + } + async #unlistenToEvent({ data }: { + data: { + eventName: string; + componentId: string; + }; + }) { + const component = this.#componentInstances.get(data.componentId); + if (!component) throw new ShouldHaveBeenZodError(); + + component.unlistenFor(data.eventName, await this.#fromModule.peer); + } }; diff --git a/src/renderer/host/utils/remoteToLocalAdapter.ts b/src/renderer/host/utils/remoteToLocalAdapter.ts index bff5153..8ba64fd 100644 --- a/src/renderer/host/utils/remoteToLocalAdapter.ts +++ b/src/renderer/host/utils/remoteToLocalAdapter.ts @@ -1,9 +1,8 @@ -import { Component, ComponentHandle } from "../classes/component.js"; +import { Component } from "../classes/component.js"; import { ComponentListHandle } from "../classes/componentList.js"; import { Latch, LatchState } from "../classes/latch.js"; import { RemoteModule } from "../classes/module.js"; import { OrderedPeer } from "../classes/orderedPeer.js"; -import { OtherNodeRegistry } from "../classes/sharedDomHost.js"; import { unwrapValue, wrapValue } from "./wrap.js"; export class ComponentListProxy implements ComponentListHandle { @@ -15,7 +14,7 @@ export class ComponentListProxy implements ComponentListHandle { } export class ComponentInstance { - ipc: Promise; + ipc: OrderedPeer; module: RemoteModule; @@ -66,10 +65,14 @@ function generateCalledOrAwaited({ called, awaited }: { called: (...args: any[]) } export function makeComponentProxy(module: RemoteModule, componentName: string, options: { componentId: string } | { args: any[] }): ComponentInstanceProxy { + if ("componentId" in options && module.instanceProxyExists(options.componentId)) { + return module.getInstanceProxy(options.componentId)!; + } + const compInst = new ComponentInstance(module, componentName, options); compInst.init(); - return new Proxy(compInst, { + const compInstProxy = new Proxy(compInst, { get(target, prop) { if (prop === Component.isComponentSymbol) return true; @@ -81,7 +84,7 @@ export function makeComponentProxy(module: RemoteModule, componentName: string, called: async function (...args: any[]) { const id = await compInst.componentId; - const val = await (await module.peer).rpc("callProperty", { + const val = await module.peer.rpc("callProperty", { methodName: prop, arguments: args.map((v) => wrapValue(v)), componentId: id, @@ -92,7 +95,7 @@ export function makeComponentProxy(module: RemoteModule, componentName: string, awaited: async () => { const id = await compInst.componentId; - const val = await (await module.peer).rpc("getProperty", { + const val = await module.peer.rpc("getProperty", { propertyName: prop, componentId: id, }); @@ -102,4 +105,10 @@ export function makeComponentProxy(module: RemoteModule, componentName: string, }); }, }) as ComponentInstanceProxy; + + (async () => { + const cid = await compInst.componentId; + module.registerInstanceProxy(cid, compInstProxy); + })(); + return compInstProxy; } diff --git a/src/renderer/host/utils/types.ts b/src/renderer/host/utils/types.ts index e60ed1c..9996009 100644 --- a/src/renderer/host/utils/types.ts +++ b/src/renderer/host/utils/types.ts @@ -46,3 +46,6 @@ export interface HandlerContext { ipc: OrderedPeer; event: MessageEvent; } + +export type ComponentClass = new (...a: any[]) => Component; +export type ComponentHandleClass = new (...a: any[]) => ComponentHandle; diff --git a/src/renderer/host/utils/utils.ts b/src/renderer/host/utils/utils.ts index b888141..01ad932 100644 --- a/src/renderer/host/utils/utils.ts +++ b/src/renderer/host/utils/utils.ts @@ -1,6 +1,12 @@ let alphabet = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict"; +export class ShouldHaveBeenZodError extends Error { + constructor(message?: string) { + super(message || "This error should have been caught by zod"); + } +} + export function nanoid(e = 21) { let t = "", r = crypto.getRandomValues(new Uint8Array(e)); @@ -15,10 +21,4 @@ export const host = hostList.join('.') export const hostWithoutSubdomain = `${location.protocol}//${hostList.join(".")}:${location.port}`; -export function findWinIndex(win: WindowProxy) { - for (let i = 0; i < window.length; i += 1) { - if (window[i] === win) { - return i; - } - } -} +export const stringStartsWithDollar = (v: string): v is `$${string}` => v.startsWith("$"); diff --git a/src/renderer/host/utils/wrap.ts b/src/renderer/host/utils/wrap.ts index 23d920d..487694f 100644 --- a/src/renderer/host/utils/wrap.ts +++ b/src/renderer/host/utils/wrap.ts @@ -56,7 +56,7 @@ export async function unwrapValue(value: any, fromModule: RemoteModule) { case "component": const moduleId = wrapped.moduleId || fromModule.opaqueToken; const module = BaseModule.moduleById(moduleId); - if (!module?.componentExists(wrapped.componentId)) { + if (!module?.instanceExists(wrapped.componentId)) { return undefined; } // if (module instanceof remote) diff --git a/src/renderer/wayland/index.ts b/src/renderer/wayland/index.ts index 4322492..d9836a8 100644 --- a/src/renderer/wayland/index.ts +++ b/src/renderer/wayland/index.ts @@ -1,16 +1,15 @@ import "../host/index.js"; import { HLCompositor } from "@cathodique/wl-serv-high"; -import { InstructionType, RegRectangle } from "@cathodique/wl-serv-high/dist/objects/wl_region.js"; -import { Modifiers } from "../classes/wayland/seat/modifiers.js"; -import { SeatRegistry } from "@cathodique/wl-serv-high/dist/registries/seat.js"; -import { OutputRegistry } from "@cathodique/wl-serv-high/dist/registries/output.js"; -import { KeyboardRegistry } from "@cathodique/wl-serv-high/dist/objects/wl_keyboard.js"; +import { InstructionType, RegRectangle } from "@cathodique/wl-serv-high/objects"; +import { OutputRegistry } from "@cathodique/wl-serv-high/registries"; +import { KeyboardRegistry } from "@cathodique/wl-serv-high/objects"; import { ipcRenderer } from "electron/renderer"; -import { Seat } from "../classes/wayland/seat/seat.js"; -import { BaseObject } from "@cathodique/wl-serv-high/dist/objects/base_object.js"; +import { BaseObject } from "@cathodique/wl-serv-high/objects"; import { objectHandlers } from "../classes/handlers/handlers.js"; import { Output } from "../classes/wayland/output/output.js"; +import { seatRegistry } from "./overlays/seatRegistryOverlay.js"; +import { outputRegistry } from "./overlays/outputRegistryOverlay.js"; // HERE // TODO::: @@ -31,13 +30,9 @@ export function isInRegion(reg: RegRectangle[], y: number, x: number, defaultVal const mySeatConfig = { name: "seat0", capabilities: 3, - modifiers: null as unknown as Modifiers, }; -const seatReg = new SeatRegistry(); -new Seat(mySeatConfig, seatReg); - -seatReg.addAuthority(mySeatConfig); +seatRegistry.addAuthority(mySeatConfig); const myOutputConfig = { x: 0, @@ -47,17 +42,14 @@ const myOutputConfig = { effectiveW: 1920, effectiveH: 1080, }; -const outputReg = new OutputRegistry(); -outputReg.addAuthority(myOutputConfig); - -new Output(myOutputConfig, outputReg); +outputRegistry.addAuthority(myOutputConfig); -new Seat(mySeatConfig, seatReg); +// new Seat(mySeatConfig, seatReg); const compo = new HLCompositor({ wl_registry: { - outputs: outputReg, - seats: seatReg, + outputs: outputRegistry, + seats: seatRegistry, }, wl_keyboard: new KeyboardRegistry({ keymap: "us" }), }); diff --git a/src/renderer/wayland/overlays/outputRegistryOverlay.ts b/src/renderer/wayland/overlays/outputRegistryOverlay.ts new file mode 100644 index 0000000..e2ea00f --- /dev/null +++ b/src/renderer/wayland/overlays/outputRegistryOverlay.ts @@ -0,0 +1,32 @@ +import { OutputConfiguration } from "@cathodique/wl-serv-high/objects"; +import { OutputRegistry } from "@cathodique/wl-serv-high/registries"; +import { Output } from "../../classes/wayland/output/output"; + +class OutputRegistryOverlay extends OutputRegistry { + // Singleton + static #instance: OutputRegistry; + static create() { + return new OutputRegistryOverlay(); + } + private constructor() { + super(); + if (OutputRegistryOverlay.#instance) throw new Error("Tried to create multiple seat registries"); + OutputRegistryOverlay.#instance = this; + } + + // TODO Memory management + seats = new Map(); + allSeats() { + return this.seats.values(); + } + seatOfCfg(config: OutputConfiguration) { + return this.seats.get(config); + } + + addAuthority(cfg: OutputConfiguration) { + super.addAuthority(cfg); + this.seats.set(cfg, new Output(cfg, this)); + } +} + +export const outputRegistry = OutputRegistryOverlay.create(); diff --git a/src/renderer/wayland/overlays/seatRegistryOverlay.ts b/src/renderer/wayland/overlays/seatRegistryOverlay.ts new file mode 100644 index 0000000..84edb1a --- /dev/null +++ b/src/renderer/wayland/overlays/seatRegistryOverlay.ts @@ -0,0 +1,32 @@ +import { SeatConfiguration } from "@cathodique/wl-serv-high/objects"; +import { SeatRegistry } from "@cathodique/wl-serv-high/registries"; +import { Seat } from "../../classes/wayland/seat/seat"; + +class SeatRegistryOverlay extends SeatRegistry { + // Singleton + static #instance: SeatRegistry; + static create() { + return new SeatRegistryOverlay(); + } + private constructor() { + super(); + if (SeatRegistryOverlay.#instance) throw new Error("Tried to create multiple seat registries"); + SeatRegistryOverlay.#instance = this; + } + + // TODO Memory management + seats = new Map(); + allSeats() { + return this.seats.values(); + } + seatOfCfg(config: SeatConfiguration) { + return this.seats.get(config); + } + + addAuthority(cfg: SeatConfiguration) { + super.addAuthority(cfg); + this.seats.set(cfg, new Seat(cfg, this)); + } +} + +export const seatRegistry = SeatRegistryOverlay.create(); diff --git a/tsconfig.json b/tsconfig.json index 92c4d44..68877ee 100755 --- a/tsconfig.json +++ b/tsconfig.json @@ -27,7 +27,7 @@ /* Modules */ "module": "Node16", /* Specify what module code is generated. */ "rootDir": "./src", /* Specify the root folder within your source files. */ - "moduleResolution": "Node16", /* Specify how TypeScript looks up a file from a given module specifier. */ + "moduleResolution": "nodenext", /* Specify how TypeScript looks up a file from a given module specifier. */ // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ diff --git a/tsconfig.modules.json b/tsconfig.modules.json index 468edce..419cf5e 100644 --- a/tsconfig.modules.json +++ b/tsconfig.modules.json @@ -93,7 +93,7 @@ // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ - // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + "exactOptionalPropertyTypes": false, /* Interpret optional property types as written, rather than adding 'undefined'. */ // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ From 71450b19a5cbe68b7fdf1c05735764ac171e654b Mon Sep 17 00:00:00 2001 From: Juliette Wang immjs Date: Sun, 4 Jan 2026 17:18:21 +0100 Subject: [PATCH 05/10] chore: Clean up a little --- src/renderer/host/ipcHandlers/cathodiqueProvider.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/renderer/host/ipcHandlers/cathodiqueProvider.ts b/src/renderer/host/ipcHandlers/cathodiqueProvider.ts index 57ba829..1fa8027 100644 --- a/src/renderer/host/ipcHandlers/cathodiqueProvider.ts +++ b/src/renderer/host/ipcHandlers/cathodiqueProvider.ts @@ -113,7 +113,7 @@ export class CathodiqueProviderHandler { const component = this.#componentInstances.get(data.componentId); if (!component) throw new ShouldHaveBeenZodError(); - component.listenFor(data.eventName, await this.#fromModule.peer); + component.listenFor(data.eventName, this.#fromModule.peer); } unlistenToEvent(arg: Record) { @@ -133,6 +133,6 @@ export class CathodiqueProviderHandler { const component = this.#componentInstances.get(data.componentId); if (!component) throw new ShouldHaveBeenZodError(); - component.unlistenFor(data.eventName, await this.#fromModule.peer); + component.unlistenFor(data.eventName, this.#fromModule.peer); } }; From 3c160d7b7e06cc399da24c10d4969959103b0f55 Mon Sep 17 00:00:00 2001 From: Juliette Wang immjs Date: Mon, 19 Jan 2026 16:18:20 +0100 Subject: [PATCH 06/10] feat: Back to parity with main --- TODO.md | 3 + package-lock.json | 82 ++++++++++++ package.json | 5 +- src/main/main.ts | 8 +- src/main/protocols.ts | 1 - src/modules/.common/classes/component.ts | 63 +++------ src/modules/.common/classes/componentList.ts | 47 ++++++- src/modules/.common/classes/module.ts | 59 +++++---- src/modules/.common/classes/orderedPeer.ts | 108 +++++++++------ src/modules/.common/classes/resolver.ts | 57 ++++++++ src/modules/.common/classes/sharedDomDummy.ts | 24 +++- .../.common/classes/sharedDomRemote.ts | 60 +++++---- src/modules/.common/classes/withTransfer.ts | 9 +- src/modules/.common/index.ts | 1 + .../.common/ipcHandlers/cathodiqueConsumer.ts | 33 +++-- .../.common/ipcHandlers/cathodiqueProvider.ts | 58 ++++---- .../.common/ipcHandlers/cathodiqueRemote.ts | 42 +++--- src/modules/.common/parentIpc.ts | 6 +- .../.common/utils/nodeEventListener.ts | 1 - .../.common/utils/remoteToLocalAdapter.ts | 30 +++-- src/modules/.common/utils/types.ts | 22 ++- src/modules/.common/utils/utils.ts | 2 +- src/modules/.common/utils/wrap.ts | 27 ++-- src/modules/cathodique.windowmanager/main.ts | 44 ++++++ .../cathodique.windowmanager/module.html | 44 +++--- .../immjs.macos-aqua-windowframe/module.html | 22 ++- src/renderer/classes/handlers/dom/popup.ts | 11 +- .../classes/handlers/dom/subsurface.ts | 81 ------------ src/renderer/classes/handlers/dom/surface.ts | 109 +++++++++++---- src/renderer/classes/handlers/dom/toplevel.ts | 38 ------ src/renderer/classes/handlers/handlers.ts | 18 ++- .../classes/handlers/lib/subsurface.ts | 83 ++++++++++++ src/renderer/classes/wayland/output/output.ts | 6 +- src/renderer/classes/wayland/seat/seat.ts | 37 ++++-- src/renderer/host/classes/component.ts | 24 ++-- src/renderer/host/classes/componentList.ts | 60 +++++++-- src/renderer/host/classes/module.ts | 89 ++++++------- src/renderer/host/classes/orchestrator.ts | 16 ++- src/renderer/host/classes/orderedPeer.ts | 105 +++++++++------ src/renderer/host/classes/polymap.ts | 10 ++ src/renderer/host/classes/sharedDomHost.ts | 12 +- src/renderer/host/classes/withTransfer.ts | 9 +- src/renderer/host/index.ts | 5 + .../host/ipcHandlers/cathodiqueConsumer.ts | 29 ++-- .../host/ipcHandlers/cathodiqueHost.ts | 58 +++++--- .../host/ipcHandlers/cathodiqueProvider.ts | 88 +++++++----- src/renderer/host/ipcHandlers/domHost.ts | 125 ++++++++++++++++-- .../host/localModules/basedomcomponent.ts | 24 ++++ .../host/localModules/loadAllLocalModules.ts | 11 +- src/renderer/host/localModules/window.ts | 11 -- .../host/localModules/window_fakeToplevel.ts | 9 ++ .../host/localModules/window_toplevel.ts | 59 +++++++++ .../host/utils/remoteToLocalAdapter.ts | 47 +++++-- src/renderer/host/utils/types.ts | 22 ++- src/renderer/host/utils/wrap.ts | 17 ++- src/renderer/index.html | 8 ++ src/renderer/utils/domIntersect.ts | 13 ++ src/renderer/wayland/index.ts | 19 ++- .../wayland/overlays/outputRegistryOverlay.ts | 15 +-- .../wayland/overlays/seatRegistryOverlay.ts | 3 +- .../wayland/overlays/strutRegistryOverlay.ts | 16 +++ tsconfig.json | 2 +- 62 files changed, 1466 insertions(+), 681 deletions(-) create mode 100644 src/modules/.common/classes/resolver.ts create mode 100644 src/modules/cathodique.windowmanager/main.ts delete mode 100644 src/renderer/classes/handlers/dom/subsurface.ts delete mode 100644 src/renderer/classes/handlers/dom/toplevel.ts create mode 100644 src/renderer/classes/handlers/lib/subsurface.ts create mode 100644 src/renderer/host/classes/polymap.ts create mode 100644 src/renderer/host/localModules/basedomcomponent.ts delete mode 100644 src/renderer/host/localModules/window.ts create mode 100644 src/renderer/host/localModules/window_fakeToplevel.ts create mode 100644 src/renderer/host/localModules/window_toplevel.ts create mode 100644 src/renderer/utils/domIntersect.ts create mode 100644 src/renderer/wayland/overlays/strutRegistryOverlay.ts diff --git a/TODO.md b/TODO.md index 80280f1..23b7050 100644 --- a/TODO.md +++ b/TODO.md @@ -6,3 +6,6 @@ # To consider - Should cathodique events follow $ also? + +# Long term +LT-TODO diff --git a/package-lock.json b/package-lock.json index 2f07500..19a01da 100755 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,7 @@ "@types/electron": "^1.4.38", "@types/events": "^3.0.3", "@types/node": "^22.10.2", + "concurrently": "^9.2.1", "prettier": "3.7.4", "typescript": "^5.7.2" } @@ -1340,6 +1341,47 @@ "dev": true, "license": "MIT" }, + "node_modules/concurrently": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz", + "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "4.1.2", + "rxjs": "7.8.2", + "shell-quote": "1.8.3", + "supports-color": "8.1.1", + "tree-kill": "1.2.2", + "yargs": "17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2770,6 +2812,16 @@ "node": ">=8.0" } }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -2854,6 +2906,19 @@ "node": ">=8" } }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -3013,6 +3078,23 @@ "node": ">=8" } }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, "node_modules/type-fest": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", diff --git a/package.json b/package.json index 3d6d3fb..1498133 100755 --- a/package.json +++ b/package.json @@ -9,7 +9,9 @@ "build-asset-homomorphic": "rm -frfr dist && mkdir dist && cp -frfr src/* dist/ && find dist/ -name \"*.ts\" -type f -delete", "build-browser-host": "esbuild dist/renderer/wayland/index.js --bundle --platform=node --target=node22.4 --external:electron --external:@cathodique/wl-serv-high --outfile=dist/renderer/index.js", "build-browser-common": "esbuild dist/modules/.common/index.js --bundle --outfile=dist/modules/.common/bundle.js --format=esm", - "build": "npm run build-asset-homomorphic && npx tsgo && npx tsc -p tsconfig.modules.json && npm run build-browser-host && npm run build-browser-common" + "build-source": "npm run build-asset-homomorphic && npx tsgo && npx tsc -p tsconfig.modules.json && npm run build-browser-host && npm run build-browser-common", + "build-bin": "npx electron-rebuild", + "build": "concurrently \"npm run build-source\" \"npm run build-bin\"" }, "repository": { "type": "git", @@ -26,6 +28,7 @@ "@types/electron": "^1.4.38", "@types/events": "^3.0.3", "@types/node": "^22.10.2", + "concurrently": "^9.2.1", "prettier": "3.7.4", "typescript": "^5.7.2" }, diff --git a/src/main/main.ts b/src/main/main.ts index 7ea34df..862e262 100755 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -5,10 +5,8 @@ import { registerProtocols } from "./protocols.js"; const createWindow = () => { const win = new BrowserWindow({ - width: 800, - height: 600, - fullscreen: true, - resizable: false, + // fullscreen: true, + // resizable: false, webPreferences: { nodeIntegration: true, nodeIntegrationInSubFrames: false, @@ -18,7 +16,7 @@ const createWindow = () => { registerProtocols(); - // win.webContents.openDevTools(); + win.webContents.openDevTools({ mode: "detach" }); win.loadURL("app://top/index.html"); }; diff --git a/src/main/protocols.ts b/src/main/protocols.ts index 782817f..0e4fa62 100644 --- a/src/main/protocols.ts +++ b/src/main/protocols.ts @@ -43,7 +43,6 @@ export const registerProtocols = () => { switch (domain) { case "raytu.be": { - console.log(reqUrl.pathname); if (reqUrl.pathname.startsWith('/.common/')) { return net.fetch(pathToFileURL(join(__dirname, '../modules', reqUrl.pathname)).toString()); } diff --git a/src/modules/.common/classes/component.ts b/src/modules/.common/classes/component.ts index c0ae098..b2106b8 100644 --- a/src/modules/.common/classes/component.ts +++ b/src/modules/.common/classes/component.ts @@ -1,68 +1,36 @@ -import { parentIpc } from "../parentIpc.js"; +import { EventEmitter } from "events"; +import { componentTypes } from "../utils/types.js"; import { nanoid } from "../utils/utils.js"; import { wrapValue } from "../utils/wrap.js"; -import { RemoteModule } from "./module.js"; +import { componentList } from "./componentList.js"; import { OrderedPeer } from "./orderedPeer.js"; -import z from "zod"; -export interface ComponentContext { - peer: OrderedPeer; -} export type ComponentHandle = { - peer: OrderedPeer; componentId: string | Promise; init(): any; } & { [k in `$${string}`]: any; }; const isComponentSymbol = Symbol(); -export abstract class Component extends EventTarget { +export abstract class Component implements ComponentHandle { [k: `$${string}`]: any; static isComponentSymbol: typeof isComponentSymbol = isComponentSymbol; componentId: string; - peer: OrderedPeer; + + static type: typeof componentTypes[number] = "NORMAL"; [isComponentSymbol] = true; - constructor(ctx: ComponentContext) { - super(); - this.peer = ctx.peer; + constructor() { this.componentId = nanoid(); - } - abstract init(): any; - - post(obj: Record) { - return this.peer.post({ ...obj, componentHandle: this.componentId }); - } - rpc(type: string, data: Record, obj: Record = {}) { - return this.peer.rpc(type, data, { ...obj, componentHandle: this.componentId }); - } - - async #getDependencyRpc(dependency: string) { - const result = await parentIpc.rpc("getDependency", { dependency }); - return z.object({ - port: z.instanceof(MessagePort), - id: z.string(), - }).parse(result); - } - async getDependency(dependency: string) { - const v = await this.#getDependencyRpc(dependency); - const remoteModule = RemoteModule.getOrCreate(v.port, v.id); - return remoteModule.localHandle; + componentList.componentInstances.set( + this.componentId, + this, + ); } - async #getAllDependencyRpc(dependency: string) { - const result = await parentIpc.rpc("getAllDependency", { dependency }); - return z.array(z.object({ - port: z.instanceof(MessagePort), - id: z.string(), - })).parse(result); - } - async getAllDependency(dependency: string) { - const handles = await this.#getAllDependencyRpc(dependency); - return handles.map((v) => RemoteModule.getOrCreate(v.port, v.id).localHandle); - } + init(): any {} #listenersFromRemote = new Map>(); listenFor(eventName: string, peer: OrderedPeer) { @@ -78,13 +46,13 @@ export abstract class Component extends EventTarget { if (innerSet.size === 0) this.#listenersFromRemote.delete(eventName); } - async emit(eventName: string, args: any[]) { + emit(eventName: string, args: any[]) { const wrapped = args.map((v) => wrapValue(v)) const innerSet = this.#listenersFromRemote.get(eventName); - if (!innerSet) return; + if (!innerSet) return false; - await Promise.all( + Promise.all( [...innerSet] .map((peer) => peer.rpc("emitEvent", { @@ -94,5 +62,6 @@ export abstract class Component extends EventTarget { }) ), ); + return true; } } diff --git a/src/modules/.common/classes/componentList.ts b/src/modules/.common/classes/componentList.ts index 499eff2..0366c06 100644 --- a/src/modules/.common/classes/componentList.ts +++ b/src/modules/.common/classes/componentList.ts @@ -1,12 +1,18 @@ -import { ComponentHandleClass } from "../utils/types.js"; -import { Component, ComponentHandle } from "./component.js"; +import { ComponentHandleClass, ComponentHandleFactory } from "../utils/types.js"; +import { Component } from "./component.js"; +import { Latch } from "./latch.js"; export class InvalidComponentError extends Error {} -export class ComponentList extends EventTarget { +export class ComponentList extends EventTarget implements ComponentListHandle { componentClasses = new Map(); componentClassToClassName = new Map() + componentInstances = new Map(); + instanceExists(id: string) { + return this.componentInstances.has(id); + } + componentTypeOf(component: Component) { // Traversing prototype chain (from most specific to least specific) // will take less time than traversing all the possible components. @@ -29,9 +35,40 @@ export class ComponentList extends EventTarget { this.componentClasses.set(componentName, componentClass); this.componentClassToClassName.set(componentClass, componentName); } + markAs(componentClass: ComponentHandleClass, componentName: string) { + this.componentClassToClassName.set(componentClass, componentName); + } + + #readyLatch = new Latch(); + get ready() { + return this.#readyLatch.promise; + } + markReady() { + this.#readyLatch.resolve?.(); + } get(componentName: string) { - return this.componentClasses.get(componentName); + const InnerClass = this.componentClasses.get(componentName); + if (!InnerClass) return; + + return { + create: (...args: any[]) => { + switch (InnerClass.type) { + case "REF_ONLY": { + throw new Error("You are not supposed to instanciate this class"); + } + case "SINGLETON": { + if (!InnerClass.singletonInstance) throw new Error("Singleton instance was not set up."); + return InnerClass.singletonInstance; + } + case "NORMAL": { + return "create" in InnerClass + ? InnerClass.create(...args) + : new InnerClass(...args); + } + } + } + }; } has(componentName: string) { @@ -41,7 +78,7 @@ export class ComponentList extends EventTarget { export type ComponentListHandle = { get(componentName: string): undefined - | ComponentHandleClass; + | ComponentHandleFactory; }; export const componentList = new ComponentList(); diff --git a/src/modules/.common/classes/module.ts b/src/modules/.common/classes/module.ts index 14e7fc4..9aec643 100644 --- a/src/modules/.common/classes/module.ts +++ b/src/modules/.common/classes/module.ts @@ -1,31 +1,42 @@ -import { CathodiqueConsumerHandler } from "../ipcHandlers/cathodiqueConsumer.js"; +import { CathodiqueAvailableComponentsHandler, CathodiqueConsumerHandler } from "../ipcHandlers/cathodiqueConsumer.js"; +import { parentIpc } from "../parentIpc.js"; import { ComponentInstanceProxy, makeComponentProxy } from "../utils/remoteToLocalAdapter.js"; -import { KeyedLatch } from "./latch.js"; +import { KeyedLatch, Latch } from "./latch.js"; import { OrderedPeer } from "./orderedPeer.js"; +import { Resolver } from "./resolver.js"; import { DummyNodeRegistry } from "./sharedDomDummy.js"; // The RemoteModule class manages the lifecycle of a RemoteModule // Mainly, the components made available (through keyed latch componentReady) export class RemoteModule { static summonnedModulesByPort = new Map(); - static summonnedModulesByOpaqueToken = new Map(); + static summonnedModulesByToken = new Map(); - static getOrCreate(port: MessagePort, id: string) { - if (this.summonnedModulesByPort.has(port)) return this.summonnedModulesByPort.get(port)!; + static getOrCreate(port: MessagePort | undefined, id: string) { + if (this.summonnedModulesByToken.has(id)) return this.summonnedModulesByToken.get(id)!; + + if (!port) throw new Error("Message port supposedly neutered yet is not in my registry"); return this.createModule(port, id); } - static createModule(port: MessagePort, opaqueToken: string) { - const mod = new RemoteModule(port, opaqueToken); + static async createModule(port: MessagePort, opaqueToken: string) { + const peer = new OrderedPeer(port, opaqueToken, "*"); + const latch = new Latch(); + peer.addHandler(new CathodiqueAvailableComponentsHandler(latch)); + + const availableComponents = await latch.promise; + + const mod = new RemoteModule(port, availableComponents, peer, opaqueToken); this.summonnedModulesByPort.set(port, mod); - this.summonnedModulesByOpaqueToken.set(opaqueToken, mod) + this.summonnedModulesByToken.set(opaqueToken, mod) return mod; } - static moduleByOpaqueToken(opaqueToken: string) { - return this.summonnedModulesByOpaqueToken.get(opaqueToken); + static async moduleByOpaqueToken(opaqueToken: string) { + return this.summonnedModulesByToken.get(opaqueToken) + || Resolver.getModuleByToken(opaqueToken); } static moduleOfPort(port: MessagePort) { return this.summonnedModulesByPort.get(port); @@ -44,34 +55,32 @@ export class RemoteModule { id: string; + availableComponents: string[]; + port: MessagePort; peer: OrderedPeer; - constructor(port: MessagePort, id: string) { + constructor(port: MessagePort, availableComponents: string[], peer: OrderedPeer, id: string) { this.port = port; this.id = id; + this.availableComponents = availableComponents; + // "*" is fine because we can trust the origin who passed the messageport onto us - this.peer = new OrderedPeer(port, "*"); + this.peer = peer; this.peer.addHandler(new CathodiqueConsumerHandler(this)); - DummyNodeRegistry.setRegistry(port, new DummyNodeRegistry(port)); - } - - componentReady = new KeyedLatch(); - - async waitForComponent(componentName: string) { - await this.componentReady.get(componentName); + DummyNodeRegistry.setRegistry(this.peer, new DummyNodeRegistry(this.peer)); } localHandle = { - get: (componentName: string) => { - return function (this: RemoteModule, ...args: any[]) { + get: (componentName: string) => ({ + create: (...args: any[]) => { return makeComponentProxy(this, componentName, { args }); - }.bind(this) as unknown as new () => ComponentInstanceProxy; - }, + }, + }), }; - async componentExists(componentId: string) { - return await this.peer.rpc("componentExists", { componentId }); + async instanceExists(componentId: string) { + return await this.peer.rpc("instanceExists", { componentId }); } } diff --git a/src/modules/.common/classes/orderedPeer.ts b/src/modules/.common/classes/orderedPeer.ts index 013cf44..2200ac0 100644 --- a/src/modules/.common/classes/orderedPeer.ts +++ b/src/modules/.common/classes/orderedPeer.ts @@ -31,18 +31,26 @@ export class OrderedPeer { source: MessageEventSource; postMessage: typeof window["postMessage"]; - constructor(source: MessageEventSource, origin = "*") { + opaqueToken: string | undefined; + constructor(source: MessageEventSource, opaqueToken: string | undefined, origin = "*") { if (OrderedPeer.actualHandlers.has(source)) throw new Error("A window may only admit a single OrderedPeer"); this.source = source; this.postMessage = source.postMessage.bind(source); this.origin = origin; + this.opaqueToken = opaqueToken; - OrderedPeer.actualHandlers.set(source, this.orderedDecoder.bind(this)); + if (source instanceof MessagePort) { + if (origin !== "*") throw new Error("Cannot restrain origin if source is a MessagePort"); + source.addEventListener("message", this.orderedDecoder.bind(this)); + } else { + OrderedPeer.actualHandlers.set(source, this.orderedDecoder.bind(this)); + } } addHandler(handler: (typeof this.handlers)[number]) { this.handlers.push(handler); + if (this.source instanceof MessagePort) this.source.start(); return this; // Builder } @@ -57,14 +65,20 @@ export class OrderedPeer { this.pendingTransfer.push(...transfer); if (this.pendingMessages.length === 1) { + const error = new Error().stack!; queueMicrotask(() => { - this.postMessage( + if (error.length < 1) { + throw new Error("Just to get the stack trace."); + } + this.source.postMessage( { messages: this.pendingMessages, currentOrder: this.currentOrderSubmission, }, - this.origin, - this.pendingTransfer, + { + targetOrigin: this.origin, + transfer: this.pendingTransfer, + }, ); this.pendingMessages = []; @@ -74,10 +88,13 @@ export class OrderedPeer { } } - async rpc(type: string, data: any | WithTransfer, obj: Record = {}) { + async rpc(type: string, oldData: any | WithTransfer, obj: Record = {}) { const promiseId = nanoid(); - this.post({ type, data, promiseId, ...obj }); + const { data, transfer } = new WithTransfer(oldData); + // console.log(window.origin, "RPC WITH", promiseId, type, data, transfer); + this.post(new WithTransfer({ type, data, promiseId, ...obj }, transfer)); const result = await this.promiseMap.consume(promiseId); + // console.log(window.origin, "RPC WAS ALL GOOD!", promiseId, type, result); if (!result.error) return result.reply; throw result.error; @@ -86,48 +103,55 @@ export class OrderedPeer { remainingMessages = new Map(); currentOrderReception: bigint = 0n; - originMatch(origin: string) { + originMatch(evt: MessageEvent) { + if (evt.origin === "" && this.source instanceof MessagePort) return true; + if (this.origin === '*') return true; - if (this.origin === '/') return origin === window.origin; - return origin === this.origin; + if (this.origin === '/') return evt.origin === window.origin; + return evt.origin === this.origin; } async orderedDecoder(evt: MessageEvent) { - if (!this.originMatch(evt.origin)) return; + if (!this.originMatch(evt)) return; const { data: { messages, currentOrder } } = evt; this.remainingMessages.set(currentOrder, messages); - if (this.currentOrderReception === currentOrder) { - do { - await Promise.all( - evt.data.messages.map(async (message: { data: any, type: string, promiseId?: string, componentHandle?: string }) => { - const { type, promiseId } = message; - - if (type === "reply") { - return this.promiseMap.resolve(promiseId!, message); - } - - const handler = this.handlers.find((v) => type in v); - - if (!handler) { - if (promiseId) this.post({ type: "reply", error: new Error("No such function"), promiseId }); - return; - } - - try { - const result = await handler[type](message, { ipc: this, event: evt }); - - if (promiseId) { - this.post({ type: "reply", reply: result, promiseId }); - } - } catch (e) { - this.post({ type: "reply", error: e, promiseId }); - } - }) - ); - this.remainingMessages.delete(this.currentOrderReception); - this.currentOrderReception += 1n; - } while (this.remainingMessages.has(this.currentOrderReception)); + + while (this.remainingMessages.has(this.currentOrderReception)) { + const messages = this.remainingMessages.get(this.currentOrderReception)!; + this.remainingMessages.delete(this.currentOrderReception); + this.currentOrderReception += 1n; + + // Used to be await Promise.all. + // Trying to prevent deadlock here... + messages.map(async (message: { data: any, type: string, error?: string, promiseId?: string, componentHandle?: string }) => { + const { type, promiseId } = message; + + if (type === "reply") { + this.promiseMap.resolve(promiseId!, message); + if (message.error) throw message.error; + return; + } + + const handler = this.handlers.find((v) => type in v); + + if (!handler) { + console.error("Client attempted", type, "which was not impl'd"); + if (promiseId) this.post({ type: "reply", error: new Error("No such function"), promiseId }); + return; + } + + try { + const result = new WithTransfer(await handler[type](message, { ipc: this, event: evt })); + + if (promiseId) { + this.post(new WithTransfer({ type: "reply", reply: result.data, promiseId }, result.transfer)); + } + } catch (e) { + console.error((e as Error).toString()); + this.post({ type: "reply", error: e, promiseId }); + } + }); } } } diff --git a/src/modules/.common/classes/resolver.ts b/src/modules/.common/classes/resolver.ts new file mode 100644 index 0000000..0382494 --- /dev/null +++ b/src/modules/.common/classes/resolver.ts @@ -0,0 +1,57 @@ +import { parentIpc } from "../parentIpc.js"; +import { RemoteModule } from "./module.js"; +import z from "zod"; + +export class Resolver { + static async getDependencyModule(dependency: string) { + const result = await parentIpc.rpc("getDependency", { dependency }); + const value = z.object({ + port: z.instanceof(MessagePort).optional(), + id: z.string(), + }).parse(result); + + return await RemoteModule.getOrCreate(value.port, value.id); + } + static async getDependency(dependency: string) { + return (await this.getDependencyModule(dependency)).localHandle; + } + static async getModuleByToken(opaqueToken: string) { + const result = await parentIpc.rpc("getModuleByToken", { opaqueToken }); + const value = z.object({ + port: z.instanceof(MessagePort).optional(), + id: z.string(), + }).parse(result); + + return await RemoteModule.getOrCreate(value.port, value.id); + } + static async getByToken(token: string) { + return (await this.getModuleByToken(token)).localHandle; + } + static async summon(id: string, args: any[] = []) { + if (id.split('.').length !== 2) throw new Error("Malformed component identifier"); + const [schema, component] = id.split('.'); + + return (await this.getDependency(schema)).get(component).create(...args); + } + + static async getAllDependency(dependency: string) { + const result = await parentIpc.rpc("getAllDependency", { dependency }); + const handles = z.array(z.object({ + port: z.instanceof(MessagePort).optional(), + id: z.string(), + })).parse(result); + return await Promise.all( + handles.map( + async (handle) => + (await RemoteModule.getOrCreate(handle.port, handle.id)) + .localHandle + ) + ); + } + static async summonAll(id: string, args: any[] = []) { + if (id.split('.').length !== 2) throw new Error("Malformed component identifier"); + const [schema, component] = id.split('.'); + + return (await this.getDependency(schema)).get(component).create(args); + } +} diff --git a/src/modules/.common/classes/sharedDomDummy.ts b/src/modules/.common/classes/sharedDomDummy.ts index 601f5eb..d54e2f4 100644 --- a/src/modules/.common/classes/sharedDomDummy.ts +++ b/src/modules/.common/classes/sharedDomDummy.ts @@ -1,10 +1,14 @@ +import { parentIpc } from "../parentIpc"; +import { OrderedPeer } from "./orderedPeer"; +import { NodeRegistry, SharedDOM } from "./sharedDomRemote"; + export class DummyNodeRegistry { - static registryPerSource = new WeakMap(); + static registryPerSource = new WeakMap(); - static registryOf(source: MessageEventSource) { + static registryOf(source: OrderedPeer) { return this.registryPerSource.get(source); } - static setRegistry(source: MessageEventSource, nr: DummyNodeRegistry) { + static setRegistry(source: OrderedPeer, nr: DummyNodeRegistry) { if (DummyNodeRegistry.registryPerSource.has(source)) throw new Error("Only one NodeRegistry per window may exist"); return this.registryPerSource.set(source, nr); } @@ -12,17 +16,16 @@ export class DummyNodeRegistry { nodeToId = new WeakMap(); idToNode = new Map>(); - source: MessageEventSource; + source: OrderedPeer; - constructor(source: MessageEventSource) { + constructor(source: OrderedPeer) { this.source = source; - DummyNodeRegistry.setRegistry(source, this); } hasNode(node: Node) { return this.nodeToId.has(node); } - getNode(id: string) { + async getNode(id: string) { if (this.idToNode.has(id)) { const result = this.idToNode.get(id)!.deref(); if (result) return result; @@ -30,6 +33,13 @@ export class DummyNodeRegistry { const node = document.createElement("div"); + const actualId = await SharedDOM.initOrGet(node); + await parentIpc.rpc("containForeign", { + id: actualId, + toId: id, + toOpaqueToken: this.source.opaqueToken, + }); + this.nodeToId.set(node, id); this.idToNode.set(id, new WeakRef(node)); return node; diff --git a/src/modules/.common/classes/sharedDomRemote.ts b/src/modules/.common/classes/sharedDomRemote.ts index 2cb873b..79c8c5c 100644 --- a/src/modules/.common/classes/sharedDomRemote.ts +++ b/src/modules/.common/classes/sharedDomRemote.ts @@ -75,21 +75,27 @@ function serializeNode(node: Node) { } class MutationDispatcher { - private static observer = new MutationObserver(MutationDispatcher.handle); + private static observers = new WeakMap(); + private static handle(mutations: MutationRecord[]) { + for (const m of mutations) { + SharedDOM.handleMutation(m); + } + } static observe(root: Node) { - this.observer.observe(root, { + const observer = new MutationObserver(this.handle); + observer.observe(root, { subtree: true, attributes: true, childList: true, characterData: true, }); + this.observers.set(root, observer); } - private static handle(mutations: MutationRecord[]) { - for (const m of mutations) { - SharedDOM.handleMutation(m); - } + static disconnect(root: Node) { + this.observers.get(root)?.disconnect(); + this.observers.delete(root); } } @@ -98,34 +104,37 @@ export class SharedDOM { parentIpc.rpc("deleteNode", { id: heldValue }); }); - static initOrGet(root: Node) { + static async initOrGet(root: Node) { if (NodeRegistry.hasNode(root)) return NodeRegistry.getId(root); - this.init(root); + await this.init(root); return NodeRegistry.getId(root); } - static init(root: Node) { - this.registerSubtree(root); + static async init(root: Node) { + await this.registerSubtree(root); MutationDispatcher.observe(root); } - static registerSubtree(node: Node) { + static async registerSubtree(node: Node) { + if (NodeRegistry.hasNode(node)) { + // We presuppose it will be registered again by init + return MutationDispatcher.disconnect(node); + } + const id = NodeRegistry.getId(node); - if (node instanceof HTMLTemplateElement) this.registerSubtree(node.content); - node.childNodes.forEach((n: Node) => this.registerSubtree(n)); - - parentIpc.post({ - type: "createNode", - data: { - id: id, - payload: serializeNode(node), - events: serializeEvents(node), - }, + if (node instanceof HTMLTemplateElement) await this.registerSubtree(node.content); + + await Promise.all(Array.from(node.childNodes).map((n: Node) => this.registerSubtree(n))); + + await parentIpc.rpc("createNode", { + id: id, + payload: serializeNode(node), + events: serializeEvents(node), }); this.finReg.register(node, id); } - static handleMutation(m: MutationRecord) { + static async handleMutation(m: MutationRecord) { const targetId = NodeRegistry.getId(m.target); switch (m.type) { @@ -134,15 +143,16 @@ export class SharedDOM { type: "changeAttribute", data: { target: targetId, - name: m.attributeName, + name: m.attributeName!, namespace: m.attributeNamespace, + value: (m.target as Element).getAttributeNS(m.attributeNamespace, m.attributeName!), }, }); break; case "childList": if (m.addedNodes.length) { - const ids = Array.from(m.addedNodes, NodeRegistry.getId); + const ids = await Promise.all(Array.from(m.addedNodes).map(SharedDOM.initOrGet.bind(SharedDOM))); parentIpc.post({ type: "addNodes", data: { @@ -154,7 +164,7 @@ export class SharedDOM { } if (m.removedNodes.length) { - const ids = Array.from(m.removedNodes, NodeRegistry.getId); + const ids = await Promise.all(Array.from(m.removedNodes).map(SharedDOM.initOrGet.bind(SharedDOM))); parentIpc.post({ type: "removeNodes", data: { diff --git a/src/modules/.common/classes/withTransfer.ts b/src/modules/.common/classes/withTransfer.ts index 099794f..e9dbd3c 100644 --- a/src/modules/.common/classes/withTransfer.ts +++ b/src/modules/.common/classes/withTransfer.ts @@ -3,8 +3,13 @@ export class WithTransfer { data: any; transfer: any[]; constructor(data: any, transfer: any[] = []) { - this.data = data; - this.transfer = transfer; + if (data instanceof WithTransfer) { + this.data = data.data; + this.transfer = [...data.transfer, ...transfer]; + } else { + this.data = data; + this.transfer = transfer; + } } clone() { return new WithTransfer(this.data, this.transfer); diff --git a/src/modules/.common/index.ts b/src/modules/.common/index.ts index d2b9d9c..7956e78 100644 --- a/src/modules/.common/index.ts +++ b/src/modules/.common/index.ts @@ -11,3 +11,4 @@ OrderedPeer.registerIpcListener(); export { Component } from "./classes/component.js"; export { componentList } from "./classes/componentList.js" +export { Resolver } from "./classes/resolver.js"; diff --git a/src/modules/.common/ipcHandlers/cathodiqueConsumer.ts b/src/modules/.common/ipcHandlers/cathodiqueConsumer.ts index 8f7fcc6..b5a2d09 100644 --- a/src/modules/.common/ipcHandlers/cathodiqueConsumer.ts +++ b/src/modules/.common/ipcHandlers/cathodiqueConsumer.ts @@ -2,23 +2,34 @@ import z from "zod"; import { HandlerContext } from "../utils/types.js"; import { RemoteModule } from "../classes/module.js"; import { unwrapValue, WrappedValue, zodWrappedValue } from "../utils/wrap.js"; +import { Latch } from "../classes/latch.js"; -export class CathodiqueConsumerHandler { +export class CathodiqueAvailableComponentsHandler { [k: string]: (arg: Record, ctx: HandlerContext) => any; - #module: RemoteModule; + #availableComponents: Latch; - constructor(module: RemoteModule) { - this.#module = module; + constructor(availableComponentsLatch: Latch) { + this.#availableComponents = availableComponentsLatch; } - componentRegistered(args: Record) { - return this.#componentRegistered(z.object({ - data: z.object({ componentName: z.string() }), + moduleReady(args: Record) { + return this.#moduleReady(z.object({ + data: z.object({ componentList: z.array(z.string()) }), }).parse(args)); } - async #componentRegistered({ data }: { data: { componentName: string } }) { - this.#module.componentReady.resolve(data.componentName, undefined); + async #moduleReady({ data }: { data: { componentList: string[] } }) { + this.#availableComponents.resolve?.(data.componentList); + } +} + +export class CathodiqueConsumerHandler { + [k: string]: (arg: Record, ctx: HandlerContext) => any; + + #module: RemoteModule; + + constructor(module: RemoteModule) { + this.#module = module; } emitEvent(arg: Record) { @@ -30,8 +41,8 @@ export class CathodiqueConsumerHandler { }), }).parse(arg)); } - #emitEvent({ data }: { data: { eventName: string, componentId: string, args: WrappedValue[] } }) { - const unwrappedArgs = data.args.map((v) => unwrapValue(v, this.#module)); + async #emitEvent({ data }: { data: { eventName: string, componentId: string, args: WrappedValue[] } }) { + const unwrappedArgs = await Promise.all(data.args.map((v) => unwrapValue(v, this.#module.peer))); this.#module.getInstanceProxy(data.componentId)?.emit(data.eventName, ...unwrappedArgs); } }; diff --git a/src/modules/.common/ipcHandlers/cathodiqueProvider.ts b/src/modules/.common/ipcHandlers/cathodiqueProvider.ts index 1a2a86c..4c656dd 100644 --- a/src/modules/.common/ipcHandlers/cathodiqueProvider.ts +++ b/src/modules/.common/ipcHandlers/cathodiqueProvider.ts @@ -3,9 +3,9 @@ import { componentList, ComponentList } from "../classes/componentList.js"; import { HandlerContext } from "../utils/types.js"; import { unwrapValue, WrappedValue, wrapValue, zodWrappedValue } from "../utils/wrap.js"; import { Component } from "../classes/component.js"; -import { RemoteModule } from "../classes/module.js"; import { ShouldHaveBeenZodError, stringStartsWithDollar } from "../utils/utils.js"; import { parentIpc } from "../parentIpc.js"; +import { OrderedPeer } from "../classes/orderedPeer.js"; // DISTINCTIONS WITH HOST // A single module has a single componentList @@ -21,37 +21,40 @@ export class CathodiqueProviderHandler { static #componentList: ComponentList = componentList; - static #componentInstances = new Map(); - static componentExists(id: string) { - return this.#componentInstances.has(id); - } + #peer: OrderedPeer; + constructor(peer: OrderedPeer) { + this.#peer = peer; - #fromModule: RemoteModule | undefined; - constructor(fromModule: RemoteModule | undefined) { - this.#fromModule = fromModule; + this.#init(); + } + async #init() { + await componentList.ready; + await this.#peer.rpc("moduleReady", { + componentList: [...componentList.componentClasses.keys()], + }); } createInstance(arg: Record) { return this.#createInstance(z.object({ data: z.object({ - className: z.string().refine(CathodiqueProviderHandler.#componentList.has), + className: z.string().refine(CathodiqueProviderHandler.#componentList.has + .bind(CathodiqueProviderHandler.#componentList)), args: z.array(zodWrappedValue), }), }).parse(arg)); } async #createInstance({ data }: { data: { className: string, args: WrappedValue[] } }) { + // alert(1); const ClassObj = CathodiqueProviderHandler.#componentList.get(data.className); if (!ClassObj) throw new ShouldHaveBeenZodError(); - const unwrapped = data.args.map((v: WrappedValue) => unwrapValue(v, this.#fromModule)); - const componentInstance = new ClassObj(...unwrapped) as Component; + const unwrapped = await Promise.all(data.args.map((v: WrappedValue) => unwrapValue(v, this.#peer))); + const pctx = {}; + + const componentInstance = (await ClassObj.create(pctx, ...unwrapped)) as Component; await componentInstance.init(); - CathodiqueProviderHandler.#componentInstances.set( - componentInstance.componentId, - componentInstance, - ); return componentInstance.componentId; } @@ -63,7 +66,7 @@ export class CathodiqueProviderHandler { }).parse(arg)); } #instanceExists({ data }: { data: { componentId: string } }) { - return CathodiqueProviderHandler.#componentInstances.has(data.componentId); + return componentList.componentInstances.has(data.componentId); } getInstanceData(arg: Record) { @@ -74,7 +77,7 @@ export class CathodiqueProviderHandler { }).parse(arg)); } #getInstanceData({ data }: { data: { componentId: string } }) { - const instance = CathodiqueProviderHandler.#componentInstances.get(data.componentId); + const instance = componentList.componentInstances.get(data.componentId); return instance && { componentName: componentList.componentTypeOf(instance), }; @@ -84,12 +87,13 @@ export class CathodiqueProviderHandler { return this.#getProperty(z.object({ data: z.object({ propertyName: z.string().startsWith("$"), - componentId: z.string().refine(CathodiqueProviderHandler.componentExists), + componentId: z.string().refine(componentList.instanceExists + .bind(componentList)), }), }).parse(arg)); } async #getProperty({ data }: { data: { propertyName: string; componentId: string } }) { - const component = CathodiqueProviderHandler.#componentInstances.get(data.componentId); + const component = componentList.componentInstances.get(data.componentId); if (!component) throw new ShouldHaveBeenZodError(); // Zod... @@ -106,7 +110,8 @@ export class CathodiqueProviderHandler { data: z.object({ methodName: z.string().startsWith('$'), arguments: z.array(z.any()), - componentId: z.string().refine(CathodiqueProviderHandler.componentExists), + componentId: z.string().refine(componentList.instanceExists + .bind(componentList)), }), }).parse(arg)); } @@ -117,7 +122,7 @@ export class CathodiqueProviderHandler { componentId: string; }; }) { - const component = CathodiqueProviderHandler.#componentInstances.get(data.componentId)!; + const component = componentList.componentInstances.get(data.componentId)!; // Zod... const methodName = data.methodName; @@ -142,17 +147,18 @@ export class CathodiqueProviderHandler { componentId: string; }; }) { - const component = CathodiqueProviderHandler.#componentInstances.get(data.componentId); + const component = componentList.componentInstances.get(data.componentId); if (!component) throw new ShouldHaveBeenZodError(); - component.listenFor(data.eventName, this.#fromModule?.peer ?? parentIpc); + component.listenFor(data.eventName, this.#peer ?? parentIpc); } unlistenToEvent(arg: Record) { return this.#unlistenToEvent(z.object({ data: z.object({ eventName: z.string(), - componentId: z.string().refine(CathodiqueProviderHandler.componentExists), + componentId: z.string().refine(componentList.instanceExists + .bind(componentList)), }), }).parse(arg)); } @@ -162,9 +168,9 @@ export class CathodiqueProviderHandler { componentId: string; }; }) { - const component = CathodiqueProviderHandler.#componentInstances.get(data.componentId); + const component = componentList.componentInstances.get(data.componentId); if (!component) throw new ShouldHaveBeenZodError(); - component.unlistenFor(data.eventName, this.#fromModule?.peer ?? parentIpc); + component.unlistenFor(data.eventName, this.#peer ?? parentIpc); } }; diff --git a/src/modules/.common/ipcHandlers/cathodiqueRemote.ts b/src/modules/.common/ipcHandlers/cathodiqueRemote.ts index abe9882..59bd81f 100644 --- a/src/modules/.common/ipcHandlers/cathodiqueRemote.ts +++ b/src/modules/.common/ipcHandlers/cathodiqueRemote.ts @@ -3,41 +3,45 @@ import { RemoteModule } from "../classes/module.js"; import { OrderedPeer } from "../classes/orderedPeer.js"; import { HandlerContext } from "../utils/types.js"; import { CathodiqueProviderHandler } from "./cathodiqueProvider.js"; +import { Latch } from "../classes/latch.js"; + +const moduleIdLatch = new Latch(); +export const moduleId = moduleIdLatch.promise; export class CathodiqueRemoteHandler { + static instance: CathodiqueRemoteHandler; [k: string]: (arg: Record, ctx: HandlerContext) => any; - constructor() {} + constructor() { + if (CathodiqueRemoteHandler.instance) throw new Error("Remote is a singleton."); + CathodiqueRemoteHandler.instance = this; + } - connectAsProvider(arg: Record) { - return this.#connectAsProvider(z.object({ + moduleId(arg: Record) { + return this.#moduleId(z.object({ data: z.object({ - port: z.instanceof(MessagePort), - moduleToken: z.string(), + moduleId: z.string(), }), }).parse(arg)); } - #connectAsProvider({ data }: { data: { port: MessagePort, moduleToken: string } }) { - const module = RemoteModule.getOrCreate(data.port, data.moduleToken); - // TODO Expose self as provider - - // Note: We can trust this messageport (unless vuln) because the parent is trusted to be raytube - const peer = new OrderedPeer(data.port, "*"); - peer.addHandler(new CathodiqueProviderHandler(module) as any); + #moduleId({ data }: { data: { moduleId: string } }) { + moduleIdLatch.resolve!(data.moduleId); } - connectAsConsumer(arg: Record) { - return this.#connectAsConsumer(z.object({ + connectAsProvider(arg: Record) { + return this.#connectAsProvider(z.object({ data: z.object({ port: z.instanceof(MessagePort), - moduleToken: z.string(), + opaqueToken: z.string().optional(), }), }).parse(arg)); } - #connectAsConsumer({ data }: { data: { port: MessagePort, moduleToken: string } }) { - // This is for, if *this* module wishes to connect to another module. + async #connectAsProvider({ data }: { data: { port: MessagePort, opaqueToken?: string } }) { + // const module = await RemoteModule.getOrCreate(data.port, data.moduleToken); + // TODO Expose self as provider - RemoteModule.createModule(data.port, data.moduleToken); - // The rest will be handled by remoteToLocalAdapter.ts + // Note: We can trust this messageport (unless vuln) because the parent is trusted to be raytube + const peer = new OrderedPeer(data.port, data.opaqueToken, "*"); + peer.addHandler(new CathodiqueProviderHandler(peer) as any); } } diff --git a/src/modules/.common/parentIpc.ts b/src/modules/.common/parentIpc.ts index a8deb83..949e608 100644 --- a/src/modules/.common/parentIpc.ts +++ b/src/modules/.common/parentIpc.ts @@ -2,14 +2,14 @@ import { OrderedPeer } from "./classes/orderedPeer.js"; import { CathodiqueProviderHandler } from "./ipcHandlers/cathodiqueProvider.js"; import { CathodiqueRemoteHandler } from "./ipcHandlers/cathodiqueRemote.js"; import { DOMRemoteHandler } from "./ipcHandlers/domRemote.js"; -import { canonicalHost } from "./utils/utils.js"; +import { parentOrigin } from "./utils/utils.js"; // We are removing the * for now because // we would rather trust the parent. // TODO expose public handle with API. -const parentIpc = new OrderedPeer(window.parent, canonicalHost); +const parentIpc = new OrderedPeer(window.parent, undefined, parentOrigin); parentIpc.addHandler(new CathodiqueRemoteHandler()); -parentIpc.addHandler(new CathodiqueProviderHandler(undefined)); +parentIpc.addHandler(new CathodiqueProviderHandler(parentIpc)); parentIpc.addHandler(new DOMRemoteHandler()); export { parentIpc }; diff --git a/src/modules/.common/utils/nodeEventListener.ts b/src/modules/.common/utils/nodeEventListener.ts index b972ec8..55e50ca 100644 --- a/src/modules/.common/utils/nodeEventListener.ts +++ b/src/modules/.common/utils/nodeEventListener.ts @@ -176,7 +176,6 @@ function patchOnEvents() { return currentListener; }, set(fn: ((...args: any[]) => any) | null) { - console.log(fn) const node = this as Node; diff --git a/src/modules/.common/utils/remoteToLocalAdapter.ts b/src/modules/.common/utils/remoteToLocalAdapter.ts index e8dd71f..206ee7f 100644 --- a/src/modules/.common/utils/remoteToLocalAdapter.ts +++ b/src/modules/.common/utils/remoteToLocalAdapter.ts @@ -7,10 +7,21 @@ import { unwrapValue, wrapValue } from "./wrap.js"; import { EventEmitter } from "events"; export class ComponentListProxy implements ComponentListHandle { - componentClasses = new Map ComponentInstanceProxy>(); + #module: RemoteModule; + constructor(mod: RemoteModule) { + this.#module = mod; + } get(componentName: string) { - return this.componentClasses.get(componentName); + const availableComponents = this.#module.availableComponents; + + if (!availableComponents.includes(componentName)) return undefined; + + return { + create: (...args: any[]) => { + return makeComponentProxy(this.#module, componentName, { args }); + } + }; } } @@ -57,20 +68,23 @@ export class ComponentInstance extends EventEmitter { async init() { if (this.#cidLatch.getState() === LatchState.Pending) { - await this.module.waitForComponent(this.componentName); + if (!this.module.availableComponents.includes(this.componentName)) { + throw new Error(`"${this.componentName}" not provided by module`); + } - await this.module.componentReady.get(this.componentName); const componentId = await this.peer.rpc("createInstance", { className: this.componentName, - args: this.#args.map((v) => wrapValue(v)), + args: await Promise.all(this.#args.map((v) => wrapValue(v))), }); this.#cidLatch.resolve!(componentId); this.ready.resolve!(); + } else { + this.ready.resolve?.(); } } } export type ComponentInstanceProxy = ComponentInstance - & Partial> + & Partial> & { [Component.isComponentSymbol]: true }; function generateCalledOrAwaited({ called, awaited }: { called: (...args: any[]) => any, awaited: () => any }) { @@ -109,7 +123,7 @@ export function makeComponentProxy(module: RemoteModule, componentName: string, componentId: id, }); - return unwrapValue(val, module); + return await unwrapValue(val, module.peer); }, awaited: async () => { const id = await compInst.componentId; @@ -119,7 +133,7 @@ export function makeComponentProxy(module: RemoteModule, componentName: string, componentId: id, }); - return unwrapValue(val, module); + return await unwrapValue(val, module.peer); }, }); }, diff --git a/src/modules/.common/utils/types.ts b/src/modules/.common/utils/types.ts index df1abdd..d56ffa8 100644 --- a/src/modules/.common/utils/types.ts +++ b/src/modules/.common/utils/types.ts @@ -1,11 +1,12 @@ import z from "zod"; import { OrderedPeer } from "../classes/orderedPeer"; import { Component, ComponentHandle } from "../classes/component"; +import { ComponentInstanceProxy } from "./remoteToLocalAdapter"; export interface ElementFromIpc { kind: "element"; tagName: string; - attributes: [string, string, string][]; + attributes: [string | null, string, string][]; children: string[]; content?: string; } @@ -39,5 +40,20 @@ export interface HandlerContext { event: MessageEvent; } -export type ComponentClass = new (...a: any[]) => Component; -export type ComponentHandleClass = new (...a: any[]) => ComponentHandle; +export const componentTypes = [ + "NORMAL", + "SINGLETON", + "REF_ONLY", +] as const; + +export type FactoryOf = { create(...args: U): T | Promise }; +export type ComponentFactory = FactoryOf; +export type ComponentHandleFactory = FactoryOf; +export type ComponentInstanceProxyFactory = FactoryOf; + +// Class is what it might be for users +export type ClassOf = (FactoryOf | (new (...args: U) => T)) + & { type: typeof componentTypes[number], singletonInstance?: T }; +export type ComponentClass = ClassOf; +export type ComponentHandleClass = ClassOf; +export type ComponentInstanceProxyClass = ClassOf; diff --git a/src/modules/.common/utils/utils.ts b/src/modules/.common/utils/utils.ts index a055ab5..981c1cb 100644 --- a/src/modules/.common/utils/utils.ts +++ b/src/modules/.common/utils/utils.ts @@ -20,6 +20,6 @@ export { projectSubdomain, userSubdomain }; export const host = hostList.join('.') export const canonicalHost = `${location.protocol}//${hostList.join(".")}:${location.port}`; -export const parentOrigin = await fetch("/.common/hostOverride").then((v) => v.text(), () => canonicalHost); +export const parentOrigin = new URLSearchParams(location.search).get("parent_origin") || canonicalHost; export const stringStartsWithDollar = (v: string): v is `$${string}` => v.startsWith("$"); diff --git a/src/modules/.common/utils/wrap.ts b/src/modules/.common/utils/wrap.ts index 6252d83..269af88 100644 --- a/src/modules/.common/utils/wrap.ts +++ b/src/modules/.common/utils/wrap.ts @@ -5,26 +5,28 @@ import { SharedDOM } from "../classes/sharedDomRemote"; import { ComponentInstanceProxy, makeComponentProxy } from "./remoteToLocalAdapter"; import { RemoteModule } from "../classes/module"; import { DummyNodeRegistry } from "../classes/sharedDomDummy"; +import { moduleId } from "../ipcHandlers/cathodiqueRemote"; +import { OrderedPeer } from "../classes/orderedPeer"; export const zodWrappedValue = z.union([ - z.object({ - value: z.any(), - }), z.object({ type: z.literal("component"), componentName: z.string(), componentId: z.string(), - moduleId: z.string().optional(), // undefined => This module comes from myself + moduleId: z.string(), }), z.object({ type: z.literal("node"), nodeId: z.string(), }), + z.object({ + value: z.any(), + }), ]); export type WrappedValue = z.output; export async function wrapValue(value: any): Promise> { - const isComponent = (v: any): v is (Component | ComponentInstanceProxy) => v[Component.isComponentSymbol]; + const isComponent = (v: any): v is (Component | ComponentInstanceProxy) => v?.[Component.isComponentSymbol]; if (isComponent(value)) { const isRemote = "module" in value; @@ -32,12 +34,12 @@ export async function wrapValue(value: any): Promise(); // Window to WindowFrame + + static async create() { + const windowFrameModule = await Resolver.getDependency("WindowFrame"); + const windowRegistry = await Resolver.summon("Cathodique::Window.WindowRegistry"); + + return new WindowManager(windowFrameModule, windowRegistry); + } + + constructor(windowFrameModule: ComponentListHandle, windowRegistry: ComponentInstanceProxy) { + super(); + + // windowRegistry.$hello(); + + this.#windowFrameModule = windowFrameModule; + this.#windowRegistry = windowRegistry; + + this.#windowRegistry.on("newWindow", this.newWindow); + } + async newWindow(window: ComponentInstanceProxy) { + alert("aaa"); + + // window is a window component :3 + const WindowFrame = this.#windowFrameModule.get("WindowFrame")!; + const windowFrame = await WindowFrame.create(window); + this.windowFrames.set(window, windowFrame); + + this.$output.append(await windowFrame.$output); + } +} + +componentList.register("WindowFrame", WindowManager); +// componentList.rea diff --git a/src/modules/cathodique.windowmanager/module.html b/src/modules/cathodique.windowmanager/module.html index 4abce79..717b222 100644 --- a/src/modules/cathodique.windowmanager/module.html +++ b/src/modules/cathodique.windowmanager/module.html @@ -7,39 +7,47 @@ - +
+

Hello World

+
diff --git a/src/modules/immjs.macos-aqua-windowframe/module.html b/src/modules/immjs.macos-aqua-windowframe/module.html index 08f2da6..a8dbdb4 100644 --- a/src/modules/immjs.macos-aqua-windowframe/module.html +++ b/src/modules/immjs.macos-aqua-windowframe/module.html @@ -7,33 +7,31 @@ - + diff --git a/src/renderer/classes/handlers/dom/popup.ts b/src/renderer/classes/handlers/dom/popup.ts index e2ed8fa..4dd8bc9 100644 --- a/src/renderer/classes/handlers/dom/popup.ts +++ b/src/renderer/classes/handlers/dom/popup.ts @@ -1,18 +1,17 @@ import { BaseDom } from "./base.js"; import { XdgPopup } from "@cathodique/wl-serv-high/objects"; +import { wlToObj } from "../handlers.js"; // Toplevels: context for other subsurfaces to appear in // Popups are the same // TODO: (to reconsider) Should Toplevels and Popups have a mother class? export class PopupDom extends BaseDom { - static wlToPopupDom = new Map(); - constructor(wl: XdgPopup) { super(wl, document.createElement('div')); - PopupDom.wlToPopupDom.set(wl, this); + wlToObj.set(wl, this); } - async init() { - - } + // async init() { + // const posOfPopup = this.wl.meta.positioner.positionWithinOutputAndStruts(); + // } } diff --git a/src/renderer/classes/handlers/dom/subsurface.ts b/src/renderer/classes/handlers/dom/subsurface.ts deleted file mode 100644 index 1037488..0000000 --- a/src/renderer/classes/handlers/dom/subsurface.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { WlSubsurface } from "@cathodique/wl-serv-high/objects"; -import { BaseDom } from "./base.js"; -import { SurfaceDom } from "./surface.js"; -import { WlSurface } from "@cathodique/wl-serv-high/objects"; - -export class SubsurfaceDom extends BaseDom { - static wlToSubsurfaceDom = new Map(); - - constructor(wl: WlSubsurface) { - super(wl, document.createElement("div")); - SubsurfaceDom.wlToSubsurfaceDom.set(wl, this); - this.init(); - } - - get kid() { - const kidWl = this.wl.meta.surface; - const kidDom = SurfaceDom.wlToSurfaceDom.get(kidWl); - - if (!kidDom) throw new Error("DOM of surface does not exist"); - return kidDom; - } - - init () { - this.dom.append(this.kid.dom); - - // Subsurface shenanigans - // TODO: Apply on commit - this.wl.on("wlPlaceAbove", function (this: SubsurfaceDom, { sibling: other }: { sibling: WlSurface }) { - switch (this.wl.getRelationWith(other)) { - case "sibling": { - const sibling = SurfaceDom.wlToSurfaceDom.get(other)!; - const commonParentWl = other.subsurface!.meta.parent; - const commonParent = SurfaceDom.wlToSurfaceDom.get(commonParentWl)!; - - commonParent.dom.insertBefore(this.dom, sibling.dom); - break; - } - case "parent": { - const parent = SurfaceDom.wlToSurfaceDom.get(other)!; - - const parentSubsurface = SubsurfaceDom.wlToSubsurfaceDom.get(other.subsurface!)!; - - parentSubsurface.dom.insertBefore(this.dom, parent.dom); - break; - } - default: - // Already handled by wl-serv-high - } - }.bind(this)); - - this.wl.on("wlPlaceBelow", function (this: SubsurfaceDom, { sibling: other }: { sibling: WlSurface }) { - switch (this.wl.getRelationWith(other)) { - case "sibling": { - const sibling = SurfaceDom.wlToSurfaceDom.get(other)!; - const commonParentWl = other.subsurface!.meta.parent; - const commonParent = SurfaceDom.wlToSurfaceDom.get(commonParentWl)!; - - commonParent.dom.insertBefore(this.dom, sibling.dom.nextSibling); - break; - } - case "parent": { - const parent = SurfaceDom.wlToSurfaceDom.get(other)!; - - const parentSubsurface = SubsurfaceDom.wlToSubsurfaceDom.get(other.subsurface!)!; - - parentSubsurface.dom.insertBefore(this.dom, parent.dom.nextSibling); - break; - } - default: - // Already handled by wl-serv-high - } - }.bind(this)); - - this.wl.on('wlSetPosition', function (this: SubsurfaceDom, { y, x }: { y: number, x: number }) { - const surface = SurfaceDom.wlToSurfaceDom.get(this.wl.meta.surface)!; - - surface.dom.style.top = `${y}px`; - surface.dom.style.left = `${x}px`; - }.bind(this)); - } -} diff --git a/src/renderer/classes/handlers/dom/surface.ts b/src/renderer/classes/handlers/dom/surface.ts index 0549cc7..d0512f0 100644 --- a/src/renderer/classes/handlers/dom/surface.ts +++ b/src/renderer/classes/handlers/dom/surface.ts @@ -2,19 +2,29 @@ import { WlSurface } from "@cathodique/wl-serv-high/objects"; import { Seat } from "../../wayland/seat/seat.js"; import { BaseDom } from "./base.js"; import { Output } from "../../wayland/output/output.js"; +import { wlToObj } from "../handlers.js"; +import { seat } from "../../../wayland/index.js"; +import { outputRegistry } from "../../../wayland/overlays/outputRegistryOverlay.js"; +import { isIntersecting } from "../../../utils/domIntersect.js"; -// HEY JULIETTE!! THIS IS WHERE WE AT -// WE NEED TO CREATE THE ACTUAL COMPONENT, THEN HOOK IT UP!! - -export class SurfaceDom extends BaseDom { - static wlToSurfaceDom = new Map(); +export class SurfaceDom extends BaseDom { + static domToSurface = new Map(); ctx: CanvasRenderingContext2D; + canvas = document.createElement("canvas"); constructor(wl: WlSurface) { - super(wl, document.createElement("canvas")); - SurfaceDom.wlToSurfaceDom.set(wl, this); + super(wl, document.createElement("div")); + this.dom.append(this.canvas); + + SurfaceDom.domToSurface.set(this.dom, this); + wlToObj.set(wl, this); + + this.canvas.style.position = "absolute"; + this.canvas.style.top = "0"; + this.canvas.style.left = "0"; + this.dom.style.position = "relative"; - const ctx = this.dom.getContext("2d"); + const ctx = this.canvas.getContext("2d"); if (!ctx) throw new Error( "Failed to derive 2d context from canvas element; is anything disabled?", @@ -25,16 +35,23 @@ export class SurfaceDom extends BaseDom { shownOnOutputs = new Set(); init() { + this.initDraw(); + this.initSeatMouse(seat); + this.initOutputs(); + } + initDraw() { let lastDimensions: [number, number] = [-Infinity, -Infinity]; const commitHandler = async function (this: SurfaceDom) { const b = this.wl.buffer.current; - if (b === null) this.dom.style.display = "none"; + this.canvas.style.display = b === null ? "none" : "block"; if (b == null) return; if (lastDimensions[0] !== b.meta.height || lastDimensions[1] !== b.meta.width) { - this.dom.width = b.meta.width; - this.dom.height = b.meta.height; + this.dom.style.width = `${b.meta.width}px`; + this.dom.style.height = `${b.meta.height}px`; + this.canvas.width = b.meta.width; + this.canvas.height = b.meta.height; lastDimensions = [b.meta.height, b.meta.width]; } @@ -49,10 +66,14 @@ export class SurfaceDom extends BaseDom { b.meta.width * b.meta.height * 4, ); if (arr.length > 0) { + console.log("Got Update?", currlyDamagedBuffer); + let imageData = new ImageData(arr, b.meta.width, b.meta.height); for (const rect of currlyDamagedBuffer) { - this.ctx.putImageData(imageData, 0, 0, rect.x, rect.y, rect.w, rect.h); + const w = Math.min(rect.w, b.meta.width - rect.x); + const h = Math.min(rect.h, b.meta.height - rect.y); + this.ctx.putImageData(imageData, 0, 0, rect.x, rect.y, w, h); } } }.bind(this); @@ -64,34 +85,74 @@ export class SurfaceDom extends BaseDom { // Unsure vvv this.dom.remove(); }); - - // this.initSeatMouse(); } initSeatMouse(seat: Seat) { - // TODO(multiparty): Single E.L. for each seat; + // LT-TODO(multiparty): Single E.L. for each seat; - const noForceLeave = (e: MouseEvent) => seat.move(e, this); - this.dom.addEventListener("mouseenter", noForceLeave); - this.onUnmount(function () { this.dom.removeEventListener("mouseenter", noForceLeave) }); - this.dom.addEventListener("mousemove", noForceLeave); - this.onUnmount(function () { this.dom.removeEventListener("mousemove", noForceLeave) }); + const enter = (e: MouseEvent) => { + seat.setKeyboardFocus(this); + seat.move(e, this); + }; + this.dom.addEventListener("mouseenter", enter); + this.onUnmount(() => { this.dom.removeEventListener("mouseenter", enter) }); + + const move = (e: MouseEvent) => seat.move(e, this); + this.dom.addEventListener("mousemove", move); + this.onUnmount(() => { this.dom.removeEventListener("mousemove", move) }); - const forceLeave = (e: MouseEvent) => seat.move(e, this, true); - this.onUnmount(function () { this.dom.removeEventListener("mouseleave", forceLeave) }); + const leave = (e: MouseEvent) => { + seat.move(e, this, true); + seat.unsetKeyboardFocus(); + }; + this.onUnmount(() => { this.dom.removeEventListener("mouseleave", leave) }); const mouseDown = (evt: MouseEvent) => { if (seat.mouseFocus) seat.mouseFocus.instances.buttonDown(Seat.mouseWebToButtonMap[evt.button]); }; this.dom.addEventListener("mousedown", mouseDown); - this.onUnmount(function () { this.dom.removeEventListener("mousedown", mouseDown) }); + this.onUnmount(() => { this.dom.removeEventListener("mousedown", mouseDown) }); const mouseUp = (evt: MouseEvent) => { if (seat.mouseFocus) seat.mouseFocus.instances.buttonUp(Seat.mouseWebToButtonMap[evt.button]); } this.dom.addEventListener("mouseup", mouseUp); - this.onUnmount(function () { this.dom.removeEventListener("mouseup", mouseUp) }); + this.onUnmount(() => { this.dom.removeEventListener("mouseup", mouseUp) }); + } + + unmounted: boolean = false; + updateAllOutputs() { + if (this.unmounted) return; + + if (this.wl.buffer.current == null) { + for (const shownOn of this.wl.outputs) { + this.wl.leaveOutput(shownOn.config); + } + } else { + for (const output of outputRegistry.allOutputs()) { + const intersect = isIntersecting(this.dom, output.dom); + + if (this.wl.outputs.has(output.wlOutputAuth.get(this.wl.connection)!) !== intersect) { + console.log(intersect); + + if (intersect) this.wl.enterOutput(output.config); + else this.wl.leaveOutput(output.config); + } + } + } + + requestAnimationFrame(this.updateAllOutputs.bind(this)); + } + initOutputs() { + requestAnimationFrame(this.updateAllOutputs.bind(this)); + this.onUnmount(() => { this.unmounted = false }); + } + enterOutput(output: Output) { + this.wl.enterOutput(output.config); + } + leaveOutput(output: Output) { + this.wl.leaveOutput(output.config); } } diff --git a/src/renderer/classes/handlers/dom/toplevel.ts b/src/renderer/classes/handlers/dom/toplevel.ts deleted file mode 100644 index ebbb888..0000000 --- a/src/renderer/classes/handlers/dom/toplevel.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { XdgToplevel } from "@cathodique/wl-serv-high/objects"; -import { BaseDom } from "./base.js"; -import { orchestrator } from "../../../host/index.js"; -import { ComponentHandle } from "../../../host/classes/component.js"; -import { BaseModule, LocalModule } from "../../../host/classes/module.js"; - -export class ToplevelDom extends BaseDom { - static wlToToplevelDom = new Map(); - - static async create(wl: XdgToplevel) { - const windowFrame = await orchestrator.load("WindowFrame"); - if (!windowFrame) throw new Error("Expected existing WindowFrame"); - - return new this(wl, windowFrame); - } - - instance: ComponentHandle; - private constructor(wl: XdgToplevel, windowFrameModule: BaseModule) { - super(wl, document.createElement("div")); - ToplevelDom.wlToToplevelDom.set(wl, this); - - this.instance = new (windowFrameModule.localHandle.get("WindowFrame")!)(); - - this.init(); - } - - async init () { - this.instance.$setGeometry!(this.wl.parent.geometry.current); - this.wl.parent.geometry.on('current', function (this: ToplevelDom) { - this.instance.$setGeometry!(this.wl.parent.geometry.current); - }.bind(this)); - - this.instance.$setTitle!(this.wl.title); - this.wl.on('wlSetTitle', function (this: ToplevelDom) { - this.instance.$setTitle!(this.wl.title); - }.bind(this)); - } -} diff --git a/src/renderer/classes/handlers/handlers.ts b/src/renderer/classes/handlers/handlers.ts index b76b841..0a1af64 100644 --- a/src/renderer/classes/handlers/handlers.ts +++ b/src/renderer/classes/handlers/handlers.ts @@ -1,10 +1,20 @@ -import { WlSurface } from "@cathodique/wl-serv-high/objects"; +import { WlSubsurface, WlSurface, XdgPopup, XdgToplevel } from "@cathodique/wl-serv-high/objects"; import { PopupDom } from "./dom/popup.js"; -import { ToplevelDom } from "./dom/toplevel.js"; +import { ToplevelDom } from "../../host/localModules/window_toplevel.js"; +import { PolyMap } from "../../host/classes/polymap.js"; +import { SurfaceDom } from "./dom/surface.js"; +import { Subsurface } from "./lib/subsurface.js"; export const objectHandlers = { 'xdg_popup': PopupDom, 'xdg_toplevel': ToplevelDom, - 'wl_surface': WlSurface, - 'wl_output': WlSurface, + 'wl_surface': SurfaceDom, + 'wl_subsurface': Subsurface }; + +export const wlToObj = new PolyMap< + | [XdgPopup, PopupDom] + | [XdgToplevel, ToplevelDom] + | [WlSurface, SurfaceDom] + | [WlSubsurface, Subsurface] +>(); diff --git a/src/renderer/classes/handlers/lib/subsurface.ts b/src/renderer/classes/handlers/lib/subsurface.ts new file mode 100644 index 0000000..a43ea8d --- /dev/null +++ b/src/renderer/classes/handlers/lib/subsurface.ts @@ -0,0 +1,83 @@ +import { WlSubsurface } from "@cathodique/wl-serv-high/objects"; +import { WlSurface } from "@cathodique/wl-serv-high/objects"; +import { wlToObj } from "../handlers.js"; + +export class Subsurface { + wl: WlSubsurface; + get surfaceDom() { return wlToObj.get(this.wl.meta.surface)! }; + get parentSurfaceDom() { return wlToObj.get(this.wl.meta.parent)! }; + + constructor(wl: WlSubsurface) { + this.wl = wl; + + wlToObj.set(wl, this); + console.log("aaasdfsfd"); + } + + init () { + console.log("aaasdfsfd"); + // Subsurface shenanigans + // TODO: Apply on commit + this.wl.on("wlPlaceAbove", this.placeAbove.bind(this)); + this.placeAbove({ sibling: this.wl.meta.parent }); + + this.wl.on("wlPlaceBelow", this.placeBelow.bind(this)); + + this.wl.on('wlSetPosition', ({ y, x }: { y: number, x: number }) => { + const surface = wlToObj.get(this.wl.meta.surface)!; + + surface.dom.style.top = `${y}px`; + surface.dom.style.left = `${x}px`; + }); + } + + get kid() { + const kidWl = this.wl.meta.surface; + const kidDom = wlToObj.get(kidWl); + + if (!kidDom) throw new Error("DOM of surface does not exist"); + return kidDom; + } + + placeAbove({ sibling: other }: { sibling: WlSurface }) { + console.log(other, this.wl.getRelationWith(other)); + switch (this.wl.getRelationWith(other)) { + case "sibling": { + const sibling = wlToObj.get(other)!; + const commonParentWl = other.subsurface!.meta.parent; + const commonParent = wlToObj.get(commonParentWl)!; + + commonParent.dom.insertBefore(this.surfaceDom.dom, sibling.dom); + break; + } + case "parent": { + const parent = wlToObj.get(other)!; + + parent.dom.insertBefore(this.surfaceDom.dom, parent.canvas); + break; + } + default: + // Already handled by wl-serv-high + } + } + placeBelow({ sibling: other }: { sibling: WlSurface }) { + switch (this.wl.getRelationWith(other)) { + case "sibling": { + const sibling = wlToObj.get(other)!; + const commonParentWl = other.subsurface!.meta.parent; + const commonParent = wlToObj.get(commonParentWl)!; + + commonParent.dom.insertBefore(this.surfaceDom.dom, sibling.dom.nextSibling); + break; + } + case "parent": { + const parent = wlToObj.get(other)!; + + parent.dom.insertBefore(this.surfaceDom.dom, parent.canvas.nextSibling); + break; + } + default: + // Already handled by wl-serv-high + } + } +} diff --git a/src/renderer/classes/wayland/output/output.ts b/src/renderer/classes/wayland/output/output.ts index deef64c..a255948 100644 --- a/src/renderer/classes/wayland/output/output.ts +++ b/src/renderer/classes/wayland/output/output.ts @@ -1,5 +1,4 @@ -import { OutputConfiguration } from "@cathodique/wl-serv-high/objects"; -import { OutputRegistry } from "@cathodique/wl-serv-high/registries"; +import { OutputConfiguration, OutputRegistry } from "@cathodique/wl-serv-high/registries"; export class Output { configToOutput = new Map(); @@ -22,11 +21,12 @@ export class Output { initOutput() { this.dom.style.position = "absolute"; + this.dom.style.pointerEvents = "none"; this.dom.style.top = `${this.config.x}px`; this.dom.style.left = `${this.config.y}px`; this.dom.style.width = `${this.config.w}px`; this.dom.style.height = `${this.config.h}px`; - document.body.querySelector('main')!.append(this.dom); + document.body.append(this.dom); } } diff --git a/src/renderer/classes/wayland/seat/seat.ts b/src/renderer/classes/wayland/seat/seat.ts index 584d3e4..e4f5c46 100644 --- a/src/renderer/classes/wayland/seat/seat.ts +++ b/src/renderer/classes/wayland/seat/seat.ts @@ -1,9 +1,8 @@ -import { SeatAuthority, SeatInstances, SeatRegistry } from "@cathodique/wl-serv-high/registries"; +import { SeatConfiguration, SeatInstances, SeatRegistry } from "@cathodique/wl-serv-high/registries"; import { Modifiers } from "./modifiers.js"; import { codeToScan } from "./codeToScancode.js"; import { SurfaceDom } from "../../handlers/dom/surface.js"; import { isInRegion } from "../../../wayland/index.js"; -import { SeatConfiguration } from "@cathodique/wl-serv-high/objects"; export class Seat { static mouseWebToButtonMap: Record = { @@ -74,7 +73,27 @@ export class Seat { }); } - move (evt: MouseEvent, surface: SurfaceDom, forceLeave?: boolean) { + setMouseFocus(surface: SurfaceDom) { + this.mouseFocus = { + surface, + instances: this.wlSeatAuth.get(surface.wl.connection)!, + }; + } + unsetMouseFocus() { + this.mouseFocus?.instances.blur(this.mouseFocus.surface.wl); + this.mouseFocus?.instances.leave(this.mouseFocus.surface.wl); + this.mouseFocus = undefined; + } + + setKeyboardFocus(surface: SurfaceDom) { + this.keyboardFocus = { + surface, + instances: this.wlSeatAuth.get(surface.wl.connection)!, + }; + } + unsetKeyboardFocus() { this.keyboardFocus = undefined; } + + move(evt: MouseEvent, surface: SurfaceDom, forceLeave?: boolean) { // (obj.xdgSurface?.parent as XdgWmBase)?.addCommand("ping", { // serial: obj.connection.time.getTime(), // }); @@ -91,19 +110,17 @@ export class Seat { !forceLeave && isInRegion(surface.wl.inputRegions.current, mouseY, mouseX, true) ) { + // We are in the region and not looking to leave if (this.mouseFocus?.surface !== surface) { + this.setMouseFocus(surface); + // We are currently focusing a surface that is not ours this.mouseFocus!.instances = this.wlSeatAuth.get(surface.wl.connection)!; const enterSerial = this.mouseFocus!.instances.focus(surface.wl, []); this.modifiers.update(this.mouseFocus!.instances.connection, enterSerial); this.mouseFocus!.instances.enter(surface.wl, mouseX, mouseY); + // ????? } - this.mouseFocus!.instances.moveTo(mouseX, mouseY); - } else { - if (this.mouseFocus?.surface === surface) { - this.mouseFocus!.instances.blur(surface.wl); - this.mouseFocus!.instances.leave(surface.wl); - this.mouseFocus = undefined; - } + this.mouseFocus?.instances.moveTo(mouseX, mouseY); } } } diff --git a/src/renderer/host/classes/component.ts b/src/renderer/host/classes/component.ts index 216198c..c66e5c7 100644 --- a/src/renderer/host/classes/component.ts +++ b/src/renderer/host/classes/component.ts @@ -1,11 +1,13 @@ +import { componentTypes } from "../utils/types.js"; import { nanoid } from "../utils/utils.js"; import { wrapValue } from "../utils/wrap.js"; import { BaseModule, LocalModule } from "./module.js"; import { orchestrator } from "./orchestrator.js"; import { OrderedPeer } from "./orderedPeer.js"; -export interface ComponentContext { - module: BaseModule; +export interface PartialComponentContext { } +export interface ComponentContext extends PartialComponentContext { + module: LocalModule; } export type ComponentHandle = { module: BaseModule; @@ -15,7 +17,7 @@ export type ComponentHandle = { } & { [k in `$${string}`]: any }; const isComponentSymbol = Symbol(); -export class Component extends EventTarget { +export abstract class Component implements ComponentHandle { static isComponentSymbol: typeof isComponentSymbol = isComponentSymbol; [x: `$${string}`]: any; @@ -23,14 +25,20 @@ export class Component extends EventTarget { componentId: string; module: LocalModule; + static type: typeof componentTypes[number] = "NORMAL"; + [isComponentSymbol] = true; constructor(module: LocalModule) { - super(); - this.componentId = nanoid(); this.module = module; + this.componentId = nanoid(); + + this.module.localHandle.componentInstances.set( + this.componentId, + this, + ); } - async init() {} + init(): any {} async getDependency(dependency: string) { const newMod = await orchestrator.load(dependency); @@ -54,8 +62,8 @@ export class Component extends EventTarget { if (innerSet.size === 0) this.#listenersFromRemote.delete(eventName); } - emit(eventName: string, args: any[]) { - const wrapped = args.map((v) => wrapValue(v)) + async emit(eventName: string, ...args: any[]) { + const wrapped = await Promise.all(args.map((v) => wrapValue(v))) const innerSet = this.#listenersFromRemote.get(eventName); if (!innerSet) return; diff --git a/src/renderer/host/classes/componentList.ts b/src/renderer/host/classes/componentList.ts index 3ca09b6..5584ee1 100644 --- a/src/renderer/host/classes/componentList.ts +++ b/src/renderer/host/classes/componentList.ts @@ -1,18 +1,18 @@ -import { ComponentClass, ComponentHandleClass } from "../utils/types.js"; -import { Component, ComponentHandle } from "./component.js"; +import { ComponentClass, ComponentHandleFactory } from "../utils/types.js"; +import { Component } from "./component.js"; +import { Latch } from "./latch.js"; import { LocalModule } from "./module.js"; export class InvalidComponentError extends Error {} // ASSUMPTION: All components will return themselves. -export class ComponentList extends EventTarget { - componentClasses = new Map ComponentHandle>(); - componentClassToClassName = new Map ComponentHandle, string>() +export class ComponentList extends EventTarget implements ComponentListHandle { + componentClasses = new Map(); + componentClassToClassName = new Map() - module: LocalModule; - constructor(mod: LocalModule) { - super(); - this.module = mod; + componentInstances = new Map(); + instanceExists(id: string) { + return this.componentInstances.has(id); } componentTypeOf(component: Component) { @@ -30,11 +30,28 @@ export class ComponentList extends EventTarget { // Implications: The object has had [Symbol(Component.isComponentSymbol)] set to true but was not a component } - register(componentName: string, componentClass: new (mod: LocalModule) => Component) { + module: LocalModule; + constructor(mod: LocalModule) { + super(); + this.module = mod; + } + + #readyLatch = new Latch(); + get ready() { + return this.#readyLatch.promise; + } + markReady() { + this.#readyLatch.resolve?.(); + } + + register(componentName: string, componentClass: ComponentClass) { if (this.componentClasses.has(componentName)) throw new Error("This component already exists"); this.componentClasses.set(componentName, componentClass); + this.markAs(componentClass, componentName); + } + markAs(componentClass: ComponentClass, componentName: string) { this.componentClassToClassName.set(componentClass, componentName); } @@ -42,9 +59,24 @@ export class ComponentList extends EventTarget { const InnerClass = this.componentClasses.get(componentName); if (!InnerClass) return; - return function (this: ComponentList, ...args: any[]) { - return new InnerClass(this.module, ...args); - }.bind(this) as unknown as ComponentClass; + return { + create: (...args: any[]) => { + switch (InnerClass.type) { + case "REF_ONLY": { + throw new Error("You are not supposed to instanciate this class"); + } + case "SINGLETON": { + if (!InnerClass.singletonInstance) throw new Error("Singleton instance was not set up."); + return InnerClass.singletonInstance; + } + case "NORMAL": { + return "create" in InnerClass + ? InnerClass.create(this.module, ...args) + : new InnerClass(this.module, ...args); + } + } + } + }; } has(componentName: string) { @@ -54,5 +86,5 @@ export class ComponentList extends EventTarget { export type ComponentListHandle = { get(componentName: string): undefined - | ComponentHandleClass; + | ComponentHandleFactory; }; diff --git a/src/renderer/host/classes/module.ts b/src/renderer/host/classes/module.ts index 8746c49..7a81c34 100644 --- a/src/renderer/host/classes/module.ts +++ b/src/renderer/host/classes/module.ts @@ -1,6 +1,6 @@ import { KeyedLatch, Latch } from "./latch.js"; import { OrderedPeer } from "./orderedPeer.js"; -import { CathodiqueConsumerHandler } from "../ipcHandlers/cathodiqueConsumer.js"; +import { CathodiqueAvailableComponentsHandler, CathodiqueConsumerHandler } from "../ipcHandlers/cathodiqueConsumer.js"; import { CathodiqueHostHandler } from "../ipcHandlers/cathodiqueHost.js"; import { DOMHostHandler } from "../ipcHandlers/domHost.js"; import { OtherNodeRegistry } from "./sharedDomHost.js"; @@ -32,9 +32,15 @@ export abstract class BaseModule { static summonnedModules = new Map(); static tokenToModule = new Map(); - static summonnedModulesById = new Map(); - static moduleById(id: string) { - return this.summonnedModulesById.get(id); + static addModule(moduleName: string, module: BaseModule) { + if (BaseModule.summonnedModules.has(moduleName)) throw new Error("Module already initialized"); + BaseModule.summonnedModulesByToken.set(module.opaqueToken, module); + BaseModule.summonnedModules.set(moduleName, module); + } + + static summonnedModulesByToken = new Map(); + static moduleByToken(id: string) { + return this.summonnedModulesByToken.get(id); } static originOfModule(module?: BaseModule) { @@ -53,19 +59,17 @@ export abstract class BaseModule { } opaqueToken: string; + abstract win: WindowProxy; moduleName: string; constructor(moduleName: string) { - if (BaseModule.summonnedModules.has(moduleName)) throw new Error("Module already initialized"); - BaseModule.summonnedModules.set(moduleName, this); - this.opaqueToken = orchestrator.data.moduleData[moduleName]!.opaqueToken; this.moduleName = moduleName; } abstract localHandle: ComponentListHandle; - abstract getRemoteHandle(to: RemoteModule): MessagePort | Promise; + abstract getRemoteHandle(to: RemoteModule): MessagePort | undefined | Promise; abstract instanceExists(id: string): boolean | Promise; abstract getComponent(id: string): ComponentHandle | undefined | Promise; } @@ -76,10 +80,11 @@ export class LocalModule extends BaseModule { static setupModule(moduleName: string) { this.availableModules.add(moduleName); const mod = new LocalModule(moduleName); - BaseModule.summonnedModules.set(moduleName, mod); return mod; } + win = window; + components = new Map(); localHandle: ComponentList; @@ -89,16 +94,16 @@ export class LocalModule extends BaseModule { this.localHandle = new ComponentList(this); } - remoteHandles = new Map(); + remoteHandles = new Set(); getRemoteHandle(from: RemoteModule) { - if (this.remoteHandles.has(from)) return this.remoteHandles.get(from)!; + if (this.remoteHandles.has(from)) return undefined; const messageChannel = new SemanticMessageChannel(); - const ipc = new OrderedPeer(messageChannel.providerPort, BaseModule.originOfModule(from)); - ipc.addHandler(new CathodiqueProviderHandler(from, this)); + const peer = new OrderedPeer(messageChannel.providerPort, "*"); + peer.addHandler(new CathodiqueProviderHandler(from, this, peer)); - this.remoteHandles.set(from, messageChannel.consumerPort); + this.remoteHandles.add(from); return messageChannel.consumerPort; } @@ -128,7 +133,7 @@ export class RemoteModule extends BaseModule { const moduleSubdomain = this.moduleSubdomainOf(moduleName); const iframe = document.createElement("iframe"); - iframe.src = `https://${moduleSubdomain}.raytu.be/module.html`; + iframe.src = `https://${moduleSubdomain}.raytu.be/module.html?parent_origin=${encodeURIComponent(window.origin)}`; iframe.hidden = true; document.body.append(iframe); @@ -136,7 +141,14 @@ export class RemoteModule extends BaseModule { const win = iframe.contentWindow!; OtherNodeRegistry.setRegistry(win, new OtherNodeRegistry(win)); - return new RemoteModule(moduleName, iframe); + const peer = new OrderedPeer( + iframe.contentWindow!, + `https://${moduleSubdomain}.raytu.be`, + ); + const availableComponents = new Latch(); + peer.addHandler(new CathodiqueAvailableComponentsHandler(availableComponents)); + + return new RemoteModule(moduleName, await availableComponents.promise, peer, iframe); } peer: OrderedPeer; @@ -144,19 +156,19 @@ export class RemoteModule extends BaseModule { get win() { return this.iframe.contentWindow! }; componentList: ComponentListHandle; - private constructor(moduleName: string, iframe: HTMLIFrameElement) { + private constructor(moduleName: string, availableComponents: string[], peer: OrderedPeer, iframe: HTMLIFrameElement) { super(moduleName); + this.peer = peer; this.iframe = iframe; - this.componentList = new ComponentListProxy(); - this.peer = new OrderedPeer( - iframe.contentWindow!, - `https://${this.#moduleSubdomain}.raytu.be`, - ); - this.peer.addHandler(new CathodiqueConsumerHandler(this)); + this.availableComponents = availableComponents; + + this.componentList = new ComponentListProxy(this); + this.peer.addHandler(new CathodiqueHostHandler(this)); this.peer.addHandler(new DOMHostHandler(this.win, this)); + this.peer.addHandler(new CathodiqueConsumerHandler(this)); } get #moduleSubdomain() { @@ -166,45 +178,32 @@ export class RemoteModule extends BaseModule { return `https://${this.#moduleSubdomain}.raytu.be`; } - #iframeLoaded = false - componentReady = new KeyedLatch(); - - async waitForComponent(componentName: string) { - await this.componentReady.get(componentName); - } + availableComponents: string[]; localHandle = { - get: function (this: RemoteModule, componentName: string) { - return function (this: RemoteModule, ...args: any[]) { + get: (componentName: string) => ({ + create: (...args: any[]) => { return makeComponentProxy(this, componentName, { args }); - }.bind(this) as unknown as new () => ComponentInstanceProxy; // Proxy magic: TS is wrong here - }.bind(this), + }, + }), } - remoteHandles = new Map(); + remoteHandles = new Set(); async getRemoteHandle(to: RemoteModule) { - if (this.remoteHandles.has(to)) return this.remoteHandles.get(to)!; + if (this.remoteHandles.has(to)) return undefined; const messageChannel = new SemanticMessageChannel(); await this.peer.rpc("connectAsProvider", new WithTransfer( - { data: { port: messageChannel.providerPort, moduleToken: undefined } }, + { port: messageChannel.providerPort, id: to.opaqueToken }, [messageChannel.providerPort], )); - this.remoteHandles.set(to, messageChannel.consumerPort); + this.remoteHandles.add(to); return messageChannel.consumerPort; } - async submitRemoteHandle(port: MessagePort, from: RemoteModule) { - await this.peer.rpc("connectAsProvider", - new WithTransfer( - { data: { port: port, moduleToken: from.opaqueToken } }, - [port], - )); - } - async instanceExists(id: string) { return await this.peer.rpc("instanceExists", { componentId: id }); } diff --git a/src/renderer/host/classes/orchestrator.ts b/src/renderer/host/classes/orchestrator.ts index 6f2fad2..4c17635 100644 --- a/src/renderer/host/classes/orchestrator.ts +++ b/src/renderer/host/classes/orchestrator.ts @@ -35,13 +35,19 @@ export class Orchestrator { this.overridesAll.set(str, modules); } - load(schemaName: string) { + async load(schemaName: string) { const overriddenBy = this.overrides.get(schemaName); const moduleName = overriddenBy ?? this.defaults.get(schemaName); if (!moduleName) return undefined; - return BaseModule.getModule(moduleName); + if (BaseModule.summonnedModules.has(moduleName)) { + return BaseModule.summonnedModules.get(moduleName); + } + + const mod = await BaseModule.getModule(moduleName); + BaseModule.addModule(moduleName, mod); + return mod; } loadAll(schemaName: string) { @@ -50,8 +56,10 @@ export class Orchestrator { const moduleNames = overriddenBy ?? this.data.defaultsAll[schemaName]; if (!moduleNames) return undefined; - return Promise.all(moduleNames.map(function (this: Orchestrator, moduleName: string) { - return BaseModule.getModule(moduleName)!; + return Promise.all(moduleNames.map(async (moduleName: string) => { + const mod = await BaseModule.getModule(moduleName)!; + BaseModule.addModule(moduleName, mod); + return mod; })); } } diff --git a/src/renderer/host/classes/orderedPeer.ts b/src/renderer/host/classes/orderedPeer.ts index 3d2d877..556a782 100644 --- a/src/renderer/host/classes/orderedPeer.ts +++ b/src/renderer/host/classes/orderedPeer.ts @@ -40,11 +40,17 @@ export class OrderedPeer { this.postMessage = source.postMessage.bind(source); this.origin = origin; - OrderedPeer.actualHandlers.set(source, this.orderedDecoder.bind(this)); + if (source instanceof MessagePort) { + if (origin !== "*") throw new Error("Cannot restrain origin if source is a MessagePort"); + source.addEventListener("message", this.orderedDecoder.bind(this)); + } else { + OrderedPeer.actualHandlers.set(source, this.orderedDecoder.bind(this)); + } } addHandler(handler: (typeof this.handlers)[number]) { this.handlers.push(handler); + if (this.source instanceof MessagePort) this.source.start(); return this; // Builder } @@ -59,14 +65,20 @@ export class OrderedPeer { this.pendingTransfer.push(...transfer); if (this.pendingMessages.length === 1) { + const error = new Error().stack!; queueMicrotask(() => { - this.postMessage( + if (error.length < 1) { + throw new Error("Just to get the stack trace."); + } + this.source.postMessage( { messages: this.pendingMessages, currentOrder: this.currentOrderSubmission, }, - this.origin, - this.pendingTransfer, + { + targetOrigin: this.origin, + transfer: this.pendingTransfer, + } ); this.pendingMessages = []; @@ -76,10 +88,13 @@ export class OrderedPeer { } } - async rpc(type: string, data: any | WithTransfer, obj: Record = {}) { + async rpc(type: string, oldData: any | WithTransfer, obj: Record = {}) { const promiseId = nanoid(); - this.post({ type, data, promiseId, ...obj }); + const { data, transfer } = new WithTransfer(oldData); + // console.log(window.origin, "RPC WITH", promiseId, type, data, transfer); + this.post(new WithTransfer({ type, data, promiseId, ...obj }, transfer)); const result = await this.promiseMap.consume(promiseId); + // console.log(window.origin, "RPC WAS ALL GOOD!", promiseId, type, result); if (!result.error) return result.reply; throw result.error; @@ -88,48 +103,56 @@ export class OrderedPeer { remainingMessages = new Map(); currentOrderReception: bigint = 0n; - originMatch(origin: string) { + originMatch(evt: MessageEvent) { + if (evt.origin === "" && this.source instanceof MessagePort) return true; + if (this.origin === '*') return true; - if (this.origin === '/') return origin === window.origin; - return origin === this.origin; + if (this.origin === '/') return evt.origin === window.origin; + return evt.origin === this.origin; } async orderedDecoder(evt: MessageEvent) { - if (!this.originMatch(evt.origin)) return; + if (!this.originMatch(evt)) return; const { data: { messages, currentOrder } } = evt; this.remainingMessages.set(currentOrder, messages); - if (this.currentOrderReception === currentOrder) { - do { - await Promise.all( - evt.data.messages.map(async (message: { data: any, type: string, promiseId?: string, componentHandle?: string }) => { - const { type, promiseId } = message; - - if (type === "reply") { - return this.promiseMap.resolve(promiseId!, message); - } - - const handler = this.handlers.find((v) => type in v); - - if (!handler) { - if (promiseId) this.post({ type: "reply", error: new Error("No such function"), promiseId }); - return; - } - - try { - const result = await handler[type](message, { ipc: this, event: evt }); - - if (promiseId) { - this.post({ type: "reply", reply: result, promiseId }); - } - } catch (e) { - this.post({ type: "reply", error: e, promiseId }); - } - }) - ); - this.remainingMessages.delete(this.currentOrderReception); - this.currentOrderReception += 1n; - } while (this.remainingMessages.has(this.currentOrderReception)); + + while (this.remainingMessages.has(this.currentOrderReception)) { + const messages = this.remainingMessages.get(this.currentOrderReception)!; + this.remainingMessages.delete(this.currentOrderReception); + this.currentOrderReception += 1n; + + // Used to be await Promise.all. + // Trying to prevent deadlock here... + messages.map(async (message: { data: any, type: string, error?: string, promiseId?: string, componentHandle?: string }) => { + const { type, promiseId } = message; + + if (type === "reply") { + this.promiseMap.resolve(promiseId!, message); + if (message.error) throw message.error; + + return; + } + + const handler = this.handlers.find((v) => type in v); + + if (!handler) { + console.error("Client attempted", type, "which was not impl'd"); + if (promiseId) this.post({ type: "reply", error: new Error("No such function"), promiseId }); + return; + } + + try { + const result = new WithTransfer(await handler[type](message, { ipc: this, event: evt })); + + if (promiseId) { + this.post(new WithTransfer({ type: "reply", reply: result.data, promiseId }, result.transfer)); + } + } catch (e) { + console.error(e); + this.post({ type: "reply", error: (e as Error).toString(), promiseId }); + } + }); } } } diff --git a/src/renderer/host/classes/polymap.ts b/src/renderer/host/classes/polymap.ts new file mode 100644 index 0000000..14d26e3 --- /dev/null +++ b/src/renderer/host/classes/polymap.ts @@ -0,0 +1,10 @@ +type ValueFor = Extract[1]; + +export class PolyMap extends Map { + set(a: T, b: ValueFor) { + return super.set(a, b); + } + get(a: T): ValueFor { + return super.get(a); + } +} diff --git a/src/renderer/host/classes/sharedDomHost.ts b/src/renderer/host/classes/sharedDomHost.ts index d7c2e2e..87fa353 100644 --- a/src/renderer/host/classes/sharedDomHost.ts +++ b/src/renderer/host/classes/sharedDomHost.ts @@ -24,12 +24,12 @@ class NodeData { } export class OtherNodeRegistry { - static registryPerSource = new WeakMap(); + static registryPerSource = new WeakMap(); - static registryOf(source: MessageEventSource) { + static registryOf(source: WindowProxy) { return this.registryPerSource.get(source); } - static setRegistry(source: MessageEventSource, nr: OtherNodeRegistry) { + static setRegistry(source: WindowProxy, nr: OtherNodeRegistry) { if (OtherNodeRegistry.registryPerSource.has(source)) throw new Error("Only one NodeRegistry per window may exist"); return this.registryPerSource.set(source, nr); } @@ -41,10 +41,10 @@ export class OtherNodeRegistry { return this.nodeData.get(node); } - source: MessageEventSource; + win: WindowProxy; - constructor(source: MessageEventSource) { - this.source = source; + constructor(source: WindowProxy) { + this.win = source; } hasNode(node: Node) { diff --git a/src/renderer/host/classes/withTransfer.ts b/src/renderer/host/classes/withTransfer.ts index 099794f..e9dbd3c 100644 --- a/src/renderer/host/classes/withTransfer.ts +++ b/src/renderer/host/classes/withTransfer.ts @@ -3,8 +3,13 @@ export class WithTransfer { data: any; transfer: any[]; constructor(data: any, transfer: any[] = []) { - this.data = data; - this.transfer = transfer; + if (data instanceof WithTransfer) { + this.data = data.data; + this.transfer = [...data.transfer, ...transfer]; + } else { + this.data = data; + this.transfer = transfer; + } } clone() { return new WithTransfer(this.data, this.transfer); diff --git a/src/renderer/host/index.ts b/src/renderer/host/index.ts index db8f248..b187b2f 100644 --- a/src/renderer/host/index.ts +++ b/src/renderer/host/index.ts @@ -1,7 +1,12 @@ +// esbuild... tf u doing,,,, +import "../classes/handlers/handlers.js"; + import { OrderedPeer } from "./classes/orderedPeer.js"; OrderedPeer.registerIpcListener(); +import "./localModules/loadAllLocalModules.js"; + import { orchestrator } from "./classes/orchestrator.js"; export { orchestrator }; diff --git a/src/renderer/host/ipcHandlers/cathodiqueConsumer.ts b/src/renderer/host/ipcHandlers/cathodiqueConsumer.ts index ff0c2f3..b18c792 100644 --- a/src/renderer/host/ipcHandlers/cathodiqueConsumer.ts +++ b/src/renderer/host/ipcHandlers/cathodiqueConsumer.ts @@ -1,22 +1,35 @@ import z from "zod"; import { HandlerContext } from "../utils/types.js"; import { unwrapValue, WrappedValue, zodWrappedValue } from "../utils/wrap.js"; +import { Latch } from "../classes/latch.js"; import { RemoteModule } from "../classes/module.js"; -export class CathodiqueConsumerHandler { +export class CathodiqueAvailableComponentsHandler { [k: string]: (arg: Record, ctx: HandlerContext) => any; - #module: RemoteModule; + #availableComponents: Latch; - constructor(module: RemoteModule ) { - this.#module = module; + constructor(availableComponentsLatch: Latch) { + this.#availableComponents = availableComponentsLatch; } - componentRegistered(arg: Record) { - return this.#componentRegistered(z.object({ data: z.object({ componentName: z.string() }) }).parse(arg)); + moduleReady(args: Record) { + return this.#moduleReady(z.object({ + data: z.object({ componentList: z.array(z.string()) }), + }).parse(args)); } - async #componentRegistered({ data }: { data: { componentName: string } }) { - this.#module.componentReady.resolve(data.componentName, undefined); + async #moduleReady({ data }: { data: { componentList: string[] } }) { + this.#availableComponents.resolve?.(data.componentList); + } +} + +export class CathodiqueConsumerHandler { + [k: string]: (arg: Record, ctx: HandlerContext) => any; + + #module: RemoteModule; + + constructor(module: RemoteModule) { + this.#module = module; } emitEvent(arg: Record) { diff --git a/src/renderer/host/ipcHandlers/cathodiqueHost.ts b/src/renderer/host/ipcHandlers/cathodiqueHost.ts index 2712cfd..72ac35c 100644 --- a/src/renderer/host/ipcHandlers/cathodiqueHost.ts +++ b/src/renderer/host/ipcHandlers/cathodiqueHost.ts @@ -9,8 +9,12 @@ export class CathodiqueHostHandler { #mod: RemoteModule; - constructor (mod: RemoteModule) { + constructor(mod: RemoteModule) { this.#mod = mod; + + this.#mod.peer.rpc("moduleId", { + moduleId: this.#mod.opaqueToken, + }); } getDependency(arg: Record) { @@ -23,12 +27,30 @@ export class CathodiqueHostHandler { async #getDependency({ data }: { data: { dependency: string } }) { const module = await orchestrator.load(data.dependency); - if (!module) return undefined; + if (!module) throw new Error("No such module"); const consumerPort = await module.getRemoteHandle(this.#mod); const moduleId = module.opaqueToken; - return new WithTransfer({ port: consumerPort, id: moduleId }, [consumerPort]); + return new WithTransfer({ port: consumerPort, id: moduleId }, consumerPort ? [consumerPort] : []); + } + + getModuleByToken(arg: Record) { + return this.#getModuleByToken(z.object({ + data: z.object({ + opaqueToken: z.string(), + }), + }).parse(arg)); + } + async #getModuleByToken({ data }: { data: { opaqueToken: string } }) { + const module = RemoteModule.moduleByToken(data.opaqueToken); + + if (!module) throw new Error("No such module"); + + const consumerPort = await module.getRemoteHandle(this.#mod); + const moduleId = module.opaqueToken; + + return new WithTransfer({ port: consumerPort, id: moduleId }, consumerPort ? [consumerPort] : []); } getAllDependency(arg: Record) { @@ -48,7 +70,7 @@ export class CathodiqueHostHandler { // Map with side effect const handles = await Promise.all(modules.map(async function (this: CathodiqueHostHandler, mod: BaseModule) { const messagePort = await mod.getRemoteHandle(this.#mod); - messagePorts.push(messagePort); + if (messagePort) messagePorts.push(messagePort); return { port: messagePort, id: mod.opaqueToken }; }.bind(this))); @@ -56,19 +78,19 @@ export class CathodiqueHostHandler { return new WithTransfer(handles, [handles]); } - establishConnection(arg: Record) { - return this.#establishConnection(z.object({ - data: z.object({ - from: z.string().refine((v) => RemoteModule.moduleById(v)), - to: z.string().refine((v) => RemoteModule.moduleById(v)), - }), - }).parse(arg)); - } - async #establishConnection({ data }: { data: { from: string, to: string } }) { - const fromModule = BaseModule.moduleById(data.from)! as RemoteModule; - const toModule = BaseModule.moduleById(data.to)! as RemoteModule; + // establishConnection(arg: Record) { + // return this.#establishConnection(z.object({ + // data: z.object({ + // from: z.string().refine((v) => RemoteModule.moduleById(v)), + // to: z.string().refine((v) => RemoteModule.moduleById(v)), + // }), + // }).parse(arg)); + // } + // async #establishConnection({ data }: { data: { from: string, to: string } }) { + // const fromModule = BaseModule.moduleById(data.from)! as RemoteModule; + // const toModule = BaseModule.moduleById(data.to)! as RemoteModule; - const port = await fromModule.getRemoteHandle(toModule); - await toModule.submitRemoteHandle(port, fromModule); - } + // const port = await fromModule.getRemoteHandle(toModule); + // await toModule.submitRemoteHandle(port, fromModule); + // } }; diff --git a/src/renderer/host/ipcHandlers/cathodiqueProvider.ts b/src/renderer/host/ipcHandlers/cathodiqueProvider.ts index 1fa8027..fdec14b 100644 --- a/src/renderer/host/ipcHandlers/cathodiqueProvider.ts +++ b/src/renderer/host/ipcHandlers/cathodiqueProvider.ts @@ -2,35 +2,37 @@ import z from "zod"; import { LocalModule, RemoteModule } from "../classes/module.js"; import { HandlerContext } from "../utils/types.js"; import { unwrapValue, WrappedValue, wrapValue, zodWrappedValue } from "../utils/wrap.js"; -import { Component, ComponentHandle } from "../classes/component.js"; import { ShouldHaveBeenZodError, stringStartsWithDollar } from "../utils/utils.js"; +import { OrderedPeer } from "../classes/orderedPeer.js"; export class CathodiqueProviderHandler { [k: string]: (arg: Record, ctx: HandlerContext) => any; #toModule: LocalModule; #fromModule: RemoteModule; - constructor(fromModule: RemoteModule, toModule: LocalModule) { + #peer: OrderedPeer; + constructor(fromModule: RemoteModule, toModule: LocalModule, peer: OrderedPeer) { this.#fromModule = fromModule; this.#toModule = toModule; - } + this.#peer = peer; - static #componentInstancesByModule = new Map>(); - get #componentInstances() { - const result = CathodiqueProviderHandler.#componentInstancesByModule.get(this.#toModule) || new Map(); - if (!CathodiqueProviderHandler.#componentInstancesByModule.has(this.#toModule)) { - CathodiqueProviderHandler.#componentInstancesByModule.set(this.#toModule, result); - } - return result; + this.#init(); + } + async #init() { + await this.#toModule.localHandle.ready; + await this.#peer.rpc("moduleReady", { + componentList: [...this.#toModule.localHandle.componentClasses.keys()], + }); } - #componentExists(id: string) { - this.#componentInstances.has(id); + + get #componentList() { + return this.#toModule.localHandle; } createInstance(arg: Record) { return this.#createInstance(z.object({ data: z.object({ - className: z.string().refine(this.#toModule.localHandle.has), + className: z.string().refine((className) => this.#toModule.localHandle.has(className)), args: z.array(zodWrappedValue), }), }).parse(arg)); @@ -38,35 +40,59 @@ export class CathodiqueProviderHandler { async #createInstance({ data }: { data: { className: string; args: WrappedValue[] } }) { // TODO Obj verification const ClassObj = this.#toModule.localHandle.get(data.className); - if (!ClassObj) return; // Quiet fail + if (!ClassObj) throw new Error("No such component"); - const unwrapped = await Promise.all(data.args.map(async function (this: CathodiqueProviderHandler, v: WrappedValue) { - return unwrapValue(v, this.#fromModule) - }.bind(this))); - const componentInstance = new ClassObj(...unwrapped); + const unwrapped = await Promise.all(data.args.map(async (v: WrappedValue) => unwrapValue(v, this.#fromModule))); + + const componentInstance = await ClassObj.create(...unwrapped); await componentInstance.init(); + return componentInstance.componentId; + } + + instanceExists(arg: Record) { + return this.#instanceExists(z.object({ + data: z.object({ + componentId: z.string(), + }), + }).parse(arg)); + } + #instanceExists({ data }: { data: { componentId: string } }) { + return this.#componentList.instanceExists(data.componentId); + } - this.#componentInstances.set(componentInstance.componentId, componentInstance); - return; + getInstanceData(arg: Record) { + return this.#getInstanceData(z.object({ + data: z.object({ + componentId: z.string(), + }), + }).parse(arg)); + } + #getInstanceData({ data }: { data: { componentId: string } }) { + const instance = this.#componentList.componentInstances.get(data.componentId); + return instance && { + componentName: this.#toModule.localHandle.componentTypeOf(instance), + }; } getProperty(arg: Record) { return this.#getProperty(z.object({ data: z.object({ propertyName: z.string().startsWith("$"), - componentId: z.string().refine(this.#componentExists), + componentId: z.string().refine((cId) => this.#componentList.instanceExists(cId)), }), }).parse(arg)); } async #getProperty({ data }: { data: { propertyName: string; componentId: string } }) { - const component = this.#componentInstances.get(data.componentId); + const component = this.#componentList.componentInstances.get(data.componentId); if (!component) throw new ShouldHaveBeenZodError(); const propertyName = data.propertyName; if (!stringStartsWithDollar(propertyName)) throw new ShouldHaveBeenZodError(); const value = component[propertyName]; + console.log({ value }); + return wrapValue(value); } @@ -75,7 +101,7 @@ export class CathodiqueProviderHandler { data: z.object({ methodName: z.string(), arguments: z.array(z.any()), - componentId: z.string().refine(this.#componentExists), + componentId: z.string().refine((cId) => this.#componentList.instanceExists(cId)), }), }).parse(arg)); } @@ -86,7 +112,7 @@ export class CathodiqueProviderHandler { componentId: string; }; }) { - const component = this.#componentInstances.get(data.componentId); + const component = this.#componentList.componentInstances.get(data.componentId); if (!component) throw new ShouldHaveBeenZodError(); const methodName = data.methodName; @@ -99,8 +125,8 @@ export class CathodiqueProviderHandler { listenToEvent(arg: Record) { return this.#listenToEvent(z.object({ data: z.object({ - eventName: z.string().startsWith("$"), - componentId: z.string().refine(this.#componentExists), + eventName: z.string(), + componentId: z.string().refine((cId) => this.#componentList.instanceExists(cId)), }), }).parse(arg)); } @@ -110,17 +136,17 @@ export class CathodiqueProviderHandler { componentId: string; }; }) { - const component = this.#componentInstances.get(data.componentId); + const component = this.#componentList.componentInstances.get(data.componentId); if (!component) throw new ShouldHaveBeenZodError(); - component.listenFor(data.eventName, this.#fromModule.peer); + component.listenFor(data.eventName, this.#peer); } unlistenToEvent(arg: Record) { return this.#unlistenToEvent(z.object({ data: z.object({ eventName: z.string(), - componentId: z.string().refine(this.#componentExists), + componentId: z.string().refine((cId) => this.#componentList.instanceExists(cId)), }), }).parse(arg)); } @@ -130,9 +156,9 @@ export class CathodiqueProviderHandler { componentId: string; }; }) { - const component = this.#componentInstances.get(data.componentId); + const component = this.#componentList.componentInstances.get(data.componentId); if (!component) throw new ShouldHaveBeenZodError(); - component.unlistenFor(data.eventName, this.#fromModule.peer); + component.unlistenFor(data.eventName, this.#peer); } }; diff --git a/src/renderer/host/ipcHandlers/domHost.ts b/src/renderer/host/ipcHandlers/domHost.ts index 3c4092b..85754be 100644 --- a/src/renderer/host/ipcHandlers/domHost.ts +++ b/src/renderer/host/ipcHandlers/domHost.ts @@ -1,7 +1,8 @@ import { EventFromIpc, HandlerContext, NodeFromIpc, zodNodeFromIpc } from "../utils/types.js"; -import { RemoteModule } from "../classes/module.js"; +import { BaseModule, RemoteModule } from "../classes/module.js"; import { OtherNodeRegistry } from "../classes/sharedDomHost.js"; import z from "zod"; +import { NodeRegistry } from "../../../modules/.common/classes/sharedDomRemote.js"; function allProperties(obj: any) { const result = []; @@ -14,15 +15,15 @@ function allProperties(obj: any) { export class DOMHostHandler { [k: string]: (arg: Record, ctx: HandlerContext) => any; - #source: MessageEventSource; + #win: WindowProxy; #module: RemoteModule; - constructor(source: MessageEventSource, module: RemoteModule) { - this.#source = source; + constructor(source: WindowProxy, module: RemoteModule) { + this.#win = source; this.#module = module; } get #nodeReg() { - return OtherNodeRegistry.registryOf(this.#source)!; + return OtherNodeRegistry.registryOf(this.#win)!; } #serializeEvent(evt: Event): EventFromIpc { @@ -63,14 +64,17 @@ export class DOMHostHandler { } #createNode({ data }: { data: { id: string, payload: NodeFromIpc, events: string[] } }) { const node = this.#nodeReg.deserializeNode(data.payload); + + // if (node instanceof Element) node.setAttribute("data-rpcid", data.id); + this.#nodeReg.setNodeId(node, data.id); for (const event of data.events) { - this.#nodeReg.registerEvent(node, event, async function (this: DOMHostHandler, v: Event) { - const ipc = await this.#module.peer; + this.#nodeReg.registerEvent(node, event, async (v: Event) => { + const ipc = this.#module.peer; await ipc.rpc("domEmitEvent", { id: data.id, event: this.#serializeEvent(v) }); - }.bind(this)); + }); } } @@ -84,4 +88,109 @@ export class DOMHostHandler { #deleteNode({ data }: { data: { id: string } }) { this.#nodeReg.deleteNode(data.id); } + + async containForeign(arg: Record) { + return this.#containForeign(z.object({ + data: z.object({ + id: z.string(), + toId: z.string(), + toOpaqueToken: z.string().optional(), + }), + }).parse(arg)); + } + #containForeign({ data }: { data: { id: string, toId: string, toOpaqueToken?: string } }) { + let win = window as Window; + if (data.toOpaqueToken) { + const module = BaseModule.moduleByToken(data.toOpaqueToken); + if (!module) throw new Error("Module not found"); + win = module.win; + } + + const toNode = OtherNodeRegistry.registryOf(win)!.getNode(data.toId); + if (!toNode) throw new Error("Node not found"); + + const node = this.#nodeReg.getNode(data.id); + if (!(node instanceof Element)) throw new Error("Can't contain foreign if node not Element"); + + console.log(this.#module.opaqueToken, data.toOpaqueToken, win, node, toNode); + + const shadowRoot = node.attachShadow({ mode: "open" }); + shadowRoot.append(toNode); + } + + changeAttribute(args: Record) { + this.#changeAttribute(z.object({ + data: z.object({ + target: z.string(), + name: z.string(), + namespace: z.string().nullable(), + value: z.string(), + }), + }).parse(args)); + } + #changeAttribute({ data }: { data: { target: string; name: string; namespace: string | null; value: string } }) { + const targetNode = this.#nodeReg.getNode(data.target); + if (!targetNode) throw new Error("Target node does not exist"); + + (targetNode as Element).setAttributeNS(data.namespace, data.name, data.value); + } + + addNodes(args: Record) { + this.#addNodes(z.object({ + data: z.object({ + target: z.string(), + added: z.array(z.string()), + before: z.string().nullable(), + }), + }).parse(args)); + } + #addNodes({ data }: { data: { target: string, added: string[], before: string | null } }) { + const targetNode = this.#nodeReg.getNode(data.target); + if (!targetNode) throw new Error("Target node does not exist"); + + const beforeNode = data.before ? this.#nodeReg.getNode(data.before) : null; + if (beforeNode === undefined) throw new Error("Before node does not exist"); + + for (const addedNodeId of data.added) { + const addedNode = this.#nodeReg.getNode(addedNodeId); + if (!addedNode) throw new Error("One of addedNodes does not exist"); + + targetNode.insertBefore(addedNode, beforeNode); + } + } + + removeNodes(args: Record) { + this.#removeNodes(z.object({ + data: z.object({ + target: z.string(), + removed: z.array(z.string()), + }), + }).parse(args)); + } + #removeNodes({ data }: { data: { target: string, removed: string[] } }) { + const targetNode = this.#nodeReg.getNode(data.target); + if (!targetNode) throw new Error("Target node does not exist"); + + for (const removedNodeId of data.removed) { + const removedNode = this.#nodeReg.getNode(removedNodeId); + if (!removedNode) throw new Error("One of removedNodes does not exist"); + + targetNode.removeChild(removedNode); + } + } + + characterData(args: Record) { + this.#characterData(z.object({ + data: z.object({ + target: z.string(), + value: z.string(), + }), + }).parse(args)); + } + #characterData({ data }: { data: { target: string; value: string } }) { + const targetNode = this.#nodeReg.getNode(data.target); + if (!targetNode) throw new Error("Target node does not exist"); + + (targetNode as Element).nodeValue = data.value; + } }; diff --git a/src/renderer/host/localModules/basedomcomponent.ts b/src/renderer/host/localModules/basedomcomponent.ts new file mode 100644 index 0000000..f59f33d --- /dev/null +++ b/src/renderer/host/localModules/basedomcomponent.ts @@ -0,0 +1,24 @@ +import { BaseObject } from "@cathodique/wl-serv-high/objects"; +import { Component } from "../classes/component"; +import { LocalModule } from "../classes/module"; + +export class BaseDomComponent extends Component { + wl: From; + $output: To; + constructor(mod: LocalModule, wl: From, dom: To) { + super(mod); + this.wl = wl; + this.$output = dom; + } + + unmount: (() => any)[] = []; + onUnmount(f: (this: this) => any) { + this.unmount.push(f.bind(this)); + } + + destroy() { + type yo = this; + + this.unmount.forEach(function (this: yo, v: typeof this.unmount[number]) { v.bind(this)(); }.bind(this)); + } +} diff --git a/src/renderer/host/localModules/loadAllLocalModules.ts b/src/renderer/host/localModules/loadAllLocalModules.ts index 7fef4d3..007e941 100644 --- a/src/renderer/host/localModules/loadAllLocalModules.ts +++ b/src/renderer/host/localModules/loadAllLocalModules.ts @@ -1,8 +1,5 @@ +import { windowModule } from "./window_toplevel.js"; +import { BaseModule } from "../classes/module.js"; +BaseModule.addModule("Cathodique::Window", windowModule); -// TODO Question: -// So do we have something like, one moduleId per module? -// Uh yes, duh -// Which would mean, more than one moduleId per frame? -// Yeah that can work - -import "./window.js"; +import "./window_fakeToplevel.js" diff --git a/src/renderer/host/localModules/window.ts b/src/renderer/host/localModules/window.ts deleted file mode 100644 index 9a0e30d..0000000 --- a/src/renderer/host/localModules/window.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { LocalModule } from "../classes/module.js"; -import { Component } from "../classes/component"; - -class WindowLocal extends Component { - $element = document.createElement("canvas"); -} - -const WindowModule = LocalModule.setupModule("Cathodique::Window"); -WindowModule.localHandle.register("Window", WindowLocal); - -export { WindowModule }; diff --git a/src/renderer/host/localModules/window_fakeToplevel.ts b/src/renderer/host/localModules/window_fakeToplevel.ts new file mode 100644 index 0000000..14c5ade --- /dev/null +++ b/src/renderer/host/localModules/window_fakeToplevel.ts @@ -0,0 +1,9 @@ +import { Component } from "../classes/component.js"; +import { windowModule } from "./window_toplevel.js"; + +export class FakeToplevel extends Component { + constructor(contents: string) { + super(windowModule); + } +} +windowModule.localHandle.markAs(FakeToplevel, "Window"); diff --git a/src/renderer/host/localModules/window_toplevel.ts b/src/renderer/host/localModules/window_toplevel.ts new file mode 100644 index 0000000..cbfe9cc --- /dev/null +++ b/src/renderer/host/localModules/window_toplevel.ts @@ -0,0 +1,59 @@ +import { XdgToplevel } from "@cathodique/wl-serv-high/objects"; +import { BaseDomComponent } from "./basedomcomponent.js"; +import { LocalModule } from "../classes/module.js"; +import { Component } from "../classes/component"; +import { componentTypes } from "../utils/types.js"; +import { wlToObj } from "../../classes/handlers/handlers.js"; + +const windowModule = LocalModule.setupModule("Cathodique::Window"); + +class WindowRegistry extends Component { + static type: typeof componentTypes[number] = "SINGLETON"; + static singletonInstance?: WindowRegistry; + + constructor(mod: LocalModule) { + super(mod); + } + + addWindow(window: ToplevelDom) { + this.emit("newWindow", window); + } +} +WindowRegistry.singletonInstance = new WindowRegistry(windowModule); + +export const wlToToplevelDom = new Map(); + +export class ToplevelDom extends BaseDomComponent { + static type: typeof componentTypes[number] = "REF_ONLY"; + + get surfaceDom() { + return wlToObj.get(this.wl.parent.surface)!; + } + + constructor(wl: XdgToplevel) { + super(windowModule, wl, document.createElement("div")); + this.$output.append(this.surfaceDom.dom); + + WindowRegistry.singletonInstance!.addWindow(this); + + wlToToplevelDom.set(wl, this); + } + + async init () { + this.emit("setGeometry", this.wl.parent.geometry.current); + this.wl.parent.geometry.on("current", () => { + this.emit("setGeometry", this.wl.parent.geometry.current); + }); + + this.emit("setTitle", this.wl.title); + this.wl.on("wlSetTitle", () => { + this.emit("setTitle", this.wl.title); + }); + } +} + +windowModule.localHandle.register("Window", ToplevelDom); +windowModule.localHandle.register("WindowRegistry", WindowRegistry); +windowModule.localHandle.markReady(); + +export { windowModule }; diff --git a/src/renderer/host/utils/remoteToLocalAdapter.ts b/src/renderer/host/utils/remoteToLocalAdapter.ts index 8ba64fd..8193205 100644 --- a/src/renderer/host/utils/remoteToLocalAdapter.ts +++ b/src/renderer/host/utils/remoteToLocalAdapter.ts @@ -1,3 +1,4 @@ +import { EventEmitter } from "events"; import { Component } from "../classes/component.js"; import { ComponentListHandle } from "../classes/componentList.js"; import { Latch, LatchState } from "../classes/latch.js"; @@ -6,15 +7,26 @@ import { OrderedPeer } from "../classes/orderedPeer.js"; import { unwrapValue, wrapValue } from "./wrap.js"; export class ComponentListProxy implements ComponentListHandle { - componentClasses = new Map ComponentInstanceProxy>(); + #module: RemoteModule; + constructor(mod: RemoteModule) { + this.#module = mod; + } get(componentName: string) { - return this.componentClasses.get(componentName); + const availableComponents = this.#module.availableComponents; + + if (!availableComponents.includes(componentName)) return undefined; + + return { + create: (...args: any[]) => { + return makeComponentProxy(this.#module, componentName, { args }); + } + }; } } -export class ComponentInstance { - ipc: OrderedPeer; +export class ComponentInstance extends EventEmitter { + peer: OrderedPeer; module: RemoteModule; @@ -22,13 +34,29 @@ export class ComponentInstance { componentName: string; constructor(module: RemoteModule, componentName: string, options: { componentId: string } | { args: any[] }) { + super(); this.module = module; - this.ipc = this.module.peer; + this.peer = this.module.peer; this.componentName = componentName; if ("componentId" in options) this.#cidLatch.resolve!(options.componentId); if ("args" in options) this.#args = options.args; + + this.on("newListener", async function (this: ComponentInstance, evtName: string) { + if (evtName === "newListener" || evtName === "removeListener") return; + + if (this.listenerCount(evtName) === 0) { + this.peer.rpc("listenToEvent", { componentId: await this.componentId, eventName: evtName }); + } + }.bind(this)); + this.on("removeListener", async function (this: ComponentInstance, evtName: string) { + if (evtName === "newListener" || evtName === "removeListener") return; + + if (this.listenerCount(evtName) === 0) { + this.peer.rpc("unlistenToEvent", { componentId: await this.componentId, eventName: evtName }); + } + }.bind(this)); } #cidLatch = new Latch(); @@ -38,20 +66,19 @@ export class ComponentInstance { async init() { if (this.#cidLatch.getState() === LatchState.Pending) { - await this.module.waitForComponent(this.componentName); - - await this.module.componentReady.get(this.componentName); - const componentId = await (await this.ipc).rpc("createInstance", { + const componentId = await this.peer.rpc("createInstance", { className: this.componentName, args: this.#args.map((v) => wrapValue(v)), }); this.#cidLatch.resolve!(componentId); this.ready.resolve!(); + } else { + this.ready.resolve?.(); } } } export type ComponentInstanceProxy = ComponentInstance - & Partial> + & Record<`$${string}`, any> & { [Component.isComponentSymbol]: true }; function generateCalledOrAwaited({ called, awaited }: { called: (...args: any[]) => any, awaited: () => any }) { diff --git a/src/renderer/host/utils/types.ts b/src/renderer/host/utils/types.ts index 9996009..ab01f2f 100644 --- a/src/renderer/host/utils/types.ts +++ b/src/renderer/host/utils/types.ts @@ -1,10 +1,12 @@ import z from "zod"; import { OrderedPeer } from "../classes/orderedPeer"; +import { Component, ComponentHandle } from "../classes/component"; +import { ComponentInstanceProxy } from "./remoteToLocalAdapter"; export const zodElementFromIpc = z.object({ kind: z.literal("element"), tagName: z.string(), - attributes: z.tuple([ z.string(), z.string(), z.string() ]), + attributes: z.array(z.tuple([ z.string().nullable(), z.string(), z.string() ])), children: z.array(z.string()), content: z.string().optional(), }); @@ -47,5 +49,19 @@ export interface HandlerContext { event: MessageEvent; } -export type ComponentClass = new (...a: any[]) => Component; -export type ComponentHandleClass = new (...a: any[]) => ComponentHandle; +export const componentTypes = [ + "NORMAL", + "SINGLETON", + "REF_ONLY", +] as const; + +export type FactoryOf = { create(...args: U): T | Promise }; +export type ComponentFactory = FactoryOf; +export type ComponentHandleFactory = FactoryOf; +export type ComponentInstanceProxyFactory = FactoryOf; + +export type ClassOf = (FactoryOf | (new (...args: U) => T)) + & { type: typeof componentTypes[number], singletonInstance?: T }; +export type ComponentClass = ClassOf; +export type ComponentHandleClass = ClassOf; +export type ComponentInstanceProxyClass = ClassOf; diff --git a/src/renderer/host/utils/wrap.ts b/src/renderer/host/utils/wrap.ts index 487694f..b39f572 100644 --- a/src/renderer/host/utils/wrap.ts +++ b/src/renderer/host/utils/wrap.ts @@ -6,24 +6,24 @@ import { BaseModule, RemoteModule } from "../classes/module"; import { OtherNodeRegistry } from "../classes/sharedDomHost"; export const zodWrappedValue = z.union([ - z.object({ - value: z.any(), - }), z.object({ type: z.literal("component"), componentName: z.string(), componentId: z.string(), - moduleId: z.string().optional(), // undefined => This module comes from myself + moduleId: z.string(), }), z.object({ type: z.literal("node"), nodeId: z.string(), }), + z.object({ + value: z.any(), + }), ]); export type WrappedValue = z.output; export async function wrapValue(value: any): Promise> { - const isComponent = (v: any): v is (Component | ComponentInstanceProxy) => v[Component.isComponentSymbol]; + const isComponent = (v: any): v is (Component | ComponentInstanceProxy) => v?.[Component.isComponentSymbol]; if (isComponent(value)) { const isRemote = "componentName" in value; @@ -55,13 +55,16 @@ export async function unwrapValue(value: any, fromModule: RemoteModule) { switch (wrapped.type) { case "component": const moduleId = wrapped.moduleId || fromModule.opaqueToken; - const module = BaseModule.moduleById(moduleId); + const module = BaseModule.moduleByToken(moduleId); if (!module?.instanceExists(wrapped.componentId)) { return undefined; } // if (module instanceof remote) // return makeComponentProxy(module, wrapped.componentName, wrapped.componentId); + return module.localHandle; case "node": - return OtherNodeRegistry.registryOf((await fromModule.peer).source)!.getNode(value.nodeId); + const { source } = fromModule.peer; + if (source instanceof MessagePort || source instanceof ServiceWorker) throw new Error("Source supposed to be a window"); + return OtherNodeRegistry.registryOf(source)!.getNode(value.nodeId); } } diff --git a/src/renderer/index.html b/src/renderer/index.html index e1b8298..915db03 100644 --- a/src/renderer/index.html +++ b/src/renderer/index.html @@ -11,9 +11,17 @@ "moduleData": { "immjs.macos-aqua-windowframe": { "opaqueToken": "asdfasdfasdfasdf" + }, + "Cathodique::Window": { + "opaqueToken": "asdfasdfasdfasdfffff" + }, + "cathodique.windowmanager": { + "opaqueToken": "asdfasdfasdfasdffffwwwf" } }, "defaults": { + "Cathodique::Window": "Cathodique::Window", + "WindowManager": "cathodique.windowmanager", "WindowFrame": "immjs.macos-aqua-windowframe" }, "defaultsAll": {} diff --git a/src/renderer/utils/domIntersect.ts b/src/renderer/utils/domIntersect.ts new file mode 100644 index 0000000..603f2a9 --- /dev/null +++ b/src/renderer/utils/domIntersect.ts @@ -0,0 +1,13 @@ +export function isIntersecting(dom1: Element, dom2: Element) { + const firstBBox = dom1.getBoundingClientRect(); + const secondBBox = dom2.getBoundingClientRect(); + + // I used to be able to do these things mentally.... I had to google that + + // console.log(firstBBox, secondBBox); + + return firstBBox.left < secondBBox.right + && firstBBox.right > secondBBox.left + && firstBBox.top < secondBBox.bottom + && firstBBox.bottom > secondBBox.top; +} diff --git a/src/renderer/wayland/index.ts b/src/renderer/wayland/index.ts index d9836a8..28d9b56 100644 --- a/src/renderer/wayland/index.ts +++ b/src/renderer/wayland/index.ts @@ -2,14 +2,14 @@ import "../host/index.js"; import { HLCompositor } from "@cathodique/wl-serv-high"; import { InstructionType, RegRectangle } from "@cathodique/wl-serv-high/objects"; -import { OutputRegistry } from "@cathodique/wl-serv-high/registries"; import { KeyboardRegistry } from "@cathodique/wl-serv-high/objects"; import { ipcRenderer } from "electron/renderer"; import { BaseObject } from "@cathodique/wl-serv-high/objects"; -import { objectHandlers } from "../classes/handlers/handlers.js"; -import { Output } from "../classes/wayland/output/output.js"; import { seatRegistry } from "./overlays/seatRegistryOverlay.js"; import { outputRegistry } from "./overlays/outputRegistryOverlay.js"; +import { strutRegistry } from "./overlays/strutRegistryOverlay.js"; +import { objectHandlers } from "../classes/handlers/handlers.js"; +import { orchestrator } from "../host/index.js"; // HERE // TODO::: @@ -31,8 +31,8 @@ const mySeatConfig = { name: "seat0", capabilities: 3, }; - seatRegistry.addAuthority(mySeatConfig); +export const seat = seatRegistry.seatOfCfg(mySeatConfig)!; const myOutputConfig = { x: 0, @@ -44,12 +44,11 @@ const myOutputConfig = { }; outputRegistry.addAuthority(myOutputConfig); -// new Seat(mySeatConfig, seatReg); - const compo = new HLCompositor({ wl_registry: { outputs: outputRegistry, seats: seatRegistry, + struts: strutRegistry, }, wl_keyboard: new KeyboardRegistry({ keymap: "us" }), }); @@ -67,13 +66,19 @@ compo.on("connection", (c) => { if (!matching) return; // @ts-ignore - new matching(obj); + new matching(obj).init(); }); }); compo.start(); compo.on("ready", () => { document.body.append(`Ready at ${compo.params.socketPath}`); + ipcRenderer.send("addToDeleteQueue", compo.params.socketPath); ipcRenderer.send(`Ready at ${compo.params.socketPath}.lock`); }); + +orchestrator.load("WindowManager").then(async (mod) => { + const stuff = await mod!.localHandle.get("WindowManager")!.create(); + document.body.append(await stuff.$output); +}); diff --git a/src/renderer/wayland/overlays/outputRegistryOverlay.ts b/src/renderer/wayland/overlays/outputRegistryOverlay.ts index e2ea00f..38706b0 100644 --- a/src/renderer/wayland/overlays/outputRegistryOverlay.ts +++ b/src/renderer/wayland/overlays/outputRegistryOverlay.ts @@ -1,5 +1,4 @@ -import { OutputConfiguration } from "@cathodique/wl-serv-high/objects"; -import { OutputRegistry } from "@cathodique/wl-serv-high/registries"; +import { OutputConfiguration, OutputRegistry } from "@cathodique/wl-serv-high/registries"; import { Output } from "../../classes/wayland/output/output"; class OutputRegistryOverlay extends OutputRegistry { @@ -15,17 +14,17 @@ class OutputRegistryOverlay extends OutputRegistry { } // TODO Memory management - seats = new Map(); - allSeats() { - return this.seats.values(); + outputs = new Map(); + allOutputs() { + return this.outputs.values(); } - seatOfCfg(config: OutputConfiguration) { - return this.seats.get(config); + outputOfCfg(config: OutputConfiguration) { + return this.outputs.get(config); } addAuthority(cfg: OutputConfiguration) { super.addAuthority(cfg); - this.seats.set(cfg, new Output(cfg, this)); + this.outputs.set(cfg, new Output(cfg, this)); } } diff --git a/src/renderer/wayland/overlays/seatRegistryOverlay.ts b/src/renderer/wayland/overlays/seatRegistryOverlay.ts index 84edb1a..dff457a 100644 --- a/src/renderer/wayland/overlays/seatRegistryOverlay.ts +++ b/src/renderer/wayland/overlays/seatRegistryOverlay.ts @@ -1,5 +1,4 @@ -import { SeatConfiguration } from "@cathodique/wl-serv-high/objects"; -import { SeatRegistry } from "@cathodique/wl-serv-high/registries"; +import { SeatConfiguration, SeatRegistry } from "@cathodique/wl-serv-high/registries"; import { Seat } from "../../classes/wayland/seat/seat"; class SeatRegistryOverlay extends SeatRegistry { diff --git a/src/renderer/wayland/overlays/strutRegistryOverlay.ts b/src/renderer/wayland/overlays/strutRegistryOverlay.ts new file mode 100644 index 0000000..f59f9f1 --- /dev/null +++ b/src/renderer/wayland/overlays/strutRegistryOverlay.ts @@ -0,0 +1,16 @@ +import { StrutRegistry } from "@cathodique/wl-serv-high/registries"; + +class StrutRegistryOverlay extends StrutRegistry { + // Singleton + static #instance: StrutRegistry; + static create() { + return new StrutRegistryOverlay(); + } + private constructor() { + super(); + if (StrutRegistryOverlay.#instance) throw new Error("Tried to create multiple strut registries"); + StrutRegistryOverlay.#instance = this; + } +} + +export const strutRegistry = StrutRegistryOverlay.create(); diff --git a/tsconfig.json b/tsconfig.json index 68877ee..6c37621 100755 --- a/tsconfig.json +++ b/tsconfig.json @@ -111,6 +111,6 @@ "src/renderer/.common/**/*.ts" ], "exclude": [ - "modules" + "de/src/modules" ] } From f73ebe20cf603a1bd2fd4a2c30949e2b034a5759 Mon Sep 17 00:00:00 2001 From: Juliette Wang immjs Date: Mon, 19 Jan 2026 18:46:45 +0100 Subject: [PATCH 07/10] fix: Fix a few bugs --- .../.common/classes/sharedDomRemote.ts | 3 + .../immjs.macos-aqua-windowframe/module.html | 188 +++++++++++++++++- src/renderer/classes/handlers/dom/base.ts | 10 +- .../handlers/{lib => dom}/subsurface.ts | 24 +-- src/renderer/classes/handlers/dom/surface.ts | 8 - src/renderer/classes/handlers/handlers.ts | 6 +- .../host/localModules/basedomcomponent.ts | 5 + .../host/localModules/window_toplevel.ts | 5 +- 8 files changed, 213 insertions(+), 36 deletions(-) rename src/renderer/classes/handlers/{lib => dom}/subsurface.ts (76%) diff --git a/src/modules/.common/classes/sharedDomRemote.ts b/src/modules/.common/classes/sharedDomRemote.ts index 79c8c5c..64359d5 100644 --- a/src/modules/.common/classes/sharedDomRemote.ts +++ b/src/modules/.common/classes/sharedDomRemote.ts @@ -34,6 +34,8 @@ function serializeNode(node: Node) { case Node.ELEMENT_NODE: { const el = node as Element; + console.log(el.cloneNode(true), el); + return { kind: "element", tagName: el.tagName, @@ -139,6 +141,7 @@ export class SharedDOM { switch (m.type) { case "attributes": + console.log(m); parentIpc.post({ type: "changeAttribute", data: { diff --git a/src/modules/immjs.macos-aqua-windowframe/module.html b/src/modules/immjs.macos-aqua-windowframe/module.html index a8dbdb4..5cecae0 100644 --- a/src/modules/immjs.macos-aqua-windowframe/module.html +++ b/src/modules/immjs.macos-aqua-windowframe/module.html @@ -1,17 +1,176 @@ - - - Test - + + + Test +
- -

- Hello World -

+ +
+
+
+
+
+
+
Hello World +
+
+
+
-
+

Hello World