fix loading

This commit is contained in:
Rizky 2023-10-21 16:42:24 +07:00
parent 812802a429
commit 214ffd84a8
25 changed files with 265 additions and 77 deletions

View File

@ -11,6 +11,9 @@
"lmdb": "^2.8.5", "lmdb": "^2.8.5",
"mime-types": "^2.1.35", "mime-types": "^2.1.35",
"msgpackr": "^1.9.9", "msgpackr": "^1.9.9",
"radix3": "^1.1.0" "radix3": "^1.1.0",
"y-pojo": "^0.0.8",
"yjs": "^13.6.8",
"yjs-types": "^0.0.1"
} }
} }

View File

@ -32,7 +32,6 @@ export const svLocal = async (ws: any, msg: WS_MSG_SV_LOCAL) => {
if (doc) { if (doc) {
const diff_remote = Y.encodeStateAsUpdate(doc, changes); const diff_remote = Y.encodeStateAsUpdate(doc, changes);
const sv_remote = Y.encodeStateVector(doc); const sv_remote = Y.encodeStateVector(doc);
const sendmsg: WS_MSG_SVDIFF_REMOTE = { const sendmsg: WS_MSG_SVDIFF_REMOTE = {
diff_remote: diff_remote.toString(), diff_remote: diff_remote.toString(),
sv_remote: sv_remote.toString(), sv_remote: sv_remote.toString(),

View File

@ -1,5 +1,5 @@
import { component, site, page } from "dbgen"; import { component, page } from "dbgen";
import { ESite } from "../../../web/src/render/ed/logic/ed-global"; import { EPage, ESite } from "../../../web/src/render/ed/logic/ed-global";
/* /*
WARNING: WARNING:
@ -15,7 +15,7 @@ export const SyncActions = {
list: async () => list: async () =>
({}) as Record<string, { id: string; name: string; domain: string }>, ({}) as Record<string, { id: string; name: string; domain: string }>,
group: async () => ({}) as Record<string, string[]>, group: async () => ({}) as Record<string, string[]>,
load: async (id: string) => ({}) as ESite | undefined, load: async (id: string) => ({}) as ESite | void,
}, },
comp: { comp: {
list: () => ({}) as Record<string, Exclude<component, "content_tree">>, list: () => ({}) as Record<string, Exclude<component, "content_tree">>,
@ -25,6 +25,6 @@ export const SyncActions = {
page: { page: {
list: (id_site: string) => list: (id_site: string) =>
({}) as Record<string, Exclude<page, "content_tree">>, ({}) as Record<string, Exclude<page, "content_tree">>,
load: (id: string) => ({}) as Uint8Array, load: async (id: string) => ({}) as EPage | void,
}, },
}; };

View File

@ -1,2 +1,3 @@
export * from "./site_load"; export * from "./site_load";
export * from "./site_group"; export * from "./site_group";
export * from "./page_load";

View File

@ -0,0 +1,55 @@
import { syncronize } from "y-pojo";
import { SAction } from "../actions";
import { Y, docs } from "../entity/docs";
import { snapshot } from "../entity/snapshot";
import { ActionCtx } from "../type";
export const page_load: SAction["page"]["load"] = async function (
this: ActionCtx,
id: string
) {
let ss = snapshot.get("page", id);
let ydoc = docs.page[id];
if (!ss || !ydoc) {
const page = await db.page.findFirst({ where: { id } });
if (page) {
const doc = new Y.Doc();
let root = doc.getMap("map");
syncronize(root, { id, root: page.content_tree });
const um = new Y.UndoManager(root, { ignoreRemoteMapChanges: true });
docs.page[id] = {
doc: doc as any,
id,
um,
};
const bin = Y.encodeStateAsUpdate(doc);
snapshot.set({
bin,
id,
type: "page",
name: page.name,
ts: Date.now(),
url: page.url,
});
return {
id: id,
url: page.url,
name: page.name,
snapshot: bin,
};
}
}
if (ss) {
return {
id: ss.id,
url: ss.url,
name: ss.name,
snapshot: ss.bin,
};
}
};

View File

@ -1,4 +1,4 @@
import { user } from "../user"; import { user } from "../entity/user";
export const loadDefaultSite = async (user_id: string) => { export const loadDefaultSite = async (user_id: string) => {
const conf = user.conf.get(user_id); const conf = user.conf.get(user_id);

View File

@ -0,0 +1,24 @@
import * as Y from "yjs";
import { TypedDoc, TypedMap } from "yjs-types";
import { MItem } from "../../../../web/src/utils/types/item";
import { DPage } from "../../../../web/src/utils/types/root";
export * as Y from "yjs";
export const docs = {
page: {} as Record<
string,
{
id: string;
doc: DPage;
um: Y.UndoManager;
}
>,
comp: {} as Record<
string,
{
id: string;
doc: TypedDoc<{ map: TypedMap<{ id: string; item: MItem }> }>;
um: Y.UndoManager;
}
>,
};

View File

@ -0,0 +1,46 @@
import { dir } from "dir";
import { RootDatabase, open } from "lmdb";
import { g } from "utils/global";
const emptySnapshot = {
type: "" as "" | "comp" | "page",
id: "",
bin: new Uint8Array(),
url: "",
name: "",
ts: Date.now(),
};
export type DocSnapshot = typeof emptySnapshot;
export const snapshot = {
_db: null as null | RootDatabase<DocSnapshot>,
init() {
this._db = open<DocSnapshot, string>({
name: "user-conf",
path: dir.path(`${g.datadir}/lmdb/doc-snapshot.lmdb`),
});
return this._db;
},
get db() {
if (!this._db) {
this._db = this.init();
}
return this._db;
},
getOrCreate(data: DocSnapshot) {
const id = `${data.type}-${data.id}`;
let res = this.db.get(id);
if (!res) {
this.db.put(id, structuredClone(emptySnapshot));
res = this.db.get(id);
}
return res as DocSnapshot;
},
get(type: string, id: string) {
return this.db.get(`${type}-${id}`);
},
set(data: DocSnapshot) {
const id = `${data.type}-${data.id}`;
this.db.put(id, data);
},
};

View File

@ -16,13 +16,11 @@ export const user = {
name: "user-conf", name: "user-conf",
path: dir.path(`${g.datadir}/lmdb/user-conf.lmdb`), path: dir.path(`${g.datadir}/lmdb/user-conf.lmdb`),
}); });
return this._db;
}, },
get db() { get db() {
if (!this._db) { if (!this._db) {
this._db = open<UserConf, string>({ this._db = this.init();
name: "user-conf",
path: dir.path(`${g.datadir}/lmdb/user-conf.lmdb`),
});
} }
return this._db; return this._db;
}, },

View File

@ -7,7 +7,7 @@ import { loadDefaultSite } from "./editor/load";
import { ActionCtx, SyncType } from "./type"; import { ActionCtx, SyncType } from "./type";
import { SyncActionPaths } from "./actions-def"; import { SyncActionPaths } from "./actions-def";
import * as actions from "./actions/index"; import * as actions from "./actions/index";
import { UserConf, user } from "./user"; import { UserConf, user } from "./entity/user";
const packr = new Packr({ structuredClone: true }); const packr = new Packr({ structuredClone: true });
const conns = new Map< const conns = new Map<
@ -79,17 +79,23 @@ export const syncHandler: WebSocketHandler<WSData> = {
const code = msg.code as keyof typeof SyncActionPaths; const code = msg.code as keyof typeof SyncActionPaths;
const actionName = SyncActionPaths[code].replace(/\./gi, "_"); const actionName = SyncActionPaths[code].replace(/\./gi, "_");
if (actionName) { if (actionName) {
const action = (actions as any)[actionName].bind({ const baseAction = (actions as any)[actionName];
user: { id: conn.user_id, conf: conn.conf }, if (!baseAction) {
} as ActionCtx); console.log(`app/ws/edit/sync/${actionName}.ts not found}`);
}
if (baseAction) {
const action = baseAction.bind({
user: { id: conn.user_id, conf: conn.conf },
} as ActionCtx);
ws.sendBinary( ws.sendBinary(
packr.pack({ packr.pack({
type: SyncType.ActionResult, type: SyncType.ActionResult,
argid: msg.argid, argid: msg.argid,
val: await action(...msg.args), val: await action(...msg.args),
}) })
); );
}
} }
} }
} }

View File

@ -1,4 +1,4 @@
import { UserConf } from "./user"; import { UserConf } from "./entity/user";
export enum SyncType { export enum SyncType {
ClientID, ClientID,

View File

@ -22,6 +22,7 @@
"esbuild-wasm": "^0.19.4", "esbuild-wasm": "^0.19.4",
"hash-wasm": "^4.10.0", "hash-wasm": "^4.10.0",
"idb-keyval": "^6.2.1", "idb-keyval": "^6.2.1",
"immer": "^10.0.3",
"js-base64": "^3.7.5", "js-base64": "^3.7.5",
"lodash.capitalize": "^4.2.1", "lodash.capitalize": "^4.2.1",
"lodash.concat": "^4.5.0", "lodash.concat": "^4.5.0",

View File

@ -1,8 +1,8 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { page, useGlobal } from "web-utils"; import { page, useGlobal } from "web-utils";
import { Loading } from "../../utils/ui/loading";
import { bootEd } from "../../render/ed/ed";
import { EDGlobal } from "../../render/ed/logic/ed-global"; import { EDGlobal } from "../../render/ed/logic/ed-global";
import { edInitSync } from "../../render/ed/logic/ed-sync";
import { Loading } from "../../utils/ui/loading";
export default page({ export default page({
url: "**", url: "**",
@ -14,7 +14,7 @@ export default page({
location.pathname === "/ed" || location.pathname === "/ed" ||
location.pathname.startsWith("/ed/") location.pathname.startsWith("/ed/")
) { ) {
bootEd(p); edInitSync(p);
} else if (location.pathname.startsWith("/editor")) { } else if (location.pathname.startsWith("/editor")) {
const arr = location.pathname.split("/"); const arr = location.pathname.split("/");
if (arr.length <= 2) { if (arr.length <= 2) {

View File

@ -1,17 +1,19 @@
import { page, useGlobal } from "web-utils"; import { page, useGlobal } from "web-utils";
import { Ed, bootEd } from "../../render/ed/ed"; import { EdBase } from "../../render/ed/ed-base";
import { EDGlobal } from "../../render/ed/logic/ed-global"; import { EDGlobal } from "../../render/ed/logic/ed-global";
import { Loading } from "../../utils/ui/loading"; import { Loading } from "../../utils/ui/loading";
import { initSync } from "wasm-gzip";
import { edInitSync } from "../../render/ed/logic/ed-sync";
export default page({ export default page({
url: "/ed/:site_id/:page_id", url: "/ed/:site_id/:page_id",
component: ({}) => { component: ({}) => {
const p = useGlobal(EDGlobal, "EDITOR"); const p = useGlobal(EDGlobal, "EDITOR");
if (!bootEd(p)) { if (!edInitSync(p)) {
return <Loading note="booting editor" />; return <Loading note="init sync" />;
} }
return <Ed />; return <EdBase />;
}, },
}); });

View File

@ -4,6 +4,9 @@ import { Root } from "./base/root";
import "./index.css"; import "./index.css";
import { createAPI, createDB, reloadDBAPI } from "./utils/script/init-api"; import { createAPI, createDB, reloadDBAPI } from "./utils/script/init-api";
import { w } from "./utils/types/general"; import { w } from "./utils/types/general";
import * as Y from "yjs";
(window as any).Y = Y;
const start = async () => { const start = async () => {
const base = `${location.protocol}//${location.host}`; const base = `${location.protocol}//${location.host}`;

View File

@ -0,0 +1,26 @@
import { useGlobal } from "web-utils";
import { Loading } from "../../utils/ui/loading";
import { EDGlobal } from "./logic/ed-global";
import { edRoute } from "./logic/ed-route";
export const EdBase = () => {
const p = useGlobal(EDGlobal, "EDITOR");
if (p.status === "init") {
p.status = "ready";
}
edRoute(p);
if (p.status === "loading") {
return <Loading note="loading-page" />;
}
if (p.status === "site-not-found" || p.status === "page-not-found") {
return (
<div className="flex fixed inset-0 items-center justify-center">
{p.status === "site-not-found" ? "Site not found" : "Page not found"}
</div>
);
}
return <div>Editor</div>;
};

View File

@ -1,4 +1,6 @@
import { clientStartSync } from "../../../utils/sync/client"; import { clientStartSync } from "../../../utils/sync/client";
import { IItem } from "../../../utils/types/item";
import { DPage, IRoot } from "../../../utils/types/root";
const EmptySite = { const EmptySite = {
id: "", id: "",
@ -9,8 +11,13 @@ const EmptySite = {
config: { api_url: "" }, config: { api_url: "" },
}; };
export type ESite = typeof EmptySite; export type ESite = typeof EmptySite;
export type EPage = typeof EmptyPage;
const EmptyPage = { const EmptyPage = {
id: "", id: "",
name: "",
url: "",
snapshot: null as null | Uint8Array,
}; };
export const EDGlobal = { export const EDGlobal = {
@ -22,7 +29,11 @@ export const EDGlobal = {
| "ready", | "ready",
sync: null as unknown as Awaited<ReturnType<typeof clientStartSync>>, sync: null as unknown as Awaited<ReturnType<typeof clientStartSync>>,
site: EmptySite, site: EmptySite,
page: EmptyPage, page: {
current: EmptyPage,
doc: null as null | DPage,
root: null as null | IRoot,
},
}; };
export type PG = typeof EDGlobal & { render: () => void }; export type PG = typeof EDGlobal & { render: () => void };

View File

@ -1,7 +1,8 @@
import { IRoot } from "../../../utils/types/root";
import { PG } from "./ed-global"; import { PG } from "./ed-global";
import { produce } from "immer";
export const edRoute = async (p: PG) => { export const edRoute = async (p: PG) => {
if (p.status === "init") { if (p.status === "ready") {
if (!p.site.domain && !p.site.name) { if (!p.site.domain && !p.site.name) {
p.status = "loading"; p.status = "loading";
const site = await p.sync.site.load(p.site.id); const site = await p.sync.site.load(p.site.id);
@ -14,8 +15,31 @@ export const edRoute = async (p: PG) => {
p.site = site; p.site = site;
} }
if (p.site) { if (p.page.current.id !== params.page_id || !p.page.current.snapshot) {
console.log(p.site); p.status = "loading";
const page = await p.sync.page.load(params.page_id);
if (!page) {
p.status = "page-not-found";
p.render();
return;
}
p.page.current = page;
if (page.snapshot) {
const doc = new Y.Doc();
Y.applyUpdate(doc, page.snapshot);
p.page.doc = doc as any;
if (p.page.doc) {
const root = p.page.doc.getMap("map").get("root")?.toJSON() as IRoot;
if (root) {
p.page.root = produce(root, () => {});
}
}
}
p.status = "ready";
p.render();
} }
} }
}; };

View File

@ -1,28 +1,8 @@
import { useGlobal } from "web-utils"; import { clientStartSync } from "../../../utils/sync/client";
import { Loading } from "../../utils/ui/loading"; import { Loading } from "../../../utils/ui/loading";
import { EDGlobal, PG } from "./logic/ed-global"; import { PG } from "./ed-global";
import { edRoute } from "./logic/ed-route";
import { clientStartSync } from "../../utils/sync/client";
export const Ed = () => { export const edInitSync = (p: PG) => {
const p = useGlobal(EDGlobal, "EDITOR");
edRoute(p);
if (p.status === "loading") {
return <Loading />;
}
if (p.status === "site-not-found" || p.status === "page-not-found") {
return (
<div className="flex fixed inset-0 items-center justify-center">
{p.status === "site-not-found" ? "Site not found" : "Page not found"}
</div>
);
}
return <div>asfa</div>;
};
export const bootEd = (p: PG) => {
const session = JSON.parse( const session = JSON.parse(
localStorage.getItem("prasi-session") || "null" localStorage.getItem("prasi-session") || "null"
) as { data: { user: { id: string } } }; ) as { data: { user: { id: string } } };

View File

@ -0,0 +1,5 @@
import { IItem } from "../../../../utils/types/item";
import { ISection } from "../../../../utils/types/section";
import { PG } from "../ed-global";
export const treeRebuild = (items: (ISection | IItem)[]) => {};

View File

@ -4,7 +4,10 @@ import { UseStore, get, set } from "idb-keyval";
import { Packr } from "msgpackr"; import { Packr } from "msgpackr";
import { stringify } from "safe-stable-stringify"; import { stringify } from "safe-stable-stringify";
import { SyncActions } from "../../../../srv/ws/sync/actions"; import { SyncActions } from "../../../../srv/ws/sync/actions";
import { SyncActionDefinition } from "../../../../srv/ws/sync/actions-def"; import {
SyncActionDefinition,
SyncActionPaths,
} from "../../../../srv/ws/sync/actions-def";
import { initIDB } from "./idb"; import { initIDB } from "./idb";
import { SyncType } from "../../../../srv/ws/sync/type"; import { SyncType } from "../../../../srv/ws/sync/type";
import { w } from "../types/general"; import { w } from "../types/general";
@ -48,31 +51,23 @@ export const clientStartSync = async (arg: {
const { user_id, events } = arg; const { user_id, events } = arg;
conf.idb = initIDB(user_id); conf.idb = initIDB(user_id);
await connect(user_id, events); await connect(user_id, events);
const path: any[] = [];
return new DeepProxy( return new DeepProxy(
SyncActionDefinition, SyncActionDefinition,
({ trapName, value, key, DEFAULT, PROXY }) => { ({ target, trapName, value, key, DEFAULT, PROXY }) => {
if (trapName === "set") { if (trapName === "set") {
throw new TypeError("target is immutable"); throw new TypeError("target is immutable");
} }
path.push(key);
if (typeof value === "string") { if (typeof value === "string") {
for (let i = 0; i < path.length; i++) { return (...args: any[]) => {
if (path[i] !== "then") { return new Promise((resolve) => {
path.splice(0, i);
break;
}
}
return (...args: any[]) =>
new Promise((resolve) => {
doAction({ doAction({
path: path.join("."),
code: value, code: value,
resolve, resolve,
args, args,
}); });
}); });
};
} }
if (trapName === "get") { if (trapName === "get") {
@ -168,16 +163,16 @@ const loadEventOffline = async (name: ClientEvent) => {
}; };
const doAction = async <T>(arg: { const doAction = async <T>(arg: {
path: string;
code: string; code: string;
resolve: (value: any) => void; resolve: (value: any) => void;
args: any[]; args: any[];
}) => { }) => {
const { path, args, code, resolve } = arg; const { args, code, resolve } = arg;
const ws = conf.ws; const ws = conf.ws;
const idb = conf.idb; const idb = conf.idb;
if (idb) { if (idb) {
const sargs = stringify(args); const sargs = stringify(args);
const path = (SyncActionPaths as any)[code];
const argid = await xxhash32(`op-${path}-${sargs}`); const argid = await xxhash32(`op-${path}-${sargs}`);
if (ws && ws.readyState === ws.OPEN) { if (ws && ws.readyState === ws.OPEN) {
@ -186,6 +181,7 @@ const doAction = async <T>(arg: {
ts: Date.now(), ts: Date.now(),
resolve, resolve,
}; };
ws.send(packr.pack({ type: SyncType.Action, code, args, argid })); ws.send(packr.pack({ type: SyncType.Action, code, args, argid }));
} else { } else {
// offline // offline

View File

@ -1,4 +1,4 @@
import { TypedArray, TypedMap } from "yjs-types"; import { TypedArray, TypedDoc, TypedMap } from "yjs-types";
import { ISection } from "./section"; import { ISection } from "./section";
export type IRoot = { export type IRoot = {
@ -12,3 +12,5 @@ export type MRoot = TypedMap<{
type: "root"; type: "root";
childs: TypedArray<ISection>; childs: TypedArray<ISection>;
}>; }>;
export type DPage = TypedDoc<{ map: TypedMap<{ id: string; root: MRoot }> }>;

BIN
bun.lockb

Binary file not shown.

View File

@ -9,7 +9,8 @@ import { g } from "./utils/global";
import { createLogger } from "./utils/logger"; import { createLogger } from "./utils/logger";
import { preparePrisma } from "./utils/prisma"; import { preparePrisma } from "./utils/prisma";
import { syncActionDefinition } from "utils/sync-def"; import { syncActionDefinition } from "utils/sync-def";
import { user } from "../../app/srv/ws/sync/user"; import { user } from "../../app/srv/ws/sync/entity/user";
import { snapshot } from "../../app/srv/ws/sync/entity/snapshot";
g.status = "init"; g.status = "init";
@ -24,7 +25,10 @@ if (g.mode === "dev") {
await startDevWatcher(); await startDevWatcher();
} }
/** init lmdb */
user.conf.init(); user.conf.init();
snapshot.init();
await preparePrisma(); await preparePrisma();
await ensureNotRunning(); await ensureNotRunning();
@ -37,8 +41,8 @@ if (g.db) {
await syncActionDefinition(); await syncActionDefinition();
await generateAPIFrm(); await generateAPIFrm();
await prepareApiRoutes(); await prepareApiRoutes();
await createServer();
await prepareAPITypes(); await prepareAPITypes();
await parcelBuild(); await parcelBuild();
await createServer();
g.status = "ready"; g.status = "ready";

View File

@ -1,5 +1,6 @@
import goober from "goober"; import goober from "goober";
import type { PrismaClient } from "../../../app/db/db"; import type { PrismaClient } from "../../../app/db/db";
import * as Yjs from "yjs";
declare global { declare global {
const navigate: (path: string) => void; const navigate: (path: string) => void;
const params: any; const params: any;
@ -9,5 +10,6 @@ declare global {
const db: PrismaClient; const db: PrismaClient;
const prasiContext: any; const prasiContext: any;
const serverurl: string; const serverurl: string;
const Y: typeof Yjs;
} }
export {}; export {};