fix
This commit is contained in:
parent
6cbbe90348
commit
3a897090c6
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"name": "srv",
|
||||||
|
"dependencies": {
|
||||||
|
"@node-rs/argon2": "^1.5.2",
|
||||||
|
"@paralleldrive/cuid2": "^2.2.2",
|
||||||
|
"@types/mime-types": "^2.1.2",
|
||||||
|
"lz-string": "^1.5.0",
|
||||||
|
"mime-types": "^2.1.35",
|
||||||
|
"radix3": "^1.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
import { Websocket } from "hyper-express";
|
||||||
|
import { decompress } from "lz-string";
|
||||||
|
import * as Y from "yjs";
|
||||||
|
import { eg } from "../edit-global";
|
||||||
|
|
||||||
|
export const diffLocal = (ws: Websocket, msg: any) => {
|
||||||
|
return new Promise<void>((resolve) => {
|
||||||
|
const diff_local = Uint8Array.from(
|
||||||
|
decompress(msg.diff_local)
|
||||||
|
.split(",")
|
||||||
|
.map((x) => parseInt(x, 10))
|
||||||
|
);
|
||||||
|
let doc = null as unknown as Y.Doc;
|
||||||
|
let wss: Set<Websocket> = null as any;
|
||||||
|
let um: Y.UndoManager = null as any;
|
||||||
|
if (msg.mode === "page") {
|
||||||
|
doc = eg.edit.page[msg.id].doc as any;
|
||||||
|
wss = eg.edit.page[msg.id].ws;
|
||||||
|
um = eg.edit.page[msg.id].undoManager;
|
||||||
|
} else if (msg.mode === "comp") {
|
||||||
|
doc = eg.edit.comp[msg.id].doc as any;
|
||||||
|
wss = eg.edit.comp[msg.id].ws;
|
||||||
|
um = eg.edit.comp[msg.id].undoManager;
|
||||||
|
} else if (msg.mode === "site") {
|
||||||
|
doc = eg.edit.site[msg.id].doc as any;
|
||||||
|
wss = eg.edit.site[msg.id].ws;
|
||||||
|
um = eg.edit.site[msg.id].undoManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (doc && wss) {
|
||||||
|
Y.applyUpdate(doc, diff_local);
|
||||||
|
|
||||||
|
if (msg.mode === "page") {
|
||||||
|
clearTimeout(eg.edit.page[msg.id].saveTimeout);
|
||||||
|
eg.edit.page[msg.id].saveTimeout = setTimeout(async () => {
|
||||||
|
if (msg.id) {
|
||||||
|
const page = eg.edit.page[msg.id].doc.getMap("map").toJSON();
|
||||||
|
try {
|
||||||
|
await db.page.update({
|
||||||
|
where: { id: page.id },
|
||||||
|
data: {
|
||||||
|
content_tree: page.content_tree,
|
||||||
|
updated_at: new Date(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
resolve();
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
console.error({
|
||||||
|
...page,
|
||||||
|
updated_at: new Date(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 1500);
|
||||||
|
} else if (msg.mode === "comp") {
|
||||||
|
eg.edit.comp[msg.id].saveTimeout = setTimeout(async () => {
|
||||||
|
const comp = eg.edit.comp[msg.id].doc.getMap("map").toJSON();
|
||||||
|
await db.component.update({
|
||||||
|
where: {
|
||||||
|
id: msg.id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
name: comp.name,
|
||||||
|
content_tree: comp.content_tree,
|
||||||
|
updated_at: new Date(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, 1500);
|
||||||
|
} else if (msg.mode === "site") {
|
||||||
|
clearTimeout(eg.edit.site[msg.id].saveTimeout);
|
||||||
|
eg.edit.site[msg.id].saveTimeout = setTimeout(async () => {
|
||||||
|
const site = eg.edit.site[msg.id].doc.getMap("site").toJSON();
|
||||||
|
delete site.page;
|
||||||
|
await db.site.update({
|
||||||
|
where: {
|
||||||
|
id: msg.id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
...site,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, 1500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
import { Websocket } from "hyper-express";
|
||||||
|
import { compress } from "lz-string";
|
||||||
|
import { syncronize } from "y-pojo";
|
||||||
|
import * as Y from "yjs";
|
||||||
|
import {
|
||||||
|
WS_MSG_GET_COMP,
|
||||||
|
WS_MSG_SET_COMP,
|
||||||
|
WS_MSG_SV_LOCAL,
|
||||||
|
} from "../../../web/src/utils/types/ws";
|
||||||
|
import { SingleComp, eg } from "../edit-global";
|
||||||
|
|
||||||
|
export const getComp = async (ws: Websocket, msg: WS_MSG_GET_COMP) => {
|
||||||
|
const comp_id = msg.comp_id;
|
||||||
|
|
||||||
|
if (!eg.edit.comp[comp_id]) {
|
||||||
|
const rawComp = await db.component.findFirst({
|
||||||
|
where: {
|
||||||
|
id: comp_id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!rawComp) {
|
||||||
|
const sent: WS_MSG_SET_COMP = {
|
||||||
|
type: "set_comp",
|
||||||
|
comp_id: comp_id,
|
||||||
|
changes: "",
|
||||||
|
};
|
||||||
|
ws.send(JSON.stringify(sent));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rawComp) {
|
||||||
|
const ydoc = new Y.Doc() as SingleComp["doc"];
|
||||||
|
const map = ydoc.getMap("map");
|
||||||
|
syncronize(map as any, rawComp);
|
||||||
|
|
||||||
|
const ws = new Set<Websocket>();
|
||||||
|
const um = new Y.UndoManager(map, { ignoreRemoteMapChanges: true });
|
||||||
|
const broadcast = () => {
|
||||||
|
const sv_local = compress(Y.encodeStateVector(ydoc as any).toString());
|
||||||
|
const broadcast: WS_MSG_SV_LOCAL = {
|
||||||
|
type: "sv_local",
|
||||||
|
sv_local,
|
||||||
|
mode: "comp",
|
||||||
|
id: comp_id,
|
||||||
|
};
|
||||||
|
ws.forEach((w) => w.send(JSON.stringify(broadcast)));
|
||||||
|
};
|
||||||
|
um.on("stack-item-added", broadcast);
|
||||||
|
um.on("stack-item-updated", broadcast);
|
||||||
|
|
||||||
|
eg.edit.comp[comp_id] = {
|
||||||
|
doc: ydoc,
|
||||||
|
id: comp_id,
|
||||||
|
undoManager: um,
|
||||||
|
ws,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const comp = eg.edit.comp[comp_id];
|
||||||
|
if (comp) {
|
||||||
|
if (!comp.ws.has(ws)) comp.ws.add(ws);
|
||||||
|
const sent: WS_MSG_SET_COMP = {
|
||||||
|
type: "set_comp",
|
||||||
|
comp_id: comp_id,
|
||||||
|
changes: compress(Y.encodeStateAsUpdate(comp.doc as any).toString()),
|
||||||
|
};
|
||||||
|
ws.send(JSON.stringify(sent));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
import { Websocket } from "hyper-express";
|
||||||
|
import { compress } from "lz-string";
|
||||||
|
import { syncronize } from "y-pojo";
|
||||||
|
import * as Y from "yjs";
|
||||||
|
import {
|
||||||
|
WS_MSG_GET_PAGE,
|
||||||
|
WS_MSG_SET_PAGE,
|
||||||
|
WS_MSG_SV_LOCAL,
|
||||||
|
} from "../../../web/src/utils/types/ws";
|
||||||
|
import { MPage } from "../../../web/src/utils/types/general";
|
||||||
|
import { eg } from "../edit-global";
|
||||||
|
import { loadPage } from "../tools/load-page";
|
||||||
|
import { validate } from "uuid";
|
||||||
|
|
||||||
|
export const getPage = async (ws: Websocket, msg: WS_MSG_GET_PAGE) => {
|
||||||
|
const page_id = msg.page_id;
|
||||||
|
if (!validate(page_id)) return;
|
||||||
|
if (!eg.edit.page[page_id]) {
|
||||||
|
const rawPage = await loadPage(page_id);
|
||||||
|
|
||||||
|
if (rawPage) {
|
||||||
|
const ydoc = new Y.Doc() as MPage;
|
||||||
|
let root = ydoc.getMap("map");
|
||||||
|
syncronize(root as any, rawPage);
|
||||||
|
const ws = new Set<Websocket>();
|
||||||
|
const um = new Y.UndoManager(root, { ignoreRemoteMapChanges: true });
|
||||||
|
const broadcast = () => {
|
||||||
|
const sv_local = compress(Y.encodeStateVector(ydoc as any).toString());
|
||||||
|
const broadcast: WS_MSG_SV_LOCAL = {
|
||||||
|
type: "sv_local",
|
||||||
|
sv_local,
|
||||||
|
mode: "page",
|
||||||
|
id: page_id,
|
||||||
|
};
|
||||||
|
ws.forEach((w) => w.send(JSON.stringify(broadcast)));
|
||||||
|
};
|
||||||
|
um.on("stack-item-added", broadcast);
|
||||||
|
um.on("stack-item-updated", broadcast);
|
||||||
|
|
||||||
|
eg.edit.page[page_id] = {
|
||||||
|
id: page_id,
|
||||||
|
doc: ydoc,
|
||||||
|
undoManager: um,
|
||||||
|
ws,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const page = eg.edit.page[page_id];
|
||||||
|
// let root = page.doc.getMap("map").get("content_tree") as unknown as MContent;
|
||||||
|
// if (root) {
|
||||||
|
// let changed = false;
|
||||||
|
// await page.doc.transact(async () => {
|
||||||
|
// changed = await validateTreePage(ws, root);
|
||||||
|
// });
|
||||||
|
|
||||||
|
// if (changed) {
|
||||||
|
// root = page.doc.getMap("map").get("content_tree") as unknown as MContent;
|
||||||
|
// await db.page.update({
|
||||||
|
// where: {
|
||||||
|
// id: page.id,
|
||||||
|
// },
|
||||||
|
// data: { content_tree: root.toJSON(), updated_at: new Date() },
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
page.ws.add(ws);
|
||||||
|
const sent: WS_MSG_SET_PAGE = {
|
||||||
|
type: "set_page",
|
||||||
|
changes: compress(Y.encodeStateAsUpdate(page.doc as any).toString()),
|
||||||
|
};
|
||||||
|
ws.send(JSON.stringify(sent));
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
import { Websocket } from "hyper-express";
|
||||||
|
import { compress, decompress } from "lz-string";
|
||||||
|
import * as Y from "yjs";
|
||||||
|
import {
|
||||||
|
WS_MSG_SVDIFF_REMOTE,
|
||||||
|
WS_MSG_SV_LOCAL,
|
||||||
|
} from "../../../web/src/utils/types/ws";
|
||||||
|
import { eg } from "../edit-global";
|
||||||
|
import { getComp } from "./get-comp";
|
||||||
|
import { getPage } from "./get-page";
|
||||||
|
|
||||||
|
export const svLocal = async (ws: Websocket, msg: WS_MSG_SV_LOCAL) => {
|
||||||
|
const changes = Uint8Array.from(
|
||||||
|
decompress(msg.sv_local)
|
||||||
|
.split(",")
|
||||||
|
.map((x) => parseInt(x, 10))
|
||||||
|
);
|
||||||
|
let doc = null as any;
|
||||||
|
if (msg.mode === "page") {
|
||||||
|
if (!eg.edit.page[msg.id]) {
|
||||||
|
await getPage(ws, { type: "get_page", page_id: msg.id });
|
||||||
|
}
|
||||||
|
|
||||||
|
doc = eg.edit.page[msg.id].doc;
|
||||||
|
} else if (msg.mode === "comp") {
|
||||||
|
if (!eg.edit.comp[msg.id]) {
|
||||||
|
await getComp(ws, { comp_id: msg.id, type: "get_comp" });
|
||||||
|
}
|
||||||
|
|
||||||
|
doc = eg.edit.comp[msg.id].doc;
|
||||||
|
} else if (msg.mode === "site") {
|
||||||
|
doc = eg.edit.site[msg.id].doc;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (doc) {
|
||||||
|
const diff_remote = Y.encodeStateAsUpdate(doc, changes);
|
||||||
|
const sv_remote = Y.encodeStateVector(doc);
|
||||||
|
|
||||||
|
const sendmsg: WS_MSG_SVDIFF_REMOTE = {
|
||||||
|
diff_remote: compress(diff_remote.toString()),
|
||||||
|
sv_remote: compress(sv_remote.toString()),
|
||||||
|
id: msg.id,
|
||||||
|
mode: msg.mode,
|
||||||
|
type: "svd_remote",
|
||||||
|
};
|
||||||
|
ws.send(JSON.stringify(sendmsg));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
import { Websocket } from "hyper-express";
|
||||||
|
import { compress, decompress } from "lz-string";
|
||||||
|
import * as Y from "yjs";
|
||||||
|
import {
|
||||||
|
WS_MSG_DIFF_LOCAL,
|
||||||
|
WS_MSG_SVDIFF_REMOTE,
|
||||||
|
} from "../../../web/src/utils/types/ws";
|
||||||
|
import { eg } from "../edit-global";
|
||||||
|
|
||||||
|
export const svdiffRemote = async (
|
||||||
|
ws: Websocket,
|
||||||
|
msg: WS_MSG_SVDIFF_REMOTE
|
||||||
|
) => {
|
||||||
|
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))
|
||||||
|
);
|
||||||
|
|
||||||
|
let doc = null as any;
|
||||||
|
if (msg.mode === "page") {
|
||||||
|
doc = eg.edit.page[msg.id].doc;
|
||||||
|
} else if (msg.mode === "comp") {
|
||||||
|
doc = eg.edit.comp[msg.id].doc;
|
||||||
|
} else if (msg.mode === "site") {
|
||||||
|
doc = eg.edit.site[msg.id].doc;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (doc) {
|
||||||
|
const diff_local = Y.encodeStateAsUpdate(doc as any, sv_remote);
|
||||||
|
Y.applyUpdate(doc as any, diff_remote);
|
||||||
|
|
||||||
|
const sendmsg: WS_MSG_DIFF_LOCAL = {
|
||||||
|
type: "diff_local",
|
||||||
|
mode: msg.mode,
|
||||||
|
id: msg.id,
|
||||||
|
diff_local: compress(diff_local.toString()),
|
||||||
|
};
|
||||||
|
ws.send(JSON.stringify(sendmsg));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { Websocket } from "hyper-express";
|
||||||
|
import { eg } from "../edit-global";
|
||||||
|
import { UndoManager } from "yjs";
|
||||||
|
import { WS_MSG_REDO, WS_MSG_UNDO } from "../../../web/src/utils/types/ws";
|
||||||
|
|
||||||
|
export const undo = (ws: Websocket, msg: WS_MSG_UNDO) => {
|
||||||
|
const um = getUndoManager(msg);
|
||||||
|
if (um && um.canUndo()) {
|
||||||
|
um.undo();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const redo = (ws: Websocket, msg: WS_MSG_REDO) => {
|
||||||
|
const um = getUndoManager(msg);
|
||||||
|
if (um && um.canRedo()) {
|
||||||
|
um.redo();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getUndoManager = (msg: WS_MSG_UNDO | WS_MSG_REDO) => {
|
||||||
|
let undoManager = null as null | UndoManager;
|
||||||
|
if (msg.mode === "page") {
|
||||||
|
if (eg.edit.page[msg.id]) {
|
||||||
|
undoManager = eg.edit.page[msg.id].undoManager;
|
||||||
|
}
|
||||||
|
} else if (msg.mode === "site") {
|
||||||
|
if (eg.edit.site[msg.id]) {
|
||||||
|
undoManager = eg.edit.site[msg.id].undoManager;
|
||||||
|
}
|
||||||
|
} else if (msg.mode === "comp") {
|
||||||
|
if (eg.edit.comp[msg.id]) {
|
||||||
|
undoManager = eg.edit.comp[msg.id].undoManager;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return undoManager;
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
import { ServerWebSocket } from "bun";
|
||||||
|
import { component } from "dbgen";
|
||||||
|
import { UndoManager } from "yjs";
|
||||||
|
import { TypedArray, TypedDoc, TypedMap } from "yjs-types";
|
||||||
|
import type { WSData } from "../../../../pkgs/core/server/create";
|
||||||
|
|
||||||
|
import { IItem } from "../../../web/src/utils/types/item";
|
||||||
|
import { IRoot } from "../../../web/src/utils/types/root";
|
||||||
|
import { Site } from "./tools/load-site";
|
||||||
|
import { MPage } from "../../../web/src/utils/types/general";
|
||||||
|
import type { RadixRouter } from "radix3";
|
||||||
|
|
||||||
|
type ArrayElement<ArrayType extends readonly unknown[]> =
|
||||||
|
ArrayType extends readonly (infer ElementType)[] ? ElementType : never;
|
||||||
|
|
||||||
|
export type SingleComp = {
|
||||||
|
id: string;
|
||||||
|
doc: TypedDoc<{
|
||||||
|
map: TypedMap<component & { content_tree: TypedMap<IItem> }>;
|
||||||
|
}>;
|
||||||
|
undoManager: UndoManager;
|
||||||
|
saveTimeout?: ReturnType<typeof setTimeout>;
|
||||||
|
ws: Set<ServerWebSocket<WSData>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const eg = global as unknown as {
|
||||||
|
cache: Record<
|
||||||
|
string,
|
||||||
|
Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
id: string;
|
||||||
|
js: string | null;
|
||||||
|
url: string;
|
||||||
|
js_compiled: string | null;
|
||||||
|
content_tree: IRoot;
|
||||||
|
lastRefresh: number;
|
||||||
|
}
|
||||||
|
>
|
||||||
|
>;
|
||||||
|
router: Record<string, RadixRouter<{ id: string; url: string }>>;
|
||||||
|
edit: {
|
||||||
|
site: Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
id: string;
|
||||||
|
doc: TypedDoc<{
|
||||||
|
site: TypedMap<
|
||||||
|
Site & { page: TypedArray<ArrayElement<Site["page"]>> }
|
||||||
|
>;
|
||||||
|
}>;
|
||||||
|
undoManager: UndoManager;
|
||||||
|
saveTimeout?: ReturnType<typeof setTimeout>;
|
||||||
|
ws: Set<ServerWebSocket<WSData>>;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
comp: Record<string, SingleComp>;
|
||||||
|
page: Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
id: string;
|
||||||
|
doc: MPage;
|
||||||
|
undoManager: UndoManager;
|
||||||
|
saveTimeout?: ReturnType<typeof setTimeout>;
|
||||||
|
ws: Set<ServerWebSocket<WSData>>;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
ws: WeakMap<ServerWebSocket<WSData>, { clientID: string }>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
import { eg } from "../edit-global";
|
||||||
|
|
||||||
|
// page cache timeout, in seconds
|
||||||
|
const PAGE_CACHE_TIMEOUT = 1;
|
||||||
|
|
||||||
|
export const loadCachedPage = async (site_id: string, page_id: string) => {
|
||||||
|
let site_cache = eg.cache[site_id];
|
||||||
|
if (!site_cache) {
|
||||||
|
eg.cache[site_id] = {};
|
||||||
|
site_cache = eg.cache[site_id];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!page_id) return {};
|
||||||
|
|
||||||
|
let cache = site_cache[page_id];
|
||||||
|
if (
|
||||||
|
!cache ||
|
||||||
|
(cache && !cache.lastRefresh) ||
|
||||||
|
(cache && Date.now() - cache.lastRefresh >= 1000 * PAGE_CACHE_TIMEOUT)
|
||||||
|
) {
|
||||||
|
if (eg.edit.page[page_id]) {
|
||||||
|
const edit = eg.edit.page[page_id].doc.getMap("map").toJSON();
|
||||||
|
edit.lastRefresh = Date.now();
|
||||||
|
site_cache[page_id] = edit as any;
|
||||||
|
} else {
|
||||||
|
const page = await db.page.findFirst({
|
||||||
|
where: { id: page_id },
|
||||||
|
select: {
|
||||||
|
js: true,
|
||||||
|
id: true,
|
||||||
|
url: true,
|
||||||
|
updated_at: true,
|
||||||
|
js_compiled: true,
|
||||||
|
content_tree: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (page) {
|
||||||
|
site_cache[page_id] = {
|
||||||
|
...page,
|
||||||
|
lastRefresh: Date.now(),
|
||||||
|
} as any;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cache = site_cache[page_id];
|
||||||
|
}
|
||||||
|
return cache;
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { validate } from "uuid";
|
||||||
|
import { Page } from "../../../../web/src/utils/types/general";
|
||||||
|
|
||||||
|
export const loadPage = async (page_id: string) => {
|
||||||
|
if (page_id && validate(page_id)) {
|
||||||
|
let page = (await db.page.findFirst({
|
||||||
|
where: { id: page_id },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
js: true,
|
||||||
|
name: true,
|
||||||
|
id_site: true,
|
||||||
|
url: true,
|
||||||
|
js_compiled: true,
|
||||||
|
updated_at: true,
|
||||||
|
content_tree: true,
|
||||||
|
},
|
||||||
|
})) as unknown as null | Page;
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
import { page, site } from "dbgen";
|
||||||
|
import { validate as isValidUUID } from "uuid";
|
||||||
|
import * as Y from "yjs";
|
||||||
|
|
||||||
|
export type SiteConfig = {
|
||||||
|
api_url?: string;
|
||||||
|
prasi?: {
|
||||||
|
port: number;
|
||||||
|
dburl: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export type Site = Exclude<Awaited<ReturnType<typeof loadSite>>, null>;
|
||||||
|
|
||||||
|
export const loadSite = async (idOrDomain: string) => {
|
||||||
|
let rname = idOrDomain;
|
||||||
|
if (!rname) {
|
||||||
|
rname = "prasi.app";
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await db.site.findFirst({
|
||||||
|
where: isValidUUID(rname)
|
||||||
|
? {
|
||||||
|
id: rname,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
domain: rname,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
page: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
url: true,
|
||||||
|
updated_at: true,
|
||||||
|
name: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res) {
|
||||||
|
if (!res.page) {
|
||||||
|
res.page = [];
|
||||||
|
}
|
||||||
|
if (res.page.length === 0) {
|
||||||
|
const page = await createPage(res as any, {
|
||||||
|
name: "Home",
|
||||||
|
url: "/",
|
||||||
|
});
|
||||||
|
|
||||||
|
res.page.push(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const p of res.page) {
|
||||||
|
const page = p as any;
|
||||||
|
page.js = new Y.Text();
|
||||||
|
page.js_compiled = new Y.Text();
|
||||||
|
page.content_tree = new Y.Map();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res as
|
||||||
|
| (Omit<site, "config"> & {
|
||||||
|
config?: SiteConfig;
|
||||||
|
page: {
|
||||||
|
id: string;
|
||||||
|
url: string;
|
||||||
|
updated_at: Date | null;
|
||||||
|
name: string;
|
||||||
|
}[];
|
||||||
|
})
|
||||||
|
| null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createPage = async (
|
||||||
|
site: site & { page: page[] },
|
||||||
|
page: WithOptional<
|
||||||
|
Parameters<typeof db.page.create>[0]["data"],
|
||||||
|
"content_tree"
|
||||||
|
>
|
||||||
|
) => {
|
||||||
|
const raw = await db.page.create({
|
||||||
|
data: {
|
||||||
|
...(page as any),
|
||||||
|
content_tree: page.content_tree ? page.content_tree : blank,
|
||||||
|
site: {
|
||||||
|
connect: { id: site.id },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return raw;
|
||||||
|
};
|
||||||
|
|
||||||
|
const blank = { id: "root", type: "root", childs: [] };
|
||||||
|
type WithOptional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
import { Websocket } from "hyper-express";
|
||||||
|
import { syncronize } from "y-pojo";
|
||||||
|
import * as Y from "yjs";
|
||||||
|
import { fillID } from "../../../web/src/utils/page/tools/fill-id";
|
||||||
|
import { IContent, MContent } from "../../../web/src/utils/types/general";
|
||||||
|
import { IItem } from "../../../web/src/utils/types/item";
|
||||||
|
import { IRoot } from "../../../web/src/utils/types/root";
|
||||||
|
import { getComp } from "../action/get-comp";
|
||||||
|
import { eg } from "../edit-global";
|
||||||
|
|
||||||
|
const MAX_STRING_LENGTH = 15000;
|
||||||
|
export const validateTreeMap = async (
|
||||||
|
ws: Websocket,
|
||||||
|
item: MContent,
|
||||||
|
changed?: boolean
|
||||||
|
) => {
|
||||||
|
let _changed = changed;
|
||||||
|
const type = item.get("type") as IContent["type"] | "root";
|
||||||
|
if (type !== "root") {
|
||||||
|
item.forEach((val, key, map) => {
|
||||||
|
if (typeof val === "string") {
|
||||||
|
if (val.length > MAX_STRING_LENGTH) {
|
||||||
|
map.set(key, "");
|
||||||
|
_changed = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (typeof val === "object" && val instanceof Y.Map) {
|
||||||
|
val._map.forEach((ival, ikey, imap) => {
|
||||||
|
if (typeof ival === "string") {
|
||||||
|
if ((ival as string).length > MAX_STRING_LENGTH) {
|
||||||
|
imap.set(ikey, "" as any);
|
||||||
|
_changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item) {
|
||||||
|
if (type !== "text") {
|
||||||
|
const childs = item.get("childs");
|
||||||
|
if (childs) {
|
||||||
|
for (const c of childs) {
|
||||||
|
if (await validateTreeMap(ws, c)) {
|
||||||
|
_changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return !!_changed;
|
||||||
|
};
|
||||||
|
|
@ -1,10 +1,101 @@
|
||||||
|
import { createId } from "@paralleldrive/cuid2";
|
||||||
import { WebSocketHandler } from "bun";
|
import { WebSocketHandler } from "bun";
|
||||||
|
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 { svdiffRemote } from "./edit/action/svdiff-remote";
|
||||||
|
import { redo, undo } from "./edit/action/undo-redo";
|
||||||
|
|
||||||
export const wsHandler: Record<string, WebSocketHandler<{ url: URL }>> = {
|
eg.edit = {
|
||||||
|
site: {},
|
||||||
|
comp: {},
|
||||||
|
page: {},
|
||||||
|
ws: new WeakMap(),
|
||||||
|
};
|
||||||
|
const site = {
|
||||||
|
saveTimeout: null as any,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const wsHandler: Record<string, WebSocketHandler<WSData>> = {
|
||||||
"/edit": {
|
"/edit": {
|
||||||
open(ws) {},
|
open(ws) {
|
||||||
message(ws, message) {},
|
eg.edit.ws.set(ws, {
|
||||||
close(ws, code, reason) {},
|
clientID: createId(),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
async message(ws, raw) {
|
||||||
|
if (typeof raw === "string") {
|
||||||
|
try {
|
||||||
|
const msg = JSON.parse(raw) as WS_MSG;
|
||||||
|
|
||||||
|
if (msg.type === "ping") {
|
||||||
|
ws.send(JSON.stringify({ type: "pong" }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (msg.type) {
|
||||||
|
case "site-js":
|
||||||
|
clearTimeout(site.saveTimeout);
|
||||||
|
site.saveTimeout = setTimeout(async () => {
|
||||||
|
const js = JSON.parse(decompress(msg.src));
|
||||||
|
await db.site.update({
|
||||||
|
where: {
|
||||||
|
id: msg.id_site,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
js: js.src,
|
||||||
|
js_compiled: js.compiled,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
break;
|
||||||
|
case "get_page":
|
||||||
|
await getPage(ws, msg);
|
||||||
|
break;
|
||||||
|
case "get_comp":
|
||||||
|
await getComp(ws, msg);
|
||||||
|
break;
|
||||||
|
case "sv_local":
|
||||||
|
await svLocal(ws, msg);
|
||||||
|
break;
|
||||||
|
case "diff_local":
|
||||||
|
await diffLocal(ws, msg);
|
||||||
|
break;
|
||||||
|
case "svd_remote":
|
||||||
|
await svdiffRemote(ws, msg);
|
||||||
|
break;
|
||||||
|
case "undo":
|
||||||
|
undo(ws, msg);
|
||||||
|
break;
|
||||||
|
case "redo":
|
||||||
|
redo(ws, msg);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
close(ws, code, reason) {
|
||||||
|
eg.edit.ws.delete(ws);
|
||||||
|
|
||||||
|
for (const page of Object.values(eg.edit.page)) {
|
||||||
|
page.ws.delete(ws);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const site of Object.values(eg.edit.site)) {
|
||||||
|
site.ws.delete(ws);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const comp of Object.values(eg.edit.comp)) {
|
||||||
|
comp.ws.delete(ws);
|
||||||
|
}
|
||||||
|
},
|
||||||
drain(ws) {},
|
drain(ws) {},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,5 @@
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"typescript": "^5.0.0"
|
"typescript": "^5.0.0"
|
||||||
},
|
}
|
||||||
"dependencies": { "@node-rs/argon2": "^1.5.2", "@types/mime-types": "^2.1.2", "mime-types": "^2.1.35" }
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,15 @@ import { WebSocketHandler } from "bun";
|
||||||
|
|
||||||
const cache = { static: {} as Record<string, any> };
|
const cache = { static: {} as Record<string, any> };
|
||||||
|
|
||||||
|
export type WSData = { url: URL };
|
||||||
|
|
||||||
export const createServer = async () => {
|
export const createServer = async () => {
|
||||||
g.api = {};
|
g.api = {};
|
||||||
g.router = createRouter({ strictTrailingSlash: true });
|
g.router = createRouter({ strictTrailingSlash: true });
|
||||||
g.server = Bun.serve({
|
g.server = Bun.serve({
|
||||||
port: g.port,
|
port: g.port,
|
||||||
websocket: {
|
websocket: {
|
||||||
|
maxPayloadLength: 99999999,
|
||||||
close(ws, code, reason) {
|
close(ws, code, reason) {
|
||||||
const pathname = ws.data.url.pathname;
|
const pathname = ws.data.url.pathname;
|
||||||
if (wsHandler[pathname]) {
|
if (wsHandler[pathname]) {
|
||||||
|
|
@ -40,7 +43,7 @@ export const createServer = async () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
} as WebSocketHandler<{ url: URL }>,
|
} as WebSocketHandler<WSData>,
|
||||||
async fetch(req, server) {
|
async fetch(req, server) {
|
||||||
if (g.status === "init") return new Response("initializing...");
|
if (g.status === "init") return new Response("initializing...");
|
||||||
const url = new URL(req.url);
|
const url = new URL(req.url);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue