adding brotli sync ws

This commit is contained in:
Rizky 2023-10-18 23:49:34 +07:00
parent e0984794de
commit 2d53709630
14 changed files with 256 additions and 311 deletions

182
app/srv/api/comp-create.ts Normal file
View File

@ -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,
};
}
}
},
};

View File

@ -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",

View File

@ -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<string, WebSocketHandler<WSData>> = {
"/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, {

View File

@ -6,6 +6,10 @@
"...",
"@tinijs/parcel-reporter-copy-public"
],
"packagers": {
"*.wasm": "@parcel/packager-wasm"
},
"transformers": {
"*.wasm": [
"...",

View File

@ -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",

View File

@ -25,6 +25,7 @@ export default page({
domain_or_siteid={params.domain}
pathname={pathname}
loader={devLoader}
liveSync
/>
);
},

View File

@ -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);
}

View File

@ -67,6 +67,11 @@ export type Loader = {
comp: (p: PG, id: string) => Promise<PRASI_COMPONENT>;
};
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",

View File

@ -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";

View File

@ -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) {

View File

@ -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);
}
};
}
};

View File

@ -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<void>(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<string>();
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<void>((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;
};

BIN
bun.lockb

Binary file not shown.

View File

@ -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`),
];