// Copyright 2024, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 import { LayoutTreeActionType, LayoutTreeInsertNodeAction, newLayoutNode } from "@/faraday/index"; import { getLayoutStateAtomForTab } from "@/faraday/lib/layoutAtom"; import { layoutTreeStateReducer } from "@/faraday/lib/layoutState"; import * as jotai from "jotai"; import * as rxjs from "rxjs"; import * as services from "./services"; import * as WOS from "./wos"; import { WSControl } from "./ws"; // TODO remove the window dependency completely // we should have the initialization be more orderly -- proceed directly from wave.ts instead of on its own. const globalStore = jotai.createStore(); let globalWindowId: string = null; let globalClientId: string = null; if (typeof window !== "undefined") { // this if statement allows us to use the code in nodejs as well const urlParams = new URLSearchParams(window.location.search); globalWindowId = urlParams.get("windowid") || "74eba2d0-22fc-4221-82ad-d028dd496342"; globalClientId = urlParams.get("clientid") || "f4bc1713-a364-41b3-a5c4-b000ba10d622"; } const windowIdAtom = jotai.atom(null) as jotai.PrimitiveAtom; const clientIdAtom = jotai.atom(null) as jotai.PrimitiveAtom; globalStore.set(windowIdAtom, globalWindowId); globalStore.set(clientIdAtom, globalClientId); const uiContextAtom = jotai.atom((get) => { const windowData = get(windowDataAtom); const uiContext: UIContext = { windowid: get(atoms.windowId), activetabid: windowData?.activetabid, }; return uiContext; }) as jotai.Atom; const clientAtom: jotai.Atom = jotai.atom((get) => { const clientId = get(clientIdAtom); if (clientId == null) { return null; } return WOS.getObjectValue(WOS.makeORef("client", clientId), get); }); const windowDataAtom: jotai.Atom = jotai.atom((get) => { const windowId = get(windowIdAtom); if (windowId == null) { return null; } const rtn = WOS.getObjectValue(WOS.makeORef("window", windowId), get); return rtn; }); const workspaceAtom: jotai.Atom = jotai.atom((get) => { const windowData = get(windowDataAtom); if (windowData == null) { return null; } return WOS.getObjectValue(WOS.makeORef("workspace", windowData.workspaceid), get); }); const tabAtom: jotai.Atom = jotai.atom((get) => { const windowData = get(windowDataAtom); if (windowData == null) { return null; } return WOS.getObjectValue(WOS.makeORef("tab", windowData.activetabid), get); }); const atoms = { // initialized in wave.ts (will not be null inside of application) windowId: windowIdAtom, clientId: clientIdAtom, uiContext: uiContextAtom, client: clientAtom, waveWindow: windowDataAtom, workspace: workspaceAtom, tabAtom: tabAtom, }; // key is "eventType" or "eventType|oref" const eventSubjects = new Map>(); const fileSubjects = new Map>(); function getSubjectInternal(subjectKey: string): SubjectWithRef { let subject = eventSubjects.get(subjectKey); if (subject == null) { subject = new rxjs.Subject() as any; subject.refCount = 0; subject.release = () => { subject.refCount--; if (subject.refCount === 0) { subject.complete(); eventSubjects.delete(subjectKey); } }; eventSubjects.set(subjectKey, subject); } subject.refCount++; return subject; } function getEventSubject(eventType: string): SubjectWithRef { return getSubjectInternal(eventType); } function getEventORefSubject(eventType: string, oref: string): SubjectWithRef { return getSubjectInternal(eventType + "|" + oref); } function getFileSubject(zoneId: string, fileName: string): SubjectWithRef { const subjectKey = zoneId + "|" + fileName; let subject = fileSubjects.get(subjectKey); if (subject == null) { subject = new rxjs.Subject() as any; subject.refCount = 0; subject.release = () => { subject.refCount--; if (subject.refCount === 0) { subject.complete(); fileSubjects.delete(subjectKey); } }; fileSubjects.set(subjectKey, subject); } subject.refCount++; return subject; } const blockCache = new Map>(); function useBlockCache(blockId: string, name: string, makeFn: () => T): T { let blockMap = blockCache.get(blockId); if (blockMap == null) { blockMap = new Map(); blockCache.set(blockId, blockMap); } let value = blockMap.get(name); if (value == null) { value = makeFn(); blockMap.set(name, value); } return value as T; } const blockAtomCache = new Map>>(); function useBlockAtom(blockId: string, name: string, makeFn: () => jotai.Atom): jotai.Atom { let blockCache = blockAtomCache.get(blockId); if (blockCache == null) { blockCache = new Map>(); blockAtomCache.set(blockId, blockCache); } let atom = blockCache.get(name); if (atom == null) { atom = makeFn(); blockCache.set(name, atom); console.log("New BlockAtom", blockId, name); } return atom as jotai.Atom; } function getBackendHostPort(): string { // TODO deal with dev/production return "http://127.0.0.1:8190"; } function getBackendWSHostPort(): string { return "ws://127.0.0.1:8191"; } let globalWS: WSControl = null; function handleWSEventMessage(msg: WSEventType) { if (msg.eventtype == null) { console.log("unsupported event", msg); return; } if (msg.eventtype == "blockfile") { const fileData: WSFileEventData = msg.data; const fileSubject = getFileSubject(fileData.zoneid, fileData.filename); if (fileSubject != null) { fileSubject.next(fileData); } return; } // we send to two subjects just eventType and eventType|oref // we don't use getORefSubject here because we don't want to create a new subject const eventSubject = eventSubjects.get(msg.eventtype); if (eventSubject != null) { eventSubject.next(msg); } const eventOrefSubject = eventSubjects.get(msg.eventtype + "|" + msg.oref); if (eventOrefSubject != null) { eventOrefSubject.next(msg); } } function handleWSMessage(msg: any) { if (msg == null) { return; } if (msg.eventtype != null) { handleWSEventMessage(msg); } } function initWS() { globalWS = new WSControl(getBackendWSHostPort(), globalStore, globalWindowId, "", (msg) => { handleWSMessage(msg); }); globalWS.connectNow("initWS"); } function sendWSCommand(command: WSCommandType) { globalWS.pushMessage(command); } // more code that could be moved into an init // here we want to set up a "waveobj:update" handler const waveobjUpdateSubject = getEventSubject("waveobj:update"); waveobjUpdateSubject.subscribe((msg: WSEventType) => { const update: WaveObjUpdate = msg.data; WOS.updateWaveObject(update); }); /** * Get the preload api. */ function getApi(): ElectronApi { return (window as any).api; } async function createBlock(blockDef: BlockDef) { const rtOpts: RuntimeOpts = { termsize: { rows: 25, cols: 80 } }; const blockId = await services.ObjectService.CreateBlock(blockDef, rtOpts); const insertNodeAction: LayoutTreeInsertNodeAction = { type: LayoutTreeActionType.InsertNode, node: newLayoutNode(undefined, undefined, undefined, { blockId }), }; const activeTabId = globalStore.get(atoms.uiContext).activetabid; const layoutStateAtom = getLayoutStateAtomForTab( activeTabId, WOS.getWaveObjectAtom(WOS.makeORef("tab", activeTabId)) ); const curState = globalStore.get(layoutStateAtom); globalStore.set(layoutStateAtom, layoutTreeStateReducer(curState, insertNodeAction)); } // when file is not found, returns {data: null, fileInfo: null} async function fetchWaveFile( zoneId: string, fileName: string, offset?: number ): Promise<{ data: Uint8Array; fileInfo: WaveFile }> { let usp = new URLSearchParams(); usp.set("zoneid", zoneId); usp.set("name", fileName); if (offset != null) { usp.set("offset", offset.toString()); } const resp = await fetch(getBackendHostPort() + "/wave/file?" + usp.toString()); if (!resp.ok) { if (resp.status === 404) { return { data: null, fileInfo: null }; } throw new Error("error getting wave file: " + resp.statusText); } if (resp.status == 204) { return { data: null, fileInfo: null }; } let fileInfo64 = resp.headers.get("X-ZoneFileInfo"); if (fileInfo64 == null) { throw new Error(`missing zone file info for ${zoneId}:${fileName}`); } let fileInfo = JSON.parse(atob(fileInfo64)); const data = await resp.arrayBuffer(); return { data: new Uint8Array(data), fileInfo }; } export { WOS, atoms, createBlock, fetchWaveFile, getApi, getBackendHostPort, getEventORefSubject, getEventSubject, getFileSubject, globalStore, globalWS, initWS, sendWSCommand, useBlockAtom, useBlockCache, };