fix live reload

This commit is contained in:
Rizky 2024-07-24 19:47:19 +07:00
parent 07d0005958
commit 3ccb5bb087
23 changed files with 500 additions and 199 deletions

View File

@ -1 +0,0 @@
var g=Object.create;var e=Object.defineProperty;var h=Object.getOwnPropertyDescriptor;var i=Object.getOwnPropertyNames;var j=Object.getPrototypeOf,k=Object.prototype.hasOwnProperty;var m=(b,a)=>()=>(a||b((a={exports:{}}).exports,a),a.exports),n=(b,a)=>{for(var c in a)e(b,c,{get:a[c],enumerable:!0})},l=(b,a,c,f)=>{if(a&&typeof a=="object"||typeof a=="function")for(let d of i(a))!k.call(b,d)&&d!==c&&e(b,d,{get:()=>a[d],enumerable:!(f=h(a,d))||f.enumerable});return b};var o=(b,a,c)=>(c=b!=null?g(j(b)):{},l(a||!b||!b.__esModule?e(c,"default",{value:b,enumerable:!0}):c,b));export{m as a,n as b,o as c};

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
import{d as o,e as f,g as s}from"./chunk-ACJPWHEH.js";o();s();f();

View File

@ -1 +0,0 @@
import"./chunk-5TBO732O.js";

File diff suppressed because one or more lines are too long

View File

@ -10,6 +10,7 @@ import { gzipAsync } from "../entity/zlib";
import { sendWS } from "../sync-handler";
import { SyncConnection, SyncType } from "../type";
import { validate } from "uuid";
export const page_load: SAction["page"]["load"] = async function (
this: SyncConnection,
id: string
@ -45,24 +46,48 @@ export const page_load: SAction["page"]["load"] = async function (
delete g.route_cache[snap.id_site];
}
const client_ids = new Set<string>();
user.active.findAll({ page_id: id }).forEach((e) => {
client_ids.add(e.client_id);
});
const found = user.active.findAll({ page_id: id });
client_ids.forEach((client_id) => {
if (origin !== um) {
if (client_id === origin) return;
if (!g.preview_page_timeout) g.preview_page_timeout = {};
clearTimeout(g.preview_page_timeout[id]);
g.preview_page_timeout[id] = setTimeout(() => {
let json = doc.toJSON();
for (const f of found) {
const client_id = f.client_id;
const ws = conns.get(client_id)?.ws;
if (client_id && ws) {
if (!f.user_id) {
console.log(client_id);
sendWS(ws, {
type: SyncType.Event,
event: "page_changed",
data: json,
});
}
}
}
}, 1000);
for (const f of found) {
const client_id = f.client_id;
const ws = conns.get(client_id)?.ws;
if (ws)
sendWS(ws, {
type: SyncType.Event,
event: "remote_svlocal",
data: { type: "page", sv_local, id },
});
});
if (client_id && ws) {
if (!!f.user_id) {
if (ws) {
if (origin !== um) {
if (client_id === origin) return;
}
sendWS(ws, {
type: SyncType.Event,
event: "remote_svlocal",
data: { type: "page", sv_local, id },
});
}
}
}
}
});
};

View File

@ -85,6 +85,9 @@ export const initFrontEnd = async (
async setup(setup) {
try {
setup.onEnd(async (res) => {
const client_ids = user.active
.findAll({ site_id: id_site })
.map((e) => e.client_id);
if (res.errors.length > 0) {
await codeError(
id_site,
@ -92,13 +95,6 @@ export const initFrontEnd = async (
"\n\n"
)
);
} else {
await codeError(id_site, "");
const client_ids = new Set<string>();
user.active.findAll({ site_id: id_site }).forEach((e) => {
client_ids.add(e.client_id);
});
const now = Date.now();
client_ids.forEach((client_id) => {
@ -107,13 +103,26 @@ export const initFrontEnd = async (
sendWS(ws, {
type: SyncType.Event,
event: "code_changes",
data: { ts: now, mode: "frontend" },
data: { ts: now, mode: "frontend", status: "error" },
});
});
} else {
await codeError(id_site, "");
await $`rm -rf ${out_dir_switch}`.quiet();
await $`mv ${out_dir} ${out_dir_switch}`.quiet();
await $`mv ${out_dir_temp} ${out_dir}`.quiet();
const now = Date.now();
client_ids.forEach((client_id) => {
const ws = conns.get(client_id)?.ws;
if (ws)
sendWS(ws, {
type: SyncType.Event,
event: "code_changes",
data: { ts: now, mode: "frontend", status: "ok" },
});
});
}
});
} catch (e) {
@ -123,6 +132,24 @@ export const initFrontEnd = async (
},
],
});
const broadcastLoading = async () => {
const client_ids = user.active
.findAll({ site_id: id_site })
.map((e) => e.client_id);
const now = Date.now();
client_ids.forEach((client_id) => {
const ws = conns.get(client_id)?.ws;
if (ws)
sendWS(ws, {
type: SyncType.Event,
event: "code_changes",
data: { ts: now, mode: "frontend", status: "building" },
});
});
};
code.internal.frontend[id_site] = {
ctx: build_ctx,
timeout: null,
@ -135,7 +162,7 @@ export const initFrontEnd = async (
async (event, filename) => {
const fe = code.internal.frontend[id_site];
const srv = code.internal.server[id_site];
if (filename?.startsWith("node_modules")) return;
if (filename?.startsWith("node_modules") || filename?.startsWith("typings")) return;
if (
filename?.endsWith(".tsx") ||
filename?.endsWith(".ts") ||
@ -144,10 +171,14 @@ export const initFrontEnd = async (
) {
if (typeof fe !== "undefined" && !fe.rebuilding) {
fe.rebuilding = true;
try {
await fe.ctx.rebuild();
} catch (e) {}
fe.rebuilding = false;
clearTimeout(fe.timeout);
fe.timeout = setTimeout(async () => {
try {
broadcastLoading();
await fe.ctx.rebuild();
} catch (e) {}
fe.rebuilding = false;
}, 500);
}
if (typeof srv !== "undefined" && !srv.rebuilding && srv.ctx) {

View File

@ -28,12 +28,9 @@ export const loadComponent = async (comp_id: string, sync?: SyncConnection) => {
const sv_local = await gzipAsync(update);
user.active.findAll({ comp_id: comp_id });
const client_ids = new Set<string>();
user.active.findAll({ comp_id: comp_id }).forEach((e) => {
client_ids.add(e.client_id);
});
const client_ids = user.active
.findAll({ comp_id: comp_id })
.map((e) => e.client_id);
client_ids.forEach((client_id) => {
if (origin !== um) {

View File

@ -1,53 +1,85 @@
import { dir } from "dir";
import { IndexedMap } from "../../../../web/src/utils/sync/idx-map";
const defaultConf = {
site_id: "",
page_id: "",
};
export type UserConf = typeof defaultConf;
export const user = {
active: IndexedMap.create<
{
user_id: string;
site_id: string;
page_id: string;
comp_id?: string;
client_id: string;
},
"client_id"
>("client_id"),
conf: {
_db: {} as any,
init() {
return this._db;
},
async getOrCreate(id: string) {
let res = this._db[id];
export type TUser = {
select?: "" | "comp" | "item" | "section" | "text";
client_id: string;
user_id: string;
site_id: string;
page_id: string;
comp_id: string | string[];
};
if (!res || !res.id) {
this._db[id] = structuredClone(defaultConf);
res = this._db[id];
}
return res as UserConf;
const g = global as unknown as { _active_user: any; _conf_user: any };
if (!g._conf_user) {
g._conf_user = {};
}
if (!g._active_user) {
g._active_user = [];
}
export const user = {
conf: {
db: g._conf_user as Record<string, UserConf>,
init() {},
async getOrCreate(user_id: string) {
if (!this.db[user_id]) this.db[user_id] = { ...defaultConf };
return this.db[user_id];
},
async set(user_id: string, key: keyof UserConf, value: any) {
const conf = await this.getOrCreate(user_id);
conf[key] = value;
},
get(user_id: string) {
return this._db[user_id];
if (!this.db[user_id]) this.db[user_id] = { ...defaultConf };
return this.db[user_id];
},
async set<T extends keyof UserConf>(
user_id: string,
key: T,
value: UserConf[T]
) {
let current = this.get(user_id);
if (!current) {
this._db[user_id] = structuredClone(defaultConf)
current = this.get(user_id);
},
active: {
db: g._active_user as TUser[],
comps: {} as Record<string, Set<string>>,
findAll(where: Partial<TUser>) {
return this.db.filter((e: any) => {
for (const [k, v] of Object.entries(where)) {
if (k === "comp_id" && where.client_id) {
if (!this.comps[where.client_id].has(v as any)) return false;
} else if (e[k] !== v) return false;
}
return true;
});
},
delAll(where: Partial<TUser>) {
const res = this.findAll(where);
this.db = this.db.filter((e) => !res.includes(e));
},
add(user: Partial<TUser>) {
let found = null;
if (user.client_id) {
found = this.db.find((e: any) => {
return e.client_id === user.client_id;
});
}
if (current) {
this._db[user_id] = { ...current, [key]: value }
if (found) {
for (const [k, v] of Object.entries(user)) {
(found as any)[k] = v;
}
} else {
this.db.push(user as any);
found = user;
}
if (typeof found.comp_id === "string") {
let cid = found.client_id || "";
if (cid && !this.comps[cid]) {
this.comps[cid] = new Set();
}
this.comps[cid].add(found.comp_id);
}
},
},

View File

@ -0,0 +1,27 @@
import { ServerWebSocket } from "bun";
import { WSData } from "../../../../../pkgs/core/server/create";
import { user } from "../entity/user";
export const previewLiveReload = (
ws: ServerWebSocket<WSData>,
msg:
| { mode: "init"; data: { client_id: string; site_id: string } }
| {
mode: "listen";
data: { type: "page"; id: string; client_id: string };
}
) => {
if (msg.mode === "init") {
user.active.add({
client_id: msg.data.client_id,
site_id: msg.data.site_id,
});
} else if (msg.mode === "listen") {
if (msg.data.type === "page") {
user.active.add({
client_id: msg.data.client_id,
page_id: msg.data.id,
});
}
}
};

View File

@ -10,6 +10,7 @@ import { loadSitePage } from "./editor/load-sitepage";
import { conns, wconns } from "./entity/conn";
import { UserConf, user } from "./entity/user";
import { SyncType } from "./type";
import { previewLiveReload } from "./preview/live-reload";
const packr = new Packr({ structuredClone: true });
export const sendWS = (ws: ServerWebSocket<WSData>, msg: any) => {
@ -43,6 +44,9 @@ export const syncHandler: WebSocketHandler<WSData> = {
const conn = conns.get(conn_id);
if (conn) {
const msg = packr.unpack(Buffer.from(raw));
if (msg.type === "preview") {
previewLiveReload(ws, msg);
}
if (msg.type === SyncType.UserID) {
const { user_id, page_id, site_id } = msg;
conn.user_id = user_id;

View File

@ -1,8 +1,8 @@
import getTime from "date-fns/getTime";
import { PG } from "./ed-global";
import { format } from "date-fns";
import { PG } from "./ed-global";
export const loadFrontEnd = async (p: PG, ts?: number) => {
const id_site = p.site.id;
const url = `/prod/${id_site}/_prasi/code/index.js?ts=${ts}`;
const fn = new Function(
@ -17,12 +17,12 @@ import("${url}")
try {
fn((exports: any) => {
const w = window as any;
for (const [k, v] of Object.entries(exports)) {
w[k] = v;
p.site_exports[k] = v;
}
resolve(exports);
console.log(`🚧 Code updated from vscode - ${format(Date.now(), "HH:mm:ss")}`);
});
} catch (e) {
console.log("Failed to load site code", e);

View File

@ -12,6 +12,7 @@ import { EmptySite, PG } from "./ed-global";
import { reloadPage } from "./ed-route";
import { loadSite } from "./ed-site";
import { treeRebuild } from "./tree/build";
import { format } from "date-fns";
const decoder = new TextDecoder();
@ -151,6 +152,7 @@ export const edInitSync = (p: PG) => {
},
shakehand(client_id) {
p.user.client_id = client_id;
console.log(`Prasi connected: ${client_id}`);
},
disconnected() {
console.log("offline, reconnecting...");
@ -185,9 +187,23 @@ export const edInitSync = (p: PG) => {
}
p.render();
},
async code_changes({ ts, mode }) {
async code_changes({ ts, mode, status }) {
if (mode === "frontend") {
await loadFrontEnd(p, ts);
if (status === "ok") {
console.clear();
console.log(
`${format(Date.now(), "HH:mm:ss")} 🚧 Code updated from vscode `
);
await loadFrontEnd(p, ts);
} else if (status === "building") {
console.log(
`${format(
Date.now(),
"HH:mm:ss"
)} Code changed from vscode, rebuilding...`
);
}
} else {
await loadTypings(p);
if (p.ui.monaco) {

View File

@ -0,0 +1,164 @@
import { format } from "date-fns";
import { get } from "idb-keyval";
import { Packr } from "msgpackr";
import { SyncType } from "../../../../../../srv/ws/sync/type";
import { w } from "../../../../utils/types/general";
import { base } from "../base";
import { scanComponent } from "../component";
import { rebuildMeta } from "../route";
const packr = new Packr({ structuredClone: true });
/** CONSTANT */
const WS_CONFIG = {
debug: !!localStorage.getItem("prasi-ws-debug"),
reconnectTimeout: 1000,
id_client: "",
id_site: "",
ws: null as any,
};
export const initDevLiveReload = () => {
if (
location.host === "localhost:4550" ||
location.host === "prasi.avolut.com"
) {
const patharr = location.pathname.split("/");
const id_site = patharr[2];
const retry = () => {
const url = new URL(w.basehost || location.href);
url.pathname = "/sync";
url.protocol = url.protocol === "http:" ? "ws:" : "wss:";
const ws = new WebSocket(`${url.protocol}//${url.host}${url.pathname}`);
let timeout = setTimeout(() => {
clearTimeout(timeout);
ws.close();
retry();
}, 2000);
WS_CONFIG.ws = ws;
WS_CONFIG.id_site = id_site;
ws.onopen = () => {
clearTimeout(timeout);
w.offline = false;
w.editorRender?.();
};
ws.onmessage = async (e) => {
const raw = e.data as Blob;
const msg = packr.unpack(Buffer.from(await raw.arrayBuffer()));
if (WS_CONFIG.debug)
console.log(`%c⬇`, `color:red`, formatBytes(raw.size, 0), msg);
if (msg.type === SyncType.ClientID) {
WS_CONFIG.id_client = msg.client_id;
send(ws, {
type: "preview",
mode: "init",
data: {
site_id: id_site,
client_id: msg.client_id,
},
});
} else if (msg.type === SyncType.Event) {
if (msg.event === "page_changed") {
const id = msg.data.map.id;
const page = base.page.cache[id];
const root = msg.data.map.root;
page.root = root;
const p = {
id: page.id,
url: page.url,
root,
meta: {},
};
await scanComponent(root.childs, true);
rebuildMeta(p.meta, root);
base.page.cache[p.id] = p;
w.prasiContext.render();
} else if (msg.event === "code_changes") {
const { mode, ts, status } = msg.data;
if (mode === "frontend") {
if (status === "ok") {
console.clear();
console.log(
`${format(
Date.now(),
"HH:mm:ss"
)} 🚧 Code updated from vscode `
);
const url = `/prod/${id_site}/_prasi/code/index.js?ts=${ts}`;
const fn = new Function(
"callback",
`
import("${url}")
.catch((e) => console.error("Failed to load site code\\n\\n", e))
.then(callback)`
);
try {
await new Promise<any>((resolve) => {
try {
fn((exports: any) => {
const w = window as any;
for (const [k, v] of Object.entries(exports)) {
w[k] = v;
}
resolve(exports);
w.prasiContext.render();
});
} catch (e) {
console.log("Failed to load site code", e);
}
});
} catch (e) {}
} else if (status === "building") {
console.log(
`${format(
Date.now(),
"HH:mm:ss"
)} Code changed from vscode, rebuilding...`
);
}
}
}
}
};
};
retry();
}
};
export const listenChanges = (
arg: { type: "page"; id: string } | { type: "comp"; ids: string[] }
) => {
if (WS_CONFIG.ws) {
send(WS_CONFIG.ws, {
type: "preview",
mode: "listen",
data: {
...arg,
client_id: WS_CONFIG.id_client,
},
});
}
};
const send = (ws: WebSocket, msg: any) => {
const raw = packr.pack(msg);
if (WS_CONFIG.debug)
console.log(`%c⬆`, "color:blue", formatBytes(raw.length, 0), msg);
ws.send(raw);
};
function formatBytes(bytes: number, decimals: number) {
if (bytes == 0) return "0 Bytes";
var k = 1024,
dm = decimals || 2,
sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"],
i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i];
}

View File

@ -48,6 +48,7 @@ export const loadPages = (page_ids: string[]) => {
break;
}
}
if (is_done) {
done(result);
}

View File

@ -1,11 +1,13 @@
import { createRoot } from "react-dom/client";
import { defineReact, defineWindow } from "web-utils";
import { initBaseConfig } from "./base/base";
import { initDevLiveReload } from "./base/live-reload/dev-live-reload";
import { Root, isPreview } from "./root";
import { w } from "./w";
(async () => {
import("./font");
initDevLiveReload();
initBaseConfig();
const div = document.getElementById("root");
if (div) {
@ -35,6 +37,5 @@ import { w } from "./w";
if (document.body.classList.contains("opacity-0")) {
document.body.classList.remove("opacity-0");
}
}
})();

View File

@ -15,6 +15,7 @@ import { detectResponsiveMode } from "./base/responsive";
import { initBaseRoute, rebuildMeta } from "./base/route";
import { w } from "./w";
import { MatchedRoute } from "radix3";
import { listenChanges } from "./base/live-reload/dev-live-reload";
export const isPreview = () => {
return (
@ -129,6 +130,8 @@ export const Root = () => {
if (page.id !== local.page_id) {
base.init_local_effect = {};
listenChanges({ type: "page", id: page.id });
}
w.params = page.params || {};

View File

@ -1,70 +0,0 @@
import hash_sum from "hash-sum";
const match = (item: any, where: any) => {
for (const [k, v] of Object.entries(where)) {
if (item[k] !== v) return false;
}
return true;
};
export const IndexedMap = {
create: <OBJ extends Record<string, any>, KEY extends string>(id: KEY) => {
const all = {} as Record<KEY, Record<string, OBJ>>;
return {
add(item: OBJ & Record<KEY, any>) {
const pk = item[id] as KEY;
if (!all[pk]) {
all[pk] = {};
}
const _id = hash_sum(item) as any;
const items = all[pk] as Record<string, OBJ>;
items[_id] = item;
return pk;
},
findAll(where: Partial<OBJ & Record<KEY, string>>, withId?: boolean) {
const founds = [];
if (where[id]) {
const _id = where[id] as KEY;
if (all[_id]) {
for (const [k, item] of Object.entries(all[_id])) {
if (match(item, where)) {
if (withId) {
founds.push({ ...item, _id: k });
} else {
founds.push(item);
}
}
}
}
} else {
for (const _items of Object.values(all)) {
const items = _items as Record<string, OBJ>;
for (const [k, item] of Object.entries(items)) {
if (match(item, where)) {
if (withId) {
founds.push({ ...item, _id: k });
} else {
founds.push(item);
}
}
}
}
}
return founds;
},
delAll(where: Partial<OBJ & Record<KEY, string>>) {
for (const item of this.findAll(where, true)) {
delete all[item[id]][item._id];
}
},
};
},
};

View File

@ -97,7 +97,11 @@ export const clientStartSync = async (arg: {
}
>
) => void;
code_changes: (arg: { ts: number; mode: "frontend" | "typings" }) => void;
code_changes: (arg: {
ts: number;
mode: "frontend" | "typings";
status: "error" | "ok" | "building";
}) => void;
disconnected: () => { reconnect: boolean };
opened: () => void;
shakehand: (client_id: string) => void;
@ -180,7 +184,7 @@ const connect = (
conf.ws = ws;
event.opened();
};
ws.onmessage = async (e) => {
const raw = e.data as Blob;
const msg = packr.unpack(Buffer.from(await raw.arrayBuffer()));

BIN
bun.lockb

Binary file not shown.

View File

@ -2,8 +2,9 @@ import { $ } from "bun";
import { dir } from "dir";
import { context } from "esbuild";
import { removeAsync } from "fs-jetpack";
import { polyfillNode } from "esbuild-plugin-polyfill-node";
await removeAsync(dir.path('/app/srv/core'))
await removeAsync(dir.path("/app/srv/core"));
await $`bun tailwindcss -i src/nova/prod/tailwind.css -m -o ../srv/core/index.css`
.cwd(dir.path(`/app/web`))
.quiet();
@ -24,6 +25,61 @@ const ctx = await context({
define: {
"process.env.NODE_ENV": `"production"`,
},
plugins: [
polyfillNode({
polyfills: {
buffer: true,
_stream_duplex: false,
_stream_passthrough: false,
_stream_readable: false,
_stream_transform: false,
_stream_writable: false,
assert: false,
async_hooks: false,
child_process: false,
cluster: false,
console: false,
constants: false,
crypto: false,
dgram: false,
diagnostics_channel: false,
dns: false,
domain: false,
events: false,
fs: false,
http: false,
http2: false,
https: false,
module: false,
net: false,
os: false,
path: false,
perf_hooks: false,
process: false,
punycode: false,
querystring: false,
readline: false,
repl: false,
stream: false,
string_decoder: false,
sys: false,
timers: false,
tls: false,
tty: false,
url: false,
util: false,
v8: false,
vm: false,
wasi: false,
worker_threads: false,
zlib: false,
"assert/strict": false,
"fs/promises": false,
"timers/promises": false,
},
}),
],
});
ctx.watch();

View File

@ -11,6 +11,7 @@
"acorn-walk": "^8.3.3",
"brotli-wasm": "^3.0.1",
"esbuild": "^0.21.5",
"esbuild-plugin-polyfill-node": "^0.3.0",
"execa": "^8.0.1",
"fs-jetpack": "^5.1.0",
"lmdb": "^2.8.5",

View File

@ -66,4 +66,5 @@ export const g = global as unknown as {
route_cache: Record<string, { br?: any; gzip?: any }>;
main_cache: Record<string, { content: any; type: string }>;
br: BrotliWasmType;
preview_page_timeout: Record<string, Timer>
};