fix
This commit is contained in:
parent
0170eeeadc
commit
2fddcf1a78
|
|
@ -0,0 +1,26 @@
|
||||||
|
export const SyncActionDefinition = {
|
||||||
|
"site": {
|
||||||
|
"all": "0",
|
||||||
|
"group": "1",
|
||||||
|
"load": "2"
|
||||||
|
},
|
||||||
|
"comp": {
|
||||||
|
"all": "3",
|
||||||
|
"group": "4",
|
||||||
|
"doc": "5"
|
||||||
|
},
|
||||||
|
"page": {
|
||||||
|
"all": "6",
|
||||||
|
"load": "7"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
export const SyncActionPaths = {
|
||||||
|
"0": "site.all",
|
||||||
|
"1": "site.group",
|
||||||
|
"2": "site.load",
|
||||||
|
"3": "comp.all",
|
||||||
|
"4": "comp.group",
|
||||||
|
"5": "comp.doc",
|
||||||
|
"6": "page.all",
|
||||||
|
"7": "page.load"
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { component, site, page } from "dbgen";
|
||||||
|
|
||||||
|
export const SyncActions = {
|
||||||
|
site: {
|
||||||
|
all: () =>
|
||||||
|
({}) as Promise<
|
||||||
|
Record<string, { id: string; name: string; domain: string }>
|
||||||
|
>,
|
||||||
|
group: () => ({}) as Promise<Record<string, string[]>>,
|
||||||
|
load: (id: string) => ({}) as Promise<site>,
|
||||||
|
},
|
||||||
|
comp: {
|
||||||
|
all: () => ({}) as Record<string, Exclude<component, "content_tree">>,
|
||||||
|
group: () => ({}) as Record<string, string[]>,
|
||||||
|
doc: (id: string) => ({}) as Uint8Array,
|
||||||
|
},
|
||||||
|
page: {
|
||||||
|
all: (id_site: string) =>
|
||||||
|
({}) as Record<string, Exclude<page, "content_tree">>,
|
||||||
|
load: (id: string) => ({}) as Uint8Array,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
import { ServerWebSocket, WebSocketHandler } from "bun";
|
|
||||||
import { WSData } from "../../../../pkgs/core/server/create";
|
|
||||||
import { Packr } from "msgpackr";
|
|
||||||
import { createId } from "@paralleldrive/cuid2";
|
import { createId } from "@paralleldrive/cuid2";
|
||||||
import { MSG_TO_SERVER } from "./type";
|
import { ServerWebSocket, WebSocketHandler } from "bun";
|
||||||
|
import { Packr } from "msgpackr";
|
||||||
|
import { WSData } from "../../../../pkgs/core/server/create";
|
||||||
const packr = new Packr({ structuredClone: true });
|
const packr = new Packr({ structuredClone: true });
|
||||||
|
|
||||||
const conns = new Map<
|
const conns = new Map<
|
||||||
string,
|
string,
|
||||||
{
|
{
|
||||||
|
user_id: string;
|
||||||
ws: ServerWebSocket<WSData>;
|
ws: ServerWebSocket<WSData>;
|
||||||
msg: {
|
msg: {
|
||||||
pending: Record<string, Promise<any>>;
|
pending: Record<string, Promise<any>>;
|
||||||
|
|
@ -18,21 +18,31 @@ const conns = new Map<
|
||||||
const wconns = new WeakMap<ServerWebSocket<WSData>, string>();
|
const wconns = new WeakMap<ServerWebSocket<WSData>, string>();
|
||||||
export const syncHandler: WebSocketHandler<WSData> = {
|
export const syncHandler: WebSocketHandler<WSData> = {
|
||||||
open(ws) {
|
open(ws) {
|
||||||
const id = createId();
|
const client_id = createId();
|
||||||
conns.set(id, { ws, msg: { pending: {}, resolve: {} } });
|
conns.set(client_id, {
|
||||||
wconns.set(ws, id);
|
user_id: "",
|
||||||
ws.sendBinary(packr.pack({ type: "identify", id }));
|
ws,
|
||||||
|
msg: { pending: {}, resolve: {} },
|
||||||
|
});
|
||||||
|
wconns.set(ws, client_id);
|
||||||
|
ws.sendBinary(packr.pack({ type: "client_id", client_id }));
|
||||||
|
},
|
||||||
|
close(ws, code, reason) {
|
||||||
|
const conn_id = wconns.get(ws);
|
||||||
|
if (conn_id) {
|
||||||
|
conns.delete(conn_id);
|
||||||
|
wconns.delete(ws);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
message(ws, raw) {
|
message(ws, raw) {
|
||||||
const conn_id = wconns.get(ws);
|
const conn_id = wconns.get(ws);
|
||||||
if (conn_id) {
|
if (conn_id) {
|
||||||
const conn = conns.get(conn_id);
|
const conn = conns.get(conn_id);
|
||||||
if (conn) {
|
if (conn) {
|
||||||
const msg = packr.unpack(Buffer.from(raw)) as MSG_TO_SERVER & {
|
const msg = packr.unpack(Buffer.from(raw));
|
||||||
msg_client_id: string;
|
if (msg.type === "user_id") {
|
||||||
};
|
const { user_id } = msg;
|
||||||
|
conn.user_id = user_id;
|
||||||
switch (msg.action) {
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
export enum DType {
|
|
||||||
Site,
|
|
||||||
Comp,
|
|
||||||
Page,
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum ServerAction {
|
|
||||||
Load,
|
|
||||||
}
|
|
||||||
|
|
||||||
export type MSG_TO_SERVER = {
|
|
||||||
action: ServerAction.Load;
|
|
||||||
type: DType;
|
|
||||||
id: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export enum ClientAction {
|
|
||||||
Identify,
|
|
||||||
}
|
|
||||||
export type MSG_TO_CLIENT = {
|
|
||||||
action: ClientAction.Identify;
|
|
||||||
id: string;
|
|
||||||
};
|
|
||||||
|
|
@ -13,13 +13,14 @@
|
||||||
"@paralleldrive/cuid2": "2.2.2",
|
"@paralleldrive/cuid2": "2.2.2",
|
||||||
"@parcel/packager-wasm": "^2.10.0",
|
"@parcel/packager-wasm": "^2.10.0",
|
||||||
"@parcel/service-worker": "^2.10.0",
|
"@parcel/service-worker": "^2.10.0",
|
||||||
"msgpackr": "^1.9.9",
|
"@qiwi/deep-proxy": "^2.0.3",
|
||||||
"@swc/wasm-web": "1.3.94-nightly-20231014.1",
|
"@swc/wasm-web": "1.3.94-nightly-20231014.1",
|
||||||
"algoliasearch": "^4.20.0",
|
"algoliasearch": "^4.20.0",
|
||||||
"date-fns": "^2.30.0",
|
"date-fns": "^2.30.0",
|
||||||
"dbgen": "workspace:*",
|
"dbgen": "workspace:*",
|
||||||
"downshift": "^8.2.2",
|
"downshift": "^8.2.2",
|
||||||
"esbuild-wasm": "^0.19.4",
|
"esbuild-wasm": "^0.19.4",
|
||||||
|
"hash-wasm": "^4.10.0",
|
||||||
"idb-keyval": "^6.2.1",
|
"idb-keyval": "^6.2.1",
|
||||||
"js-base64": "^3.7.5",
|
"js-base64": "^3.7.5",
|
||||||
"lodash.capitalize": "^4.2.1",
|
"lodash.capitalize": "^4.2.1",
|
||||||
|
|
@ -38,6 +39,7 @@
|
||||||
"lodash.uniq": "^4.5.0",
|
"lodash.uniq": "^4.5.0",
|
||||||
"lodash.uniqby": "^4.7.0",
|
"lodash.uniqby": "^4.7.0",
|
||||||
"monaco-jsx-syntax-highlight-v2": "^1.2.2",
|
"monaco-jsx-syntax-highlight-v2": "^1.2.2",
|
||||||
|
"msgpackr": "^1.9.9",
|
||||||
"polywasm": "^0.1.4",
|
"polywasm": "^0.1.4",
|
||||||
"prettier": "3.0.3",
|
"prettier": "3.0.3",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
|
|
@ -50,6 +52,7 @@
|
||||||
"react-is": "^18.2.0",
|
"react-is": "^18.2.0",
|
||||||
"react-use-error-boundary": "^3.0.0",
|
"react-use-error-boundary": "^3.0.0",
|
||||||
"react-virtuoso": "^4.6.1",
|
"react-virtuoso": "^4.6.1",
|
||||||
|
"safe-stable-stringify": "^2.4.3",
|
||||||
"svgo": "^3.0.2",
|
"svgo": "^3.0.2",
|
||||||
"textdiff-create": "^1.1.9",
|
"textdiff-create": "^1.1.9",
|
||||||
"tinycolor2": "^1.6.0",
|
"tinycolor2": "^1.6.0",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { page, useGlobal } from "web-utils";
|
||||||
|
import { EditorGlobal } from "../../render/editor/logic/global";
|
||||||
|
import { Loading } from "../../utils/ui/loading";
|
||||||
|
import { clientStartSync } from "../../utils/sync/client";
|
||||||
|
|
||||||
|
export default page({
|
||||||
|
url: "/ned/:site_id/:page_id",
|
||||||
|
component: ({}) => {
|
||||||
|
const p = useGlobal(EditorGlobal, "EDITOR");
|
||||||
|
|
||||||
|
const session = JSON.parse(
|
||||||
|
localStorage.getItem("prasi-session") || "null"
|
||||||
|
) as { data: { user: { id: string } } };
|
||||||
|
if (!session) {
|
||||||
|
navigate("/login");
|
||||||
|
return <Loading />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!p.sync) {
|
||||||
|
// p.sync = clientStartSync({
|
||||||
|
// user_id: session.data.user.id,
|
||||||
|
// });
|
||||||
|
|
||||||
|
return <Loading />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div></div>;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -26,3 +26,7 @@ export const live = {
|
||||||
url: "/live/:domain/**",
|
url: "/live/:domain/**",
|
||||||
page: () => import("./page/live"),
|
page: () => import("./page/live"),
|
||||||
};
|
};
|
||||||
|
export const ned = {
|
||||||
|
url: "/ned/:site_id/:page_id",
|
||||||
|
page: () => import("./page/ned"),
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { createRoot } from "react-dom/client";
|
import { Root as ReactRoot, createRoot } from "react-dom/client";
|
||||||
import { defineReact, defineWindow } from "web-utils";
|
import { defineReact, defineWindow } from "web-utils";
|
||||||
import { Root } from "./base/root";
|
import { Root } from "./base/root";
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
|
|
@ -6,35 +6,107 @@ import { createAPI, createDB, reloadDBAPI } from "./utils/script/init-api";
|
||||||
import { w } from "./utils/types/general";
|
import { w } from "./utils/types/general";
|
||||||
|
|
||||||
const start = async () => {
|
const start = async () => {
|
||||||
|
const base = `${location.protocol}//${location.host}`;
|
||||||
|
let react = {
|
||||||
|
root: null as null | ReactRoot,
|
||||||
|
};
|
||||||
if (!["localhost", "127.0.0.1"].includes(location.hostname)) {
|
if (!["localhost", "127.0.0.1"].includes(location.hostname)) {
|
||||||
const sw = await registerServiceWorker();
|
const sw = await registerServiceWorker();
|
||||||
navigator.serviceWorker.addEventListener("message", (e) => {
|
navigator.serviceWorker.addEventListener("message", (e) => {
|
||||||
if (e.data.type === "activated") {
|
if (react.root) {
|
||||||
if (e.data.shouldRefresh && sw) {
|
if (e.data.type === "offline") {
|
||||||
sw.unregister().then(() => {
|
w.offline = true;
|
||||||
window.location.reload();
|
const click = () => {
|
||||||
});
|
if (react.root) react.root.render(<Root />);
|
||||||
}
|
};
|
||||||
}
|
setTimeout(click, 5000);
|
||||||
if (e.data.type === "ready") {
|
react.root.render(
|
||||||
const sw = navigator.serviceWorker.controller;
|
<>
|
||||||
|
<Root />
|
||||||
if (sw) {
|
<div
|
||||||
const routes = Object.entries(w.prasiApi[base].apiEntry).map(
|
className={cx(
|
||||||
([k, v]: any) => ({
|
css`
|
||||||
url: v.url,
|
position: fixed;
|
||||||
name: k,
|
bottom: 20px;
|
||||||
})
|
left: 0px;
|
||||||
|
right: 0px;
|
||||||
|
z-index: 999;
|
||||||
|
`,
|
||||||
|
"flex justify-center cursor-pointer"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="bg-orange-500 text-white px-4 py-2 rounded-full text-sm"
|
||||||
|
onClick={click}
|
||||||
|
>
|
||||||
|
Network Failed: Offline Mode
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
sw.postMessage({
|
if (e.data.type === "activated") {
|
||||||
type: "add-cache",
|
if (e.data.shouldRefresh && sw) {
|
||||||
url: location.href,
|
react.root.render(
|
||||||
});
|
<>
|
||||||
sw.postMessage({
|
<Root />
|
||||||
type: "define-route",
|
<div
|
||||||
routes,
|
className={cx(
|
||||||
});
|
css`
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20px;
|
||||||
|
left: 0px;
|
||||||
|
right: 0px;
|
||||||
|
z-index: 999;
|
||||||
|
`,
|
||||||
|
"flex justify-center"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="bg-blue-400 text-white px-4 py-2 rounded-full text-sm">
|
||||||
|
Updating App...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
sw.unregister().then(() => {
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const localVersion = localStorage.getItem("prasi-version");
|
||||||
|
if (localVersion !== e.data.version) {
|
||||||
|
localStorage.setItem("prasi-version", e.data.version);
|
||||||
|
const click = () => {
|
||||||
|
if (react.root) react.root.render(<Root />);
|
||||||
|
};
|
||||||
|
setTimeout(click, 5000);
|
||||||
|
react.root.render(
|
||||||
|
<>
|
||||||
|
<Root />
|
||||||
|
<div
|
||||||
|
className={cx(
|
||||||
|
css`
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20px;
|
||||||
|
left: 0px;
|
||||||
|
right: 0px;
|
||||||
|
z-index: 999;
|
||||||
|
`,
|
||||||
|
"flex justify-center cursor-pointer"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="bg-green-600 text-white px-4 py-2 rounded-full text-sm"
|
||||||
|
onClick={click}
|
||||||
|
>
|
||||||
|
App Updated, Ready to use offline
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -48,15 +120,36 @@ const start = async () => {
|
||||||
|
|
||||||
defineReact();
|
defineReact();
|
||||||
await defineWindow(false);
|
await defineWindow(false);
|
||||||
const base = `${location.protocol}//${location.host}`;
|
|
||||||
w.serverurl = base;
|
w.serverurl = base;
|
||||||
await reloadDBAPI(base, "prod");
|
await reloadDBAPI(base, "prod");
|
||||||
|
|
||||||
|
const swc = navigator.serviceWorker.controller;
|
||||||
|
if (swc) {
|
||||||
|
swc.postMessage({
|
||||||
|
type: "add-cache",
|
||||||
|
url: location.href,
|
||||||
|
});
|
||||||
|
if (w.prasiApi && w.prasiApi[base] && w.prasiApi[base].apiEntry) {
|
||||||
|
const routes = Object.entries(w.prasiApi[base].apiEntry).map(
|
||||||
|
([k, v]: any) => ({
|
||||||
|
url: v.url,
|
||||||
|
name: k,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
swc.postMessage({
|
||||||
|
type: "define-route",
|
||||||
|
routes,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
w.api = createAPI(base);
|
w.api = createAPI(base);
|
||||||
w.db = createDB(base);
|
w.db = createDB(base);
|
||||||
|
|
||||||
const el = document.getElementById("root");
|
const el = document.getElementById("root");
|
||||||
if (el) {
|
if (el) {
|
||||||
createRoot(el).render(<Root />);
|
react.root = createRoot(el);
|
||||||
|
react.root.render(<Root />);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import { IRoot } from "../../../utils/types/root";
|
||||||
import { LSite } from "../../live/logic/global";
|
import { LSite } from "../../live/logic/global";
|
||||||
import { ISection } from "../../../utils/types/section";
|
import { ISection } from "../../../utils/types/section";
|
||||||
import { IText } from "../../../utils/types/text";
|
import { IText } from "../../../utils/types/text";
|
||||||
|
import { clientStartSync } from "../../../utils/sync/client";
|
||||||
|
|
||||||
export type WithRequired<T, K extends keyof T> = T & { [P in K]-?: T[P] };
|
export type WithRequired<T, K extends keyof T> = T & { [P in K]-?: T[P] };
|
||||||
export type NodeMeta = { meta: ItemMeta; idx: number };
|
export type NodeMeta = { meta: ItemMeta; idx: number };
|
||||||
|
|
@ -40,6 +41,7 @@ export type ItemMeta = {
|
||||||
export const EditorGlobal = {
|
export const EditorGlobal = {
|
||||||
/** ui */
|
/** ui */
|
||||||
mode: "" as "desktop" | "mobile",
|
mode: "" as "desktop" | "mobile",
|
||||||
|
sync: null as unknown as ReturnType<typeof clientStartSync>,
|
||||||
status: "init" as
|
status: "init" as
|
||||||
| "init"
|
| "init"
|
||||||
| "loading"
|
| "loading"
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { UseStore, del, getMany, keys } from "idb-keyval";
|
import { UseStore, getMany, keys } from "idb-keyval";
|
||||||
import { useGlobal, useLocal } from "web-utils";
|
import { useGlobal, useLocal } from "web-utils";
|
||||||
import { EditorGlobal } from "../../../logic/global";
|
|
||||||
import { Tooltip } from "../../../../../utils/ui/tooltip";
|
import { Tooltip } from "../../../../../utils/ui/tooltip";
|
||||||
|
import { EditorGlobal } from "../../../logic/global";
|
||||||
|
|
||||||
export const MonacoElHistory = ({
|
export const MonacoElHistory = ({
|
||||||
store,
|
store,
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { manifest, version } from "@parcel/service-worker";
|
||||||
import { RadixRouter, createRouter } from "radix3";
|
import { RadixRouter, createRouter } from "radix3";
|
||||||
const g = {
|
const g = {
|
||||||
router: null as null | RadixRouter<any>,
|
router: null as null | RadixRouter<any>,
|
||||||
|
offline: false,
|
||||||
broadcast(msg: any) {
|
broadcast(msg: any) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const c: Clients = self.clients;
|
const c: Clients = self.clients;
|
||||||
|
|
@ -22,17 +23,19 @@ addEventListener("install", (e) => (e as ExtendableEvent).waitUntil(install()));
|
||||||
|
|
||||||
async function activate() {
|
async function activate() {
|
||||||
let shouldRefresh = false;
|
let shouldRefresh = false;
|
||||||
const keys = await caches.keys();
|
if (!g.offline) {
|
||||||
await Promise.all(
|
const keys = await caches.keys();
|
||||||
keys.map(async (key) => {
|
await Promise.all(
|
||||||
if (key !== version) {
|
keys.map(async (key) => {
|
||||||
await caches.delete(key);
|
if (key !== version) {
|
||||||
shouldRefresh = true;
|
await caches.delete(key);
|
||||||
}
|
shouldRefresh = true;
|
||||||
})
|
}
|
||||||
);
|
})
|
||||||
|
);
|
||||||
|
|
||||||
g.broadcast({ type: "activated", shouldRefresh });
|
g.broadcast({ type: "activated", shouldRefresh, version });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
addEventListener("activate", (e) =>
|
addEventListener("activate", (e) =>
|
||||||
(e as ExtendableEvent).waitUntil(activate())
|
(e as ExtendableEvent).waitUntil(activate())
|
||||||
|
|
@ -56,19 +59,29 @@ addEventListener("fetch", async (evt) => {
|
||||||
if (r) {
|
if (r) {
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
return fetch(e.request);
|
|
||||||
|
try {
|
||||||
|
g.offline = false;
|
||||||
|
return await fetch(e.request);
|
||||||
|
} catch (e) {
|
||||||
|
g.offline = true;
|
||||||
|
g.broadcast({ type: "offline" });
|
||||||
|
return new Response();
|
||||||
|
}
|
||||||
})()
|
})()
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
g.broadcast({ type: "ready" });
|
|
||||||
addEventListener("message", async (e) => {
|
addEventListener("message", async (e) => {
|
||||||
const type = e.data.type;
|
const type = e.data.type;
|
||||||
const cache = await caches.open(version);
|
const cache = await caches.open(version);
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "add-cache":
|
case "add-cache":
|
||||||
if (!(await cache.match(e.data.url))) {
|
{
|
||||||
await cache.add(e.data.url);
|
const cached = await cache.match(e.data.url);
|
||||||
|
if (!cached) {
|
||||||
|
await cache.add(e.data.url);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "define-route":
|
case "define-route":
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,6 @@ export const initApi = async (config: any, mode: "dev" | "prod" = "dev") => {
|
||||||
if (url) {
|
if (url) {
|
||||||
if (!w.prasiApi[url]) {
|
if (!w.prasiApi[url]) {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
await reloadDBAPI(url, mode);
|
await reloadDBAPI(url, mode);
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
|
|
@ -137,12 +136,7 @@ export const reloadDBAPI = async (
|
||||||
const found = await get(url, cache);
|
const found = await get(url, cache);
|
||||||
if (found) {
|
if (found) {
|
||||||
w.prasiApi[url] = JSON.parse(found);
|
w.prasiApi[url] = JSON.parse(found);
|
||||||
forceReload().catch(() => {
|
forceReload();
|
||||||
if (url === prasiBase) {
|
|
||||||
console.error("Failed to load prasi. Reloading...");
|
|
||||||
setTimeout(() => location.reload(), 3000);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
await forceReload();
|
await forceReload();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,91 +1,106 @@
|
||||||
|
import { DeepProxy } from "@qiwi/deep-proxy";
|
||||||
|
import { xxhash32 } from "hash-wasm";
|
||||||
|
import { UseStore, get } from "idb-keyval";
|
||||||
import { Packr } from "msgpackr";
|
import { Packr } from "msgpackr";
|
||||||
import {
|
import { stringify } from "safe-stable-stringify";
|
||||||
ClientAction,
|
import { SyncActions } from "../../../../srv/ws/sync/actions";
|
||||||
MSG_TO_CLIENT,
|
import { SyncActionDefinition } from "../../../../srv/ws/sync/actions-def";
|
||||||
MSG_TO_SERVER,
|
import { initIDB } from "./idb";
|
||||||
ServerAction,
|
|
||||||
} from "../../../../srv/ws/sync/type";
|
|
||||||
import { SyncSite } from "./site";
|
|
||||||
import { createId } from "@paralleldrive/cuid2";
|
|
||||||
const packr = new Packr({ structuredClone: true });
|
const packr = new Packr({ structuredClone: true });
|
||||||
|
const conf = {
|
||||||
|
ws: null as null | WebSocket,
|
||||||
|
client_id: "",
|
||||||
|
idb: null as null | UseStore,
|
||||||
|
};
|
||||||
|
|
||||||
export class SyncClient {
|
type User = {
|
||||||
private id = "";
|
id: string;
|
||||||
private ws: WebSocket;
|
name: string;
|
||||||
private wsPending?: Promise<void>;
|
};
|
||||||
public connected = false;
|
|
||||||
public loaded = {
|
export const clientStartSync = async (arg: {
|
||||||
site: new Map<string, SyncSite>(),
|
user_id: string;
|
||||||
|
events: {
|
||||||
|
site_open: (arg: { site_id: string; user: User }) => void;
|
||||||
};
|
};
|
||||||
|
}) => {
|
||||||
public site = {
|
const { user_id, events } = arg;
|
||||||
load: async (id: string) => {
|
conf.idb = initIDB(user_id);
|
||||||
this.loaded.site.set(id, new SyncSite(this, id));
|
await connect(user_id);
|
||||||
},
|
const path: any[] = [];
|
||||||
};
|
return new DeepProxy(
|
||||||
|
SyncActionDefinition,
|
||||||
public _internal = {
|
({ trapName, value, key, DEFAULT, PROXY }) => {
|
||||||
msg: {
|
if (trapName === "set") {
|
||||||
pending: {} as Record<string, Promise<any>>,
|
throw new TypeError("target is immutable");
|
||||||
resolve: {} as Record<string, (result: any) => void>,
|
|
||||||
},
|
|
||||||
send: async (msg: MSG_TO_SERVER) => {
|
|
||||||
const { resolve, pending } = this._internal.msg;
|
|
||||||
const msg_client_id = createId();
|
|
||||||
pending[msg_client_id] = new Promise((done) => {
|
|
||||||
resolve[msg_client_id] = done;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (this.wsPending) {
|
|
||||||
await this.wsPending;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.ws.send(packr.pack({ ...msg, msg_client_id: createId() }));
|
path.push(key);
|
||||||
},
|
if (typeof value === "string") {
|
||||||
};
|
if (path[0] === "then") path.shift();
|
||||||
|
return (...args: any[]) =>
|
||||||
|
new Promise((resolve) => {
|
||||||
|
operation({
|
||||||
|
path: path.join("."),
|
||||||
|
resolve,
|
||||||
|
args,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
constructor(ws: WebSocket) {
|
if (trapName === "get") {
|
||||||
this.ws = ws;
|
if (typeof value === "object" && value !== null) {
|
||||||
}
|
return PROXY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static instance = null as SyncClient | null;
|
return DEFAULT;
|
||||||
static connect() {
|
}
|
||||||
if (SyncClient.instance) return SyncClient.instance;
|
) as unknown as typeof SyncActions;
|
||||||
|
};
|
||||||
|
|
||||||
const url = new URL(location.href);
|
const connect = (user_id: string) => {
|
||||||
url.pathname = "/sync";
|
return new Promise<WebSocket>((resolve) => {
|
||||||
url.protocol = url.protocol === "http:" ? "ws:" : "wss:";
|
if (!conf.ws) {
|
||||||
|
const url = new URL(location.href);
|
||||||
|
url.pathname = "/sync";
|
||||||
|
url.protocol = url.protocol === "http:" ? "ws:" : "wss:";
|
||||||
|
|
||||||
const ws = new WebSocket(url.toString());
|
const ws = new WebSocket(url.toString());
|
||||||
const client = new SyncClient(ws);
|
conf.ws = ws;
|
||||||
SyncClient.instance = client;
|
ws.onopen = () => {
|
||||||
let promise = {
|
ws.send(packr.pack({ type: "user_id", user_id }));
|
||||||
resolve: null as null | (() => void),
|
|
||||||
};
|
|
||||||
client.wsPending = new Promise((resolve) => {
|
|
||||||
promise.resolve = resolve;
|
|
||||||
});
|
|
||||||
ws.onopen = () => {
|
|
||||||
promise.resolve?.();
|
|
||||||
};
|
|
||||||
|
|
||||||
ws.onmessage = async (e) => {
|
|
||||||
const raw = e.data as Blob;
|
|
||||||
const msg = packr.unpack(
|
|
||||||
Buffer.from(await raw.arrayBuffer())
|
|
||||||
) as MSG_TO_CLIENT & {
|
|
||||||
msg_server_id: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!client.id) {
|
ws.onmessage = async (e) => {
|
||||||
if (msg.action === ClientAction.Identify) {
|
const raw = e.data as Blob;
|
||||||
client.id = msg.id;
|
const msg = packr.unpack(Buffer.from(await raw.arrayBuffer()));
|
||||||
client.connected = true;
|
if (msg.type === "client_id") {
|
||||||
|
conf.client_id = msg.client_id;
|
||||||
|
resolve(ws);
|
||||||
}
|
}
|
||||||
} else {
|
};
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return client;
|
const operation = async (arg: {
|
||||||
|
path: string;
|
||||||
|
resolve: (value: any) => void;
|
||||||
|
args: any[];
|
||||||
|
}) => {
|
||||||
|
const ws = conf.ws;
|
||||||
|
const idb = conf.idb;
|
||||||
|
if (idb) {
|
||||||
|
const sargs = stringify(arg.args);
|
||||||
|
const hargs = await xxhash32(`${arg.path}-${sargs}`);
|
||||||
|
|
||||||
|
if (ws && ws.readyState === ws.OPEN) {
|
||||||
|
// online
|
||||||
|
} else {
|
||||||
|
// offline
|
||||||
|
const cache = await get(hargs, idb);
|
||||||
|
console.log(cache);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { createStore } from "idb-keyval";
|
||||||
|
export const initIDB = (user_id: string) => {
|
||||||
|
const store = createStore(`prasi-user-${user_id}`, "default");
|
||||||
|
return store;
|
||||||
|
};
|
||||||
|
|
@ -33,6 +33,7 @@ export const w = window as unknown as {
|
||||||
serverurl: string;
|
serverurl: string;
|
||||||
api: any;
|
api: any;
|
||||||
db: any;
|
db: any;
|
||||||
|
offline: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Page = {
|
export type Page = {
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { spawn } from "bun";
|
||||||
import { dir } from "dir";
|
import { dir } from "dir";
|
||||||
import { Plugin, context } from "esbuild";
|
import { Plugin, context } from "esbuild";
|
||||||
import { $ } from "execa";
|
import { $ } from "execa";
|
||||||
import { removeAsync, writeAsync } from "fs-jetpack";
|
import { listAsync, removeAsync, writeAsync } from "fs-jetpack";
|
||||||
|
|
||||||
await removeAsync(dir.path("app/web/.parcel-cache"));
|
await removeAsync(dir.path("app/web/.parcel-cache"));
|
||||||
await removeAsync(dir.path("app/static"));
|
await removeAsync(dir.path("app/static"));
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import { ensureNotRunning } from "./utils/ensure";
|
||||||
import { g } from "./utils/global";
|
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";
|
||||||
|
|
||||||
g.status = "init";
|
g.status = "init";
|
||||||
|
|
||||||
|
|
@ -17,7 +18,6 @@ g.mode = process.argv.includes("dev") ? "dev" : "prod";
|
||||||
g.datadir = g.mode == "prod" ? "../data" : "data";
|
g.datadir = g.mode == "prod" ? "../data" : "data";
|
||||||
g.port = parseInt(process.env.PORT || "4550");
|
g.port = parseInt(process.env.PORT || "4550");
|
||||||
|
|
||||||
|
|
||||||
g.log.info(g.mode === "dev" ? "DEVELOPMENT" : "PRODUCTION");
|
g.log.info(g.mode === "dev" ? "DEVELOPMENT" : "PRODUCTION");
|
||||||
if (g.mode === "dev") {
|
if (g.mode === "dev") {
|
||||||
await startDevWatcher();
|
await startDevWatcher();
|
||||||
|
|
@ -31,19 +31,11 @@ if (g.db) {
|
||||||
g.log.error(`[DB ERROR]\n${e.message}`);
|
g.log.error(`[DB ERROR]\n${e.message}`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await syncActionDefinition();
|
||||||
await parcelBuild();
|
await parcelBuild();
|
||||||
|
|
||||||
await generateAPIFrm();
|
await generateAPIFrm();
|
||||||
await prepareApiRoutes();
|
await prepareApiRoutes();
|
||||||
|
|
||||||
// Bun.serve({
|
|
||||||
// port: g.port,
|
|
||||||
// async fetch(req, server) {
|
|
||||||
// return new Response("test. sabar. ya....");
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
await createServer();
|
await createServer();
|
||||||
await prepareAPITypes();
|
await prepareAPITypes();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { dir } from "dir";
|
||||||
|
import { SyncActions } from "../../../app/srv/ws/sync/actions";
|
||||||
|
import { writeAsync } from "fs-jetpack";
|
||||||
|
|
||||||
|
export const syncActionDefinition = async () => {
|
||||||
|
const def: any = {};
|
||||||
|
let idx = 0;
|
||||||
|
|
||||||
|
const paths = {} as Record<string, string>;
|
||||||
|
const walk = (act: any, d: any, parentPaths: string[]) => {
|
||||||
|
for (const [k, v] of Object.entries(act)) {
|
||||||
|
d[k] = typeof v === "function" ? idx++ + "" : {};
|
||||||
|
|
||||||
|
if (typeof d[k] === "string") {
|
||||||
|
paths[d[k]] = [...parentPaths, k].join(".");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof d[k] === "object") {
|
||||||
|
walk(v, d[k], [...parentPaths, k]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
walk(SyncActions, def, []);
|
||||||
|
|
||||||
|
await writeAsync(
|
||||||
|
dir.path("app/srv/ws/sync/actions-def.ts"),
|
||||||
|
`\
|
||||||
|
export const SyncActionDefinition = ${JSON.stringify(def, null, 2)};
|
||||||
|
export const SyncActionPaths = ${JSON.stringify(paths, null, 2)}; `
|
||||||
|
);
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue