diff --git a/app/web/src/nova/vi/load/load-snapshot.tsx b/app/web/src/nova/vi/load/load-snapshot.tsx index 863f1896..ceebfe58 100644 --- a/app/web/src/nova/vi/load/load-snapshot.tsx +++ b/app/web/src/nova/vi/load/load-snapshot.tsx @@ -1,11 +1,12 @@ import { compress, decompress } from "wasm-gzip"; +import { apiProxy } from "../../../base/load/api/api-proxy"; import { loadApiProxyDef } from "../../../base/load/api/api-proxy-def"; +import { dbProxy } from "../../../base/load/db/db-proxy"; +import { w } from "../../../utils/types/general"; import { PG } from "../../ed/logic/ed-global"; import { evalCJS } from "../../ed/logic/ed-sync"; import { treeRebuild } from "../../ed/logic/tree/build"; -import { w } from "../../../utils/types/general"; -import { dbProxy } from "../../../base/load/db/db-proxy"; -import { apiProxy } from "../../../base/load/api/api-proxy"; +import { simpleHash } from "../utils/simple-hash"; const encoder = new TextEncoder(); export const viLoadSnapshot = async (p: PG) => { @@ -22,7 +23,8 @@ export const viLoadSnapshot = async (p: PG) => { api: api.apiTypes, prisma: api.prismaTypes, }); - const hash = hashCode(zip); + + const hash = simpleHash(zip); const res = await p.sync?.code.action({ type: "check-typings", site_id: p.site.id, @@ -84,11 +86,3 @@ export const applyEnv = (p: PG, src?: string) => { } } }; - -const hashCode = function (s: string) { - var h = 0, - l = s.length, - i = 0; - if (l > 0) while (i < l) h = ((h << 5) - h + s.charCodeAt(i++)) | 0; - return h; -}; diff --git a/app/web/src/nova/vi/utils/simple-hash.ts b/app/web/src/nova/vi/utils/simple-hash.ts new file mode 100644 index 00000000..bf2d5c67 --- /dev/null +++ b/app/web/src/nova/vi/utils/simple-hash.ts @@ -0,0 +1,15 @@ +export const simpleHash = (str: string, seed = 0) => { + let h1 = 0xdeadbeef ^ seed, + h2 = 0x41c6ce57 ^ seed; + for (let i = 0, ch; i < str.length; i++) { + ch = str.charCodeAt(i); + h1 = Math.imul(h1 ^ ch, 2654435761); + h2 = Math.imul(h2 ^ ch, 1597334677); + } + h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507); + h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909); + h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507); + h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909); + + return 4294967296 * (2097151 & h2) + (h1 >>> 0); +}; diff --git a/bun.lockb b/bun.lockb index d1beb6b8..94bce961 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/pkgs/core/package.json b/pkgs/core/package.json index 160f6c57..a92ec684 100644 --- a/pkgs/core/package.json +++ b/pkgs/core/package.json @@ -13,10 +13,12 @@ "lmdb": "^2.8.5", "mime": "^3.0.0", "pino": "^8.16.1", + "msgpackr": "^1.10.1", "pino-pretty": "^10.2.3", "radash": "^11.0.0", "radix3": "^1.1.0", "typescript": "^5.2.2", - "unzipper": "^0.10.14" + "unzipper": "^0.10.14", + "fast-myers-diff": "^3.2.0" } -} +} \ No newline at end of file diff --git a/pkgs/core/utils/diff.test.ts b/pkgs/core/utils/diff.test.ts new file mode 100644 index 00000000..a785f570 --- /dev/null +++ b/pkgs/core/utils/diff.test.ts @@ -0,0 +1,16 @@ +import { describe, expect, test } from "bun:test"; +import { Diff } from "./diff"; + +describe("simple diff", async () => { + const server = await Diff.server("ini ionadi a"); + const patch = server.getPatch("new"); + + const client = await Diff.client(patch); + + await server.update("rako12"); + + const newPatch = server.getPatch(client.ts); + + await client.applyPatch(newPatch); + expect(await client.data).toBe("rako12"); +}); diff --git a/pkgs/core/utils/diff.ts b/pkgs/core/utils/diff.ts new file mode 100644 index 00000000..274597c3 --- /dev/null +++ b/pkgs/core/utils/diff.ts @@ -0,0 +1,134 @@ +import { applyPatch, calcPatch } from "fast-myers-diff"; +import { Packr } from "msgpackr"; +import { gunzip, gzip } from "zlib"; + +const MAX_HISTORY = 10; + +const packr = new Packr({}); + +type PATCH_RESULT = + | { + mode: "new"; + ts: string; + data: number[]; + } + | { + mode: "patch"; + ts: string; + diff: any; + }; + +export class Diff { + ts = ""; + mode: "client" | "server" = "server"; + + private _data: number[] = []; + private _history = {} as Record; + + constructor() {} + + get data() { + const _data = new Uint8Array(this._data); + return new Promise((done) => { + if (typeof window === "undefined") { + gunzipAsync(_data).then((result) => { + done(packr.unpack(result)); + }); + } + }); + } + + async update(data: T) { + if (this.mode === "server") { + this._data = (await gzipAsync(packr.pack(data))).toJSON().data; + this.ts = + (Date.now() + "").substring(5) + Math.round(performance.now() * 10000); + this._history[this.ts] = this._data; + + let i = 0; + for (const k of Object.keys(this._history).sort( + (a, b) => parseInt(b) - parseInt(a) + )) { + if (i > MAX_HISTORY) { + delete this._history[k]; + } + + i++; + } + } + } + + getPatch(ts: "new" | string): PATCH_RESULT { + if (ts !== "new") { + const old_data = this._history[ts]; + + if (old_data) { + const result_diff = [...calcPatch(old_data, this._data)]; + return { diff: result_diff, mode: "patch", ts: this.ts }; + } + } + + return { data: this._data, mode: "new", ts: this.ts }; + } + + async applyPatch(patch: PATCH_RESULT) { + if (patch.mode === "new") { + this.ts = patch.ts; + if (patch.data) this._data = patch.data; + } else { + this.ts = patch.ts; + const num_array = []; + for (const num of applyPatch(this._data, patch.diff)) { + if (Array.isArray(num)) { + for (const n of num) { + num_array.push(n); + } + } else { + num_array.push(num); + } + } + this._data = num_array; + } + } + + static async server(str: T) { + const diff = new Diff(); + await diff.update(str); + return diff; + } + + static async client(patch: PATCH_RESULT) { + const diff = new Diff(); + diff.mode = "client"; + + if (patch.mode === "new") { + diff.ts = patch.ts; + if (patch.data) diff._data = patch.data; + } + return diff; + } +} + +export const gzipAsync = (bin: Uint8Array | string) => { + return new Promise((resolve, reject) => { + gzip(bin, (err, res) => { + if (err) { + reject(err); + } else { + resolve(res); + } + }); + }); +}; + +export const gunzipAsync = (bin: Uint8Array) => { + return new Promise((resolve, reject) => { + gunzip(bin, (err, result) => { + if (err) { + reject(err); + } else { + resolve(result); + } + }); + }); +};