diff --git a/app/srv/api/comp-create.ts b/app/srv/api/comp-create.ts new file mode 100644 index 00000000..6f775b16 --- /dev/null +++ b/app/srv/api/comp-create.ts @@ -0,0 +1,182 @@ +import { syncronize } from "y-pojo"; +import { IItem, MItem } from "../../web/src/utils/types/item"; +import { eg } from "../ws/edit/edit-global"; + +export const _ = { + url: "/comp-create", + async api(arg: { + site_id: string; + page_id?: string; + item_id: string; + comp_id?: string; + group_id?: string; + }) { + const { page_id, site_id, item_id, comp_id, group_id } = arg; + let element = undefined as MItem | undefined; + const walk = (el: MItem): MItem | undefined => { + if (el.get("id") === item_id) { + return el; + } + let final = null; + + const props = el.get("component")?.get("props"); + if (props) { + props.forEach((e) => { + const content = e.get("content"); + if (content) { + const result = walk(content); + if (result) final = result; + } + }); + } + + const childs = el.get("childs"); + childs?.forEach((e: any) => { + const result = walk(e); + if (result) final = result; + }); + if (final) return final; + }; + + const page = eg.edit.page[page_id || ""]; + if (page_id) { + if (page) { + const root = page.doc.getMap("map").get("content_tree"); + if (root) { + element = walk(root as any); + } + } + } + + const comp = eg.edit.comp[comp_id || ""]; + if (comp_id) { + if (comp) { + const root = comp.doc.getMap("map").get("content_tree"); + if (root) { + element = walk(root as any); + } + } + } + + let gid = group_id; + if (!gid) { + let group = await db.component_group.findFirst({ + where: { + component_site: { + some: { + id_site: site_id, + }, + }, + name: { + not: { + equals: "__TRASH__", + }, + }, + }, + select: { + id: true, + name: true, + }, + }); + + if (!group) { + group = await db.component_group.create({ + data: { + name: "All", + component_site: { + create: { + id_site: site_id, + }, + }, + }, + select: { + id: true, + name: true, + }, + }); + } + gid = group.id; + } + if (element) { + const newcomp = await db.component.create({ + data: { + name: element.get("name") || "", + content_tree: element.toJSON(), + component_group: { + connect: { + id: gid, + }, + }, + }, + select: { + content_tree: true, + id: true, + }, + }); + if (newcomp) { + const content_tree = { + ...(newcomp.content_tree as any), + component: { + id: newcomp.id, + group: { + id: gid, + }, + }, + }; + await db.component.update({ + data: { + content_tree: content_tree, + }, + where: { + id: newcomp.id, + }, + }); + + const json = element.toJSON() as IItem; + syncronize( + element as any, + { + ...json, + childs: [], + component: { + id: newcomp.id, + name: "", + props: {}, + }, + } as IItem + ); + + if (comp_id) { + await db.component.update({ + where: { + id: comp_id, + }, + data: { + content_tree: comp.doc + .getMap("map") + .get("content_tree") + ?.toJSON(), + }, + }); + } else if (page && page.id) { + await db.page.update({ + where: { + id: page.id, + }, + data: { + content_tree: page.doc + .getMap("map") + .get("content_tree") + ?.toJSON(), + }, + }); + } + + return { + id: newcomp.id, + group_id: gid, + }; + } + } + }, +}; diff --git a/app/srv/package.json b/app/srv/package.json index 53ea65ac..ed795ab9 100644 --- a/app/srv/package.json +++ b/app/srv/package.json @@ -7,6 +7,7 @@ "@node-rs/argon2": "^1.5.2", "@paralleldrive/cuid2": "^2.2.2", "@types/mime-types": "^2.1.2", + "brotli-wasm": "^2.0.1", "esbuild": "^0.19.4", "lz-string": "^1.5.0", "mime-types": "^2.1.35", diff --git a/app/srv/ws/handler.ts b/app/srv/ws/handler.ts index d0bcf87e..06815e3d 100644 --- a/app/srv/ws/handler.ts +++ b/app/srv/ws/handler.ts @@ -1,15 +1,16 @@ import { createId } from "@paralleldrive/cuid2"; +import brotliPromise from "brotli-wasm"; import { WebSocketHandler } from "bun"; +import { decompress } from "lz-string"; import { WSData } from "../../../pkgs/core/server/create"; import { WS_MSG } from "../../web/src/utils/types/ws"; -import { eg } from "./edit/edit-global"; -import { decompress } from "lz-string"; -import { getPage } from "./edit/action/get-page"; -import { getComp } from "./edit/action/get-comp"; -import { svLocal } from "./edit/action/sv-local"; import { diffLocal } from "./edit/action/diff-local"; +import { getComp } from "./edit/action/get-comp"; +import { getPage } from "./edit/action/get-page"; +import { svLocal } from "./edit/action/sv-local"; import { svdiffRemote } from "./edit/action/svdiff-remote"; import { redo, undo } from "./edit/action/undo-redo"; +import { eg } from "./edit/edit-global"; eg.edit = { site: {}, @@ -21,7 +22,25 @@ const site = { saveTimeout: null as any, }; +const brotli = await brotliPromise; export const wsHandler: Record> = { + "/live": { + async open(ws) { + ws.send( + brotli.compress( + Buffer.from( + JSON.stringify( + await db.page.findFirst({ + where: { id: "324dde34-1e01-46ff-929c-124e5e01f585" }, + }) + ) + ), + { quality: 11 } + ) + ); + }, + message(ws, message) {}, + }, "/edit": { open(ws) { eg.edit.ws.set(ws, { diff --git a/app/web/.parcelrc b/app/web/.parcelrc index 7e326d88..d135bff9 100644 --- a/app/web/.parcelrc +++ b/app/web/.parcelrc @@ -6,6 +6,10 @@ "...", "@tinijs/parcel-reporter-copy-public" ], + "packagers": { + "*.wasm": "@parcel/packager-wasm" + }, + "transformers": { "*.wasm": [ "...", diff --git a/app/web/package.json b/app/web/package.json index 71a1c3cc..cf030f33 100644 --- a/app/web/package.json +++ b/app/web/package.json @@ -11,9 +11,11 @@ "@minoru/react-dnd-treeview": "^3.4.4", "@monaco-editor/react": "^4.6.0", "@paralleldrive/cuid2": "2.2.2", + "@parcel/packager-wasm": "^2.10.0", "@parcel/service-worker": "^2.10.0", "@swc/wasm-web": "1.3.94-nightly-20231014.1", "algoliasearch": "^4.20.0", + "brotli-dec-wasm": "^2.0.1", "date-fns": "^2.30.0", "dbgen": "workspace:*", "downshift": "^8.2.2", diff --git a/app/web/src/base/page/live.tsx b/app/web/src/base/page/live.tsx index 81f3c19d..4adea67e 100644 --- a/app/web/src/base/page/live.tsx +++ b/app/web/src/base/page/live.tsx @@ -25,6 +25,7 @@ export default page({ domain_or_siteid={params.domain} pathname={pathname} loader={devLoader} + liveSync /> ); }, diff --git a/app/web/src/render/live/live.tsx b/app/web/src/render/live/live.tsx index 768a1d18..31c6b72a 100644 --- a/app/web/src/render/live/live.tsx +++ b/app/web/src/render/live/live.tsx @@ -5,12 +5,14 @@ import { LPage } from "./elements/l-page"; import { LiveGlobal, Loader } from "./logic/global"; import { initLive, w } from "./logic/init"; import { preload, routeLive } from "./logic/route"; +import { liveSyncWS } from "./logic/ws-sync"; export const Live: FC<{ domain_or_siteid: string; pathname: string; loader: Loader; -}> = ({ domain_or_siteid, pathname, loader }) => { + liveSync?: boolean; +}> = ({ domain_or_siteid, pathname, loader, liveSync }) => { const p = useGlobal(LiveGlobal, "LIVE"); p.loader = loader; @@ -55,6 +57,9 @@ export const Live: FC<{ }, [p.site.responsive]); if (p.status === "init") { + if (liveSync) { + liveSyncWS(p); + } initLive(p, domain_or_siteid); } diff --git a/app/web/src/render/live/logic/global.ts b/app/web/src/render/live/logic/global.ts index 4f0f7847..48aaa3a5 100644 --- a/app/web/src/render/live/logic/global.ts +++ b/app/web/src/render/live/logic/global.ts @@ -67,6 +67,11 @@ export type Loader = { comp: (p: PG, id: string) => Promise; }; export const LiveGlobal = { + liveSync: { + ws: null as null | WebSocket, + init: false, + decompress: null as null | ((buf: Uint8Array) => Uint8Array), + }, prod: false, loader: undefined as unknown as Loader, mode: "" as "desktop" | "mobile", diff --git a/app/web/src/render/live/logic/init.tsx b/app/web/src/render/live/logic/init.tsx index b4f08f99..f25f590d 100644 --- a/app/web/src/render/live/logic/init.tsx +++ b/app/web/src/render/live/logic/init.tsx @@ -4,8 +4,7 @@ import { type apiClient } from "web-utils"; import { createAPI, createDB, - initApi, - reloadDBAPI, + initApi } from "../../../utils/script/init-api"; import importModule from "../../editor/tools/dynamic-import"; import { LSite, PG } from "./global"; diff --git a/app/web/src/render/live/logic/route.ts b/app/web/src/render/live/logic/route.ts index 0c270764..1e0466fb 100644 --- a/app/web/src/render/live/logic/route.ts +++ b/app/web/src/render/live/logic/route.ts @@ -1,12 +1,8 @@ -import { page } from "dbgen"; -import { validate } from "uuid"; import { w } from "../../../utils/types/general"; -import { WS_MSG_GET_PAGE } from "../../../utils/types/ws"; import importModule from "../../editor/tools/dynamic-import"; import { loadComponent } from "./comp"; import { LPage, PG } from "./global"; import { rebuildTree } from "./tree-logic"; -import { liveWS, wsend } from "./ws"; export const routeLive = (p: PG, pathname: string) => { if (p.status !== "loading" && p.status !== "not-found") { @@ -127,7 +123,6 @@ const loadPage = async (p: PG, id: string) => { content_tree: page.content_tree as any, js: (page as any).js_compiled as any, }; - console.log(p.pages[page.id]); const cur = p.pages[page.id]; if (cur && cur.content_tree) { diff --git a/app/web/src/render/live/logic/ws-sync.ts b/app/web/src/render/live/logic/ws-sync.ts new file mode 100644 index 00000000..e127dcf4 --- /dev/null +++ b/app/web/src/render/live/logic/ws-sync.ts @@ -0,0 +1,28 @@ +import { PG } from "./global"; + +export const liveSyncWS = async (p: PG) => { + if (!p.liveSync.init) { + p.liveSync.init = true; + + if (!p.liveSync.decompress) { + const brotliPromise = (await import("brotli-dec-wasm")).default; + p.liveSync.decompress = (await brotliPromise).decompress; + } + const decoder = new TextDecoder(); + const url = new URL(location.href); + url.pathname = "/live"; + url.protocol = url.protocol === "http:" ? "ws:" : "wss:"; + + const ws = new WebSocket(url); + p.liveSync.ws = ws; + ws.onmessage = async (e) => { + const decompress = p.liveSync.decompress; + if (decompress) { + const raw = e.data as Blob; + const extracted = decompress(new Uint8Array(await raw.arrayBuffer())); + const json = JSON.parse(decoder.decode(extracted)); + console.log(json); + } + }; + } +}; diff --git a/app/web/src/render/live/logic/ws.ts b/app/web/src/render/live/logic/ws.ts deleted file mode 100644 index e42886d2..00000000 --- a/app/web/src/render/live/logic/ws.ts +++ /dev/null @@ -1,297 +0,0 @@ -import throttle from "lodash.throttle"; -import { compress, decompress } from "lz-string"; -import * as Y from "yjs"; -import { createAPI, createDB } from "../../../utils/script/init-api"; -import { MPage } from "../../../utils/types/general"; -import { - WS_MSG, - WS_MSG_DIFF_LOCAL, - WS_MSG_SET_PAGE, - WS_MSG_SVDIFF_REMOTE, - WS_MSG_SV_LOCAL, -} from "../../../utils/types/ws"; -import importModule from "../../editor/tools/dynamic-import"; -import { PG } from "./global"; -import { rebuildTree } from "./tree-logic"; -import { CompDoc } from "../../../base/global/content-editor"; -import { IItem } from "../../../utils/types/item"; -import { scanComponent } from "./comp"; -import { PRASI_COMPONENT } from "../../../utils/types/render"; - -export const liveWS = async (p: PG) => { - return new Promise(async (resolve) => { - const wsurl = new URL(serverurl); - wsurl.protocol = wsurl.protocol.startsWith("http:") ? "ws:" : "wss:"; - - if ( - p.wsRetry.localIP && - ["localhost", "127.0.0.1"].includes(wsurl.hostname) - ) { - const ips = await api.local_ip(); - wsurl.hostname = ips[0]; - } - - wsurl.pathname = "/edit"; - - if (p.ws && p.ws.readyState === p.ws.OPEN) { - resolve(); - return; - } - p.ws = new WebSocket(wsurl); - const ws = p.ws; - - if (ws) { - const retry = (e: any) => { - if (p.wsRetry.disabled) return; - - p.wsRetry.reconnecting = true; - p.wsRetry.localIP = true; - if (p.wsRetry.fast) { - liveWS(p); - } else { - setTimeout(() => { - console.log("Reconnecting..."); - liveWS(p); - }, 2000); - } - }; - ws.addEventListener("error", retry); - ws.addEventListener("close", retry); - ws.addEventListener("open", () => { - if (p.wsRetry.reconnecting) { - p.wsRetry.reconnecting = false; - console.log("Connected"); - } - resolve(); - }); - ws.addEventListener("message", async (e) => { - const msg = JSON.parse(e.data) as WS_MSG; - - switch (msg.type) { - case "get_page": - break; - case "set_page": - if (p.mpage) { - p.mpage.destroy(); - } - p.mpage = await setPage(msg); - p.mpage.on( - "update", - throttle((e, origin) => { - if (p.mpage) { - p.page = p.mpage.getMap("map").toJSON() as any; - - console.clear(); - console.log( - `🔥 Page updated: ${p.page - ?.url} ${new Date().toLocaleString()}` - ); - } - }) - ); - p.page = p.mpage.getMap("map").toJSON() as any; - - if (p.mpageLoaded) { - p.mpageLoaded(p.mpage); - p.mpageLoaded = null; - } - break; - case "sv_local": - svLocal({ p, bin: extract(msg.sv_local), msg }); - break; - case "svd_remote": - svdRemote({ p, bin: extract(msg.diff_remote), msg }); - rebuildTree(p, { note: "page-changed" }); - break; - case "diff_local": - if (msg.mode === "page") { - Y.applyUpdate(p.mpage as any, extract(msg.diff_local), "remote"); - } - if (msg.mode === "comp") { - Y.applyUpdate( - p.comps.doc[msg.id] as any, - extract(msg.diff_local), - "remote" - ); - } - break; - case "set_comp": - { - const callback = p.comps.resolve[msg.comp_id]; - if (callback) { - p.comps.doc[msg.comp_id] = new Y.Doc() as CompDoc; - Y.applyUpdate( - p.comps.doc[msg.comp_id] as any, - extract(msg.changes), - "remote" - ); - setTimeout(() => { - p.comps.doc[msg.comp_id].on( - "update", - throttle((e, origin) => { - if (origin === "remote") { - return; - } - - const doc = p.comps.doc[msg.comp_id]; - if (doc) { - if (!origin && origin !== "updated_at") { - const id = doc.getMap("map").get("id"); - if (id) { - doc.transact(() => { - doc - .getMap("map") - .set("updated_at", new Date().toISOString()); - }, "updated_at"); - - const sendmsg: WS_MSG_SV_LOCAL = { - type: "sv_local", - mode: "comp", - id, - sv_local: compress( - Y.encodeStateVector(doc as any).toString() - ), - }; - wsend(p, JSON.stringify(sendmsg)); - } - } - } - }, 200) - ); - }, 500); - const comp = p.comps.doc[msg.comp_id] - .getMap("map") - .get("content_tree") - ?.toJSON() as IItem; - - const ids = new Set(); - scanComponent(comp, ids); - - callback( - p.comps.doc[msg.comp_id] - .getMap("map") - .toJSON() as PRASI_COMPONENT - ); - delete p.comps.pending[msg.comp_id]; - delete p.comps.resolve[msg.comp_id]; - } - } - break; - case "undo": - case "redo": - case "new_comp": - case "get_comp": - } - }); - ws.addEventListener("open", () => { - p.wsRetry.disabled = false; - }); - } - }); -}; - -const extract = (str: string) => { - return Uint8Array.from( - decompress(str) - .split(",") - .map((x) => parseInt(x, 10)) - ); -}; - -const svLocal = async (arg: { - bin: Uint8Array; - msg: { - id: string; - mode: string; - type: string; - }; - p: PG; -}) => { - const { bin, msg, p } = arg; - const { id, mode, type } = msg; - - let doc = null as any; - if (mode === "page") doc = p.mpage; - if (mode === "comp") doc = p.comps.doc[id]; - if (!doc) return; - - const diff_remote = Y.encodeStateAsUpdate(doc, bin); - const sv_remote = Y.encodeStateVector(doc); - - const sendmsg: any = { - diff_remote: compress(diff_remote.toString()), - sv_remote: compress(sv_remote.toString()), - id: id, - mode: mode, - type: type, - }; - await wsend(p, JSON.stringify(sendmsg)); -}; - -const svdRemote = async (arg: { - bin: Uint8Array; - msg: WS_MSG_SVDIFF_REMOTE; - p: PG; -}) => { - const { bin, msg, p } = arg; - const { id, mode, type } = msg; - const sv_remote = Uint8Array.from( - decompress(msg.sv_remote) - .split(",") - .map((x) => parseInt(x, 10)) - ); - const diff_remote = Uint8Array.from( - decompress(msg.diff_remote) - .split(",") - .map((x) => parseInt(x, 10)) - ); - - const sendDoc = async (doc: any) => { - const diff_local = Y.encodeStateAsUpdate(doc as any, sv_remote); - Y.applyUpdate(doc as any, diff_remote, "local"); - const sendmsg: WS_MSG_DIFF_LOCAL = { - type: "diff_local", - mode: msg.mode, - id: msg.id, - diff_local: compress(diff_local.toString()), - }; - await wsend(p, JSON.stringify(sendmsg)); - }; - - let doc = null as any; - if (mode === "page") doc = p.mpage; - if (mode === "comp") doc = p.comps.doc[id]; - if (!doc) return; - sendDoc(doc); -}; - -export const wsend = async (local: PG, payload: string) => { - const ws = local.ws; - if (ws) { - if (ws.readyState !== ws.OPEN) { - await new Promise((resolve) => { - const ival = setInterval(() => { - if (ws.readyState === ws.OPEN) { - clearInterval(ival); - resolve(); - } - }, 50); - }); - } - - ws.send(payload); - } -}; - -const setPage = async (msg: WS_MSG_SET_PAGE) => { - const page = Uint8Array.from( - decompress(msg.changes) - .split(",") - .map((x) => parseInt(x, 10)) - ); - - const doc = new Y.Doc(); - Y.applyUpdate(doc as any, page, "remote"); - - return doc as unknown as MPage; -}; diff --git a/bun.lockb b/bun.lockb index 319a8bfb..a358c1b0 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/pkgs/core/build.ts b/pkgs/core/build.ts index b7e25903..60997759 100644 --- a/pkgs/core/build.ts +++ b/pkgs/core/build.ts @@ -11,7 +11,8 @@ const args = [ dir.path("node_modules/.bin/parcel"), "build", "./src/index.tsx", - "--no-optimize", + // "--no-optimize", + "--no-scope-hoist", "--dist-dir", dir.path(`app/static`), ];