From 64491439c3864e221a51bb6ac4e55065d5cbdd9a Mon Sep 17 00:00:00 2001 From: Rizky Date: Thu, 19 Oct 2023 16:51:57 +0700 Subject: [PATCH] fix --- app/srv/package.json | 1 + app/srv/ws/handler.ts | 2 + app/srv/ws/sync/sync-handler.ts | 40 ++++++++++++++ app/srv/ws/sync/type.ts | 23 ++++++++ app/web/package.json | 1 + app/web/src/utils/sync/client.ts | 91 +++++++++++++++++++++++++++++++ app/web/src/utils/sync/site.ts | 13 +++++ app/web/src/utils/sync/type.ts | 0 bun.lockb | Bin 291152 -> 291216 bytes 9 files changed, 171 insertions(+) create mode 100644 app/srv/ws/sync/sync-handler.ts create mode 100644 app/srv/ws/sync/type.ts create mode 100644 app/web/src/utils/sync/client.ts create mode 100644 app/web/src/utils/sync/site.ts create mode 100644 app/web/src/utils/sync/type.ts 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 8e4b583ec75b603625b4442d913aa2e63c556bfb..00c654b908ed84f4562b815e810845be9cd8be70 100755 GIT binary patch delta 20143 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 PG3T07d9@