diff --git a/app/srv/package.json b/app/srv/package.json index 4fb92bbc..d6c775bc 100644 --- a/app/srv/package.json +++ b/app/srv/package.json @@ -9,6 +9,7 @@ "@types/mime-types": "^2.1.2", "esbuild": "^0.19.4", "mime-types": "^2.1.35", + "msgpackr": "^1.9.9", "radix3": "^1.1.0" } } diff --git a/app/srv/ws/handler.ts b/app/srv/ws/handler.ts index d4c9f70c..380b8fe6 100644 --- a/app/srv/ws/handler.ts +++ b/app/srv/ws/handler.ts @@ -9,6 +9,7 @@ import { svLocal } from "./edit/action/sv-local"; import { svdiffRemote } from "./edit/action/svdiff-remote"; import { redo, undo } from "./edit/action/undo-redo"; import { eg } from "./edit/edit-global"; +import { syncHandler } from "./sync/sync-handler"; eg.edit = { site: {}, @@ -21,6 +22,7 @@ const site = { }; export const wsHandler: Record> = { + "/sync": syncHandler, "/edit": { open(ws) { eg.edit.ws.set(ws, { diff --git a/app/srv/ws/sync/sync-handler.ts b/app/srv/ws/sync/sync-handler.ts new file mode 100644 index 00000000..cb71bca7 --- /dev/null +++ b/app/srv/ws/sync/sync-handler.ts @@ -0,0 +1,40 @@ +import { ServerWebSocket, WebSocketHandler } from "bun"; +import { WSData } from "../../../../pkgs/core/server/create"; +import { Packr } from "msgpackr"; +import { createId } from "@paralleldrive/cuid2"; +import { MSG_TO_SERVER } from "./type"; +const packr = new Packr({ structuredClone: true }); + +const conns = new Map< + string, + { + ws: ServerWebSocket; + msg: { + pending: Record>; + resolve: Record void>; + }; + } +>(); +const wconns = new WeakMap, string>(); +export const syncHandler: WebSocketHandler = { + open(ws) { + const id = createId(); + conns.set(id, { ws, msg: { pending: {}, resolve: {} } }); + wconns.set(ws, id); + ws.sendBinary(packr.pack({ type: "identify", id })); + }, + message(ws, raw) { + const conn_id = wconns.get(ws); + if (conn_id) { + const conn = conns.get(conn_id); + if (conn) { + const msg = packr.unpack(Buffer.from(raw)) as MSG_TO_SERVER & { + msg_client_id: string; + }; + + switch (msg.action) { + } + } + } + }, +}; diff --git a/app/srv/ws/sync/type.ts b/app/srv/ws/sync/type.ts new file mode 100644 index 00000000..6f2f9adb --- /dev/null +++ b/app/srv/ws/sync/type.ts @@ -0,0 +1,23 @@ +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; +}; diff --git a/app/web/package.json b/app/web/package.json index 41546e6f..f324d2fc 100644 --- a/app/web/package.json +++ b/app/web/package.json @@ -13,6 +13,7 @@ "@paralleldrive/cuid2": "2.2.2", "@parcel/packager-wasm": "^2.10.0", "@parcel/service-worker": "^2.10.0", + "msgpackr": "^1.9.9", "@swc/wasm-web": "1.3.94-nightly-20231014.1", "algoliasearch": "^4.20.0", "date-fns": "^2.30.0", diff --git a/app/web/src/utils/sync/client.ts b/app/web/src/utils/sync/client.ts new file mode 100644 index 00000000..784cc459 --- /dev/null +++ b/app/web/src/utils/sync/client.ts @@ -0,0 +1,91 @@ +import { Packr } from "msgpackr"; +import { + ClientAction, + MSG_TO_CLIENT, + MSG_TO_SERVER, + ServerAction, +} from "../../../../srv/ws/sync/type"; +import { SyncSite } from "./site"; +import { createId } from "@paralleldrive/cuid2"; +const packr = new Packr({ structuredClone: true }); + +export class SyncClient { + private id = ""; + private ws: WebSocket; + private wsPending?: Promise; + public connected = false; + public loaded = { + site: new Map(), + }; + + public site = { + load: async (id: string) => { + this.loaded.site.set(id, new SyncSite(this, id)); + }, + }; + + public _internal = { + msg: { + pending: {} as Record>, + resolve: {} as Record 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() })); + }, + }; + + constructor(ws: WebSocket) { + this.ws = ws; + } + + private static instance = null as SyncClient | null; + static connect() { + if (SyncClient.instance) return SyncClient.instance; + + const url = new URL(location.href); + url.pathname = "/sync"; + url.protocol = url.protocol === "http:" ? "ws:" : "wss:"; + + const ws = new WebSocket(url.toString()); + const client = new SyncClient(ws); + SyncClient.instance = client; + let promise = { + 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) { + if (msg.action === ClientAction.Identify) { + client.id = msg.id; + client.connected = true; + } + } else { + } + }; + + return client; + } +} diff --git a/app/web/src/utils/sync/site.ts b/app/web/src/utils/sync/site.ts new file mode 100644 index 00000000..8488b61f --- /dev/null +++ b/app/web/src/utils/sync/site.ts @@ -0,0 +1,13 @@ +import { DType, ServerAction } from "../../../../srv/ws/sync/type"; +import { SyncClient } from "./client"; + +export class SyncSite { + private c: SyncClient; + private id = ""; + public status = "loading" as "loading" | "ready"; + + constructor(c: SyncClient, id: string) { + this.c = c; + c._internal.send({ type: DType.Site, action: ServerAction.Load, id }); + } +} diff --git a/app/web/src/utils/sync/type.ts b/app/web/src/utils/sync/type.ts new file mode 100644 index 00000000..e69de29b diff --git a/bun.lockb b/bun.lockb index 8e4b583e..00c654b9 100755 Binary files a/bun.lockb and b/bun.lockb differ