From 063eade29129bdaac93ee056e03f50f3ebbfc3c4 Mon Sep 17 00:00:00 2001 From: RAHEEL Date: Sat, 22 Aug 2026 01:38:46 +0500 Subject: [PATCH] added proxy server for collaboration apps --- client/packages/lowcoder/vite.config.mts | 6 + deploy/docker/default-multi.env | 1 + deploy/docker/default.env | 15 + deploy/docker/docker-compose-multi.yaml | 31 + .../docker/frontend/01-update-nginx-conf.sh | 0 deploy/docker/frontend/server.conf | 12 + deploy/docker/override.env | 5 + server/proxy-service/.dockerignore | 4 + server/proxy-service/Dockerfile | 15 + server/proxy-service/build/auth.js | 83 + .../build/bridge/google-forms-bridge.js | 11635 +++++++++++++++ .../build/bridge/typeform-bridge.js | 11842 ++++++++++++++++ .../build/bridge/website-bridge.js | 11270 +++++++++++++++ server/proxy-service/build/googleCookieJar.js | 149 + server/proxy-service/build/googleFormUrls.js | 127 + server/proxy-service/build/googleFormsApi.js | 151 + .../proxy-service/build/googleFormsHosts.js | 72 + .../proxy-service/build/googleFormsRewrite.js | 93 + server/proxy-service/build/googleProxy.js | 353 + server/proxy-service/build/googleSession.js | 88 + server/proxy-service/build/googleUrls.js | 45 + server/proxy-service/build/server.js | 242 + server/proxy-service/build/session.js | 88 + server/proxy-service/build/urls.js | 33 + .../proxy-service/build/websiteAllowlist.js | 100 + server/proxy-service/build/websiteProxy.js | 235 + server/proxy-service/build/websiteSession.js | 88 + server/proxy-service/build/websiteUrls.js | 33 + server/proxy-service/package.json | 30 + server/proxy-service/scripts/build-bridge.mjs | 22 + server/proxy-service/src/auth.ts | 102 + .../bridge/cursor-presence/CursorOverlay.ts | 192 + .../cursor-presence/CursorPresenceProvider.ts | 80 + .../bridge/cursor-presence/RemoteCursor.ts | 122 + .../bridge/cursor-presence/caretMetrics.ts | 216 + .../initTypeformCursorPresence.ts | 207 + .../src/bridge/cursor-presence/textField.ts | 155 + .../src/bridge/cursor-presence/types.ts | 62 + .../src/bridge/cursor-presence/userColor.ts | 28 + .../src/bridge/google-forms-bridge.ts | 577 + .../bridge/pointer-presence/PointerOverlay.ts | 263 + .../PointerPresenceProvider.ts | 98 + .../pointer-presence/initPointerPresence.ts | 209 + .../src/bridge/pointer-presence/types.ts | 44 + .../src/bridge/typeform-bridge.ts | 1061 ++ .../src/bridge/website-bridge.ts | 700 + server/proxy-service/src/googleFormUrls.ts | 136 + server/proxy-service/src/googleFormsApi.ts | 219 + server/proxy-service/src/googleProxy.ts | 285 + server/proxy-service/src/googleSession.ts | 133 + server/proxy-service/src/googleUrls.ts | 41 + server/proxy-service/src/server.ts | 274 + server/proxy-service/src/session.ts | 122 + server/proxy-service/src/urls.ts | 36 + server/proxy-service/src/websiteAllowlist.ts | 92 + server/proxy-service/src/websiteProxy.ts | 281 + server/proxy-service/src/websiteSession.ts | 134 + server/proxy-service/src/websiteUrls.ts | 41 + server/proxy-service/tsconfig.json | 15 + server/proxy-service/yarn.lock | 1315 ++ 60 files changed, 44108 insertions(+) mode change 100644 => 100755 deploy/docker/frontend/01-update-nginx-conf.sh create mode 100644 server/proxy-service/.dockerignore create mode 100644 server/proxy-service/Dockerfile create mode 100644 server/proxy-service/build/auth.js create mode 100644 server/proxy-service/build/bridge/google-forms-bridge.js create mode 100644 server/proxy-service/build/bridge/typeform-bridge.js create mode 100644 server/proxy-service/build/bridge/website-bridge.js create mode 100644 server/proxy-service/build/googleCookieJar.js create mode 100644 server/proxy-service/build/googleFormUrls.js create mode 100644 server/proxy-service/build/googleFormsApi.js create mode 100644 server/proxy-service/build/googleFormsHosts.js create mode 100644 server/proxy-service/build/googleFormsRewrite.js create mode 100644 server/proxy-service/build/googleProxy.js create mode 100644 server/proxy-service/build/googleSession.js create mode 100644 server/proxy-service/build/googleUrls.js create mode 100644 server/proxy-service/build/server.js create mode 100644 server/proxy-service/build/session.js create mode 100644 server/proxy-service/build/urls.js create mode 100644 server/proxy-service/build/websiteAllowlist.js create mode 100644 server/proxy-service/build/websiteProxy.js create mode 100644 server/proxy-service/build/websiteSession.js create mode 100644 server/proxy-service/build/websiteUrls.js create mode 100644 server/proxy-service/package.json create mode 100644 server/proxy-service/scripts/build-bridge.mjs create mode 100644 server/proxy-service/src/auth.ts create mode 100644 server/proxy-service/src/bridge/cursor-presence/CursorOverlay.ts create mode 100644 server/proxy-service/src/bridge/cursor-presence/CursorPresenceProvider.ts create mode 100644 server/proxy-service/src/bridge/cursor-presence/RemoteCursor.ts create mode 100644 server/proxy-service/src/bridge/cursor-presence/caretMetrics.ts create mode 100644 server/proxy-service/src/bridge/cursor-presence/initTypeformCursorPresence.ts create mode 100644 server/proxy-service/src/bridge/cursor-presence/textField.ts create mode 100644 server/proxy-service/src/bridge/cursor-presence/types.ts create mode 100644 server/proxy-service/src/bridge/cursor-presence/userColor.ts create mode 100644 server/proxy-service/src/bridge/google-forms-bridge.ts create mode 100644 server/proxy-service/src/bridge/pointer-presence/PointerOverlay.ts create mode 100644 server/proxy-service/src/bridge/pointer-presence/PointerPresenceProvider.ts create mode 100644 server/proxy-service/src/bridge/pointer-presence/initPointerPresence.ts create mode 100644 server/proxy-service/src/bridge/pointer-presence/types.ts create mode 100644 server/proxy-service/src/bridge/typeform-bridge.ts create mode 100644 server/proxy-service/src/bridge/website-bridge.ts create mode 100644 server/proxy-service/src/googleFormUrls.ts create mode 100644 server/proxy-service/src/googleFormsApi.ts create mode 100644 server/proxy-service/src/googleProxy.ts create mode 100644 server/proxy-service/src/googleSession.ts create mode 100644 server/proxy-service/src/googleUrls.ts create mode 100644 server/proxy-service/src/server.ts create mode 100644 server/proxy-service/src/session.ts create mode 100644 server/proxy-service/src/urls.ts create mode 100644 server/proxy-service/src/websiteAllowlist.ts create mode 100644 server/proxy-service/src/websiteProxy.ts create mode 100644 server/proxy-service/src/websiteSession.ts create mode 100644 server/proxy-service/src/websiteUrls.ts create mode 100644 server/proxy-service/tsconfig.json create mode 100644 server/proxy-service/yarn.lock diff --git a/client/packages/lowcoder/vite.config.mts b/client/packages/lowcoder/vite.config.mts index 72644a35ce..ea671442a7 100644 --- a/client/packages/lowcoder/vite.config.mts +++ b/client/packages/lowcoder/vite.config.mts @@ -35,11 +35,17 @@ if (!apiServiceUrl && isDev) { process.exit(1); } +const proxyServiceUrl = process.env.LOWCODER_PROXY_SERVICE_URL || "http://localhost:6070"; + const proxyConfig: ServerOptions["proxy"] = { "/api": { target: apiServiceUrl, changeOrigin: false, }, + "/proxy": { + target: proxyServiceUrl, + changeOrigin: true, + }, }; if (nodeServiceUrl) { diff --git a/deploy/docker/default-multi.env b/deploy/docker/default-multi.env index 7daba8e66e..3dcb666fd5 100644 --- a/deploy/docker/default-multi.env +++ b/deploy/docker/default-multi.env @@ -18,4 +18,5 @@ LOWCODER_MONGODB_URL="mongodb://lowcoder:secret123@mongodb/lowcoder?authSource=a LOWCODER_REDIS_URL="redis://redis:6379" LOWCODER_NODE_SERVICE_URL="http://lowcoder-node-service:6060" LOWCODER_API_SERVICE_URL="http://lowcoder-api-service:8080" +LOWCODER_PROXY_SERVICE_URL="http://lowcoder-proxy-service:6070" diff --git a/deploy/docker/default.env b/deploy/docker/default.env index d37f0ce817..db82a2ace8 100644 --- a/deploy/docker/default.env +++ b/deploy/docker/default.env @@ -104,6 +104,21 @@ LOWCODER_API_RATE_LIMIT=100 LOWCODER_API_SERVICE_URL="http://localhost:8080" # Lowcoder Node service URL LOWCODER_NODE_SERVICE_URL="http://localhost:6060" +# Lowcoder Proxy service URL +# Local dev (proxy running on host): http://localhost:6070 +# Docker multi setup: http://lowcoder-proxy-service:6070 +LOWCODER_PROXY_SERVICE_URL="http://localhost:6070" +# Typeform proxy rate limit per minute per client IP +LOWCODER_PROXY_RATE_LIMIT=120 +# Allowed upstream hosts for Typeform proxy +LOWCODER_PROXY_ALLOWED_HOSTS="form.typeform.com,embed.typeform.com,admin.typeform.com" +# Allowed upstream hosts for the Google Forms proxy (public forms) +LOWCODER_GOOGLE_FORMS_ALLOWED_HOSTS="docs.google.com" +# Hocuspocus URL injected into form iframe bridges for driver/follower sync +LOWCODER_HOCUSPOCUS_URL="ws://localhost:3006" +LOWCODER_HOCUSPOCUS_SECRET="" +# API service URL used by proxy-service to resolve logged-in user for session creation +LOWCODER_API_SERVICE_URL="http://localhost:8080" # # ! PLEASE CHANGE THESE TO SOMETHING UNIQUE ! diff --git a/deploy/docker/docker-compose-multi.yaml b/deploy/docker/docker-compose-multi.yaml index bb92c31873..85ad50b0a2 100644 --- a/deploy/docker/docker-compose-multi.yaml +++ b/deploy/docker/docker-compose-multi.yaml @@ -110,6 +110,34 @@ services: test: curl -sS http://lowcoder-node-service:6060 | grep -c "Lowcoder Node Service is up and running" > /dev/null interval: 3s timeout: 5s + retries: 15 + start_period: 40s + + lowcoder-proxy-service: + build: + context: ../../server/proxy-service + dockerfile: Dockerfile + container_name: lowcoder-proxy-service + ports: + - "6070:6070" + environment: + PROXY_SERVICE_PORT: "6070" + env_file: + - path: ./default.env + required: true + - path: ./default-multi.env + required: true + - path: ./override.env + required: false + restart: unless-stopped + depends_on: + lowcoder-api-service: + condition: service_healthy + restart: true + healthcheck: + test: curl -sS http://lowcoder-proxy-service:6070 | grep -c "Lowcoder Proxy Service is up and running" > /dev/null + interval: 3s + timeout: 5s retries: 10 ## @@ -135,6 +163,9 @@ services: lowcoder-api-service: condition: service_healthy restart: true + lowcoder-proxy-service: + condition: service_healthy + restart: true volumes: - ./lowcoder-stacks/assets:/lowcoder/assets - ./lowcoder-stacks/ssl:/lowcoder-stacks/ssl diff --git a/deploy/docker/frontend/01-update-nginx-conf.sh b/deploy/docker/frontend/01-update-nginx-conf.sh old mode 100644 new mode 100755 diff --git a/deploy/docker/frontend/server.conf b/deploy/docker/frontend/server.conf index b068162982..2bbcd740a6 100644 --- a/deploy/docker/frontend/server.conf +++ b/deploy/docker/frontend/server.conf @@ -56,3 +56,15 @@ proxy_pass __LOWCODER_NODE_SERVICE_URL__; } + location /proxy/ { + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Real-IP $remote_addr; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_read_timeout 120s; + proxy_pass __LOWCODER_PROXY_SERVICE_URL__; + } + diff --git a/deploy/docker/override.env b/deploy/docker/override.env index 8785627b8c..f15ae05422 100644 --- a/deploy/docker/override.env +++ b/deploy/docker/override.env @@ -6,4 +6,9 @@ ## ## ##################################################################### +# Proxy service runs in Docker (lowcoder-proxy-service container). +# Frontend nginx forwards /proxy/* to the proxy container on the internal network. +LOWCODER_PROXY_SERVICE_URL="http://lowcoder-proxy-service:6070" +# Public URL used when proxy-service builds proxied iframe links for the browser. +LOWCODER_PUBLIC_URL="http://localhost:3000/" diff --git a/server/proxy-service/.dockerignore b/server/proxy-service/.dockerignore new file mode 100644 index 0000000000..f5048a0b0a --- /dev/null +++ b/server/proxy-service/.dockerignore @@ -0,0 +1,4 @@ +node_modules +build +yarn.lock +package-lock.json diff --git a/server/proxy-service/Dockerfile b/server/proxy-service/Dockerfile new file mode 100644 index 0000000000..3ed02b2f17 --- /dev/null +++ b/server/proxy-service/Dockerfile @@ -0,0 +1,15 @@ +FROM node:20-alpine + +WORKDIR /lowcoder/proxy-service + +RUN apk add --no-cache curl + +COPY package.json ./ +RUN yarn install || npm install + +COPY . . +RUN yarn build || npm run build + +EXPOSE 6070 + +CMD ["node", "build/server.js"] diff --git a/server/proxy-service/build/auth.js b/server/proxy-service/build/auth.js new file mode 100644 index 0000000000..76f60aaf5a --- /dev/null +++ b/server/proxy-service/build/auth.js @@ -0,0 +1,83 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.getSigningSecret = getSigningSecret; +exports.createProxyToken = createProxyToken; +exports.verifyProxyToken = verifyProxyToken; +exports.getBearerToken = getBearerToken; +exports.resolveParticipantId = resolveParticipantId; +exports.resolveEditorId = resolveEditorId; +const jsonwebtoken_1 = __importDefault(require("jsonwebtoken")); +const node_fetch_1 = __importDefault(require("node-fetch")); +const node_crypto_1 = require("node:crypto"); +const API_KEY_SECRET = process.env.LOWCODER_API_KEY_SECRET ?? ""; +const TOKEN_TTL_MS = 60 * 60 * 1000; +const API_SERVICE_URL = (process.env.LOWCODER_API_SERVICE_URL ?? "http://localhost:8080").replace(/\/$/, ""); +function getSigningSecret() { + if (!API_KEY_SECRET) + return null; + return Buffer.from(API_KEY_SECRET).toString("base64"); +} +function createProxyToken(userId, roomId, role, scope = "typeform-proxy") { + const secret = getSigningSecret(); + if (!secret) { + return jsonwebtoken_1.default.sign({ userId, roomId, role, scope }, "dev-proxy-secret", { + expiresIn: "1h", + }); + } + return jsonwebtoken_1.default.sign({ sub: userId, userId, roomId, role, scope }, secret, { + algorithm: "HS256", + expiresIn: "1h", + }); +} +function verifyProxyToken(token, expectedScope) { + if (!token) + return false; + const secret = getSigningSecret(); + if (!secret) + return true; + try { + const payload = jsonwebtoken_1.default.verify(token, secret); + return !expectedScope || payload.scope === expectedScope; + } + catch { + return false; + } +} +function getBearerToken(authHeader) { + if (!authHeader || !authHeader.startsWith("Bearer ")) + return null; + return authHeader.slice("Bearer ".length); +} +async function resolveParticipantId(req, options = {}) { + const editorId = options.editorId?.trim(); + if (editorId) + return editorId; + const guestId = options.guestId?.trim(); + if (guestId) + return guestId; + const cookie = req.headers.cookie; + if (cookie) { + const response = await (0, node_fetch_1.default)(`${API_SERVICE_URL}/api/users/me`, { + headers: { cookie }, + }); + if (response.ok) { + const payload = (await response.json()); + const userId = payload?.data?.id?.trim(); + if (userId) + return userId; + } + } + const roomId = options.roomId?.trim(); + const role = (options.role?.trim() || "driver").trim() || "driver"; + if (roomId) { + return `guest-${roomId}-${role}`; + } + return `guest-${(0, node_crypto_1.randomUUID)()}`; +} +/** @deprecated Use resolveParticipantId */ +async function resolveEditorId(req, fallbackEditorId) { + return resolveParticipantId(req, { editorId: fallbackEditorId }); +} diff --git a/server/proxy-service/build/bridge/google-forms-bridge.js b/server/proxy-service/build/bridge/google-forms-bridge.js new file mode 100644 index 0000000000..a12b2b5e46 --- /dev/null +++ b/server/proxy-service/build/bridge/google-forms-bridge.js @@ -0,0 +1,11635 @@ +(() => { + var __defProp = Object.defineProperty; + var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; + var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); + + // node_modules/lib0/map.js + var create = () => /* @__PURE__ */ new Map(); + var copy = (m) => { + const r = create(); + m.forEach((v, k) => { + r.set(k, v); + }); + return r; + }; + var setIfUndefined = (map2, key, createT) => { + let set = map2.get(key); + if (set === void 0) { + map2.set(key, set = createT()); + } + return set; + }; + var map = (m, f) => { + const res = []; + for (const [key, value] of m) { + res.push(f(value, key)); + } + return res; + }; + var any = (m, f) => { + for (const [key, value] of m) { + if (f(value, key)) { + return true; + } + } + return false; + }; + + // node_modules/lib0/set.js + var create2 = () => /* @__PURE__ */ new Set(); + + // node_modules/lib0/array.js + var last = (arr) => arr[arr.length - 1]; + var appendTo = (dest, src) => { + for (let i = 0; i < src.length; i++) { + dest.push(src[i]); + } + }; + var from = Array.from; + var every = (arr, f) => { + for (let i = 0; i < arr.length; i++) { + if (!f(arr[i], i, arr)) { + return false; + } + } + return true; + }; + var some = (arr, f) => { + for (let i = 0; i < arr.length; i++) { + if (f(arr[i], i, arr)) { + return true; + } + } + return false; + }; + var unfold = (len, f) => { + const array = new Array(len); + for (let i = 0; i < len; i++) { + array[i] = f(i, array); + } + return array; + }; + var isArray = Array.isArray; + + // node_modules/lib0/observable.js + var ObservableV2 = class { + constructor() { + this._observers = create(); + } + /** + * @template {keyof EVENTS & string} NAME + * @param {NAME} name + * @param {EVENTS[NAME]} f + */ + on(name, f) { + setIfUndefined( + this._observers, + /** @type {string} */ + name, + create2 + ).add(f); + return f; + } + /** + * @template {keyof EVENTS & string} NAME + * @param {NAME} name + * @param {EVENTS[NAME]} f + */ + once(name, f) { + const _f = (...args2) => { + this.off( + name, + /** @type {any} */ + _f + ); + f(...args2); + }; + this.on( + name, + /** @type {any} */ + _f + ); + } + /** + * @template {keyof EVENTS & string} NAME + * @param {NAME} name + * @param {EVENTS[NAME]} f + */ + off(name, f) { + const observers = this._observers.get(name); + if (observers !== void 0) { + observers.delete(f); + if (observers.size === 0) { + this._observers.delete(name); + } + } + } + /** + * Emit a named event. All registered event listeners that listen to the + * specified name will receive the event. + * + * @todo This should catch exceptions + * + * @template {keyof EVENTS & string} NAME + * @param {NAME} name The event name. + * @param {Parameters} args The arguments that are applied to the event listener. + */ + emit(name, args2) { + return from((this._observers.get(name) || create()).values()).forEach((f) => f(...args2)); + } + destroy() { + this._observers = create(); + } + }; + + // node_modules/lib0/math.js + var floor = Math.floor; + var abs = Math.abs; + var min = (a, b) => a < b ? a : b; + var max = (a, b) => a > b ? a : b; + var isNaN = Number.isNaN; + var isNegativeZero = (n) => n !== 0 ? n < 0 : 1 / n < 0; + + // node_modules/lib0/binary.js + var BIT1 = 1; + var BIT2 = 2; + var BIT3 = 4; + var BIT4 = 8; + var BIT6 = 32; + var BIT7 = 64; + var BIT8 = 128; + var BIT18 = 1 << 17; + var BIT19 = 1 << 18; + var BIT20 = 1 << 19; + var BIT21 = 1 << 20; + var BIT22 = 1 << 21; + var BIT23 = 1 << 22; + var BIT24 = 1 << 23; + var BIT25 = 1 << 24; + var BIT26 = 1 << 25; + var BIT27 = 1 << 26; + var BIT28 = 1 << 27; + var BIT29 = 1 << 28; + var BIT30 = 1 << 29; + var BIT31 = 1 << 30; + var BIT32 = 1 << 31; + var BITS5 = 31; + var BITS6 = 63; + var BITS7 = 127; + var BITS17 = BIT18 - 1; + var BITS18 = BIT19 - 1; + var BITS19 = BIT20 - 1; + var BITS20 = BIT21 - 1; + var BITS21 = BIT22 - 1; + var BITS22 = BIT23 - 1; + var BITS23 = BIT24 - 1; + var BITS24 = BIT25 - 1; + var BITS25 = BIT26 - 1; + var BITS26 = BIT27 - 1; + var BITS27 = BIT28 - 1; + var BITS28 = BIT29 - 1; + var BITS29 = BIT30 - 1; + var BITS30 = BIT31 - 1; + var BITS31 = 2147483647; + + // node_modules/lib0/number.js + var MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER; + var MIN_SAFE_INTEGER = Number.MIN_SAFE_INTEGER; + var LOWEST_INT32 = 1 << 31; + var isInteger = Number.isInteger || ((num) => typeof num === "number" && isFinite(num) && floor(num) === num); + var isNaN2 = Number.isNaN; + var parseInt = Number.parseInt; + + // node_modules/lib0/string.js + var fromCharCode = String.fromCharCode; + var fromCodePoint = String.fromCodePoint; + var MAX_UTF16_CHARACTER = fromCharCode(65535); + var toLowerCase = (s) => s.toLowerCase(); + var trimLeftRegex = /^\s*/g; + var trimLeft = (s) => s.replace(trimLeftRegex, ""); + var fromCamelCaseRegex = /([A-Z])/g; + var fromCamelCase = (s, separator) => trimLeft(s.replace(fromCamelCaseRegex, (match2) => `${separator}${toLowerCase(match2)}`)); + var _encodeUtf8Polyfill = (str) => { + const encodedString = unescape(encodeURIComponent(str)); + const len = encodedString.length; + const buf = new Uint8Array(len); + for (let i = 0; i < len; i++) { + buf[i] = /** @type {number} */ + encodedString.codePointAt(i); + } + return buf; + }; + var utf8TextEncoder = ( + /** @type {TextEncoder} */ + typeof TextEncoder !== "undefined" ? new TextEncoder() : null + ); + var _encodeUtf8Native = (str) => utf8TextEncoder.encode(str); + var encodeUtf8 = utf8TextEncoder ? _encodeUtf8Native : _encodeUtf8Polyfill; + var utf8TextDecoder = typeof TextDecoder === "undefined" ? null : new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }); + if (utf8TextDecoder && utf8TextDecoder.decode(new Uint8Array()).length === 1) { + utf8TextDecoder = null; + } + var repeat = (source, n) => unfold(n, () => source).join(""); + + // node_modules/lib0/encoding.js + var Encoder = class { + constructor() { + this.cpos = 0; + this.cbuf = new Uint8Array(100); + this.bufs = []; + } + }; + var createEncoder = () => new Encoder(); + var length = (encoder) => { + let len = encoder.cpos; + for (let i = 0; i < encoder.bufs.length; i++) { + len += encoder.bufs[i].length; + } + return len; + }; + var toUint8Array = (encoder) => { + const uint8arr = new Uint8Array(length(encoder)); + let curPos = 0; + for (let i = 0; i < encoder.bufs.length; i++) { + const d = encoder.bufs[i]; + uint8arr.set(d, curPos); + curPos += d.length; + } + uint8arr.set(new Uint8Array(encoder.cbuf.buffer, 0, encoder.cpos), curPos); + return uint8arr; + }; + var verifyLen = (encoder, len) => { + const bufferLen = encoder.cbuf.length; + if (bufferLen - encoder.cpos < len) { + encoder.bufs.push(new Uint8Array(encoder.cbuf.buffer, 0, encoder.cpos)); + encoder.cbuf = new Uint8Array(max(bufferLen, len) * 2); + encoder.cpos = 0; + } + }; + var write = (encoder, num) => { + const bufferLen = encoder.cbuf.length; + if (encoder.cpos === bufferLen) { + encoder.bufs.push(encoder.cbuf); + encoder.cbuf = new Uint8Array(bufferLen * 2); + encoder.cpos = 0; + } + encoder.cbuf[encoder.cpos++] = num; + }; + var writeUint8 = write; + var writeVarUint = (encoder, num) => { + while (num > BITS7) { + write(encoder, BIT8 | BITS7 & num); + num = floor(num / 128); + } + write(encoder, BITS7 & num); + }; + var writeVarInt = (encoder, num) => { + const isNegative = isNegativeZero(num); + if (isNegative) { + num = -num; + } + write(encoder, (num > BITS6 ? BIT8 : 0) | (isNegative ? BIT7 : 0) | BITS6 & num); + num = floor(num / 64); + while (num > 0) { + write(encoder, (num > BITS7 ? BIT8 : 0) | BITS7 & num); + num = floor(num / 128); + } + }; + var _strBuffer = new Uint8Array(3e4); + var _maxStrBSize = _strBuffer.length / 3; + var _writeVarStringNative = (encoder, str) => { + if (str.length < _maxStrBSize) { + const written = utf8TextEncoder.encodeInto(str, _strBuffer).written || 0; + writeVarUint(encoder, written); + for (let i = 0; i < written; i++) { + write(encoder, _strBuffer[i]); + } + } else { + writeVarUint8Array(encoder, encodeUtf8(str)); + } + }; + var _writeVarStringPolyfill = (encoder, str) => { + const encodedString = unescape(encodeURIComponent(str)); + const len = encodedString.length; + writeVarUint(encoder, len); + for (let i = 0; i < len; i++) { + write( + encoder, + /** @type {number} */ + encodedString.codePointAt(i) + ); + } + }; + var writeVarString = utf8TextEncoder && /** @type {any} */ + utf8TextEncoder.encodeInto ? _writeVarStringNative : _writeVarStringPolyfill; + var writeUint8Array = (encoder, uint8Array) => { + const bufferLen = encoder.cbuf.length; + const cpos = encoder.cpos; + const leftCopyLen = min(bufferLen - cpos, uint8Array.length); + const rightCopyLen = uint8Array.length - leftCopyLen; + encoder.cbuf.set(uint8Array.subarray(0, leftCopyLen), cpos); + encoder.cpos += leftCopyLen; + if (rightCopyLen > 0) { + encoder.bufs.push(encoder.cbuf); + encoder.cbuf = new Uint8Array(max(bufferLen * 2, rightCopyLen)); + encoder.cbuf.set(uint8Array.subarray(leftCopyLen)); + encoder.cpos = rightCopyLen; + } + }; + var writeVarUint8Array = (encoder, uint8Array) => { + writeVarUint(encoder, uint8Array.byteLength); + writeUint8Array(encoder, uint8Array); + }; + var writeOnDataView = (encoder, len) => { + verifyLen(encoder, len); + const dview = new DataView(encoder.cbuf.buffer, encoder.cpos, len); + encoder.cpos += len; + return dview; + }; + var writeFloat32 = (encoder, num) => writeOnDataView(encoder, 4).setFloat32(0, num, false); + var writeFloat64 = (encoder, num) => writeOnDataView(encoder, 8).setFloat64(0, num, false); + var writeBigInt64 = (encoder, num) => ( + /** @type {any} */ + writeOnDataView(encoder, 8).setBigInt64(0, num, false) + ); + var floatTestBed = new DataView(new ArrayBuffer(4)); + var isFloat32 = (num) => { + floatTestBed.setFloat32(0, num); + return floatTestBed.getFloat32(0) === num; + }; + var writeAny = (encoder, data) => { + switch (typeof data) { + case "string": + write(encoder, 119); + writeVarString(encoder, data); + break; + case "number": + if (isInteger(data) && abs(data) <= BITS31) { + write(encoder, 125); + writeVarInt(encoder, data); + } else if (isFloat32(data)) { + write(encoder, 124); + writeFloat32(encoder, data); + } else { + write(encoder, 123); + writeFloat64(encoder, data); + } + break; + case "bigint": + write(encoder, 122); + writeBigInt64(encoder, data); + break; + case "object": + if (data === null) { + write(encoder, 126); + } else if (isArray(data)) { + write(encoder, 117); + writeVarUint(encoder, data.length); + for (let i = 0; i < data.length; i++) { + writeAny(encoder, data[i]); + } + } else if (data instanceof Uint8Array) { + write(encoder, 116); + writeVarUint8Array(encoder, data); + } else { + write(encoder, 118); + const keys3 = Object.keys(data); + writeVarUint(encoder, keys3.length); + for (let i = 0; i < keys3.length; i++) { + const key = keys3[i]; + writeVarString(encoder, key); + writeAny(encoder, data[key]); + } + } + break; + case "boolean": + write(encoder, data ? 120 : 121); + break; + default: + write(encoder, 127); + } + }; + var RleEncoder = class extends Encoder { + /** + * @param {function(Encoder, T):void} writer + */ + constructor(writer) { + super(); + this.w = writer; + this.s = null; + this.count = 0; + } + /** + * @param {T} v + */ + write(v) { + if (this.s === v) { + this.count++; + } else { + if (this.count > 0) { + writeVarUint(this, this.count - 1); + } + this.count = 1; + this.w(this, v); + this.s = v; + } + } + }; + var flushUintOptRleEncoder = (encoder) => { + if (encoder.count > 0) { + writeVarInt(encoder.encoder, encoder.count === 1 ? encoder.s : -encoder.s); + if (encoder.count > 1) { + writeVarUint(encoder.encoder, encoder.count - 2); + } + } + }; + var UintOptRleEncoder = class { + constructor() { + this.encoder = new Encoder(); + this.s = 0; + this.count = 0; + } + /** + * @param {number} v + */ + write(v) { + if (this.s === v) { + this.count++; + } else { + flushUintOptRleEncoder(this); + this.count = 1; + this.s = v; + } + } + /** + * Flush the encoded state and transform this to a Uint8Array. + * + * Note that this should only be called once. + */ + toUint8Array() { + flushUintOptRleEncoder(this); + return toUint8Array(this.encoder); + } + }; + var flushIntDiffOptRleEncoder = (encoder) => { + if (encoder.count > 0) { + const encodedDiff = encoder.diff * 2 + (encoder.count === 1 ? 0 : 1); + writeVarInt(encoder.encoder, encodedDiff); + if (encoder.count > 1) { + writeVarUint(encoder.encoder, encoder.count - 2); + } + } + }; + var IntDiffOptRleEncoder = class { + constructor() { + this.encoder = new Encoder(); + this.s = 0; + this.count = 0; + this.diff = 0; + } + /** + * @param {number} v + */ + write(v) { + if (this.diff === v - this.s) { + this.s = v; + this.count++; + } else { + flushIntDiffOptRleEncoder(this); + this.count = 1; + this.diff = v - this.s; + this.s = v; + } + } + /** + * Flush the encoded state and transform this to a Uint8Array. + * + * Note that this should only be called once. + */ + toUint8Array() { + flushIntDiffOptRleEncoder(this); + return toUint8Array(this.encoder); + } + }; + var StringEncoder = class { + constructor() { + this.sarr = []; + this.s = ""; + this.lensE = new UintOptRleEncoder(); + } + /** + * @param {string} string + */ + write(string) { + this.s += string; + if (this.s.length > 19) { + this.sarr.push(this.s); + this.s = ""; + } + this.lensE.write(string.length); + } + toUint8Array() { + const encoder = new Encoder(); + this.sarr.push(this.s); + this.s = ""; + writeVarString(encoder, this.sarr.join("")); + writeUint8Array(encoder, this.lensE.toUint8Array()); + return toUint8Array(encoder); + } + }; + + // node_modules/lib0/error.js + var create3 = (s) => new Error(s); + var methodUnimplemented = () => { + throw create3("Method unimplemented"); + }; + var unexpectedCase = () => { + throw create3("Unexpected case"); + }; + + // node_modules/lib0/decoding.js + var errorUnexpectedEndOfArray = create3("Unexpected end of array"); + var errorIntegerOutOfRange = create3("Integer out of Range"); + var Decoder = class { + /** + * @param {Uint8Array} uint8Array Binary data to decode + */ + constructor(uint8Array) { + this.arr = uint8Array; + this.pos = 0; + } + }; + var createDecoder = (uint8Array) => new Decoder(uint8Array); + var hasContent = (decoder) => decoder.pos !== decoder.arr.length; + var readUint8Array = (decoder, len) => { + const view = new Uint8Array(decoder.arr.buffer, decoder.pos + decoder.arr.byteOffset, len); + decoder.pos += len; + return view; + }; + var readVarUint8Array = (decoder) => readUint8Array(decoder, readVarUint(decoder)); + var readUint8 = (decoder) => decoder.arr[decoder.pos++]; + var readVarUint = (decoder) => { + let num = 0; + let mult = 1; + const len = decoder.arr.length; + while (decoder.pos < len) { + const r = decoder.arr[decoder.pos++]; + num = num + (r & BITS7) * mult; + mult *= 128; + if (r < BIT8) { + return num; + } + if (num > MAX_SAFE_INTEGER) { + throw errorIntegerOutOfRange; + } + } + throw errorUnexpectedEndOfArray; + }; + var readVarInt = (decoder) => { + let r = decoder.arr[decoder.pos++]; + let num = r & BITS6; + let mult = 64; + const sign = (r & BIT7) > 0 ? -1 : 1; + if ((r & BIT8) === 0) { + return sign * num; + } + const len = decoder.arr.length; + while (decoder.pos < len) { + r = decoder.arr[decoder.pos++]; + num = num + (r & BITS7) * mult; + mult *= 128; + if (r < BIT8) { + return sign * num; + } + if (num > MAX_SAFE_INTEGER) { + throw errorIntegerOutOfRange; + } + } + throw errorUnexpectedEndOfArray; + }; + var _readVarStringPolyfill = (decoder) => { + let remainingLen = readVarUint(decoder); + if (remainingLen === 0) { + return ""; + } else { + let encodedString = String.fromCodePoint(readUint8(decoder)); + if (--remainingLen < 100) { + while (remainingLen--) { + encodedString += String.fromCodePoint(readUint8(decoder)); + } + } else { + while (remainingLen > 0) { + const nextLen = remainingLen < 1e4 ? remainingLen : 1e4; + const bytes = decoder.arr.subarray(decoder.pos, decoder.pos + nextLen); + decoder.pos += nextLen; + encodedString += String.fromCodePoint.apply( + null, + /** @type {any} */ + bytes + ); + remainingLen -= nextLen; + } + } + return decodeURIComponent(escape(encodedString)); + } + }; + var _readVarStringNative = (decoder) => ( + /** @type any */ + utf8TextDecoder.decode(readVarUint8Array(decoder)) + ); + var readVarString = utf8TextDecoder ? _readVarStringNative : _readVarStringPolyfill; + var readFromDataView = (decoder, len) => { + const dv = new DataView(decoder.arr.buffer, decoder.arr.byteOffset + decoder.pos, len); + decoder.pos += len; + return dv; + }; + var readFloat32 = (decoder) => readFromDataView(decoder, 4).getFloat32(0, false); + var readFloat64 = (decoder) => readFromDataView(decoder, 8).getFloat64(0, false); + var readBigInt64 = (decoder) => ( + /** @type {any} */ + readFromDataView(decoder, 8).getBigInt64(0, false) + ); + var readAnyLookupTable = [ + (decoder) => void 0, + // CASE 127: undefined + (decoder) => null, + // CASE 126: null + readVarInt, + // CASE 125: integer + readFloat32, + // CASE 124: float32 + readFloat64, + // CASE 123: float64 + readBigInt64, + // CASE 122: bigint + (decoder) => false, + // CASE 121: boolean (false) + (decoder) => true, + // CASE 120: boolean (true) + readVarString, + // CASE 119: string + (decoder) => { + const len = readVarUint(decoder); + const obj = {}; + for (let i = 0; i < len; i++) { + const key = readVarString(decoder); + obj[key] = readAny(decoder); + } + return obj; + }, + (decoder) => { + const len = readVarUint(decoder); + const arr = []; + for (let i = 0; i < len; i++) { + arr.push(readAny(decoder)); + } + return arr; + }, + readVarUint8Array + // CASE 116: Uint8Array + ]; + var readAny = (decoder) => readAnyLookupTable[127 - readUint8(decoder)](decoder); + var RleDecoder = class extends Decoder { + /** + * @param {Uint8Array} uint8Array + * @param {function(Decoder):T} reader + */ + constructor(uint8Array, reader) { + super(uint8Array); + this.reader = reader; + this.s = null; + this.count = 0; + } + read() { + if (this.count === 0) { + this.s = this.reader(this); + if (hasContent(this)) { + this.count = readVarUint(this) + 1; + } else { + this.count = -1; + } + } + this.count--; + return ( + /** @type {T} */ + this.s + ); + } + }; + var UintOptRleDecoder = class extends Decoder { + /** + * @param {Uint8Array} uint8Array + */ + constructor(uint8Array) { + super(uint8Array); + this.s = 0; + this.count = 0; + } + read() { + if (this.count === 0) { + this.s = readVarInt(this); + const isNegative = isNegativeZero(this.s); + this.count = 1; + if (isNegative) { + this.s = -this.s; + this.count = readVarUint(this) + 2; + } + } + this.count--; + return ( + /** @type {number} */ + this.s + ); + } + }; + var IntDiffOptRleDecoder = class extends Decoder { + /** + * @param {Uint8Array} uint8Array + */ + constructor(uint8Array) { + super(uint8Array); + this.s = 0; + this.count = 0; + this.diff = 0; + } + /** + * @return {number} + */ + read() { + if (this.count === 0) { + const diff = readVarInt(this); + const hasCount = diff & 1; + this.diff = floor(diff / 2); + this.count = 1; + if (hasCount) { + this.count = readVarUint(this) + 2; + } + } + this.s += this.diff; + this.count--; + return this.s; + } + }; + var StringDecoder = class { + /** + * @param {Uint8Array} uint8Array + */ + constructor(uint8Array) { + this.decoder = new UintOptRleDecoder(uint8Array); + this.str = readVarString(this.decoder); + this.spos = 0; + } + /** + * @return {string} + */ + read() { + const end = this.spos + this.decoder.read(); + const res = this.str.slice(this.spos, end); + this.spos = end; + return res; + } + }; + + // node_modules/lib0/webcrypto.js + var subtle = crypto.subtle; + var getRandomValues = crypto.getRandomValues.bind(crypto); + + // node_modules/lib0/random.js + var uint32 = () => getRandomValues(new Uint32Array(1))[0]; + var uuidv4Template = "10000000-1000-4000-8000" + -1e11; + var uuidv4 = () => uuidv4Template.replace( + /[018]/g, + /** @param {number} c */ + (c) => (c ^ uint32() & 15 >> c / 4).toString(16) + ); + + // node_modules/lib0/time.js + var getUnixTime = Date.now; + + // node_modules/lib0/promise.js + var create4 = (f) => ( + /** @type {Promise} */ + new Promise(f) + ); + var all = Promise.all.bind(Promise); + + // node_modules/lib0/conditions.js + var undefinedToNull = (v) => v === void 0 ? null : v; + + // node_modules/lib0/storage.js + var VarStoragePolyfill = class { + constructor() { + this.map = /* @__PURE__ */ new Map(); + } + /** + * @param {string} key + * @param {any} newValue + */ + setItem(key, newValue) { + this.map.set(key, newValue); + } + /** + * @param {string} key + */ + getItem(key) { + return this.map.get(key); + } + }; + var _localStorage = new VarStoragePolyfill(); + var usePolyfill = true; + try { + if (typeof localStorage !== "undefined" && localStorage) { + _localStorage = localStorage; + usePolyfill = false; + } + } catch (e) { + } + var varStorage = _localStorage; + + // node_modules/lib0/trait/equality.js + var EqualityTraitSymbol = Symbol("Equality"); + var equals = (a, b) => a === b || !!a?.[EqualityTraitSymbol]?.(b) || false; + + // node_modules/lib0/object.js + var isObject = (o) => typeof o === "object"; + var assign = Object.assign; + var keys = Object.keys; + var forEach = (obj, f) => { + for (const key in obj) { + f(obj[key], key); + } + }; + var size = (obj) => keys(obj).length; + var isEmpty = (obj) => { + for (const _k in obj) { + return false; + } + return true; + }; + var every2 = (obj, f) => { + for (const key in obj) { + if (!f(obj[key], key)) { + return false; + } + } + return true; + }; + var hasProperty = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key); + var equalFlat = (a, b) => a === b || size(a) === size(b) && every2(a, (val, key) => (val !== void 0 || hasProperty(b, key)) && equals(b[key], val)); + var freeze = Object.freeze; + var deepFreeze = (o) => { + for (const key in o) { + const c = o[key]; + if (typeof c === "object" || typeof c === "function") { + deepFreeze(o[key]); + } + } + return freeze(o); + }; + + // node_modules/lib0/function.js + var callAll = (fs, args2, i = 0) => { + try { + for (; i < fs.length; i++) { + fs[i](...args2); + } + } finally { + if (i < fs.length) { + callAll(fs, args2, i + 1); + } + } + }; + var id = (a) => a; + var equalityDeep = (a, b) => { + if (a === b) { + return true; + } + if (a == null || b == null || a.constructor !== b.constructor && (a.constructor || Object) !== (b.constructor || Object)) { + return false; + } + if (a[EqualityTraitSymbol] != null) { + return a[EqualityTraitSymbol](b); + } + switch (a.constructor) { + case ArrayBuffer: + a = new Uint8Array(a); + b = new Uint8Array(b); + // eslint-disable-next-line no-fallthrough + case Uint8Array: { + if (a.byteLength !== b.byteLength) { + return false; + } + for (let i = 0; i < a.length; i++) { + if (a[i] !== b[i]) { + return false; + } + } + break; + } + case Set: { + if (a.size !== b.size) { + return false; + } + for (const value of a) { + if (!b.has(value)) { + return false; + } + } + break; + } + case Map: { + if (a.size !== b.size) { + return false; + } + for (const key of a.keys()) { + if (!b.has(key) || !equalityDeep(a.get(key), b.get(key))) { + return false; + } + } + break; + } + case void 0: + case Object: + if (size(a) !== size(b)) { + return false; + } + for (const key in a) { + if (!hasProperty(a, key) || !equalityDeep(a[key], b[key])) { + return false; + } + } + break; + case Array: + if (a.length !== b.length) { + return false; + } + for (let i = 0; i < a.length; i++) { + if (!equalityDeep(a[i], b[i])) { + return false; + } + } + break; + default: + return false; + } + return true; + }; + var isOneOf = (value, options) => options.includes(value); + + // node_modules/lib0/environment.js + var isNode = typeof process !== "undefined" && process.release && /node|io\.js/.test(process.release.name) && Object.prototype.toString.call(typeof process !== "undefined" ? process : 0) === "[object process]"; + var isMac = typeof navigator !== "undefined" ? /Mac/.test(navigator.platform) : false; + var params; + var args = []; + var computeParams = () => { + if (params === void 0) { + if (isNode) { + params = create(); + const pargs = process.argv; + let currParamName = null; + for (let i = 0; i < pargs.length; i++) { + const parg = pargs[i]; + if (parg[0] === "-") { + if (currParamName !== null) { + params.set(currParamName, ""); + } + currParamName = parg; + } else { + if (currParamName !== null) { + params.set(currParamName, parg); + currParamName = null; + } else { + args.push(parg); + } + } + } + if (currParamName !== null) { + params.set(currParamName, ""); + } + } else if (typeof location === "object") { + params = create(); + (location.search || "?").slice(1).split("&").forEach((kv) => { + if (kv.length !== 0) { + const [key, value] = kv.split("="); + params.set(`--${fromCamelCase(key, "-")}`, value); + params.set(`-${fromCamelCase(key, "-")}`, value); + } + }); + } else { + params = create(); + } + } + return params; + }; + var hasParam = (name) => computeParams().has(name); + var getVariable = (name) => isNode ? undefinedToNull(process.env[name.toUpperCase().replaceAll("-", "_")]) : undefinedToNull(varStorage.getItem(name)); + var hasConf = (name) => hasParam("--" + name) || getVariable(name) !== null; + var production = hasConf("production"); + var forceColor = isNode && isOneOf(process.env.FORCE_COLOR, ["true", "1", "2"]); + var supportsColor = forceColor || !hasParam("--no-colors") && // @todo deprecate --no-colors + !hasConf("no-color") && (!isNode || process.stdout.isTTY) && (!isNode || hasParam("--color") || getVariable("COLORTERM") !== null || (getVariable("TERM") || "").includes("color")); + + // node_modules/lib0/buffer.js + var createUint8ArrayFromLen = (len) => new Uint8Array(len); + var copyUint8Array = (uint8Array) => { + const newBuf = createUint8ArrayFromLen(uint8Array.byteLength); + newBuf.set(uint8Array); + return newBuf; + }; + + // node_modules/lib0/pair.js + var Pair = class { + /** + * @param {L} left + * @param {R} right + */ + constructor(left, right) { + this.left = left; + this.right = right; + } + }; + var create5 = (left, right) => new Pair(left, right); + + // node_modules/lib0/prng.js + var bool = (gen) => gen.next() >= 0.5; + var int53 = (gen, min4, max4) => floor(gen.next() * (max4 + 1 - min4) + min4); + var int32 = (gen, min4, max4) => floor(gen.next() * (max4 + 1 - min4) + min4); + var int31 = (gen, min4, max4) => int32(gen, min4, max4); + var letter = (gen) => fromCharCode(int31(gen, 97, 122)); + var word = (gen, minLen = 0, maxLen = 20) => { + const len = int31(gen, minLen, maxLen); + let str = ""; + for (let i = 0; i < len; i++) { + str += letter(gen); + } + return str; + }; + var oneOf = (gen, array) => array[int31(gen, 0, array.length - 1)]; + + // node_modules/lib0/schema.js + var schemaSymbol = Symbol("0schema"); + var ValidationError = class { + constructor() { + this._rerrs = []; + } + /** + * @param {string?} path + * @param {string} expected + * @param {string} has + * @param {string?} message + */ + extend(path, expected, has, message = null) { + this._rerrs.push({ path, expected, has, message }); + } + toString() { + const s = []; + for (let i = this._rerrs.length - 1; i > 0; i--) { + const r = this._rerrs[i]; + s.push(repeat(" ", (this._rerrs.length - i) * 2) + `${r.path != null ? `[${r.path}] ` : ""}${r.has} doesn't match ${r.expected}. ${r.message}`); + } + return s.join("\n"); + } + }; + var shapeExtends = (a, b) => { + if (a === b) return true; + if (a == null || b == null || a.constructor !== b.constructor) return false; + if (a[EqualityTraitSymbol]) return equals(a, b); + if (isArray(a)) { + return every( + a, + (aitem) => some(b, (bitem) => shapeExtends(aitem, bitem)) + ); + } else if (isObject(a)) { + return every2( + a, + (aitem, akey) => shapeExtends(aitem, b[akey]) + ); + } + return false; + }; + var Schema = class { + /** + * @param {Schema} other + */ + extends(other) { + let [a, b] = [ + /** @type {any} */ + this.shape, + /** @type {any} */ + other.shape + ]; + if ( + /** @type {typeof Schema} */ + this.constructor._dilutes + ) [b, a] = [a, b]; + return shapeExtends(a, b); + } + /** + * Overwrite this when necessary. By default, we only check the `shape` property which every shape + * should have. + * @param {Schema} other + */ + equals(other) { + return this.constructor === other.constructor && equalityDeep(this.shape, other.shape); + } + [schemaSymbol]() { + return true; + } + /** + * @param {object} other + */ + [EqualityTraitSymbol](other) { + return this.equals( + /** @type {any} */ + other + ); + } + /** + * Use `schema.validate(obj)` with a typed parameter that is already of typed to be an instance of + * Schema. Validate will check the structure of the parameter and return true iff the instance + * really is an instance of Schema. + * + * @param {T} o + * @return {boolean} + */ + validate(o) { + return this.check(o); + } + /* c8 ignore start */ + /** + * Similar to validate, but this method accepts untyped parameters. + * + * @param {any} _o + * @param {ValidationError} [_err] + * @return {_o is T} + */ + check(_o, _err) { + methodUnimplemented(); + } + /* c8 ignore stop */ + /** + * @type {Schema} + */ + get nullable() { + return $union(this, $null); + } + /** + * @type {$Optional>} + */ + get optional() { + return new $Optional( + /** @type {Schema} */ + this + ); + } + /** + * Cast a variable to a specific type. Returns the casted value, or throws an exception otherwise. + * Use this if you know that the type is of a specific type and you just want to convince the type + * system. + * + * **Do not rely on these error messages!** + * Performs an assertion check only if not in a production environment. + * + * @template OO + * @param {OO} o + * @return {Extract extends never ? T : (OO extends Array ? T : Extract)} + */ + cast(o) { + assert(o, this); + return ( + /** @type {any} */ + o + ); + } + /** + * EXPECTO PATRONUM!! 🪄 + * This function protects against type errors. Though it may not work in the real world. + * + * "After all this time?" + * "Always." - Snape, talking about type safety + * + * Ensures that a variable is a a specific type. Returns the value, or throws an exception if the assertion check failed. + * Use this if you know that the type is of a specific type and you just want to convince the type + * system. + * + * Can be useful when defining lambdas: `s.lambda(s.$number, s.$void).expect((n) => n + 1)` + * + * **Do not rely on these error messages!** + * Performs an assertion check if not in a production environment. + * + * @param {T} o + * @return {o extends T ? T : never} + */ + expect(o) { + assert(o, this); + return o; + } + }; + // this.shape must not be defined on Schema. Otherwise typecheck on metatypes (e.g. $$object) won't work as expected anymore + /** + * If true, the more things are added to the shape the more objects this schema will accept (e.g. + * union). By default, the more objects are added, the the fewer objects this schema will accept. + * @protected + */ + __publicField(Schema, "_dilutes", false); + var $ConstructedBy = class extends Schema { + /** + * @param {C} c + * @param {((o:Instance)=>boolean)|null} check + */ + constructor(c, check) { + super(); + this.shape = c; + this._c = check; + } + /** + * @param {any} o + * @param {ValidationError} [err] + * @return {o is C extends ((...args:any[]) => infer T) ? T : (C extends (new (...args:any[]) => any) ? InstanceType : never)} o + */ + check(o, err = void 0) { + const c = o?.constructor === this.shape && (this._c == null || this._c(o)); + !c && err?.extend(null, this.shape.name, o?.constructor.name, o?.constructor !== this.shape ? "Constructor match failed" : "Check failed"); + return c; + } + }; + var $constructedBy = (c, check = null) => new $ConstructedBy(c, check); + var $$constructedBy = $constructedBy($ConstructedBy); + var $Custom = class extends Schema { + /** + * @param {(o:any) => boolean} check + */ + constructor(check) { + super(); + this.shape = check; + } + /** + * @param {any} o + * @param {ValidationError} err + * @return {o is any} + */ + check(o, err) { + const c = this.shape(o); + !c && err?.extend(null, "custom prop", o?.constructor.name, "failed to check custom prop"); + return c; + } + }; + var $custom = (check) => new $Custom(check); + var $$custom = $constructedBy($Custom); + var $Literal = class extends Schema { + /** + * @param {Array} literals + */ + constructor(literals) { + super(); + this.shape = literals; + } + /** + * + * @param {any} o + * @param {ValidationError} [err] + * @return {o is T} + */ + check(o, err) { + const c = this.shape.some((a) => a === o); + !c && err?.extend(null, this.shape.join(" | "), o.toString()); + return c; + } + }; + var $literal = (...literals) => new $Literal(literals); + var $$literal = $constructedBy($Literal); + var _regexEscape = ( + /** @type {any} */ + RegExp.escape || /** @type {(str:string) => string} */ + ((str) => str.replace(/[().|&,$^[\]]/g, (s) => "\\" + s)) + ); + var _schemaStringTemplateToRegex = (s) => { + if ($string.check(s)) { + return [_regexEscape(s)]; + } + if ($$literal.check(s)) { + return ( + /** @type {Array} */ + s.shape.map((v) => v + "") + ); + } + if ($$number.check(s)) { + return ["[+-]?\\d+.?\\d*"]; + } + if ($$string.check(s)) { + return [".*"]; + } + if ($$union.check(s)) { + return s.shape.map(_schemaStringTemplateToRegex).flat(1); + } + unexpectedCase(); + }; + var $StringTemplate = class extends Schema { + /** + * @param {T} shape + */ + constructor(shape) { + super(); + this.shape = shape; + this._r = new RegExp("^" + shape.map(_schemaStringTemplateToRegex).map((opts) => `(${opts.join("|")})`).join("") + "$"); + } + /** + * @param {any} o + * @param {ValidationError} [err] + * @return {o is CastStringTemplateArgsToTemplate} + */ + check(o, err) { + const c = this._r.exec(o) != null; + !c && err?.extend(null, this._r.toString(), o.toString(), "String doesn't match string template."); + return c; + } + }; + var $$stringTemplate = $constructedBy($StringTemplate); + var isOptionalSymbol = Symbol("optional"); + var $Optional = class extends Schema { + /** + * @param {S} shape + */ + constructor(shape) { + super(); + this.shape = shape; + } + /** + * @param {any} o + * @param {ValidationError} [err] + * @return {o is (Unwrap|undefined)} + */ + check(o, err) { + const c = o === void 0 || this.shape.check(o); + !c && err?.extend(null, "undefined (optional)", "()"); + return c; + } + get [isOptionalSymbol]() { + return true; + } + }; + var $$optional = $constructedBy($Optional); + var $Never = class extends Schema { + /** + * @param {any} _o + * @param {ValidationError} [err] + * @return {_o is never} + */ + check(_o, err) { + err?.extend(null, "never", typeof _o); + return false; + } + }; + var $never = new $Never(); + var $$never = $constructedBy($Never); + var _$Object = class _$Object extends Schema { + /** + * @param {S} shape + * @param {boolean} partial + */ + constructor(shape, partial = false) { + super(); + this.shape = shape; + this._isPartial = partial; + } + /** + * @type {Schema>>} + */ + get partial() { + return new _$Object(this.shape, true); + } + /** + * @param {any} o + * @param {ValidationError} err + * @return {o is $ObjectToType} + */ + check(o, err) { + if (o == null) { + err?.extend(null, "object", "null"); + return false; + } + return every2(this.shape, (vv, vk) => { + const c = this._isPartial && !hasProperty(o, vk) || vv.check(o[vk], err); + !c && err?.extend(vk.toString(), vv.toString(), typeof o[vk], "Object property does not match"); + return c; + }); + } + }; + __publicField(_$Object, "_dilutes", true); + var $Object = _$Object; + var $object = (def) => ( + /** @type {any} */ + new $Object(def) + ); + var $$object = $constructedBy($Object); + var $objectAny = $custom((o) => o != null && (o.constructor === Object || o.constructor == null)); + var $Record = class extends Schema { + /** + * @param {Keys} keys + * @param {Values} values + */ + constructor(keys3, values) { + super(); + this.shape = { + keys: keys3, + values + }; + } + /** + * @param {any} o + * @param {ValidationError} err + * @return {o is { [key in Unwrap]: Unwrap }} + */ + check(o, err) { + return o != null && every2(o, (vv, vk) => { + const ck = this.shape.keys.check(vk, err); + !ck && err?.extend(vk + "", "Record", typeof o, ck ? "Key doesn't match schema" : "Value doesn't match value"); + return ck && this.shape.values.check(vv, err); + }); + } + }; + var $record = (keys3, values) => new $Record(keys3, values); + var $$record = $constructedBy($Record); + var $Tuple = class extends Schema { + /** + * @param {S} shape + */ + constructor(shape) { + super(); + this.shape = shape; + } + /** + * @param {any} o + * @param {ValidationError} err + * @return {o is { [K in keyof S]: S[K] extends Schema ? Type : never }} + */ + check(o, err) { + return o != null && every2(this.shape, (vv, vk) => { + const c = ( + /** @type {Schema} */ + vv.check(o[vk], err) + ); + !c && err?.extend(vk.toString(), "Tuple", typeof vv); + return c; + }); + } + }; + var $tuple = (...def) => new $Tuple(def); + var $$tuple = $constructedBy($Tuple); + var $Array = class extends Schema { + /** + * @param {Array} v + */ + constructor(v) { + super(); + this.shape = v.length === 1 ? v[0] : new $Union(v); + } + /** + * @param {any} o + * @param {ValidationError} [err] + * @return {o is Array ? T : never>} o + */ + check(o, err) { + const c = isArray(o) && every(o, (oi) => this.shape.check(oi)); + !c && err?.extend(null, "Array", ""); + return c; + } + }; + var $array = (...def) => new $Array(def); + var $$array = $constructedBy($Array); + var $arrayAny = $custom((o) => isArray(o)); + var $InstanceOf = class extends Schema { + /** + * @param {new (...args:any) => T} constructor + * @param {((o:T) => boolean)|null} check + */ + constructor(constructor, check) { + super(); + this.shape = constructor; + this._c = check; + } + /** + * @param {any} o + * @param {ValidationError} err + * @return {o is T} + */ + check(o, err) { + const c = o instanceof this.shape && (this._c == null || this._c(o)); + !c && err?.extend(null, this.shape.name, o?.constructor.name); + return c; + } + }; + var $instanceOf = (c, check = null) => new $InstanceOf(c, check); + var $$instanceOf = $constructedBy($InstanceOf); + var $$schema = $instanceOf(Schema); + var $Lambda = class extends Schema { + /** + * @param {Args} args + */ + constructor(args2) { + super(); + this.len = args2.length - 1; + this.args = $tuple(...args2.slice(-1)); + this.res = args2[this.len]; + } + /** + * @param {any} f + * @param {ValidationError} err + * @return {f is _LArgsToLambdaDef} + */ + check(f, err) { + const c = f.constructor === Function && f.length <= this.len; + !c && err?.extend(null, "function", typeof f); + return c; + } + }; + var $$lambda = $constructedBy($Lambda); + var $function = $custom((o) => typeof o === "function"); + var $Intersection = class extends Schema { + /** + * @param {T} v + */ + constructor(v) { + super(); + this.shape = v; + } + /** + * @param {any} o + * @param {ValidationError} [err] + * @return {o is Intersect>} + */ + check(o, err) { + const c = every(this.shape, (check) => check.check(o, err)); + !c && err?.extend(null, "Intersectinon", typeof o); + return c; + } + }; + var $$intersect = $constructedBy($Intersection, (o) => o.shape.length > 0); + var $Union = class extends Schema { + /** + * @param {Array>} v + */ + constructor(v) { + super(); + this.shape = v; + } + /** + * @param {any} o + * @param {ValidationError} [err] + * @return {o is S} + */ + check(o, err) { + const c = some(this.shape, (vv) => vv.check(o, err)); + err?.extend(null, "Union", typeof o); + return c; + } + }; + __publicField($Union, "_dilutes", true); + var $union = (...schemas) => schemas.findIndex(($s) => $$union.check($s)) >= 0 ? $union(...schemas.map(($s) => $($s)).map(($s) => $$union.check($s) ? $s.shape : [$s]).flat(1)) : schemas.length === 1 ? schemas[0] : new $Union(schemas); + var $$union = ( + /** @type {Schema<$Union>} */ + $constructedBy($Union) + ); + var _t = () => true; + var $any = $custom(_t); + var $$any = ( + /** @type {Schema>} */ + $constructedBy($Custom, (o) => o.shape === _t) + ); + var $bigint = $custom((o) => typeof o === "bigint"); + var $$bigint = ( + /** @type {Schema>} */ + $custom((o) => o === $bigint) + ); + var $symbol = $custom((o) => typeof o === "symbol"); + var $$symbol = ( + /** @type {Schema>} */ + $custom((o) => o === $symbol) + ); + var $number = $custom((o) => typeof o === "number"); + var $$number = ( + /** @type {Schema>} */ + $custom((o) => o === $number) + ); + var $string = $custom((o) => typeof o === "string"); + var $$string = ( + /** @type {Schema>} */ + $custom((o) => o === $string) + ); + var $boolean = $custom((o) => typeof o === "boolean"); + var $$boolean = ( + /** @type {Schema>} */ + $custom((o) => o === $boolean) + ); + var $undefined = $literal(void 0); + var $$undefined = ( + /** @type {Schema>} */ + $constructedBy($Literal, (o) => o.shape.length === 1 && o.shape[0] === void 0) + ); + var $void = $literal(void 0); + var $null = $literal(null); + var $$null = ( + /** @type {Schema>} */ + $constructedBy($Literal, (o) => o.shape.length === 1 && o.shape[0] === null) + ); + var $uint8Array = $constructedBy(Uint8Array); + var $$uint8Array = ( + /** @type {Schema>} */ + $constructedBy($ConstructedBy, (o) => o.shape === Uint8Array) + ); + var $primitive = $union($number, $string, $null, $undefined, $bigint, $boolean, $symbol); + var $json = (() => { + const $jsonArr = ( + /** @type {$Array<$any>} */ + $array($any) + ); + const $jsonRecord = ( + /** @type {$Record<$string,$any>} */ + $record($string, $any) + ); + const $json2 = $union($number, $string, $null, $boolean, $jsonArr, $jsonRecord); + $jsonArr.shape = $json2; + $jsonRecord.shape.values = $json2; + return $json2; + })(); + var $ = (o) => { + if ($$schema.check(o)) { + return ( + /** @type {any} */ + o + ); + } else if ($objectAny.check(o)) { + const o2 = {}; + for (const k in o) { + o2[k] = $(o[k]); + } + return ( + /** @type {any} */ + $object(o2) + ); + } else if ($arrayAny.check(o)) { + return ( + /** @type {any} */ + $union(...o.map($)) + ); + } else if ($primitive.check(o)) { + return ( + /** @type {any} */ + $literal(o) + ); + } else if ($function.check(o)) { + return ( + /** @type {any} */ + $constructedBy( + /** @type {any} */ + o + ) + ); + } + unexpectedCase(); + }; + var assert = production ? () => { + } : (o, schema) => { + const err = new ValidationError(); + if (!schema.check(o, err)) { + throw create3(`Expected value to be of type ${schema.constructor.name}. +${err.toString()}`); + } + }; + var PatternMatcher = class { + /** + * @param {Schema} [$state] + */ + constructor($state) { + this.patterns = []; + this.$state = $state; + } + /** + * @template P + * @template R + * @param {P} pattern + * @param {(o:NoInfer>>,s:State)=>R} handler + * @return {PatternMatcher>,R>>} + */ + if(pattern, handler) { + this.patterns.push({ if: $(pattern), h: handler }); + return this; + } + /** + * @template R + * @param {(o:any,s:State)=>R} h + */ + else(h) { + return this.if($any, h); + } + /** + * @return {State extends undefined + * ? >(o:In,state?:undefined)=>PatternMatchResult + * : >(o:In,state:State)=>PatternMatchResult} + */ + done() { + return ( + /** @type {any} */ + (o, s) => { + for (let i = 0; i < this.patterns.length; i++) { + const p = this.patterns[i]; + if (p.if.check(o)) { + return p.h(o, s); + } + } + throw create3("Unhandled pattern"); + } + ); + } + }; + var match = (state) => new PatternMatcher( + /** @type {any} */ + state + ); + var _random = ( + /** @type {any} */ + match( + /** @type {Schema} */ + $any + ).if($$number, (_o, gen) => int53(gen, MIN_SAFE_INTEGER, MAX_SAFE_INTEGER)).if($$string, (_o, gen) => word(gen)).if($$boolean, (_o, gen) => bool(gen)).if($$bigint, (_o, gen) => BigInt(int53(gen, MIN_SAFE_INTEGER, MAX_SAFE_INTEGER))).if($$union, (o, gen) => random(gen, oneOf(gen, o.shape))).if($$object, (o, gen) => { + const res = {}; + for (const k in o.shape) { + let prop = o.shape[k]; + if ($$optional.check(prop)) { + if (bool(gen)) { + continue; + } + prop = prop.shape; + } + res[k] = _random(prop, gen); + } + return res; + }).if($$array, (o, gen) => { + const arr = []; + const n = int32(gen, 0, 42); + for (let i = 0; i < n; i++) { + arr.push(random(gen, o.shape)); + } + return arr; + }).if($$literal, (o, gen) => { + return oneOf(gen, o.shape); + }).if($$null, (o, gen) => { + return null; + }).if($$lambda, (o, gen) => { + const res = random(gen, o.res); + return () => res; + }).if($$any, (o, gen) => random(gen, oneOf(gen, [ + $number, + $string, + $null, + $undefined, + $bigint, + $boolean, + $array($number), + $record($union("a", "b", "c"), $number) + ]))).if($$record, (o, gen) => { + const res = {}; + const keysN = int53(gen, 0, 3); + for (let i = 0; i < keysN; i++) { + const key = random(gen, o.shape.keys); + const val = random(gen, o.shape.values); + res[key] = val; + } + return res; + }).done() + ); + var random = (gen, schema) => ( + /** @type {any} */ + _random($(schema), gen) + ); + + // node_modules/lib0/dom.js + var doc = ( + /** @type {Document} */ + typeof document !== "undefined" ? document : {} + ); + var $fragment = $custom((el) => el.nodeType === DOCUMENT_FRAGMENT_NODE); + var domParser = ( + /** @type {DOMParser} */ + typeof DOMParser !== "undefined" ? new DOMParser() : null + ); + var $element = $custom((el) => el.nodeType === ELEMENT_NODE); + var $text = $custom((el) => el.nodeType === TEXT_NODE); + var mapToStyleString = (m) => map(m, (value, key) => `${key}:${value};`).join(""); + var ELEMENT_NODE = doc.ELEMENT_NODE; + var TEXT_NODE = doc.TEXT_NODE; + var CDATA_SECTION_NODE = doc.CDATA_SECTION_NODE; + var COMMENT_NODE = doc.COMMENT_NODE; + var DOCUMENT_NODE = doc.DOCUMENT_NODE; + var DOCUMENT_TYPE_NODE = doc.DOCUMENT_TYPE_NODE; + var DOCUMENT_FRAGMENT_NODE = doc.DOCUMENT_FRAGMENT_NODE; + var $node = $custom((el) => el.nodeType === DOCUMENT_NODE); + + // node_modules/lib0/symbol.js + var create6 = Symbol; + + // node_modules/lib0/logging.common.js + var BOLD = create6(); + var UNBOLD = create6(); + var BLUE = create6(); + var GREY = create6(); + var GREEN = create6(); + var RED = create6(); + var PURPLE = create6(); + var ORANGE = create6(); + var UNCOLOR = create6(); + var computeNoColorLoggingArgs = (args2) => { + if (args2.length === 1 && args2[0]?.constructor === Function) { + args2 = /** @type {Array} */ + /** @type {[function]} */ + args2[0](); + } + const strBuilder = []; + const logArgs = []; + let i = 0; + for (; i < args2.length; i++) { + const arg = args2[i]; + if (arg === void 0) { + break; + } else if (arg.constructor === String || arg.constructor === Number) { + strBuilder.push(arg); + } else if (arg.constructor === Object) { + break; + } + } + if (i > 0) { + logArgs.push(strBuilder.join("")); + } + for (; i < args2.length; i++) { + const arg = args2[i]; + if (!(arg instanceof Symbol)) { + logArgs.push(arg); + } + } + return logArgs; + }; + var lastLoggingTime = getUnixTime(); + + // node_modules/lib0/logging.js + var _browserStyleMap = { + [BOLD]: create5("font-weight", "bold"), + [UNBOLD]: create5("font-weight", "normal"), + [BLUE]: create5("color", "blue"), + [GREEN]: create5("color", "green"), + [GREY]: create5("color", "grey"), + [RED]: create5("color", "red"), + [PURPLE]: create5("color", "purple"), + [ORANGE]: create5("color", "orange"), + // not well supported in chrome when debugging node with inspector - TODO: deprecate + [UNCOLOR]: create5("color", "black") + }; + var computeBrowserLoggingArgs = (args2) => { + if (args2.length === 1 && args2[0]?.constructor === Function) { + args2 = /** @type {Array} */ + /** @type {[function]} */ + args2[0](); + } + const strBuilder = []; + const styles = []; + const currentStyle = create(); + let logArgs = []; + let i = 0; + for (; i < args2.length; i++) { + const arg = args2[i]; + const style = _browserStyleMap[arg]; + if (style !== void 0) { + currentStyle.set(style.left, style.right); + } else { + if (arg === void 0) { + break; + } + if (arg.constructor === String || arg.constructor === Number) { + const style2 = mapToStyleString(currentStyle); + if (i > 0 || style2.length > 0) { + strBuilder.push("%c" + arg); + styles.push(style2); + } else { + strBuilder.push(arg); + } + } else { + break; + } + } + } + if (i > 0) { + logArgs = styles; + logArgs.unshift(strBuilder.join("")); + } + for (; i < args2.length; i++) { + const arg = args2[i]; + if (!(arg instanceof Symbol)) { + logArgs.push(arg); + } + } + return logArgs; + }; + var computeLoggingArgs = supportsColor ? computeBrowserLoggingArgs : computeNoColorLoggingArgs; + var print = (...args2) => { + console.log(...computeLoggingArgs(args2)); + vconsoles.forEach((vc) => vc.print(args2)); + }; + var warn = (...args2) => { + console.warn(...computeLoggingArgs(args2)); + args2.unshift(ORANGE); + vconsoles.forEach((vc) => vc.print(args2)); + }; + var vconsoles = create2(); + + // node_modules/lib0/iterator.js + var createIterator = (next) => ({ + /** + * @return {IterableIterator} + */ + [Symbol.iterator]() { + return this; + }, + // @ts-ignore + next + }); + var iteratorFilter = (iterator, filter) => createIterator(() => { + let res; + do { + res = iterator.next(); + } while (!res.done && !filter(res.value)); + return res; + }); + var iteratorMap = (iterator, fmap) => createIterator(() => { + const { done, value } = iterator.next(); + return { done, value: done ? void 0 : fmap(value) }; + }); + + // node_modules/yjs/dist/yjs.mjs + var DeleteItem = class { + /** + * @param {number} clock + * @param {number} len + */ + constructor(clock, len) { + this.clock = clock; + this.len = len; + } + }; + var DeleteSet = class { + constructor() { + this.clients = /* @__PURE__ */ new Map(); + } + }; + var iterateDeletedStructs = (transaction, ds, f) => ds.clients.forEach((deletes, clientid) => { + const structs = ( + /** @type {Array} */ + transaction.doc.store.clients.get(clientid) + ); + if (structs != null) { + const lastStruct = structs[structs.length - 1]; + const clockState = lastStruct.id.clock + lastStruct.length; + for (let i = 0, del = deletes[i]; i < deletes.length && del.clock < clockState; del = deletes[++i]) { + iterateStructs(transaction, structs, del.clock, del.len, f); + } + } + }); + var findIndexDS = (dis, clock) => { + let left = 0; + let right = dis.length - 1; + while (left <= right) { + const midindex = floor((left + right) / 2); + const mid = dis[midindex]; + const midclock = mid.clock; + if (midclock <= clock) { + if (clock < midclock + mid.len) { + return midindex; + } + left = midindex + 1; + } else { + right = midindex - 1; + } + } + return null; + }; + var isDeleted = (ds, id2) => { + const dis = ds.clients.get(id2.client); + return dis !== void 0 && findIndexDS(dis, id2.clock) !== null; + }; + var sortAndMergeDeleteSet = (ds) => { + ds.clients.forEach((dels) => { + dels.sort((a, b) => a.clock - b.clock); + let i, j; + for (i = 1, j = 1; i < dels.length; i++) { + const left = dels[j - 1]; + const right = dels[i]; + if (left.clock + left.len >= right.clock) { + dels[j - 1] = new DeleteItem(left.clock, max(left.len, right.clock + right.len - left.clock)); + } else { + if (j < i) { + dels[j] = right; + } + j++; + } + } + dels.length = j; + }); + }; + var mergeDeleteSets = (dss) => { + const merged = new DeleteSet(); + for (let dssI = 0; dssI < dss.length; dssI++) { + dss[dssI].clients.forEach((delsLeft, client) => { + if (!merged.clients.has(client)) { + const dels = delsLeft.slice(); + for (let i = dssI + 1; i < dss.length; i++) { + appendTo(dels, dss[i].clients.get(client) || []); + } + merged.clients.set(client, dels); + } + }); + } + sortAndMergeDeleteSet(merged); + return merged; + }; + var addToDeleteSet = (ds, client, clock, length3) => { + setIfUndefined(ds.clients, client, () => ( + /** @type {Array} */ + [] + )).push(new DeleteItem(clock, length3)); + }; + var createDeleteSet = () => new DeleteSet(); + var createDeleteSetFromStructStore = (ss) => { + const ds = createDeleteSet(); + ss.clients.forEach((structs, client) => { + const dsitems = []; + for (let i = 0; i < structs.length; i++) { + const struct = structs[i]; + if (struct.deleted) { + const clock = struct.id.clock; + let len = struct.length; + if (i + 1 < structs.length) { + for (let next = structs[i + 1]; i + 1 < structs.length && next.deleted; next = structs[++i + 1]) { + len += next.length; + } + } + dsitems.push(new DeleteItem(clock, len)); + } + } + if (dsitems.length > 0) { + ds.clients.set(client, dsitems); + } + }); + return ds; + }; + var writeDeleteSet = (encoder, ds) => { + writeVarUint(encoder.restEncoder, ds.clients.size); + from(ds.clients.entries()).sort((a, b) => b[0] - a[0]).forEach(([client, dsitems]) => { + encoder.resetDsCurVal(); + writeVarUint(encoder.restEncoder, client); + const len = dsitems.length; + writeVarUint(encoder.restEncoder, len); + for (let i = 0; i < len; i++) { + const item = dsitems[i]; + encoder.writeDsClock(item.clock); + encoder.writeDsLen(item.len); + } + }); + }; + var readDeleteSet = (decoder) => { + const ds = new DeleteSet(); + const numClients = readVarUint(decoder.restDecoder); + for (let i = 0; i < numClients; i++) { + decoder.resetDsCurVal(); + const client = readVarUint(decoder.restDecoder); + const numberOfDeletes = readVarUint(decoder.restDecoder); + if (numberOfDeletes > 0) { + const dsField = setIfUndefined(ds.clients, client, () => ( + /** @type {Array} */ + [] + )); + for (let i2 = 0; i2 < numberOfDeletes; i2++) { + dsField.push(new DeleteItem(decoder.readDsClock(), decoder.readDsLen())); + } + } + } + return ds; + }; + var readAndApplyDeleteSet = (decoder, transaction, store) => { + const unappliedDS = new DeleteSet(); + const numClients = readVarUint(decoder.restDecoder); + for (let i = 0; i < numClients; i++) { + decoder.resetDsCurVal(); + const client = readVarUint(decoder.restDecoder); + const numberOfDeletes = readVarUint(decoder.restDecoder); + const structs = store.clients.get(client) || []; + const state = getState(store, client); + for (let i2 = 0; i2 < numberOfDeletes; i2++) { + const clock = decoder.readDsClock(); + const clockEnd = clock + decoder.readDsLen(); + if (clock < state) { + if (state < clockEnd) { + addToDeleteSet(unappliedDS, client, state, clockEnd - state); + } + let index = findIndexSS(structs, clock); + let struct = structs[index]; + if (!struct.deleted && struct.id.clock < clock) { + structs.splice(index + 1, 0, splitItem(transaction, struct, clock - struct.id.clock)); + index++; + } + while (index < structs.length) { + struct = structs[index++]; + if (struct.id.clock < clockEnd) { + if (!struct.deleted) { + if (clockEnd < struct.id.clock + struct.length) { + structs.splice(index, 0, splitItem(transaction, struct, clockEnd - struct.id.clock)); + } + struct.delete(transaction); + } + } else { + break; + } + } + } else { + addToDeleteSet(unappliedDS, client, clock, clockEnd - clock); + } + } + } + if (unappliedDS.clients.size > 0) { + const ds = new UpdateEncoderV2(); + writeVarUint(ds.restEncoder, 0); + writeDeleteSet(ds, unappliedDS); + return ds.toUint8Array(); + } + return null; + }; + var generateNewClientId = uint32; + var Doc = class _Doc extends ObservableV2 { + /** + * @param {DocOpts} opts configuration + */ + constructor({ guid = uuidv4(), collectionid = null, gc = true, gcFilter = () => true, meta = null, autoLoad = false, shouldLoad = true } = {}) { + super(); + this.gc = gc; + this.gcFilter = gcFilter; + this.clientID = generateNewClientId(); + this.guid = guid; + this.collectionid = collectionid; + this.share = /* @__PURE__ */ new Map(); + this.store = new StructStore(); + this._transaction = null; + this._transactionCleanups = []; + this.subdocs = /* @__PURE__ */ new Set(); + this._item = null; + this.shouldLoad = shouldLoad; + this.autoLoad = autoLoad; + this.meta = meta; + this.isLoaded = false; + this.isSynced = false; + this.isDestroyed = false; + this.whenLoaded = create4((resolve) => { + this.on("load", () => { + this.isLoaded = true; + resolve(this); + }); + }); + const provideSyncedPromise = () => create4((resolve) => { + const eventHandler = (isSynced) => { + if (isSynced === void 0 || isSynced === true) { + this.off("sync", eventHandler); + resolve(); + } + }; + this.on("sync", eventHandler); + }); + this.on("sync", (isSynced) => { + if (isSynced === false && this.isSynced) { + this.whenSynced = provideSyncedPromise(); + } + this.isSynced = isSynced === void 0 || isSynced === true; + if (this.isSynced && !this.isLoaded) { + this.emit("load", [this]); + } + }); + this.whenSynced = provideSyncedPromise(); + } + /** + * Notify the parent document that you request to load data into this subdocument (if it is a subdocument). + * + * `load()` might be used in the future to request any provider to load the most current data. + * + * It is safe to call `load()` multiple times. + */ + load() { + const item = this._item; + if (item !== null && !this.shouldLoad) { + transact( + /** @type {any} */ + item.parent.doc, + (transaction) => { + transaction.subdocsLoaded.add(this); + }, + null, + true + ); + } + this.shouldLoad = true; + } + getSubdocs() { + return this.subdocs; + } + getSubdocGuids() { + return new Set(from(this.subdocs).map((doc2) => doc2.guid)); + } + /** + * Changes that happen inside of a transaction are bundled. This means that + * the observer fires _after_ the transaction is finished and that all changes + * that happened inside of the transaction are sent as one message to the + * other peers. + * + * @template T + * @param {function(Transaction):T} f The function that should be executed as a transaction + * @param {any} [origin] Origin of who started the transaction. Will be stored on transaction.origin + * @return T + * + * @public + */ + transact(f, origin = null) { + return transact(this, f, origin); + } + /** + * Define a shared data type. + * + * Multiple calls of `ydoc.get(name, TypeConstructor)` yield the same result + * and do not overwrite each other. I.e. + * `ydoc.get(name, Y.Array) === ydoc.get(name, Y.Array)` + * + * After this method is called, the type is also available on `ydoc.share.get(name)`. + * + * *Best Practices:* + * Define all types right after the Y.Doc instance is created and store them in a separate object. + * Also use the typed methods `getText(name)`, `getArray(name)`, .. + * + * @template {typeof AbstractType} Type + * @example + * const ydoc = new Y.Doc(..) + * const appState = { + * document: ydoc.getText('document') + * comments: ydoc.getArray('comments') + * } + * + * @param {string} name + * @param {Type} TypeConstructor The constructor of the type definition. E.g. Y.Text, Y.Array, Y.Map, ... + * @return {InstanceType} The created type. Constructed with TypeConstructor + * + * @public + */ + get(name, TypeConstructor = ( + /** @type {any} */ + AbstractType + )) { + const type = setIfUndefined(this.share, name, () => { + const t = new TypeConstructor(); + t._integrate(this, null); + return t; + }); + const Constr = type.constructor; + if (TypeConstructor !== AbstractType && Constr !== TypeConstructor) { + if (Constr === AbstractType) { + const t = new TypeConstructor(); + t._map = type._map; + type._map.forEach( + /** @param {Item?} n */ + (n) => { + for (; n !== null; n = n.left) { + n.parent = t; + } + } + ); + t._start = type._start; + for (let n = t._start; n !== null; n = n.right) { + n.parent = t; + } + t._length = type._length; + this.share.set(name, t); + t._integrate(this, null); + return ( + /** @type {InstanceType} */ + t + ); + } else { + throw new Error(`Type with the name ${name} has already been defined with a different constructor`); + } + } + return ( + /** @type {InstanceType} */ + type + ); + } + /** + * @template T + * @param {string} [name] + * @return {YArray} + * + * @public + */ + getArray(name = "") { + return ( + /** @type {YArray} */ + this.get(name, YArray) + ); + } + /** + * @param {string} [name] + * @return {YText} + * + * @public + */ + getText(name = "") { + return this.get(name, YText); + } + /** + * @template T + * @param {string} [name] + * @return {YMap} + * + * @public + */ + getMap(name = "") { + return ( + /** @type {YMap} */ + this.get(name, YMap) + ); + } + /** + * @param {string} [name] + * @return {YXmlElement} + * + * @public + */ + getXmlElement(name = "") { + return ( + /** @type {YXmlElement<{[key:string]:string}>} */ + this.get(name, YXmlElement) + ); + } + /** + * @param {string} [name] + * @return {YXmlFragment} + * + * @public + */ + getXmlFragment(name = "") { + return this.get(name, YXmlFragment); + } + /** + * Converts the entire document into a js object, recursively traversing each yjs type + * Doesn't log types that have not been defined (using ydoc.getType(..)). + * + * @deprecated Do not use this method and rather call toJSON directly on the shared types. + * + * @return {Object} + */ + toJSON() { + const doc2 = {}; + this.share.forEach((value, key) => { + doc2[key] = value.toJSON(); + }); + return doc2; + } + /** + * Emit `destroy` event and unregister all event handlers. + */ + destroy() { + this.isDestroyed = true; + from(this.subdocs).forEach((subdoc) => subdoc.destroy()); + const item = this._item; + if (item !== null) { + this._item = null; + const content = ( + /** @type {ContentDoc} */ + item.content + ); + content.doc = new _Doc({ guid: this.guid, ...content.opts, shouldLoad: false }); + content.doc._item = item; + transact( + /** @type {any} */ + item.parent.doc, + (transaction) => { + const doc2 = content.doc; + if (!item.deleted) { + transaction.subdocsAdded.add(doc2); + } + transaction.subdocsRemoved.add(this); + }, + null, + true + ); + } + this.emit("destroyed", [true]); + this.emit("destroy", [this]); + super.destroy(); + } + }; + var DSDecoderV1 = class { + /** + * @param {decoding.Decoder} decoder + */ + constructor(decoder) { + this.restDecoder = decoder; + } + resetDsCurVal() { + } + /** + * @return {number} + */ + readDsClock() { + return readVarUint(this.restDecoder); + } + /** + * @return {number} + */ + readDsLen() { + return readVarUint(this.restDecoder); + } + }; + var UpdateDecoderV1 = class extends DSDecoderV1 { + /** + * @return {ID} + */ + readLeftID() { + return createID(readVarUint(this.restDecoder), readVarUint(this.restDecoder)); + } + /** + * @return {ID} + */ + readRightID() { + return createID(readVarUint(this.restDecoder), readVarUint(this.restDecoder)); + } + /** + * Read the next client id. + * Use this in favor of readID whenever possible to reduce the number of objects created. + */ + readClient() { + return readVarUint(this.restDecoder); + } + /** + * @return {number} info An unsigned 8-bit integer + */ + readInfo() { + return readUint8(this.restDecoder); + } + /** + * @return {string} + */ + readString() { + return readVarString(this.restDecoder); + } + /** + * @return {boolean} isKey + */ + readParentInfo() { + return readVarUint(this.restDecoder) === 1; + } + /** + * @return {number} info An unsigned 8-bit integer + */ + readTypeRef() { + return readVarUint(this.restDecoder); + } + /** + * Write len of a struct - well suited for Opt RLE encoder. + * + * @return {number} len + */ + readLen() { + return readVarUint(this.restDecoder); + } + /** + * @return {any} + */ + readAny() { + return readAny(this.restDecoder); + } + /** + * @return {Uint8Array} + */ + readBuf() { + return copyUint8Array(readVarUint8Array(this.restDecoder)); + } + /** + * Legacy implementation uses JSON parse. We use any-decoding in v2. + * + * @return {any} + */ + readJSON() { + return JSON.parse(readVarString(this.restDecoder)); + } + /** + * @return {string} + */ + readKey() { + return readVarString(this.restDecoder); + } + }; + var DSDecoderV2 = class { + /** + * @param {decoding.Decoder} decoder + */ + constructor(decoder) { + this.dsCurrVal = 0; + this.restDecoder = decoder; + } + resetDsCurVal() { + this.dsCurrVal = 0; + } + /** + * @return {number} + */ + readDsClock() { + this.dsCurrVal += readVarUint(this.restDecoder); + return this.dsCurrVal; + } + /** + * @return {number} + */ + readDsLen() { + const diff = readVarUint(this.restDecoder) + 1; + this.dsCurrVal += diff; + return diff; + } + }; + var UpdateDecoderV2 = class extends DSDecoderV2 { + /** + * @param {decoding.Decoder} decoder + */ + constructor(decoder) { + super(decoder); + this.keys = []; + readVarUint(decoder); + this.keyClockDecoder = new IntDiffOptRleDecoder(readVarUint8Array(decoder)); + this.clientDecoder = new UintOptRleDecoder(readVarUint8Array(decoder)); + this.leftClockDecoder = new IntDiffOptRleDecoder(readVarUint8Array(decoder)); + this.rightClockDecoder = new IntDiffOptRleDecoder(readVarUint8Array(decoder)); + this.infoDecoder = new RleDecoder(readVarUint8Array(decoder), readUint8); + this.stringDecoder = new StringDecoder(readVarUint8Array(decoder)); + this.parentInfoDecoder = new RleDecoder(readVarUint8Array(decoder), readUint8); + this.typeRefDecoder = new UintOptRleDecoder(readVarUint8Array(decoder)); + this.lenDecoder = new UintOptRleDecoder(readVarUint8Array(decoder)); + } + /** + * @return {ID} + */ + readLeftID() { + return new ID(this.clientDecoder.read(), this.leftClockDecoder.read()); + } + /** + * @return {ID} + */ + readRightID() { + return new ID(this.clientDecoder.read(), this.rightClockDecoder.read()); + } + /** + * Read the next client id. + * Use this in favor of readID whenever possible to reduce the number of objects created. + */ + readClient() { + return this.clientDecoder.read(); + } + /** + * @return {number} info An unsigned 8-bit integer + */ + readInfo() { + return ( + /** @type {number} */ + this.infoDecoder.read() + ); + } + /** + * @return {string} + */ + readString() { + return this.stringDecoder.read(); + } + /** + * @return {boolean} + */ + readParentInfo() { + return this.parentInfoDecoder.read() === 1; + } + /** + * @return {number} An unsigned 8-bit integer + */ + readTypeRef() { + return this.typeRefDecoder.read(); + } + /** + * Write len of a struct - well suited for Opt RLE encoder. + * + * @return {number} + */ + readLen() { + return this.lenDecoder.read(); + } + /** + * @return {any} + */ + readAny() { + return readAny(this.restDecoder); + } + /** + * @return {Uint8Array} + */ + readBuf() { + return readVarUint8Array(this.restDecoder); + } + /** + * This is mainly here for legacy purposes. + * + * Initial we incoded objects using JSON. Now we use the much faster lib0/any-encoder. This method mainly exists for legacy purposes for the v1 encoder. + * + * @return {any} + */ + readJSON() { + return readAny(this.restDecoder); + } + /** + * @return {string} + */ + readKey() { + const keyClock = this.keyClockDecoder.read(); + if (keyClock < this.keys.length) { + return this.keys[keyClock]; + } else { + const key = this.stringDecoder.read(); + this.keys.push(key); + return key; + } + } + }; + var DSEncoderV1 = class { + constructor() { + this.restEncoder = createEncoder(); + } + toUint8Array() { + return toUint8Array(this.restEncoder); + } + resetDsCurVal() { + } + /** + * @param {number} clock + */ + writeDsClock(clock) { + writeVarUint(this.restEncoder, clock); + } + /** + * @param {number} len + */ + writeDsLen(len) { + writeVarUint(this.restEncoder, len); + } + }; + var UpdateEncoderV1 = class extends DSEncoderV1 { + /** + * @param {ID} id + */ + writeLeftID(id2) { + writeVarUint(this.restEncoder, id2.client); + writeVarUint(this.restEncoder, id2.clock); + } + /** + * @param {ID} id + */ + writeRightID(id2) { + writeVarUint(this.restEncoder, id2.client); + writeVarUint(this.restEncoder, id2.clock); + } + /** + * Use writeClient and writeClock instead of writeID if possible. + * @param {number} client + */ + writeClient(client) { + writeVarUint(this.restEncoder, client); + } + /** + * @param {number} info An unsigned 8-bit integer + */ + writeInfo(info) { + writeUint8(this.restEncoder, info); + } + /** + * @param {string} s + */ + writeString(s) { + writeVarString(this.restEncoder, s); + } + /** + * @param {boolean} isYKey + */ + writeParentInfo(isYKey) { + writeVarUint(this.restEncoder, isYKey ? 1 : 0); + } + /** + * @param {number} info An unsigned 8-bit integer + */ + writeTypeRef(info) { + writeVarUint(this.restEncoder, info); + } + /** + * Write len of a struct - well suited for Opt RLE encoder. + * + * @param {number} len + */ + writeLen(len) { + writeVarUint(this.restEncoder, len); + } + /** + * @param {any} any + */ + writeAny(any2) { + writeAny(this.restEncoder, any2); + } + /** + * @param {Uint8Array} buf + */ + writeBuf(buf) { + writeVarUint8Array(this.restEncoder, buf); + } + /** + * @param {any} embed + */ + writeJSON(embed) { + writeVarString(this.restEncoder, JSON.stringify(embed)); + } + /** + * @param {string} key + */ + writeKey(key) { + writeVarString(this.restEncoder, key); + } + }; + var DSEncoderV2 = class { + constructor() { + this.restEncoder = createEncoder(); + this.dsCurrVal = 0; + } + toUint8Array() { + return toUint8Array(this.restEncoder); + } + resetDsCurVal() { + this.dsCurrVal = 0; + } + /** + * @param {number} clock + */ + writeDsClock(clock) { + const diff = clock - this.dsCurrVal; + this.dsCurrVal = clock; + writeVarUint(this.restEncoder, diff); + } + /** + * @param {number} len + */ + writeDsLen(len) { + if (len === 0) { + unexpectedCase(); + } + writeVarUint(this.restEncoder, len - 1); + this.dsCurrVal += len; + } + }; + var UpdateEncoderV2 = class extends DSEncoderV2 { + constructor() { + super(); + this.keyMap = /* @__PURE__ */ new Map(); + this.keyClock = 0; + this.keyClockEncoder = new IntDiffOptRleEncoder(); + this.clientEncoder = new UintOptRleEncoder(); + this.leftClockEncoder = new IntDiffOptRleEncoder(); + this.rightClockEncoder = new IntDiffOptRleEncoder(); + this.infoEncoder = new RleEncoder(writeUint8); + this.stringEncoder = new StringEncoder(); + this.parentInfoEncoder = new RleEncoder(writeUint8); + this.typeRefEncoder = new UintOptRleEncoder(); + this.lenEncoder = new UintOptRleEncoder(); + } + toUint8Array() { + const encoder = createEncoder(); + writeVarUint(encoder, 0); + writeVarUint8Array(encoder, this.keyClockEncoder.toUint8Array()); + writeVarUint8Array(encoder, this.clientEncoder.toUint8Array()); + writeVarUint8Array(encoder, this.leftClockEncoder.toUint8Array()); + writeVarUint8Array(encoder, this.rightClockEncoder.toUint8Array()); + writeVarUint8Array(encoder, toUint8Array(this.infoEncoder)); + writeVarUint8Array(encoder, this.stringEncoder.toUint8Array()); + writeVarUint8Array(encoder, toUint8Array(this.parentInfoEncoder)); + writeVarUint8Array(encoder, this.typeRefEncoder.toUint8Array()); + writeVarUint8Array(encoder, this.lenEncoder.toUint8Array()); + writeUint8Array(encoder, toUint8Array(this.restEncoder)); + return toUint8Array(encoder); + } + /** + * @param {ID} id + */ + writeLeftID(id2) { + this.clientEncoder.write(id2.client); + this.leftClockEncoder.write(id2.clock); + } + /** + * @param {ID} id + */ + writeRightID(id2) { + this.clientEncoder.write(id2.client); + this.rightClockEncoder.write(id2.clock); + } + /** + * @param {number} client + */ + writeClient(client) { + this.clientEncoder.write(client); + } + /** + * @param {number} info An unsigned 8-bit integer + */ + writeInfo(info) { + this.infoEncoder.write(info); + } + /** + * @param {string} s + */ + writeString(s) { + this.stringEncoder.write(s); + } + /** + * @param {boolean} isYKey + */ + writeParentInfo(isYKey) { + this.parentInfoEncoder.write(isYKey ? 1 : 0); + } + /** + * @param {number} info An unsigned 8-bit integer + */ + writeTypeRef(info) { + this.typeRefEncoder.write(info); + } + /** + * Write len of a struct - well suited for Opt RLE encoder. + * + * @param {number} len + */ + writeLen(len) { + this.lenEncoder.write(len); + } + /** + * @param {any} any + */ + writeAny(any2) { + writeAny(this.restEncoder, any2); + } + /** + * @param {Uint8Array} buf + */ + writeBuf(buf) { + writeVarUint8Array(this.restEncoder, buf); + } + /** + * This is mainly here for legacy purposes. + * + * Initial we incoded objects using JSON. Now we use the much faster lib0/any-encoder. This method mainly exists for legacy purposes for the v1 encoder. + * + * @param {any} embed + */ + writeJSON(embed) { + writeAny(this.restEncoder, embed); + } + /** + * Property keys are often reused. For example, in y-prosemirror the key `bold` might + * occur very often. For a 3d application, the key `position` might occur very often. + * + * We cache these keys in a Map and refer to them via a unique number. + * + * @param {string} key + */ + writeKey(key) { + const clock = this.keyMap.get(key); + if (clock === void 0) { + this.keyClockEncoder.write(this.keyClock++); + this.stringEncoder.write(key); + } else { + this.keyClockEncoder.write(clock); + } + } + }; + var writeStructs = (encoder, structs, client, clock) => { + clock = max(clock, structs[0].id.clock); + const startNewStructs = findIndexSS(structs, clock); + writeVarUint(encoder.restEncoder, structs.length - startNewStructs); + encoder.writeClient(client); + writeVarUint(encoder.restEncoder, clock); + const firstStruct = structs[startNewStructs]; + firstStruct.write(encoder, clock - firstStruct.id.clock); + for (let i = startNewStructs + 1; i < structs.length; i++) { + structs[i].write(encoder, 0); + } + }; + var writeClientsStructs = (encoder, store, _sm) => { + const sm = /* @__PURE__ */ new Map(); + _sm.forEach((clock, client) => { + if (getState(store, client) > clock) { + sm.set(client, clock); + } + }); + getStateVector(store).forEach((_clock, client) => { + if (!_sm.has(client)) { + sm.set(client, 0); + } + }); + writeVarUint(encoder.restEncoder, sm.size); + from(sm.entries()).sort((a, b) => b[0] - a[0]).forEach(([client, clock]) => { + writeStructs( + encoder, + /** @type {Array} */ + store.clients.get(client), + client, + clock + ); + }); + }; + var readClientsStructRefs = (decoder, doc2) => { + const clientRefs = create(); + const numOfStateUpdates = readVarUint(decoder.restDecoder); + for (let i = 0; i < numOfStateUpdates; i++) { + const numberOfStructs = readVarUint(decoder.restDecoder); + const refs = new Array(numberOfStructs); + const client = decoder.readClient(); + let clock = readVarUint(decoder.restDecoder); + clientRefs.set(client, { i: 0, refs }); + for (let i2 = 0; i2 < numberOfStructs; i2++) { + const info = decoder.readInfo(); + switch (BITS5 & info) { + case 0: { + const len = decoder.readLen(); + refs[i2] = new GC(createID(client, clock), len); + clock += len; + break; + } + case 10: { + const len = readVarUint(decoder.restDecoder); + refs[i2] = new Skip(createID(client, clock), len); + clock += len; + break; + } + default: { + const cantCopyParentInfo = (info & (BIT7 | BIT8)) === 0; + const struct = new Item( + createID(client, clock), + null, + // left + (info & BIT8) === BIT8 ? decoder.readLeftID() : null, + // origin + null, + // right + (info & BIT7) === BIT7 ? decoder.readRightID() : null, + // right origin + cantCopyParentInfo ? decoder.readParentInfo() ? doc2.get(decoder.readString()) : decoder.readLeftID() : null, + // parent + cantCopyParentInfo && (info & BIT6) === BIT6 ? decoder.readString() : null, + // parentSub + readItemContent(decoder, info) + // item content + ); + refs[i2] = struct; + clock += struct.length; + } + } + } + } + return clientRefs; + }; + var integrateStructs = (transaction, store, clientsStructRefs) => { + const stack = []; + let clientsStructRefsIds = from(clientsStructRefs.keys()).sort((a, b) => a - b); + if (clientsStructRefsIds.length === 0) { + return null; + } + const getNextStructTarget = () => { + if (clientsStructRefsIds.length === 0) { + return null; + } + let nextStructsTarget = ( + /** @type {{i:number,refs:Array}} */ + clientsStructRefs.get(clientsStructRefsIds[clientsStructRefsIds.length - 1]) + ); + while (nextStructsTarget.refs.length === nextStructsTarget.i) { + clientsStructRefsIds.pop(); + if (clientsStructRefsIds.length > 0) { + nextStructsTarget = /** @type {{i:number,refs:Array}} */ + clientsStructRefs.get(clientsStructRefsIds[clientsStructRefsIds.length - 1]); + } else { + return null; + } + } + return nextStructsTarget; + }; + let curStructsTarget = getNextStructTarget(); + if (curStructsTarget === null) { + return null; + } + const restStructs = new StructStore(); + const missingSV = /* @__PURE__ */ new Map(); + const updateMissingSv = (client, clock) => { + const mclock = missingSV.get(client); + if (mclock == null || mclock > clock) { + missingSV.set(client, clock); + } + }; + let stackHead = ( + /** @type {any} */ + curStructsTarget.refs[ + /** @type {any} */ + curStructsTarget.i++ + ] + ); + const state = /* @__PURE__ */ new Map(); + const addStackToRestSS = () => { + for (const item of stack) { + const client = item.id.client; + const inapplicableItems = clientsStructRefs.get(client); + if (inapplicableItems) { + inapplicableItems.i--; + restStructs.clients.set(client, inapplicableItems.refs.slice(inapplicableItems.i)); + clientsStructRefs.delete(client); + inapplicableItems.i = 0; + inapplicableItems.refs = []; + } else { + restStructs.clients.set(client, [item]); + } + clientsStructRefsIds = clientsStructRefsIds.filter((c) => c !== client); + } + stack.length = 0; + }; + while (true) { + if (stackHead.constructor !== Skip) { + const localClock = setIfUndefined(state, stackHead.id.client, () => getState(store, stackHead.id.client)); + const offset = localClock - stackHead.id.clock; + if (offset < 0) { + stack.push(stackHead); + updateMissingSv(stackHead.id.client, stackHead.id.clock - 1); + addStackToRestSS(); + } else { + const missing = stackHead.getMissing(transaction, store); + if (missing !== null) { + stack.push(stackHead); + const structRefs = clientsStructRefs.get( + /** @type {number} */ + missing + ) || { refs: [], i: 0 }; + if (structRefs.refs.length === structRefs.i) { + updateMissingSv( + /** @type {number} */ + missing, + getState(store, missing) + ); + addStackToRestSS(); + } else { + stackHead = structRefs.refs[structRefs.i++]; + continue; + } + } else if (offset === 0 || offset < stackHead.length) { + stackHead.integrate(transaction, offset); + state.set(stackHead.id.client, stackHead.id.clock + stackHead.length); + } + } + } + if (stack.length > 0) { + stackHead = /** @type {GC|Item} */ + stack.pop(); + } else if (curStructsTarget !== null && curStructsTarget.i < curStructsTarget.refs.length) { + stackHead = /** @type {GC|Item} */ + curStructsTarget.refs[curStructsTarget.i++]; + } else { + curStructsTarget = getNextStructTarget(); + if (curStructsTarget === null) { + break; + } else { + stackHead = /** @type {GC|Item} */ + curStructsTarget.refs[curStructsTarget.i++]; + } + } + } + if (restStructs.clients.size > 0) { + const encoder = new UpdateEncoderV2(); + writeClientsStructs(encoder, restStructs, /* @__PURE__ */ new Map()); + writeVarUint(encoder.restEncoder, 0); + return { missing: missingSV, update: encoder.toUint8Array() }; + } + return null; + }; + var writeStructsFromTransaction = (encoder, transaction) => writeClientsStructs(encoder, transaction.doc.store, transaction.beforeState); + var readUpdateV2 = (decoder, ydoc, transactionOrigin, structDecoder = new UpdateDecoderV2(decoder)) => transact(ydoc, (transaction) => { + transaction.local = false; + let retry2 = false; + const doc2 = transaction.doc; + const store = doc2.store; + const ss = readClientsStructRefs(structDecoder, doc2); + const restStructs = integrateStructs(transaction, store, ss); + const pending = store.pendingStructs; + if (pending) { + for (const [client, clock] of pending.missing) { + if (clock < getState(store, client)) { + retry2 = true; + break; + } + } + if (restStructs) { + for (const [client, clock] of restStructs.missing) { + const mclock = pending.missing.get(client); + if (mclock == null || mclock > clock) { + pending.missing.set(client, clock); + } + } + pending.update = mergeUpdatesV2([pending.update, restStructs.update]); + } + } else { + store.pendingStructs = restStructs; + } + const dsRest = readAndApplyDeleteSet(structDecoder, transaction, store); + if (store.pendingDs) { + const pendingDSUpdate = new UpdateDecoderV2(createDecoder(store.pendingDs)); + readVarUint(pendingDSUpdate.restDecoder); + const dsRest2 = readAndApplyDeleteSet(pendingDSUpdate, transaction, store); + if (dsRest && dsRest2) { + store.pendingDs = mergeUpdatesV2([dsRest, dsRest2]); + } else { + store.pendingDs = dsRest || dsRest2; + } + } else { + store.pendingDs = dsRest; + } + if (retry2) { + const update = ( + /** @type {{update: Uint8Array}} */ + store.pendingStructs.update + ); + store.pendingStructs = null; + applyUpdateV2(transaction.doc, update); + } + }, transactionOrigin, false); + var applyUpdateV2 = (ydoc, update, transactionOrigin, YDecoder = UpdateDecoderV2) => { + const decoder = createDecoder(update); + readUpdateV2(decoder, ydoc, transactionOrigin, new YDecoder(decoder)); + }; + var applyUpdate = (ydoc, update, transactionOrigin) => applyUpdateV2(ydoc, update, transactionOrigin, UpdateDecoderV1); + var writeStateAsUpdate = (encoder, doc2, targetStateVector = /* @__PURE__ */ new Map()) => { + writeClientsStructs(encoder, doc2.store, targetStateVector); + writeDeleteSet(encoder, createDeleteSetFromStructStore(doc2.store)); + }; + var encodeStateAsUpdateV2 = (doc2, encodedTargetStateVector = new Uint8Array([0]), encoder = new UpdateEncoderV2()) => { + const targetStateVector = decodeStateVector(encodedTargetStateVector); + writeStateAsUpdate(encoder, doc2, targetStateVector); + const updates = [encoder.toUint8Array()]; + if (doc2.store.pendingDs) { + updates.push(doc2.store.pendingDs); + } + if (doc2.store.pendingStructs) { + updates.push(diffUpdateV2(doc2.store.pendingStructs.update, encodedTargetStateVector)); + } + if (updates.length > 1) { + if (encoder.constructor === UpdateEncoderV1) { + return mergeUpdates(updates.map((update, i) => i === 0 ? update : convertUpdateFormatV2ToV1(update))); + } else if (encoder.constructor === UpdateEncoderV2) { + return mergeUpdatesV2(updates); + } + } + return updates[0]; + }; + var encodeStateAsUpdate = (doc2, encodedTargetStateVector) => encodeStateAsUpdateV2(doc2, encodedTargetStateVector, new UpdateEncoderV1()); + var readStateVector = (decoder) => { + const ss = /* @__PURE__ */ new Map(); + const ssLength = readVarUint(decoder.restDecoder); + for (let i = 0; i < ssLength; i++) { + const client = readVarUint(decoder.restDecoder); + const clock = readVarUint(decoder.restDecoder); + ss.set(client, clock); + } + return ss; + }; + var decodeStateVector = (decodedState) => readStateVector(new DSDecoderV1(createDecoder(decodedState))); + var writeStateVector = (encoder, sv) => { + writeVarUint(encoder.restEncoder, sv.size); + from(sv.entries()).sort((a, b) => b[0] - a[0]).forEach(([client, clock]) => { + writeVarUint(encoder.restEncoder, client); + writeVarUint(encoder.restEncoder, clock); + }); + return encoder; + }; + var writeDocumentStateVector = (encoder, doc2) => writeStateVector(encoder, getStateVector(doc2.store)); + var encodeStateVectorV2 = (doc2, encoder = new DSEncoderV2()) => { + if (doc2 instanceof Map) { + writeStateVector(encoder, doc2); + } else { + writeDocumentStateVector(encoder, doc2); + } + return encoder.toUint8Array(); + }; + var encodeStateVector = (doc2) => encodeStateVectorV2(doc2, new DSEncoderV1()); + var EventHandler = class { + constructor() { + this.l = []; + } + }; + var createEventHandler = () => new EventHandler(); + var addEventHandlerListener = (eventHandler, f) => eventHandler.l.push(f); + var removeEventHandlerListener = (eventHandler, f) => { + const l = eventHandler.l; + const len = l.length; + eventHandler.l = l.filter((g) => f !== g); + if (len === eventHandler.l.length) { + console.error("[yjs] Tried to remove event handler that doesn't exist."); + } + }; + var callEventHandlerListeners = (eventHandler, arg0, arg1) => callAll(eventHandler.l, [arg0, arg1]); + var ID = class { + /** + * @param {number} client client id + * @param {number} clock unique per client id, continuous number + */ + constructor(client, clock) { + this.client = client; + this.clock = clock; + } + }; + var compareIDs = (a, b) => a === b || a !== null && b !== null && a.client === b.client && a.clock === b.clock; + var createID = (client, clock) => new ID(client, clock); + var findRootTypeKey = (type) => { + for (const [key, value] of type.doc.share.entries()) { + if (value === type) { + return key; + } + } + throw unexpectedCase(); + }; + var Snapshot = class { + /** + * @param {DeleteSet} ds + * @param {Map} sv state map + */ + constructor(ds, sv) { + this.ds = ds; + this.sv = sv; + } + }; + var createSnapshot = (ds, sm) => new Snapshot(ds, sm); + var emptySnapshot = createSnapshot(createDeleteSet(), /* @__PURE__ */ new Map()); + var isVisible = (item, snapshot) => snapshot === void 0 ? !item.deleted : snapshot.sv.has(item.id.client) && (snapshot.sv.get(item.id.client) || 0) > item.id.clock && !isDeleted(snapshot.ds, item.id); + var splitSnapshotAffectedStructs = (transaction, snapshot) => { + const meta = setIfUndefined(transaction.meta, splitSnapshotAffectedStructs, create2); + const store = transaction.doc.store; + if (!meta.has(snapshot)) { + snapshot.sv.forEach((clock, client) => { + if (clock < getState(store, client)) { + getItemCleanStart(transaction, createID(client, clock)); + } + }); + iterateDeletedStructs(transaction, snapshot.ds, (_item) => { + }); + meta.add(snapshot); + } + }; + var StructStore = class { + constructor() { + this.clients = /* @__PURE__ */ new Map(); + this.pendingStructs = null; + this.pendingDs = null; + } + }; + var getStateVector = (store) => { + const sm = /* @__PURE__ */ new Map(); + store.clients.forEach((structs, client) => { + const struct = structs[structs.length - 1]; + sm.set(client, struct.id.clock + struct.length); + }); + return sm; + }; + var getState = (store, client) => { + const structs = store.clients.get(client); + if (structs === void 0) { + return 0; + } + const lastStruct = structs[structs.length - 1]; + return lastStruct.id.clock + lastStruct.length; + }; + var addStruct = (store, struct) => { + let structs = store.clients.get(struct.id.client); + if (structs === void 0) { + structs = []; + store.clients.set(struct.id.client, structs); + } else { + const lastStruct = structs[structs.length - 1]; + if (lastStruct.id.clock + lastStruct.length !== struct.id.clock) { + throw unexpectedCase(); + } + } + structs.push(struct); + }; + var findIndexSS = (structs, clock) => { + let left = 0; + let right = structs.length - 1; + let mid = structs[right]; + let midclock = mid.id.clock; + if (midclock === clock) { + return right; + } + let midindex = floor(clock / (midclock + mid.length - 1) * right); + while (left <= right) { + mid = structs[midindex]; + midclock = mid.id.clock; + if (midclock <= clock) { + if (clock < midclock + mid.length) { + return midindex; + } + left = midindex + 1; + } else { + right = midindex - 1; + } + midindex = floor((left + right) / 2); + } + throw unexpectedCase(); + }; + var find = (store, id2) => { + const structs = store.clients.get(id2.client); + return structs[findIndexSS(structs, id2.clock)]; + }; + var getItem = ( + /** @type {function(StructStore,ID):Item} */ + find + ); + var findIndexCleanStart = (transaction, structs, clock) => { + const index = findIndexSS(structs, clock); + const struct = structs[index]; + if (struct.id.clock < clock && struct instanceof Item) { + structs.splice(index + 1, 0, splitItem(transaction, struct, clock - struct.id.clock)); + return index + 1; + } + return index; + }; + var getItemCleanStart = (transaction, id2) => { + const structs = ( + /** @type {Array} */ + transaction.doc.store.clients.get(id2.client) + ); + return structs[findIndexCleanStart(transaction, structs, id2.clock)]; + }; + var getItemCleanEnd = (transaction, store, id2) => { + const structs = store.clients.get(id2.client); + const index = findIndexSS(structs, id2.clock); + const struct = structs[index]; + if (id2.clock !== struct.id.clock + struct.length - 1 && struct.constructor !== GC) { + structs.splice(index + 1, 0, splitItem(transaction, struct, id2.clock - struct.id.clock + 1)); + } + return struct; + }; + var replaceStruct = (store, struct, newStruct) => { + const structs = ( + /** @type {Array} */ + store.clients.get(struct.id.client) + ); + structs[findIndexSS(structs, struct.id.clock)] = newStruct; + }; + var iterateStructs = (transaction, structs, clockStart, len, f) => { + if (len === 0) { + return; + } + const clockEnd = clockStart + len; + let index = findIndexCleanStart(transaction, structs, clockStart); + let struct; + do { + struct = structs[index++]; + if (clockEnd < struct.id.clock + struct.length) { + findIndexCleanStart(transaction, structs, clockEnd); + } + f(struct); + } while (index < structs.length && structs[index].id.clock < clockEnd); + }; + var Transaction = class { + /** + * @param {Doc} doc + * @param {any} origin + * @param {boolean} local + */ + constructor(doc2, origin, local) { + this.doc = doc2; + this.deleteSet = new DeleteSet(); + this.beforeState = getStateVector(doc2.store); + this.afterState = /* @__PURE__ */ new Map(); + this.changed = /* @__PURE__ */ new Map(); + this.changedParentTypes = /* @__PURE__ */ new Map(); + this._mergeStructs = []; + this.origin = origin; + this.meta = /* @__PURE__ */ new Map(); + this.local = local; + this.subdocsAdded = /* @__PURE__ */ new Set(); + this.subdocsRemoved = /* @__PURE__ */ new Set(); + this.subdocsLoaded = /* @__PURE__ */ new Set(); + this._needFormattingCleanup = false; + } + }; + var writeUpdateMessageFromTransaction = (encoder, transaction) => { + if (transaction.deleteSet.clients.size === 0 && !any(transaction.afterState, (clock, client) => transaction.beforeState.get(client) !== clock)) { + return false; + } + sortAndMergeDeleteSet(transaction.deleteSet); + writeStructsFromTransaction(encoder, transaction); + writeDeleteSet(encoder, transaction.deleteSet); + return true; + }; + var addChangedTypeToTransaction = (transaction, type, parentSub) => { + const item = type._item; + if (item === null || item.id.clock < (transaction.beforeState.get(item.id.client) || 0) && !item.deleted) { + setIfUndefined(transaction.changed, type, create2).add(parentSub); + } + }; + var tryToMergeWithLefts = (structs, pos) => { + let right = structs[pos]; + let left = structs[pos - 1]; + let i = pos; + for (; i > 0; right = left, left = structs[--i - 1]) { + if (left.deleted === right.deleted && left.constructor === right.constructor) { + if (left.mergeWith(right)) { + if (right instanceof Item && right.parentSub !== null && /** @type {AbstractType} */ + right.parent._map.get(right.parentSub) === right) { + right.parent._map.set( + right.parentSub, + /** @type {Item} */ + left + ); + } + continue; + } + } + break; + } + const merged = pos - i; + if (merged) { + structs.splice(pos + 1 - merged, merged); + } + return merged; + }; + var tryGcDeleteSet = (ds, store, gcFilter) => { + for (const [client, deleteItems] of ds.clients.entries()) { + const structs = ( + /** @type {Array} */ + store.clients.get(client) + ); + for (let di = deleteItems.length - 1; di >= 0; di--) { + const deleteItem = deleteItems[di]; + const endDeleteItemClock = deleteItem.clock + deleteItem.len; + for (let si = findIndexSS(structs, deleteItem.clock), struct = structs[si]; si < structs.length && struct.id.clock < endDeleteItemClock; struct = structs[++si]) { + const struct2 = structs[si]; + if (deleteItem.clock + deleteItem.len <= struct2.id.clock) { + break; + } + if (struct2 instanceof Item && struct2.deleted && !struct2.keep && gcFilter(struct2)) { + struct2.gc(store, false); + } + } + } + } + }; + var tryMergeDeleteSet = (ds, store) => { + ds.clients.forEach((deleteItems, client) => { + const structs = ( + /** @type {Array} */ + store.clients.get(client) + ); + for (let di = deleteItems.length - 1; di >= 0; di--) { + const deleteItem = deleteItems[di]; + const mostRightIndexToCheck = min(structs.length - 1, 1 + findIndexSS(structs, deleteItem.clock + deleteItem.len - 1)); + for (let si = mostRightIndexToCheck, struct = structs[si]; si > 0 && struct.id.clock >= deleteItem.clock; struct = structs[si]) { + si -= 1 + tryToMergeWithLefts(structs, si); + } + } + }); + }; + var cleanupTransactions = (transactionCleanups, i) => { + if (i < transactionCleanups.length) { + const transaction = transactionCleanups[i]; + const doc2 = transaction.doc; + const store = doc2.store; + const ds = transaction.deleteSet; + const mergeStructs = transaction._mergeStructs; + try { + sortAndMergeDeleteSet(ds); + transaction.afterState = getStateVector(transaction.doc.store); + doc2.emit("beforeObserverCalls", [transaction, doc2]); + const fs = []; + transaction.changed.forEach( + (subs, itemtype) => fs.push(() => { + if (itemtype._item === null || !itemtype._item.deleted) { + itemtype._callObserver(transaction, subs); + } + }) + ); + fs.push(() => { + transaction.changedParentTypes.forEach((events, type) => { + if (type._dEH.l.length > 0 && (type._item === null || !type._item.deleted)) { + events = events.filter( + (event) => event.target._item === null || !event.target._item.deleted + ); + events.forEach((event) => { + event.currentTarget = type; + event._path = null; + }); + events.sort((event1, event2) => event1.path.length - event2.path.length); + fs.push(() => { + callEventHandlerListeners(type._dEH, events, transaction); + }); + } + }); + fs.push(() => doc2.emit("afterTransaction", [transaction, doc2])); + fs.push(() => { + if (transaction._needFormattingCleanup) { + cleanupYTextAfterTransaction(transaction); + } + }); + }); + callAll(fs, []); + } finally { + if (doc2.gc) { + tryGcDeleteSet(ds, store, doc2.gcFilter); + } + tryMergeDeleteSet(ds, store); + transaction.afterState.forEach((clock, client) => { + const beforeClock = transaction.beforeState.get(client) || 0; + if (beforeClock !== clock) { + const structs = ( + /** @type {Array} */ + store.clients.get(client) + ); + const firstChangePos = max(findIndexSS(structs, beforeClock), 1); + for (let i2 = structs.length - 1; i2 >= firstChangePos; ) { + i2 -= 1 + tryToMergeWithLefts(structs, i2); + } + } + }); + for (let i2 = mergeStructs.length - 1; i2 >= 0; i2--) { + const { client, clock } = mergeStructs[i2].id; + const structs = ( + /** @type {Array} */ + store.clients.get(client) + ); + const replacedStructPos = findIndexSS(structs, clock); + if (replacedStructPos + 1 < structs.length) { + if (tryToMergeWithLefts(structs, replacedStructPos + 1) > 1) { + continue; + } + } + if (replacedStructPos > 0) { + tryToMergeWithLefts(structs, replacedStructPos); + } + } + if (!transaction.local && transaction.afterState.get(doc2.clientID) !== transaction.beforeState.get(doc2.clientID)) { + print(ORANGE, BOLD, "[yjs] ", UNBOLD, RED, "Changed the client-id because another client seems to be using it."); + doc2.clientID = generateNewClientId(); + } + doc2.emit("afterTransactionCleanup", [transaction, doc2]); + if (doc2._observers.has("update")) { + const encoder = new UpdateEncoderV1(); + const hasContent2 = writeUpdateMessageFromTransaction(encoder, transaction); + if (hasContent2) { + doc2.emit("update", [encoder.toUint8Array(), transaction.origin, doc2, transaction]); + } + } + if (doc2._observers.has("updateV2")) { + const encoder = new UpdateEncoderV2(); + const hasContent2 = writeUpdateMessageFromTransaction(encoder, transaction); + if (hasContent2) { + doc2.emit("updateV2", [encoder.toUint8Array(), transaction.origin, doc2, transaction]); + } + } + const { subdocsAdded, subdocsLoaded, subdocsRemoved } = transaction; + if (subdocsAdded.size > 0 || subdocsRemoved.size > 0 || subdocsLoaded.size > 0) { + subdocsAdded.forEach((subdoc) => { + subdoc.clientID = doc2.clientID; + if (subdoc.collectionid == null) { + subdoc.collectionid = doc2.collectionid; + } + doc2.subdocs.add(subdoc); + }); + subdocsRemoved.forEach((subdoc) => doc2.subdocs.delete(subdoc)); + doc2.emit("subdocs", [{ loaded: subdocsLoaded, added: subdocsAdded, removed: subdocsRemoved }, doc2, transaction]); + subdocsRemoved.forEach((subdoc) => subdoc.destroy()); + } + if (transactionCleanups.length <= i + 1) { + doc2._transactionCleanups = []; + doc2.emit("afterAllTransactions", [doc2, transactionCleanups]); + } else { + cleanupTransactions(transactionCleanups, i + 1); + } + } + } + }; + var transact = (doc2, f, origin = null, local = true) => { + const transactionCleanups = doc2._transactionCleanups; + let initialCall = false; + let result = null; + if (doc2._transaction === null) { + initialCall = true; + doc2._transaction = new Transaction(doc2, origin, local); + transactionCleanups.push(doc2._transaction); + if (transactionCleanups.length === 1) { + doc2.emit("beforeAllTransactions", [doc2]); + } + doc2.emit("beforeTransaction", [doc2._transaction, doc2]); + } + try { + result = f(doc2._transaction); + } finally { + if (initialCall) { + const finishCleanup = doc2._transaction === transactionCleanups[0]; + doc2._transaction = null; + if (finishCleanup) { + cleanupTransactions(transactionCleanups, 0); + } + } + } + return result; + }; + function* lazyStructReaderGenerator(decoder) { + const numOfStateUpdates = readVarUint(decoder.restDecoder); + for (let i = 0; i < numOfStateUpdates; i++) { + const numberOfStructs = readVarUint(decoder.restDecoder); + const client = decoder.readClient(); + let clock = readVarUint(decoder.restDecoder); + for (let i2 = 0; i2 < numberOfStructs; i2++) { + const info = decoder.readInfo(); + if (info === 10) { + const len = readVarUint(decoder.restDecoder); + yield new Skip(createID(client, clock), len); + clock += len; + } else if ((BITS5 & info) !== 0) { + const cantCopyParentInfo = (info & (BIT7 | BIT8)) === 0; + const struct = new Item( + createID(client, clock), + null, + // left + (info & BIT8) === BIT8 ? decoder.readLeftID() : null, + // origin + null, + // right + (info & BIT7) === BIT7 ? decoder.readRightID() : null, + // right origin + // @ts-ignore Force writing a string here. + cantCopyParentInfo ? decoder.readParentInfo() ? decoder.readString() : decoder.readLeftID() : null, + // parent + cantCopyParentInfo && (info & BIT6) === BIT6 ? decoder.readString() : null, + // parentSub + readItemContent(decoder, info) + // item content + ); + yield struct; + clock += struct.length; + } else { + const len = decoder.readLen(); + yield new GC(createID(client, clock), len); + clock += len; + } + } + } + } + var LazyStructReader = class { + /** + * @param {UpdateDecoderV1 | UpdateDecoderV2} decoder + * @param {boolean} filterSkips + */ + constructor(decoder, filterSkips) { + this.gen = lazyStructReaderGenerator(decoder); + this.curr = null; + this.done = false; + this.filterSkips = filterSkips; + this.next(); + } + /** + * @return {Item | GC | Skip |null} + */ + next() { + do { + this.curr = this.gen.next().value || null; + } while (this.filterSkips && this.curr !== null && this.curr.constructor === Skip); + return this.curr; + } + }; + var LazyStructWriter = class { + /** + * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder + */ + constructor(encoder) { + this.currClient = 0; + this.startClock = 0; + this.written = 0; + this.encoder = encoder; + this.clientStructs = []; + } + }; + var mergeUpdates = (updates) => mergeUpdatesV2(updates, UpdateDecoderV1, UpdateEncoderV1); + var sliceStruct = (left, diff) => { + if (left.constructor === GC) { + const { client, clock } = left.id; + return new GC(createID(client, clock + diff), left.length - diff); + } else if (left.constructor === Skip) { + const { client, clock } = left.id; + return new Skip(createID(client, clock + diff), left.length - diff); + } else { + const leftItem = ( + /** @type {Item} */ + left + ); + const { client, clock } = leftItem.id; + return new Item( + createID(client, clock + diff), + null, + createID(client, clock + diff - 1), + null, + leftItem.rightOrigin, + leftItem.parent, + leftItem.parentSub, + leftItem.content.splice(diff) + ); + } + }; + var mergeUpdatesV2 = (updates, YDecoder = UpdateDecoderV2, YEncoder = UpdateEncoderV2) => { + if (updates.length === 1) { + return updates[0]; + } + const updateDecoders = updates.map((update) => new YDecoder(createDecoder(update))); + let lazyStructDecoders = updateDecoders.map((decoder) => new LazyStructReader(decoder, true)); + let currWrite = null; + const updateEncoder = new YEncoder(); + const lazyStructEncoder = new LazyStructWriter(updateEncoder); + while (true) { + lazyStructDecoders = lazyStructDecoders.filter((dec) => dec.curr !== null); + lazyStructDecoders.sort( + /** @type {function(any,any):number} */ + (dec1, dec2) => { + if (dec1.curr.id.client === dec2.curr.id.client) { + const clockDiff = dec1.curr.id.clock - dec2.curr.id.clock; + if (clockDiff === 0) { + return dec1.curr.constructor === dec2.curr.constructor ? 0 : dec1.curr.constructor === Skip ? 1 : -1; + } else { + return clockDiff; + } + } else { + return dec2.curr.id.client - dec1.curr.id.client; + } + } + ); + if (lazyStructDecoders.length === 0) { + break; + } + const currDecoder = lazyStructDecoders[0]; + const firstClient = ( + /** @type {Item | GC} */ + currDecoder.curr.id.client + ); + if (currWrite !== null) { + let curr = ( + /** @type {Item | GC | null} */ + currDecoder.curr + ); + let iterated = false; + while (curr !== null && curr.id.clock + curr.length <= currWrite.struct.id.clock + currWrite.struct.length && curr.id.client >= currWrite.struct.id.client) { + curr = currDecoder.next(); + iterated = true; + } + if (curr === null || // current decoder is empty + curr.id.client !== firstClient || // check whether there is another decoder that has has updates from `firstClient` + iterated && curr.id.clock > currWrite.struct.id.clock + currWrite.struct.length) { + continue; + } + if (firstClient !== currWrite.struct.id.client) { + writeStructToLazyStructWriter(lazyStructEncoder, currWrite.struct, currWrite.offset); + currWrite = { struct: curr, offset: 0 }; + currDecoder.next(); + } else { + if (currWrite.struct.id.clock + currWrite.struct.length < curr.id.clock) { + if (currWrite.struct.constructor === Skip) { + currWrite.struct.length = curr.id.clock + curr.length - currWrite.struct.id.clock; + } else { + writeStructToLazyStructWriter(lazyStructEncoder, currWrite.struct, currWrite.offset); + const diff = curr.id.clock - currWrite.struct.id.clock - currWrite.struct.length; + const struct = new Skip(createID(firstClient, currWrite.struct.id.clock + currWrite.struct.length), diff); + currWrite = { struct, offset: 0 }; + } + } else { + const diff = currWrite.struct.id.clock + currWrite.struct.length - curr.id.clock; + if (diff > 0) { + if (currWrite.struct.constructor === Skip) { + currWrite.struct.length -= diff; + } else { + curr = sliceStruct(curr, diff); + } + } + if (!currWrite.struct.mergeWith( + /** @type {any} */ + curr + )) { + writeStructToLazyStructWriter(lazyStructEncoder, currWrite.struct, currWrite.offset); + currWrite = { struct: curr, offset: 0 }; + currDecoder.next(); + } + } + } + } else { + currWrite = { struct: ( + /** @type {Item | GC} */ + currDecoder.curr + ), offset: 0 }; + currDecoder.next(); + } + for (let next = currDecoder.curr; next !== null && next.id.client === firstClient && next.id.clock === currWrite.struct.id.clock + currWrite.struct.length && next.constructor !== Skip; next = currDecoder.next()) { + writeStructToLazyStructWriter(lazyStructEncoder, currWrite.struct, currWrite.offset); + currWrite = { struct: next, offset: 0 }; + } + } + if (currWrite !== null) { + writeStructToLazyStructWriter(lazyStructEncoder, currWrite.struct, currWrite.offset); + currWrite = null; + } + finishLazyStructWriting(lazyStructEncoder); + const dss = updateDecoders.map((decoder) => readDeleteSet(decoder)); + const ds = mergeDeleteSets(dss); + writeDeleteSet(updateEncoder, ds); + return updateEncoder.toUint8Array(); + }; + var diffUpdateV2 = (update, sv, YDecoder = UpdateDecoderV2, YEncoder = UpdateEncoderV2) => { + const state = decodeStateVector(sv); + const encoder = new YEncoder(); + const lazyStructWriter = new LazyStructWriter(encoder); + const decoder = new YDecoder(createDecoder(update)); + const reader = new LazyStructReader(decoder, false); + while (reader.curr) { + const curr = reader.curr; + const currClient = curr.id.client; + const svClock = state.get(currClient) || 0; + if (reader.curr.constructor === Skip) { + reader.next(); + continue; + } + if (curr.id.clock + curr.length > svClock) { + writeStructToLazyStructWriter(lazyStructWriter, curr, max(svClock - curr.id.clock, 0)); + reader.next(); + while (reader.curr && reader.curr.id.client === currClient) { + writeStructToLazyStructWriter(lazyStructWriter, reader.curr, 0); + reader.next(); + } + } else { + while (reader.curr && reader.curr.id.client === currClient && reader.curr.id.clock + reader.curr.length <= svClock) { + reader.next(); + } + } + } + finishLazyStructWriting(lazyStructWriter); + const ds = readDeleteSet(decoder); + writeDeleteSet(encoder, ds); + return encoder.toUint8Array(); + }; + var flushLazyStructWriter = (lazyWriter) => { + if (lazyWriter.written > 0) { + lazyWriter.clientStructs.push({ written: lazyWriter.written, restEncoder: toUint8Array(lazyWriter.encoder.restEncoder) }); + lazyWriter.encoder.restEncoder = createEncoder(); + lazyWriter.written = 0; + } + }; + var writeStructToLazyStructWriter = (lazyWriter, struct, offset) => { + if (lazyWriter.written > 0 && lazyWriter.currClient !== struct.id.client) { + flushLazyStructWriter(lazyWriter); + } + if (lazyWriter.written === 0) { + lazyWriter.currClient = struct.id.client; + lazyWriter.encoder.writeClient(struct.id.client); + writeVarUint(lazyWriter.encoder.restEncoder, struct.id.clock + offset); + } + struct.write(lazyWriter.encoder, offset); + lazyWriter.written++; + }; + var finishLazyStructWriting = (lazyWriter) => { + flushLazyStructWriter(lazyWriter); + const restEncoder = lazyWriter.encoder.restEncoder; + writeVarUint(restEncoder, lazyWriter.clientStructs.length); + for (let i = 0; i < lazyWriter.clientStructs.length; i++) { + const partStructs = lazyWriter.clientStructs[i]; + writeVarUint(restEncoder, partStructs.written); + writeUint8Array(restEncoder, partStructs.restEncoder); + } + }; + var convertUpdateFormat = (update, blockTransformer, YDecoder, YEncoder) => { + const updateDecoder = new YDecoder(createDecoder(update)); + const lazyDecoder = new LazyStructReader(updateDecoder, false); + const updateEncoder = new YEncoder(); + const lazyWriter = new LazyStructWriter(updateEncoder); + for (let curr = lazyDecoder.curr; curr !== null; curr = lazyDecoder.next()) { + writeStructToLazyStructWriter(lazyWriter, blockTransformer(curr), 0); + } + finishLazyStructWriting(lazyWriter); + const ds = readDeleteSet(updateDecoder); + writeDeleteSet(updateEncoder, ds); + return updateEncoder.toUint8Array(); + }; + var convertUpdateFormatV2ToV1 = (update) => convertUpdateFormat(update, id, UpdateDecoderV2, UpdateEncoderV1); + var errorComputeChanges = "You must not compute changes after the event-handler fired."; + var YEvent = class { + /** + * @param {T} target The changed type. + * @param {Transaction} transaction + */ + constructor(target, transaction) { + this.target = target; + this.currentTarget = target; + this.transaction = transaction; + this._changes = null; + this._keys = null; + this._delta = null; + this._path = null; + } + /** + * Computes the path from `y` to the changed type. + * + * @todo v14 should standardize on path: Array<{parent, index}> because that is easier to work with. + * + * The following property holds: + * @example + * let type = y + * event.path.forEach(dir => { + * type = type.get(dir) + * }) + * type === event.target // => true + */ + get path() { + return this._path || (this._path = getPathTo(this.currentTarget, this.target)); + } + /** + * Check if a struct is deleted by this event. + * + * In contrast to change.deleted, this method also returns true if the struct was added and then deleted. + * + * @param {AbstractStruct} struct + * @return {boolean} + */ + deletes(struct) { + return isDeleted(this.transaction.deleteSet, struct.id); + } + /** + * @type {Map} + */ + get keys() { + if (this._keys === null) { + if (this.transaction.doc._transactionCleanups.length === 0) { + throw create3(errorComputeChanges); + } + const keys3 = /* @__PURE__ */ new Map(); + const target = this.target; + const changed = ( + /** @type Set */ + this.transaction.changed.get(target) + ); + changed.forEach((key) => { + if (key !== null) { + const item = ( + /** @type {Item} */ + target._map.get(key) + ); + let action; + let oldValue; + if (this.adds(item)) { + let prev = item.left; + while (prev !== null && this.adds(prev)) { + prev = prev.left; + } + if (this.deletes(item)) { + if (prev !== null && this.deletes(prev)) { + action = "delete"; + oldValue = last(prev.content.getContent()); + } else { + return; + } + } else { + if (prev !== null && this.deletes(prev)) { + action = "update"; + oldValue = last(prev.content.getContent()); + } else { + action = "add"; + oldValue = void 0; + } + } + } else { + if (this.deletes(item)) { + action = "delete"; + oldValue = last( + /** @type {Item} */ + item.content.getContent() + ); + } else { + return; + } + } + keys3.set(key, { action, oldValue }); + } + }); + this._keys = keys3; + } + return this._keys; + } + /** + * This is a computed property. Note that this can only be safely computed during the + * event call. Computing this property after other changes happened might result in + * unexpected behavior (incorrect computation of deltas). A safe way to collect changes + * is to store the `changes` or the `delta` object. Avoid storing the `transaction` object. + * + * @type {Array<{insert?: string | Array | object | AbstractType, retain?: number, delete?: number, attributes?: Object}>} + */ + get delta() { + return this.changes.delta; + } + /** + * Check if a struct is added by this event. + * + * In contrast to change.deleted, this method also returns true if the struct was added and then deleted. + * + * @param {AbstractStruct} struct + * @return {boolean} + */ + adds(struct) { + return struct.id.clock >= (this.transaction.beforeState.get(struct.id.client) || 0); + } + /** + * This is a computed property. Note that this can only be safely computed during the + * event call. Computing this property after other changes happened might result in + * unexpected behavior (incorrect computation of deltas). A safe way to collect changes + * is to store the `changes` or the `delta` object. Avoid storing the `transaction` object. + * + * @type {{added:Set,deleted:Set,keys:Map,delta:Array<{insert?:Array|string, delete?:number, retain?:number}>}} + */ + get changes() { + let changes = this._changes; + if (changes === null) { + if (this.transaction.doc._transactionCleanups.length === 0) { + throw create3(errorComputeChanges); + } + const target = this.target; + const added = create2(); + const deleted = create2(); + const delta = []; + changes = { + added, + deleted, + delta, + keys: this.keys + }; + const changed = ( + /** @type Set */ + this.transaction.changed.get(target) + ); + if (changed.has(null)) { + let lastOp = null; + const packOp = () => { + if (lastOp) { + delta.push(lastOp); + } + }; + for (let item = target._start; item !== null; item = item.right) { + if (item.deleted) { + if (this.deletes(item) && !this.adds(item)) { + if (lastOp === null || lastOp.delete === void 0) { + packOp(); + lastOp = { delete: 0 }; + } + lastOp.delete += item.length; + deleted.add(item); + } + } else { + if (this.adds(item)) { + if (lastOp === null || lastOp.insert === void 0) { + packOp(); + lastOp = { insert: [] }; + } + lastOp.insert = lastOp.insert.concat(item.content.getContent()); + added.add(item); + } else { + if (lastOp === null || lastOp.retain === void 0) { + packOp(); + lastOp = { retain: 0 }; + } + lastOp.retain += item.length; + } + } + } + if (lastOp !== null && lastOp.retain === void 0) { + packOp(); + } + } + this._changes = changes; + } + return ( + /** @type {any} */ + changes + ); + } + }; + var getPathTo = (parent, child) => { + const path = []; + while (child._item !== null && child !== parent) { + if (child._item.parentSub !== null) { + path.unshift(child._item.parentSub); + } else { + let i = 0; + let c = ( + /** @type {AbstractType} */ + child._item.parent._start + ); + while (c !== child._item && c !== null) { + if (!c.deleted && c.countable) { + i += c.length; + } + c = c.right; + } + path.unshift(i); + } + child = /** @type {AbstractType} */ + child._item.parent; + } + return path; + }; + var warnPrematureAccess = () => { + warn("Invalid access: Add Yjs type to a document before reading data."); + }; + var maxSearchMarker = 80; + var globalSearchMarkerTimestamp = 0; + var ArraySearchMarker = class { + /** + * @param {Item} p + * @param {number} index + */ + constructor(p, index) { + p.marker = true; + this.p = p; + this.index = index; + this.timestamp = globalSearchMarkerTimestamp++; + } + }; + var refreshMarkerTimestamp = (marker) => { + marker.timestamp = globalSearchMarkerTimestamp++; + }; + var overwriteMarker = (marker, p, index) => { + marker.p.marker = false; + marker.p = p; + p.marker = true; + marker.index = index; + marker.timestamp = globalSearchMarkerTimestamp++; + }; + var markPosition = (searchMarker, p, index) => { + if (searchMarker.length >= maxSearchMarker) { + const marker = searchMarker.reduce((a, b) => a.timestamp < b.timestamp ? a : b); + overwriteMarker(marker, p, index); + return marker; + } else { + const pm = new ArraySearchMarker(p, index); + searchMarker.push(pm); + return pm; + } + }; + var findMarker = (yarray, index) => { + if (yarray._start === null || index === 0 || yarray._searchMarker === null) { + return null; + } + const marker = yarray._searchMarker.length === 0 ? null : yarray._searchMarker.reduce((a, b) => abs(index - a.index) < abs(index - b.index) ? a : b); + let p = yarray._start; + let pindex = 0; + if (marker !== null) { + p = marker.p; + pindex = marker.index; + refreshMarkerTimestamp(marker); + } + while (p.right !== null && pindex < index) { + if (!p.deleted && p.countable) { + if (index < pindex + p.length) { + break; + } + pindex += p.length; + } + p = p.right; + } + while (p.left !== null && pindex > index) { + p = p.left; + if (!p.deleted && p.countable) { + pindex -= p.length; + } + } + while (p.left !== null && p.left.id.client === p.id.client && p.left.id.clock + p.left.length === p.id.clock) { + p = p.left; + if (!p.deleted && p.countable) { + pindex -= p.length; + } + } + if (marker !== null && abs(marker.index - pindex) < /** @type {YText|YArray} */ + p.parent.length / maxSearchMarker) { + overwriteMarker(marker, p, pindex); + return marker; + } else { + return markPosition(yarray._searchMarker, p, pindex); + } + }; + var updateMarkerChanges = (searchMarker, index, len) => { + for (let i = searchMarker.length - 1; i >= 0; i--) { + const m = searchMarker[i]; + if (len > 0) { + let p = m.p; + p.marker = false; + while (p && (p.deleted || !p.countable)) { + p = p.left; + if (p && !p.deleted && p.countable) { + m.index -= p.length; + } + } + if (p === null || p.marker === true) { + searchMarker.splice(i, 1); + continue; + } + m.p = p; + p.marker = true; + } + if (index < m.index || len > 0 && index === m.index) { + m.index = max(index, m.index + len); + } + } + }; + var callTypeObservers = (type, transaction, event) => { + const changedType = type; + const changedParentTypes = transaction.changedParentTypes; + while (true) { + setIfUndefined(changedParentTypes, type, () => []).push(event); + if (type._item === null) { + break; + } + type = /** @type {AbstractType} */ + type._item.parent; + } + callEventHandlerListeners(changedType._eH, event, transaction); + }; + var AbstractType = class { + constructor() { + this._item = null; + this._map = /* @__PURE__ */ new Map(); + this._start = null; + this.doc = null; + this._length = 0; + this._eH = createEventHandler(); + this._dEH = createEventHandler(); + this._searchMarker = null; + } + /** + * @return {AbstractType|null} + */ + get parent() { + return this._item ? ( + /** @type {AbstractType} */ + this._item.parent + ) : null; + } + /** + * Integrate this type into the Yjs instance. + * + * * Save this struct in the os + * * This type is sent to other client + * * Observer functions are fired + * + * @param {Doc} y The Yjs instance + * @param {Item|null} item + */ + _integrate(y, item) { + this.doc = y; + this._item = item; + } + /** + * @return {AbstractType} + */ + _copy() { + throw methodUnimplemented(); + } + /** + * Makes a copy of this data type that can be included somewhere else. + * + * Note that the content is only readable _after_ it has been included somewhere in the Ydoc. + * + * @return {AbstractType} + */ + clone() { + throw methodUnimplemented(); + } + /** + * @param {UpdateEncoderV1 | UpdateEncoderV2} _encoder + */ + _write(_encoder) { + } + /** + * The first non-deleted item + */ + get _first() { + let n = this._start; + while (n !== null && n.deleted) { + n = n.right; + } + return n; + } + /** + * Creates YEvent and calls all type observers. + * Must be implemented by each type. + * + * @param {Transaction} transaction + * @param {Set} _parentSubs Keys changed on this type. `null` if list was modified. + */ + _callObserver(transaction, _parentSubs) { + if (!transaction.local && this._searchMarker) { + this._searchMarker.length = 0; + } + } + /** + * Observe all events that are created on this type. + * + * @param {function(EventType, Transaction):void} f Observer function + */ + observe(f) { + addEventHandlerListener(this._eH, f); + } + /** + * Observe all events that are created by this type and its children. + * + * @param {function(Array>,Transaction):void} f Observer function + */ + observeDeep(f) { + addEventHandlerListener(this._dEH, f); + } + /** + * Unregister an observer function. + * + * @param {function(EventType,Transaction):void} f Observer function + */ + unobserve(f) { + removeEventHandlerListener(this._eH, f); + } + /** + * Unregister an observer function. + * + * @param {function(Array>,Transaction):void} f Observer function + */ + unobserveDeep(f) { + removeEventHandlerListener(this._dEH, f); + } + /** + * @abstract + * @return {any} + */ + toJSON() { + } + }; + var typeListSlice = (type, start, end) => { + type.doc ?? warnPrematureAccess(); + if (start < 0) { + start = type._length + start; + } + if (end < 0) { + end = type._length + end; + } + let len = end - start; + const cs = []; + let n = type._start; + while (n !== null && len > 0) { + if (n.countable && !n.deleted) { + const c = n.content.getContent(); + if (c.length <= start) { + start -= c.length; + } else { + for (let i = start; i < c.length && len > 0; i++) { + cs.push(c[i]); + len--; + } + start = 0; + } + } + n = n.right; + } + return cs; + }; + var typeListToArray = (type) => { + type.doc ?? warnPrematureAccess(); + const cs = []; + let n = type._start; + while (n !== null) { + if (n.countable && !n.deleted) { + const c = n.content.getContent(); + for (let i = 0; i < c.length; i++) { + cs.push(c[i]); + } + } + n = n.right; + } + return cs; + }; + var typeListForEach = (type, f) => { + let index = 0; + let n = type._start; + type.doc ?? warnPrematureAccess(); + while (n !== null) { + if (n.countable && !n.deleted) { + const c = n.content.getContent(); + for (let i = 0; i < c.length; i++) { + f(c[i], index++, type); + } + } + n = n.right; + } + }; + var typeListMap = (type, f) => { + const result = []; + typeListForEach(type, (c, i) => { + result.push(f(c, i, type)); + }); + return result; + }; + var typeListCreateIterator = (type) => { + let n = type._start; + let currentContent = null; + let currentContentIndex = 0; + return { + [Symbol.iterator]() { + return this; + }, + next: () => { + if (currentContent === null) { + while (n !== null && n.deleted) { + n = n.right; + } + if (n === null) { + return { + done: true, + value: void 0 + }; + } + currentContent = n.content.getContent(); + currentContentIndex = 0; + n = n.right; + } + const value = currentContent[currentContentIndex++]; + if (currentContent.length <= currentContentIndex) { + currentContent = null; + } + return { + done: false, + value + }; + } + }; + }; + var typeListGet = (type, index) => { + type.doc ?? warnPrematureAccess(); + const marker = findMarker(type, index); + let n = type._start; + if (marker !== null) { + n = marker.p; + index -= marker.index; + } + for (; n !== null; n = n.right) { + if (!n.deleted && n.countable) { + if (index < n.length) { + return n.content.getContent()[index]; + } + index -= n.length; + } + } + }; + var typeListInsertGenericsAfter = (transaction, parent, referenceItem, content) => { + let left = referenceItem; + const doc2 = transaction.doc; + const ownClientId = doc2.clientID; + const store = doc2.store; + const right = referenceItem === null ? parent._start : referenceItem.right; + let jsonContent = []; + const packJsonContent = () => { + if (jsonContent.length > 0) { + left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentAny(jsonContent)); + left.integrate(transaction, 0); + jsonContent = []; + } + }; + content.forEach((c) => { + if (c === null) { + jsonContent.push(c); + } else { + switch (c.constructor) { + case Number: + case Object: + case Boolean: + case Array: + case String: + jsonContent.push(c); + break; + default: + packJsonContent(); + switch (c.constructor) { + case Uint8Array: + case ArrayBuffer: + left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentBinary(new Uint8Array( + /** @type {Uint8Array} */ + c + ))); + left.integrate(transaction, 0); + break; + case Doc: + left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentDoc( + /** @type {Doc} */ + c + )); + left.integrate(transaction, 0); + break; + default: + if (c instanceof AbstractType) { + left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentType(c)); + left.integrate(transaction, 0); + } else { + throw new Error("Unexpected content type in insert operation"); + } + } + } + } + }); + packJsonContent(); + }; + var lengthExceeded = () => create3("Length exceeded!"); + var typeListInsertGenerics = (transaction, parent, index, content) => { + if (index > parent._length) { + throw lengthExceeded(); + } + if (index === 0) { + if (parent._searchMarker) { + updateMarkerChanges(parent._searchMarker, index, content.length); + } + return typeListInsertGenericsAfter(transaction, parent, null, content); + } + const startIndex = index; + const marker = findMarker(parent, index); + let n = parent._start; + if (marker !== null) { + n = marker.p; + index -= marker.index; + if (index === 0) { + n = n.prev; + index += n && n.countable && !n.deleted ? n.length : 0; + } + } + for (; n !== null; n = n.right) { + if (!n.deleted && n.countable) { + if (index <= n.length) { + if (index < n.length) { + getItemCleanStart(transaction, createID(n.id.client, n.id.clock + index)); + } + break; + } + index -= n.length; + } + } + if (parent._searchMarker) { + updateMarkerChanges(parent._searchMarker, startIndex, content.length); + } + return typeListInsertGenericsAfter(transaction, parent, n, content); + }; + var typeListPushGenerics = (transaction, parent, content) => { + const marker = (parent._searchMarker || []).reduce((maxMarker, currMarker) => currMarker.index > maxMarker.index ? currMarker : maxMarker, { index: 0, p: parent._start }); + let n = marker.p; + if (n) { + while (n.right) { + n = n.right; + } + } + return typeListInsertGenericsAfter(transaction, parent, n, content); + }; + var typeListDelete = (transaction, parent, index, length3) => { + if (length3 === 0) { + return; + } + const startIndex = index; + const startLength = length3; + const marker = findMarker(parent, index); + let n = parent._start; + if (marker !== null) { + n = marker.p; + index -= marker.index; + } + for (; n !== null && index > 0; n = n.right) { + if (!n.deleted && n.countable) { + if (index < n.length) { + getItemCleanStart(transaction, createID(n.id.client, n.id.clock + index)); + } + index -= n.length; + } + } + while (length3 > 0 && n !== null) { + if (!n.deleted) { + if (length3 < n.length) { + getItemCleanStart(transaction, createID(n.id.client, n.id.clock + length3)); + } + n.delete(transaction); + length3 -= n.length; + } + n = n.right; + } + if (length3 > 0) { + throw lengthExceeded(); + } + if (parent._searchMarker) { + updateMarkerChanges( + parent._searchMarker, + startIndex, + -startLength + length3 + /* in case we remove the above exception */ + ); + } + }; + var typeMapDelete = (transaction, parent, key) => { + const c = parent._map.get(key); + if (c !== void 0) { + c.delete(transaction); + } + }; + var typeMapSet = (transaction, parent, key, value) => { + const left = parent._map.get(key) || null; + const doc2 = transaction.doc; + const ownClientId = doc2.clientID; + let content; + if (value == null) { + content = new ContentAny([value]); + } else { + switch (value.constructor) { + case Number: + case Object: + case Boolean: + case Array: + case String: + case Date: + case BigInt: + content = new ContentAny([value]); + break; + case Uint8Array: + content = new ContentBinary( + /** @type {Uint8Array} */ + value + ); + break; + case Doc: + content = new ContentDoc( + /** @type {Doc} */ + value + ); + break; + default: + if (value instanceof AbstractType) { + content = new ContentType(value); + } else { + throw new Error("Unexpected content type"); + } + } + } + new Item(createID(ownClientId, getState(doc2.store, ownClientId)), left, left && left.lastId, null, null, parent, key, content).integrate(transaction, 0); + }; + var typeMapGet = (parent, key) => { + parent.doc ?? warnPrematureAccess(); + const val = parent._map.get(key); + return val !== void 0 && !val.deleted ? val.content.getContent()[val.length - 1] : void 0; + }; + var typeMapGetAll = (parent) => { + const res = {}; + parent.doc ?? warnPrematureAccess(); + parent._map.forEach((value, key) => { + if (!value.deleted) { + res[key] = value.content.getContent()[value.length - 1]; + } + }); + return res; + }; + var typeMapHas = (parent, key) => { + parent.doc ?? warnPrematureAccess(); + const val = parent._map.get(key); + return val !== void 0 && !val.deleted; + }; + var typeMapGetAllSnapshot = (parent, snapshot) => { + const res = {}; + parent._map.forEach((value, key) => { + let v = value; + while (v !== null && (!snapshot.sv.has(v.id.client) || v.id.clock >= (snapshot.sv.get(v.id.client) || 0))) { + v = v.left; + } + if (v !== null && isVisible(v, snapshot)) { + res[key] = v.content.getContent()[v.length - 1]; + } + }); + return res; + }; + var createMapIterator = (type) => { + type.doc ?? warnPrematureAccess(); + return iteratorFilter( + type._map.entries(), + /** @param {any} entry */ + (entry) => !entry[1].deleted + ); + }; + var YArrayEvent = class extends YEvent { + }; + var YArray = class _YArray extends AbstractType { + constructor() { + super(); + this._prelimContent = []; + this._searchMarker = []; + } + /** + * Construct a new YArray containing the specified items. + * @template {Object|Array|number|null|string|Uint8Array} T + * @param {Array} items + * @return {YArray} + */ + static from(items) { + const a = new _YArray(); + a.push(items); + return a; + } + /** + * Integrate this type into the Yjs instance. + * + * * Save this struct in the os + * * This type is sent to other client + * * Observer functions are fired + * + * @param {Doc} y The Yjs instance + * @param {Item} item + */ + _integrate(y, item) { + super._integrate(y, item); + this.insert( + 0, + /** @type {Array} */ + this._prelimContent + ); + this._prelimContent = null; + } + /** + * @return {YArray} + */ + _copy() { + return new _YArray(); + } + /** + * Makes a copy of this data type that can be included somewhere else. + * + * Note that the content is only readable _after_ it has been included somewhere in the Ydoc. + * + * @return {YArray} + */ + clone() { + const arr = new _YArray(); + arr.insert(0, this.toArray().map( + (el) => el instanceof AbstractType ? ( + /** @type {typeof el} */ + el.clone() + ) : el + )); + return arr; + } + get length() { + this.doc ?? warnPrematureAccess(); + return this._length; + } + /** + * Creates YArrayEvent and calls observers. + * + * @param {Transaction} transaction + * @param {Set} parentSubs Keys changed on this type. `null` if list was modified. + */ + _callObserver(transaction, parentSubs) { + super._callObserver(transaction, parentSubs); + callTypeObservers(this, transaction, new YArrayEvent(this, transaction)); + } + /** + * Inserts new content at an index. + * + * Important: This function expects an array of content. Not just a content + * object. The reason for this "weirdness" is that inserting several elements + * is very efficient when it is done as a single operation. + * + * @example + * // Insert character 'a' at position 0 + * yarray.insert(0, ['a']) + * // Insert numbers 1, 2 at position 1 + * yarray.insert(1, [1, 2]) + * + * @param {number} index The index to insert content at. + * @param {Array} content The array of content + */ + insert(index, content) { + if (this.doc !== null) { + transact(this.doc, (transaction) => { + typeListInsertGenerics( + transaction, + this, + index, + /** @type {any} */ + content + ); + }); + } else { + this._prelimContent.splice(index, 0, ...content); + } + } + /** + * Appends content to this YArray. + * + * @param {Array} content Array of content to append. + * + * @todo Use the following implementation in all types. + */ + push(content) { + if (this.doc !== null) { + transact(this.doc, (transaction) => { + typeListPushGenerics( + transaction, + this, + /** @type {any} */ + content + ); + }); + } else { + this._prelimContent.push(...content); + } + } + /** + * Prepends content to this YArray. + * + * @param {Array} content Array of content to prepend. + */ + unshift(content) { + this.insert(0, content); + } + /** + * Deletes elements starting from an index. + * + * @param {number} index Index at which to start deleting elements + * @param {number} length The number of elements to remove. Defaults to 1. + */ + delete(index, length3 = 1) { + if (this.doc !== null) { + transact(this.doc, (transaction) => { + typeListDelete(transaction, this, index, length3); + }); + } else { + this._prelimContent.splice(index, length3); + } + } + /** + * Returns the i-th element from a YArray. + * + * @param {number} index The index of the element to return from the YArray + * @return {T} + */ + get(index) { + return typeListGet(this, index); + } + /** + * Transforms this YArray to a JavaScript Array. + * + * @return {Array} + */ + toArray() { + return typeListToArray(this); + } + /** + * Returns a portion of this YArray into a JavaScript Array selected + * from start to end (end not included). + * + * @param {number} [start] + * @param {number} [end] + * @return {Array} + */ + slice(start = 0, end = this.length) { + return typeListSlice(this, start, end); + } + /** + * Transforms this Shared Type to a JSON object. + * + * @return {Array} + */ + toJSON() { + return this.map((c) => c instanceof AbstractType ? c.toJSON() : c); + } + /** + * Returns an Array with the result of calling a provided function on every + * element of this YArray. + * + * @template M + * @param {function(T,number,YArray):M} f Function that produces an element of the new Array + * @return {Array} A new array with each element being the result of the + * callback function + */ + map(f) { + return typeListMap( + this, + /** @type {any} */ + f + ); + } + /** + * Executes a provided function once on every element of this YArray. + * + * @param {function(T,number,YArray):void} f A function to execute on every element of this YArray. + */ + forEach(f) { + typeListForEach(this, f); + } + /** + * @return {IterableIterator} + */ + [Symbol.iterator]() { + return typeListCreateIterator(this); + } + /** + * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder + */ + _write(encoder) { + encoder.writeTypeRef(YArrayRefID); + } + }; + var readYArray = (_decoder) => new YArray(); + var YMapEvent = class extends YEvent { + /** + * @param {YMap} ymap The YArray that changed. + * @param {Transaction} transaction + * @param {Set} subs The keys that changed. + */ + constructor(ymap, transaction, subs) { + super(ymap, transaction); + this.keysChanged = subs; + } + }; + var YMap = class _YMap extends AbstractType { + /** + * + * @param {Iterable=} entries - an optional iterable to initialize the YMap + */ + constructor(entries) { + super(); + this._prelimContent = null; + if (entries === void 0) { + this._prelimContent = /* @__PURE__ */ new Map(); + } else { + this._prelimContent = new Map(entries); + } + } + /** + * Integrate this type into the Yjs instance. + * + * * Save this struct in the os + * * This type is sent to other client + * * Observer functions are fired + * + * @param {Doc} y The Yjs instance + * @param {Item} item + */ + _integrate(y, item) { + super._integrate(y, item); + this._prelimContent.forEach((value, key) => { + this.set(key, value); + }); + this._prelimContent = null; + } + /** + * @return {YMap} + */ + _copy() { + return new _YMap(); + } + /** + * Makes a copy of this data type that can be included somewhere else. + * + * Note that the content is only readable _after_ it has been included somewhere in the Ydoc. + * + * @return {YMap} + */ + clone() { + const map2 = new _YMap(); + this.forEach((value, key) => { + map2.set(key, value instanceof AbstractType ? ( + /** @type {typeof value} */ + value.clone() + ) : value); + }); + return map2; + } + /** + * Creates YMapEvent and calls observers. + * + * @param {Transaction} transaction + * @param {Set} parentSubs Keys changed on this type. `null` if list was modified. + */ + _callObserver(transaction, parentSubs) { + callTypeObservers(this, transaction, new YMapEvent(this, transaction, parentSubs)); + } + /** + * Transforms this Shared Type to a JSON object. + * + * @return {Object} + */ + toJSON() { + this.doc ?? warnPrematureAccess(); + const map2 = {}; + this._map.forEach((item, key) => { + if (!item.deleted) { + const v = item.content.getContent()[item.length - 1]; + map2[key] = v instanceof AbstractType ? v.toJSON() : v; + } + }); + return map2; + } + /** + * Returns the size of the YMap (count of key/value pairs) + * + * @return {number} + */ + get size() { + return [...createMapIterator(this)].length; + } + /** + * Returns the keys for each element in the YMap Type. + * + * @return {IterableIterator} + */ + keys() { + return iteratorMap( + createMapIterator(this), + /** @param {any} v */ + (v) => v[0] + ); + } + /** + * Returns the values for each element in the YMap Type. + * + * @return {IterableIterator} + */ + values() { + return iteratorMap( + createMapIterator(this), + /** @param {any} v */ + (v) => v[1].content.getContent()[v[1].length - 1] + ); + } + /** + * Returns an Iterator of [key, value] pairs + * + * @return {IterableIterator<[string, MapType]>} + */ + entries() { + return iteratorMap( + createMapIterator(this), + /** @param {any} v */ + (v) => ( + /** @type {any} */ + [v[0], v[1].content.getContent()[v[1].length - 1]] + ) + ); + } + /** + * Executes a provided function on once on every key-value pair. + * + * @param {function(MapType,string,YMap):void} f A function to execute on every element of this YArray. + */ + forEach(f) { + this.doc ?? warnPrematureAccess(); + this._map.forEach((item, key) => { + if (!item.deleted) { + f(item.content.getContent()[item.length - 1], key, this); + } + }); + } + /** + * Returns an Iterator of [key, value] pairs + * + * @return {IterableIterator<[string, MapType]>} + */ + [Symbol.iterator]() { + return this.entries(); + } + /** + * Remove a specified element from this YMap. + * + * @param {string} key The key of the element to remove. + */ + delete(key) { + if (this.doc !== null) { + transact(this.doc, (transaction) => { + typeMapDelete(transaction, this, key); + }); + } else { + this._prelimContent.delete(key); + } + } + /** + * Adds or updates an element with a specified key and value. + * @template {MapType} VAL + * + * @param {string} key The key of the element to add to this YMap + * @param {VAL} value The value of the element to add + * @return {VAL} + */ + set(key, value) { + if (this.doc !== null) { + transact(this.doc, (transaction) => { + typeMapSet( + transaction, + this, + key, + /** @type {any} */ + value + ); + }); + } else { + this._prelimContent.set(key, value); + } + return value; + } + /** + * Returns a specified element from this YMap. + * + * @param {string} key + * @return {MapType|undefined} + */ + get(key) { + return ( + /** @type {any} */ + typeMapGet(this, key) + ); + } + /** + * Returns a boolean indicating whether the specified key exists or not. + * + * @param {string} key The key to test. + * @return {boolean} + */ + has(key) { + return typeMapHas(this, key); + } + /** + * Removes all elements from this YMap. + */ + clear() { + if (this.doc !== null) { + transact(this.doc, (transaction) => { + this.forEach(function(_value, key, map2) { + typeMapDelete(transaction, map2, key); + }); + }); + } else { + this._prelimContent.clear(); + } + } + /** + * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder + */ + _write(encoder) { + encoder.writeTypeRef(YMapRefID); + } + }; + var readYMap = (_decoder) => new YMap(); + var equalAttrs = (a, b) => a === b || typeof a === "object" && typeof b === "object" && a && b && equalFlat(a, b); + var ItemTextListPosition = class { + /** + * @param {Item|null} left + * @param {Item|null} right + * @param {number} index + * @param {Map} currentAttributes + */ + constructor(left, right, index, currentAttributes) { + this.left = left; + this.right = right; + this.index = index; + this.currentAttributes = currentAttributes; + } + /** + * Only call this if you know that this.right is defined + */ + forward() { + if (this.right === null) { + unexpectedCase(); + } + switch (this.right.content.constructor) { + case ContentFormat: + if (!this.right.deleted) { + updateCurrentAttributes( + this.currentAttributes, + /** @type {ContentFormat} */ + this.right.content + ); + } + break; + default: + if (!this.right.deleted) { + this.index += this.right.length; + } + break; + } + this.left = this.right; + this.right = this.right.right; + } + }; + var findNextPosition = (transaction, pos, count) => { + while (pos.right !== null && count > 0) { + switch (pos.right.content.constructor) { + case ContentFormat: + if (!pos.right.deleted) { + updateCurrentAttributes( + pos.currentAttributes, + /** @type {ContentFormat} */ + pos.right.content + ); + } + break; + default: + if (!pos.right.deleted) { + if (count < pos.right.length) { + getItemCleanStart(transaction, createID(pos.right.id.client, pos.right.id.clock + count)); + } + pos.index += pos.right.length; + count -= pos.right.length; + } + break; + } + pos.left = pos.right; + pos.right = pos.right.right; + } + return pos; + }; + var findPosition = (transaction, parent, index, useSearchMarker) => { + const currentAttributes = /* @__PURE__ */ new Map(); + const marker = useSearchMarker ? findMarker(parent, index) : null; + if (marker) { + const pos = new ItemTextListPosition(marker.p.left, marker.p, marker.index, currentAttributes); + return findNextPosition(transaction, pos, index - marker.index); + } else { + const pos = new ItemTextListPosition(null, parent._start, 0, currentAttributes); + return findNextPosition(transaction, pos, index); + } + }; + var insertNegatedAttributes = (transaction, parent, currPos, negatedAttributes) => { + while (currPos.right !== null && (currPos.right.deleted === true || currPos.right.content.constructor === ContentFormat && equalAttrs( + negatedAttributes.get( + /** @type {ContentFormat} */ + currPos.right.content.key + ), + /** @type {ContentFormat} */ + currPos.right.content.value + ))) { + if (!currPos.right.deleted) { + negatedAttributes.delete( + /** @type {ContentFormat} */ + currPos.right.content.key + ); + } + currPos.forward(); + } + const doc2 = transaction.doc; + const ownClientId = doc2.clientID; + negatedAttributes.forEach((val, key) => { + const left = currPos.left; + const right = currPos.right; + const nextFormat = new Item(createID(ownClientId, getState(doc2.store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentFormat(key, val)); + nextFormat.integrate(transaction, 0); + currPos.right = nextFormat; + currPos.forward(); + }); + }; + var updateCurrentAttributes = (currentAttributes, format) => { + const { key, value } = format; + if (value === null) { + currentAttributes.delete(key); + } else { + currentAttributes.set(key, value); + } + }; + var minimizeAttributeChanges = (currPos, attributes) => { + while (true) { + if (currPos.right === null) { + break; + } else if (currPos.right.deleted || currPos.right.content.constructor === ContentFormat && equalAttrs( + attributes[ + /** @type {ContentFormat} */ + currPos.right.content.key + ] ?? null, + /** @type {ContentFormat} */ + currPos.right.content.value + )) ; + else { + break; + } + currPos.forward(); + } + }; + var insertAttributes = (transaction, parent, currPos, attributes) => { + const doc2 = transaction.doc; + const ownClientId = doc2.clientID; + const negatedAttributes = /* @__PURE__ */ new Map(); + for (const key in attributes) { + const val = attributes[key]; + const currentVal = currPos.currentAttributes.get(key) ?? null; + if (!equalAttrs(currentVal, val)) { + negatedAttributes.set(key, currentVal); + const { left, right } = currPos; + currPos.right = new Item(createID(ownClientId, getState(doc2.store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentFormat(key, val)); + currPos.right.integrate(transaction, 0); + currPos.forward(); + } + } + return negatedAttributes; + }; + var insertText = (transaction, parent, currPos, text2, attributes) => { + currPos.currentAttributes.forEach((_val, key) => { + if (attributes[key] === void 0) { + attributes[key] = null; + } + }); + const doc2 = transaction.doc; + const ownClientId = doc2.clientID; + minimizeAttributeChanges(currPos, attributes); + const negatedAttributes = insertAttributes(transaction, parent, currPos, attributes); + const content = text2.constructor === String ? new ContentString( + /** @type {string} */ + text2 + ) : text2 instanceof AbstractType ? new ContentType(text2) : new ContentEmbed(text2); + let { left, right, index } = currPos; + if (parent._searchMarker) { + updateMarkerChanges(parent._searchMarker, currPos.index, content.getLength()); + } + right = new Item(createID(ownClientId, getState(doc2.store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, content); + right.integrate(transaction, 0); + currPos.right = right; + currPos.index = index; + currPos.forward(); + insertNegatedAttributes(transaction, parent, currPos, negatedAttributes); + }; + var formatText = (transaction, parent, currPos, length3, attributes) => { + const doc2 = transaction.doc; + const ownClientId = doc2.clientID; + minimizeAttributeChanges(currPos, attributes); + const negatedAttributes = insertAttributes(transaction, parent, currPos, attributes); + iterationLoop: while (currPos.right !== null && (length3 > 0 || negatedAttributes.size > 0 && (currPos.right.deleted || currPos.right.content.constructor === ContentFormat))) { + if (!currPos.right.deleted) { + switch (currPos.right.content.constructor) { + case ContentFormat: { + const { key, value } = ( + /** @type {ContentFormat} */ + currPos.right.content + ); + const attr = attributes[key]; + if (attr !== void 0) { + if (equalAttrs(attr, value)) { + negatedAttributes.delete(key); + } else { + if (length3 === 0) { + break iterationLoop; + } + negatedAttributes.set(key, value); + } + currPos.right.delete(transaction); + } else { + currPos.currentAttributes.set(key, value); + } + break; + } + default: + if (length3 < currPos.right.length) { + getItemCleanStart(transaction, createID(currPos.right.id.client, currPos.right.id.clock + length3)); + } + length3 -= currPos.right.length; + break; + } + } + currPos.forward(); + } + if (length3 > 0) { + let newlines = ""; + for (; length3 > 0; length3--) { + newlines += "\n"; + } + currPos.right = new Item(createID(ownClientId, getState(doc2.store, ownClientId)), currPos.left, currPos.left && currPos.left.lastId, currPos.right, currPos.right && currPos.right.id, parent, null, new ContentString(newlines)); + currPos.right.integrate(transaction, 0); + currPos.forward(); + } + insertNegatedAttributes(transaction, parent, currPos, negatedAttributes); + }; + var cleanupFormattingGap = (transaction, start, curr, startAttributes, currAttributes) => { + let end = start; + const endFormats = create(); + while (end && (!end.countable || end.deleted)) { + if (!end.deleted && end.content.constructor === ContentFormat) { + const cf = ( + /** @type {ContentFormat} */ + end.content + ); + endFormats.set(cf.key, cf); + } + end = end.right; + } + let cleanups = 0; + let reachedCurr = false; + while (start !== end) { + if (curr === start) { + reachedCurr = true; + } + if (!start.deleted) { + const content = start.content; + switch (content.constructor) { + case ContentFormat: { + const { key, value } = ( + /** @type {ContentFormat} */ + content + ); + const startAttrValue = startAttributes.get(key) ?? null; + if (endFormats.get(key) !== content || startAttrValue === value) { + start.delete(transaction); + cleanups++; + if (!reachedCurr && (currAttributes.get(key) ?? null) === value && startAttrValue !== value) { + if (startAttrValue === null) { + currAttributes.delete(key); + } else { + currAttributes.set(key, startAttrValue); + } + } + } + if (!reachedCurr && !start.deleted) { + updateCurrentAttributes( + currAttributes, + /** @type {ContentFormat} */ + content + ); + } + break; + } + } + } + start = /** @type {Item} */ + start.right; + } + return cleanups; + }; + var cleanupContextlessFormattingGap = (transaction, item) => { + while (item && item.right && (item.right.deleted || !item.right.countable)) { + item = item.right; + } + const attrs = /* @__PURE__ */ new Set(); + while (item && (item.deleted || !item.countable)) { + if (!item.deleted && item.content.constructor === ContentFormat) { + const key = ( + /** @type {ContentFormat} */ + item.content.key + ); + if (attrs.has(key)) { + item.delete(transaction); + } else { + attrs.add(key); + } + } + item = item.left; + } + }; + var cleanupYTextFormatting = (type) => { + let res = 0; + transact( + /** @type {Doc} */ + type.doc, + (transaction) => { + let start = ( + /** @type {Item} */ + type._start + ); + let end = type._start; + let startAttributes = create(); + const currentAttributes = copy(startAttributes); + while (end) { + if (end.deleted === false) { + switch (end.content.constructor) { + case ContentFormat: + updateCurrentAttributes( + currentAttributes, + /** @type {ContentFormat} */ + end.content + ); + break; + default: + res += cleanupFormattingGap(transaction, start, end, startAttributes, currentAttributes); + startAttributes = copy(currentAttributes); + start = end; + break; + } + } + end = end.right; + } + } + ); + return res; + }; + var cleanupYTextAfterTransaction = (transaction) => { + const needFullCleanup = /* @__PURE__ */ new Set(); + const doc2 = transaction.doc; + for (const [client, afterClock] of transaction.afterState.entries()) { + const clock = transaction.beforeState.get(client) || 0; + if (afterClock === clock) { + continue; + } + iterateStructs( + transaction, + /** @type {Array} */ + doc2.store.clients.get(client), + clock, + afterClock, + (item) => { + if (!item.deleted && /** @type {Item} */ + item.content.constructor === ContentFormat && item.constructor !== GC) { + needFullCleanup.add( + /** @type {any} */ + item.parent + ); + } + } + ); + } + transact(doc2, (t) => { + iterateDeletedStructs(transaction, transaction.deleteSet, (item) => { + if (item instanceof GC || !/** @type {YText} */ + item.parent._hasFormatting || needFullCleanup.has( + /** @type {YText} */ + item.parent + )) { + return; + } + const parent = ( + /** @type {YText} */ + item.parent + ); + if (item.content.constructor === ContentFormat) { + needFullCleanup.add(parent); + } else { + cleanupContextlessFormattingGap(t, item); + } + }); + for (const yText of needFullCleanup) { + cleanupYTextFormatting(yText); + } + }); + }; + var deleteText = (transaction, currPos, length3) => { + const startLength = length3; + const startAttrs = copy(currPos.currentAttributes); + const start = currPos.right; + while (length3 > 0 && currPos.right !== null) { + if (currPos.right.deleted === false) { + switch (currPos.right.content.constructor) { + case ContentType: + case ContentEmbed: + case ContentString: + if (length3 < currPos.right.length) { + getItemCleanStart(transaction, createID(currPos.right.id.client, currPos.right.id.clock + length3)); + } + length3 -= currPos.right.length; + currPos.right.delete(transaction); + break; + } + } + currPos.forward(); + } + if (start) { + cleanupFormattingGap(transaction, start, currPos.right, startAttrs, currPos.currentAttributes); + } + const parent = ( + /** @type {AbstractType} */ + /** @type {Item} */ + (currPos.left || currPos.right).parent + ); + if (parent._searchMarker) { + updateMarkerChanges(parent._searchMarker, currPos.index, -startLength + length3); + } + return currPos; + }; + var YTextEvent = class extends YEvent { + /** + * @param {YText} ytext + * @param {Transaction} transaction + * @param {Set} subs The keys that changed + */ + constructor(ytext, transaction, subs) { + super(ytext, transaction); + this.childListChanged = false; + this.keysChanged = /* @__PURE__ */ new Set(); + subs.forEach((sub) => { + if (sub === null) { + this.childListChanged = true; + } else { + this.keysChanged.add(sub); + } + }); + } + /** + * @type {{added:Set,deleted:Set,keys:Map,delta:Array<{insert?:Array|string, delete?:number, retain?:number}>}} + */ + get changes() { + if (this._changes === null) { + const changes = { + keys: this.keys, + delta: this.delta, + added: /* @__PURE__ */ new Set(), + deleted: /* @__PURE__ */ new Set() + }; + this._changes = changes; + } + return ( + /** @type {any} */ + this._changes + ); + } + /** + * Compute the changes in the delta format. + * A {@link https://quilljs.com/docs/delta/|Quill Delta}) that represents the changes on the document. + * + * @type {Array<{insert?:string|object|AbstractType, delete?:number, retain?:number, attributes?: Object}>} + * + * @public + */ + get delta() { + if (this._delta === null) { + const y = ( + /** @type {Doc} */ + this.target.doc + ); + const delta = []; + transact(y, (transaction) => { + const currentAttributes = /* @__PURE__ */ new Map(); + const oldAttributes = /* @__PURE__ */ new Map(); + let item = this.target._start; + let action = null; + const attributes = {}; + let insert = ""; + let retain = 0; + let deleteLen = 0; + const addOp = () => { + if (action !== null) { + let op = null; + switch (action) { + case "delete": + if (deleteLen > 0) { + op = { delete: deleteLen }; + } + deleteLen = 0; + break; + case "insert": + if (typeof insert === "object" || insert.length > 0) { + op = { insert }; + if (currentAttributes.size > 0) { + op.attributes = {}; + currentAttributes.forEach((value, key) => { + if (value !== null) { + op.attributes[key] = value; + } + }); + } + } + insert = ""; + break; + case "retain": + if (retain > 0) { + op = { retain }; + if (!isEmpty(attributes)) { + op.attributes = assign({}, attributes); + } + } + retain = 0; + break; + } + if (op) delta.push(op); + action = null; + } + }; + while (item !== null) { + switch (item.content.constructor) { + case ContentType: + case ContentEmbed: + if (this.adds(item)) { + if (!this.deletes(item)) { + addOp(); + action = "insert"; + insert = item.content.getContent()[0]; + addOp(); + } + } else if (this.deletes(item)) { + if (action !== "delete") { + addOp(); + action = "delete"; + } + deleteLen += 1; + } else if (!item.deleted) { + if (action !== "retain") { + addOp(); + action = "retain"; + } + retain += 1; + } + break; + case ContentString: + if (this.adds(item)) { + if (!this.deletes(item)) { + if (action !== "insert") { + addOp(); + action = "insert"; + } + insert += /** @type {ContentString} */ + item.content.str; + } + } else if (this.deletes(item)) { + if (action !== "delete") { + addOp(); + action = "delete"; + } + deleteLen += item.length; + } else if (!item.deleted) { + if (action !== "retain") { + addOp(); + action = "retain"; + } + retain += item.length; + } + break; + case ContentFormat: { + const { key, value } = ( + /** @type {ContentFormat} */ + item.content + ); + if (this.adds(item)) { + if (!this.deletes(item)) { + const curVal = currentAttributes.get(key) ?? null; + if (!equalAttrs(curVal, value)) { + if (action === "retain") { + addOp(); + } + if (equalAttrs(value, oldAttributes.get(key) ?? null)) { + delete attributes[key]; + } else { + attributes[key] = value; + } + } else if (value !== null) { + item.delete(transaction); + } + } + } else if (this.deletes(item)) { + oldAttributes.set(key, value); + const curVal = currentAttributes.get(key) ?? null; + if (!equalAttrs(curVal, value)) { + if (action === "retain") { + addOp(); + } + attributes[key] = curVal; + } + } else if (!item.deleted) { + oldAttributes.set(key, value); + const attr = attributes[key]; + if (attr !== void 0) { + if (!equalAttrs(attr, value)) { + if (action === "retain") { + addOp(); + } + if (value === null) { + delete attributes[key]; + } else { + attributes[key] = value; + } + } else if (attr !== null) { + item.delete(transaction); + } + } + } + if (!item.deleted) { + if (action === "insert") { + addOp(); + } + updateCurrentAttributes( + currentAttributes, + /** @type {ContentFormat} */ + item.content + ); + } + break; + } + } + item = item.right; + } + addOp(); + while (delta.length > 0) { + const lastOp = delta[delta.length - 1]; + if (lastOp.retain !== void 0 && lastOp.attributes === void 0) { + delta.pop(); + } else { + break; + } + } + }); + this._delta = delta; + } + return ( + /** @type {any} */ + this._delta + ); + } + }; + var YText = class _YText extends AbstractType { + /** + * @param {String} [string] The initial value of the YText. + */ + constructor(string) { + super(); + this._pending = string !== void 0 ? [() => this.insert(0, string)] : []; + this._searchMarker = []; + this._hasFormatting = false; + } + /** + * Number of characters of this text type. + * + * @type {number} + */ + get length() { + this.doc ?? warnPrematureAccess(); + return this._length; + } + /** + * @param {Doc} y + * @param {Item} item + */ + _integrate(y, item) { + super._integrate(y, item); + try { + this._pending.forEach((f) => f()); + } catch (e) { + console.error(e); + } + this._pending = null; + } + _copy() { + return new _YText(); + } + /** + * Makes a copy of this data type that can be included somewhere else. + * + * Note that the content is only readable _after_ it has been included somewhere in the Ydoc. + * + * @return {YText} + */ + clone() { + const text2 = new _YText(); + text2.applyDelta(this.toDelta()); + return text2; + } + /** + * Creates YTextEvent and calls observers. + * + * @param {Transaction} transaction + * @param {Set} parentSubs Keys changed on this type. `null` if list was modified. + */ + _callObserver(transaction, parentSubs) { + super._callObserver(transaction, parentSubs); + const event = new YTextEvent(this, transaction, parentSubs); + callTypeObservers(this, transaction, event); + if (!transaction.local && this._hasFormatting) { + transaction._needFormattingCleanup = true; + } + } + /** + * Returns the unformatted string representation of this YText type. + * + * @public + */ + toString() { + this.doc ?? warnPrematureAccess(); + let str = ""; + let n = this._start; + while (n !== null) { + if (!n.deleted && n.countable && n.content.constructor === ContentString) { + str += /** @type {ContentString} */ + n.content.str; + } + n = n.right; + } + return str; + } + /** + * Returns the unformatted string representation of this YText type. + * + * @return {string} + * @public + */ + toJSON() { + return this.toString(); + } + /** + * Apply a {@link Delta} on this shared YText type. + * + * @param {Array} delta The changes to apply on this element. + * @param {object} opts + * @param {boolean} [opts.sanitize] Sanitize input delta. Removes ending newlines if set to true. + * + * + * @public + */ + applyDelta(delta, { sanitize = true } = {}) { + if (this.doc !== null) { + transact(this.doc, (transaction) => { + const currPos = new ItemTextListPosition(null, this._start, 0, /* @__PURE__ */ new Map()); + for (let i = 0; i < delta.length; i++) { + const op = delta[i]; + if (op.insert !== void 0) { + const ins = !sanitize && typeof op.insert === "string" && i === delta.length - 1 && currPos.right === null && op.insert.slice(-1) === "\n" ? op.insert.slice(0, -1) : op.insert; + if (typeof ins !== "string" || ins.length > 0) { + insertText(transaction, this, currPos, ins, op.attributes || {}); + } + } else if (op.retain !== void 0) { + formatText(transaction, this, currPos, op.retain, op.attributes || {}); + } else if (op.delete !== void 0) { + deleteText(transaction, currPos, op.delete); + } + } + }); + } else { + this._pending.push(() => this.applyDelta(delta)); + } + } + /** + * Returns the Delta representation of this YText type. + * + * @param {Snapshot} [snapshot] + * @param {Snapshot} [prevSnapshot] + * @param {function('removed' | 'added', ID):any} [computeYChange] + * @return {any} The Delta representation of this type. + * + * @public + */ + toDelta(snapshot, prevSnapshot, computeYChange) { + this.doc ?? warnPrematureAccess(); + const ops = []; + const currentAttributes = /* @__PURE__ */ new Map(); + const doc2 = ( + /** @type {Doc} */ + this.doc + ); + let str = ""; + let n = this._start; + function packStr() { + if (str.length > 0) { + const attributes = {}; + let addAttributes = false; + currentAttributes.forEach((value, key) => { + addAttributes = true; + attributes[key] = value; + }); + const op = { insert: str }; + if (addAttributes) { + op.attributes = attributes; + } + ops.push(op); + str = ""; + } + } + const computeDelta = () => { + while (n !== null) { + if (isVisible(n, snapshot) || prevSnapshot !== void 0 && isVisible(n, prevSnapshot)) { + switch (n.content.constructor) { + case ContentString: { + const cur = currentAttributes.get("ychange"); + if (snapshot !== void 0 && !isVisible(n, snapshot)) { + if (cur === void 0 || cur.user !== n.id.client || cur.type !== "removed") { + packStr(); + currentAttributes.set("ychange", computeYChange ? computeYChange("removed", n.id) : { type: "removed" }); + } + } else if (prevSnapshot !== void 0 && !isVisible(n, prevSnapshot)) { + if (cur === void 0 || cur.user !== n.id.client || cur.type !== "added") { + packStr(); + currentAttributes.set("ychange", computeYChange ? computeYChange("added", n.id) : { type: "added" }); + } + } else if (cur !== void 0) { + packStr(); + currentAttributes.delete("ychange"); + } + str += /** @type {ContentString} */ + n.content.str; + break; + } + case ContentType: + case ContentEmbed: { + packStr(); + const op = { + insert: n.content.getContent()[0] + }; + if (currentAttributes.size > 0) { + const attrs = ( + /** @type {Object} */ + {} + ); + op.attributes = attrs; + currentAttributes.forEach((value, key) => { + attrs[key] = value; + }); + } + ops.push(op); + break; + } + case ContentFormat: + if (isVisible(n, snapshot)) { + packStr(); + updateCurrentAttributes( + currentAttributes, + /** @type {ContentFormat} */ + n.content + ); + } + break; + } + } + n = n.right; + } + packStr(); + }; + if (snapshot || prevSnapshot) { + transact(doc2, (transaction) => { + if (snapshot) { + splitSnapshotAffectedStructs(transaction, snapshot); + } + if (prevSnapshot) { + splitSnapshotAffectedStructs(transaction, prevSnapshot); + } + computeDelta(); + }, "cleanup"); + } else { + computeDelta(); + } + return ops; + } + /** + * Insert text at a given index. + * + * @param {number} index The index at which to start inserting. + * @param {String} text The text to insert at the specified position. + * @param {TextAttributes} [attributes] Optionally define some formatting + * information to apply on the inserted + * Text. + * @public + */ + insert(index, text2, attributes) { + if (text2.length <= 0) { + return; + } + const y = this.doc; + if (y !== null) { + transact(y, (transaction) => { + const pos = findPosition(transaction, this, index, !attributes); + if (!attributes) { + attributes = {}; + pos.currentAttributes.forEach((v, k) => { + attributes[k] = v; + }); + } + insertText(transaction, this, pos, text2, attributes); + }); + } else { + this._pending.push(() => this.insert(index, text2, attributes)); + } + } + /** + * Inserts an embed at a index. + * + * @param {number} index The index to insert the embed at. + * @param {Object | AbstractType} embed The Object that represents the embed. + * @param {TextAttributes} [attributes] Attribute information to apply on the + * embed + * + * @public + */ + insertEmbed(index, embed, attributes) { + const y = this.doc; + if (y !== null) { + transact(y, (transaction) => { + const pos = findPosition(transaction, this, index, !attributes); + insertText(transaction, this, pos, embed, attributes || {}); + }); + } else { + this._pending.push(() => this.insertEmbed(index, embed, attributes || {})); + } + } + /** + * Deletes text starting from an index. + * + * @param {number} index Index at which to start deleting. + * @param {number} length The number of characters to remove. Defaults to 1. + * + * @public + */ + delete(index, length3) { + if (length3 === 0) { + return; + } + const y = this.doc; + if (y !== null) { + transact(y, (transaction) => { + deleteText(transaction, findPosition(transaction, this, index, true), length3); + }); + } else { + this._pending.push(() => this.delete(index, length3)); + } + } + /** + * Assigns properties to a range of text. + * + * @param {number} index The position where to start formatting. + * @param {number} length The amount of characters to assign properties to. + * @param {TextAttributes} attributes Attribute information to apply on the + * text. + * + * @public + */ + format(index, length3, attributes) { + if (length3 === 0) { + return; + } + const y = this.doc; + if (y !== null) { + transact(y, (transaction) => { + const pos = findPosition(transaction, this, index, false); + if (pos.right === null) { + return; + } + formatText(transaction, this, pos, length3, attributes); + }); + } else { + this._pending.push(() => this.format(index, length3, attributes)); + } + } + /** + * Removes an attribute. + * + * @note Xml-Text nodes don't have attributes. You can use this feature to assign properties to complete text-blocks. + * + * @param {String} attributeName The attribute name that is to be removed. + * + * @public + */ + removeAttribute(attributeName) { + if (this.doc !== null) { + transact(this.doc, (transaction) => { + typeMapDelete(transaction, this, attributeName); + }); + } else { + this._pending.push(() => this.removeAttribute(attributeName)); + } + } + /** + * Sets or updates an attribute. + * + * @note Xml-Text nodes don't have attributes. You can use this feature to assign properties to complete text-blocks. + * + * @param {String} attributeName The attribute name that is to be set. + * @param {any} attributeValue The attribute value that is to be set. + * + * @public + */ + setAttribute(attributeName, attributeValue) { + if (this.doc !== null) { + transact(this.doc, (transaction) => { + typeMapSet(transaction, this, attributeName, attributeValue); + }); + } else { + this._pending.push(() => this.setAttribute(attributeName, attributeValue)); + } + } + /** + * Returns an attribute value that belongs to the attribute name. + * + * @note Xml-Text nodes don't have attributes. You can use this feature to assign properties to complete text-blocks. + * + * @param {String} attributeName The attribute name that identifies the + * queried value. + * @return {any} The queried attribute value. + * + * @public + */ + getAttribute(attributeName) { + return ( + /** @type {any} */ + typeMapGet(this, attributeName) + ); + } + /** + * Returns all attribute name/value pairs in a JSON Object. + * + * @note Xml-Text nodes don't have attributes. You can use this feature to assign properties to complete text-blocks. + * + * @return {Object} A JSON Object that describes the attributes. + * + * @public + */ + getAttributes() { + return typeMapGetAll(this); + } + /** + * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder + */ + _write(encoder) { + encoder.writeTypeRef(YTextRefID); + } + }; + var readYText = (_decoder) => new YText(); + var YXmlTreeWalker = class { + /** + * @param {YXmlFragment | YXmlElement} root + * @param {function(AbstractType):boolean} [f] + */ + constructor(root, f = () => true) { + this._filter = f; + this._root = root; + this._currentNode = /** @type {Item} */ + root._start; + this._firstCall = true; + root.doc ?? warnPrematureAccess(); + } + [Symbol.iterator]() { + return this; + } + /** + * Get the next node. + * + * @return {IteratorResult} The next node. + * + * @public + */ + next() { + let n = this._currentNode; + let type = n && n.content && /** @type {any} */ + n.content.type; + if (n !== null && (!this._firstCall || n.deleted || !this._filter(type))) { + do { + type = /** @type {any} */ + n.content.type; + if (!n.deleted && (type.constructor === YXmlElement || type.constructor === YXmlFragment) && type._start !== null) { + n = type._start; + } else { + while (n !== null) { + const nxt = n.next; + if (nxt !== null) { + n = nxt; + break; + } else if (n.parent === this._root) { + n = null; + } else { + n = /** @type {AbstractType} */ + n.parent._item; + } + } + } + } while (n !== null && (n.deleted || !this._filter( + /** @type {ContentType} */ + n.content.type + ))); + } + this._firstCall = false; + if (n === null) { + return { value: void 0, done: true }; + } + this._currentNode = n; + return { value: ( + /** @type {any} */ + n.content.type + ), done: false }; + } + }; + var YXmlFragment = class _YXmlFragment extends AbstractType { + constructor() { + super(); + this._prelimContent = []; + } + /** + * @type {YXmlElement|YXmlText|null} + */ + get firstChild() { + const first = this._first; + return first ? first.content.getContent()[0] : null; + } + /** + * Integrate this type into the Yjs instance. + * + * * Save this struct in the os + * * This type is sent to other client + * * Observer functions are fired + * + * @param {Doc} y The Yjs instance + * @param {Item} item + */ + _integrate(y, item) { + super._integrate(y, item); + this.insert( + 0, + /** @type {Array} */ + this._prelimContent + ); + this._prelimContent = null; + } + _copy() { + return new _YXmlFragment(); + } + /** + * Makes a copy of this data type that can be included somewhere else. + * + * Note that the content is only readable _after_ it has been included somewhere in the Ydoc. + * + * @return {YXmlFragment} + */ + clone() { + const el = new _YXmlFragment(); + el.insert(0, this.toArray().map((item) => item instanceof AbstractType ? item.clone() : item)); + return el; + } + get length() { + this.doc ?? warnPrematureAccess(); + return this._prelimContent === null ? this._length : this._prelimContent.length; + } + /** + * Create a subtree of childNodes. + * + * @example + * const walker = elem.createTreeWalker(dom => dom.nodeName === 'div') + * for (let node in walker) { + * // `node` is a div node + * nop(node) + * } + * + * @param {function(AbstractType):boolean} filter Function that is called on each child element and + * returns a Boolean indicating whether the child + * is to be included in the subtree. + * @return {YXmlTreeWalker} A subtree and a position within it. + * + * @public + */ + createTreeWalker(filter) { + return new YXmlTreeWalker(this, filter); + } + /** + * Returns the first YXmlElement that matches the query. + * Similar to DOM's {@link querySelector}. + * + * Query support: + * - tagname + * TODO: + * - id + * - attribute + * + * @param {CSS_Selector} query The query on the children. + * @return {YXmlElement|YXmlText|YXmlHook|null} The first element that matches the query or null. + * + * @public + */ + querySelector(query) { + query = query.toUpperCase(); + const iterator = new YXmlTreeWalker(this, (element2) => element2.nodeName && element2.nodeName.toUpperCase() === query); + const next = iterator.next(); + if (next.done) { + return null; + } else { + return next.value; + } + } + /** + * Returns all YXmlElements that match the query. + * Similar to Dom's {@link querySelectorAll}. + * + * @todo Does not yet support all queries. Currently only query by tagName. + * + * @param {CSS_Selector} query The query on the children + * @return {Array} The elements that match this query. + * + * @public + */ + querySelectorAll(query) { + query = query.toUpperCase(); + return from(new YXmlTreeWalker(this, (element2) => element2.nodeName && element2.nodeName.toUpperCase() === query)); + } + /** + * Creates YXmlEvent and calls observers. + * + * @param {Transaction} transaction + * @param {Set} parentSubs Keys changed on this type. `null` if list was modified. + */ + _callObserver(transaction, parentSubs) { + callTypeObservers(this, transaction, new YXmlEvent(this, parentSubs, transaction)); + } + /** + * Get the string representation of all the children of this YXmlFragment. + * + * @return {string} The string representation of all children. + */ + toString() { + return typeListMap(this, (xml) => xml.toString()).join(""); + } + /** + * @return {string} + */ + toJSON() { + return this.toString(); + } + /** + * Creates a Dom Element that mirrors this YXmlElement. + * + * @param {Document} [_document=document] The document object (you must define + * this when calling this method in + * nodejs) + * @param {Object} [hooks={}] Optional property to customize how hooks + * are presented in the DOM + * @param {any} [binding] You should not set this property. This is + * used if DomBinding wants to create a + * association to the created DOM type. + * @return {Node} The {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element} + * + * @public + */ + toDOM(_document = document, hooks = {}, binding) { + const fragment = _document.createDocumentFragment(); + if (binding !== void 0) { + binding._createAssociation(fragment, this); + } + typeListForEach(this, (xmlType) => { + fragment.insertBefore(xmlType.toDOM(_document, hooks, binding), null); + }); + return fragment; + } + /** + * Inserts new content at an index. + * + * @example + * // Insert character 'a' at position 0 + * xml.insert(0, [new Y.XmlText('text')]) + * + * @param {number} index The index to insert content at + * @param {Array} content The array of content + */ + insert(index, content) { + if (this.doc !== null) { + transact(this.doc, (transaction) => { + typeListInsertGenerics(transaction, this, index, content); + }); + } else { + this._prelimContent.splice(index, 0, ...content); + } + } + /** + * Inserts new content at an index. + * + * @example + * // Insert character 'a' at position 0 + * xml.insert(0, [new Y.XmlText('text')]) + * + * @param {null|Item|YXmlElement|YXmlText} ref The index to insert content at + * @param {Array} content The array of content + */ + insertAfter(ref, content) { + if (this.doc !== null) { + transact(this.doc, (transaction) => { + const refItem = ref && ref instanceof AbstractType ? ref._item : ref; + typeListInsertGenericsAfter(transaction, this, refItem, content); + }); + } else { + const pc = ( + /** @type {Array} */ + this._prelimContent + ); + const index = ref === null ? 0 : pc.findIndex((el) => el === ref) + 1; + if (index === 0 && ref !== null) { + throw create3("Reference item not found"); + } + pc.splice(index, 0, ...content); + } + } + /** + * Deletes elements starting from an index. + * + * @param {number} index Index at which to start deleting elements + * @param {number} [length=1] The number of elements to remove. Defaults to 1. + */ + delete(index, length3 = 1) { + if (this.doc !== null) { + transact(this.doc, (transaction) => { + typeListDelete(transaction, this, index, length3); + }); + } else { + this._prelimContent.splice(index, length3); + } + } + /** + * Transforms this YArray to a JavaScript Array. + * + * @return {Array} + */ + toArray() { + return typeListToArray(this); + } + /** + * Appends content to this YArray. + * + * @param {Array} content Array of content to append. + */ + push(content) { + this.insert(this.length, content); + } + /** + * Prepends content to this YArray. + * + * @param {Array} content Array of content to prepend. + */ + unshift(content) { + this.insert(0, content); + } + /** + * Returns the i-th element from a YArray. + * + * @param {number} index The index of the element to return from the YArray + * @return {YXmlElement|YXmlText} + */ + get(index) { + return typeListGet(this, index); + } + /** + * Returns a portion of this YXmlFragment into a JavaScript Array selected + * from start to end (end not included). + * + * @param {number} [start] + * @param {number} [end] + * @return {Array} + */ + slice(start = 0, end = this.length) { + return typeListSlice(this, start, end); + } + /** + * Executes a provided function on once on every child element. + * + * @param {function(YXmlElement|YXmlText,number, typeof self):void} f A function to execute on every element of this YArray. + */ + forEach(f) { + typeListForEach(this, f); + } + /** + * Transform the properties of this type to binary and write it to an + * BinaryEncoder. + * + * This is called when this Item is sent to a remote peer. + * + * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder The encoder to write data to. + */ + _write(encoder) { + encoder.writeTypeRef(YXmlFragmentRefID); + } + }; + var readYXmlFragment = (_decoder) => new YXmlFragment(); + var YXmlElement = class _YXmlElement extends YXmlFragment { + constructor(nodeName = "UNDEFINED") { + super(); + this.nodeName = nodeName; + this._prelimAttrs = /* @__PURE__ */ new Map(); + } + /** + * @type {YXmlElement|YXmlText|null} + */ + get nextSibling() { + const n = this._item ? this._item.next : null; + return n ? ( + /** @type {YXmlElement|YXmlText} */ + /** @type {ContentType} */ + n.content.type + ) : null; + } + /** + * @type {YXmlElement|YXmlText|null} + */ + get prevSibling() { + const n = this._item ? this._item.prev : null; + return n ? ( + /** @type {YXmlElement|YXmlText} */ + /** @type {ContentType} */ + n.content.type + ) : null; + } + /** + * Integrate this type into the Yjs instance. + * + * * Save this struct in the os + * * This type is sent to other client + * * Observer functions are fired + * + * @param {Doc} y The Yjs instance + * @param {Item} item + */ + _integrate(y, item) { + super._integrate(y, item); + /** @type {Map} */ + this._prelimAttrs.forEach((value, key) => { + this.setAttribute(key, value); + }); + this._prelimAttrs = null; + } + /** + * Creates an Item with the same effect as this Item (without position effect) + * + * @return {YXmlElement} + */ + _copy() { + return new _YXmlElement(this.nodeName); + } + /** + * Makes a copy of this data type that can be included somewhere else. + * + * Note that the content is only readable _after_ it has been included somewhere in the Ydoc. + * + * @return {YXmlElement} + */ + clone() { + const el = new _YXmlElement(this.nodeName); + const attrs = this.getAttributes(); + forEach(attrs, (value, key) => { + el.setAttribute( + key, + /** @type {any} */ + value + ); + }); + el.insert(0, this.toArray().map((v) => v instanceof AbstractType ? v.clone() : v)); + return el; + } + /** + * Returns the XML serialization of this YXmlElement. + * The attributes are ordered by attribute-name, so you can easily use this + * method to compare YXmlElements + * + * @return {string} The string representation of this type. + * + * @public + */ + toString() { + const attrs = this.getAttributes(); + const stringBuilder = []; + const keys3 = []; + for (const key in attrs) { + keys3.push(key); + } + keys3.sort(); + const keysLen = keys3.length; + for (let i = 0; i < keysLen; i++) { + const key = keys3[i]; + stringBuilder.push(key + '="' + attrs[key] + '"'); + } + const nodeName = this.nodeName.toLocaleLowerCase(); + const attrsString = stringBuilder.length > 0 ? " " + stringBuilder.join(" ") : ""; + return `<${nodeName}${attrsString}>${super.toString()}`; + } + /** + * Removes an attribute from this YXmlElement. + * + * @param {string} attributeName The attribute name that is to be removed. + * + * @public + */ + removeAttribute(attributeName) { + if (this.doc !== null) { + transact(this.doc, (transaction) => { + typeMapDelete(transaction, this, attributeName); + }); + } else { + this._prelimAttrs.delete(attributeName); + } + } + /** + * Sets or updates an attribute. + * + * @template {keyof KV & string} KEY + * + * @param {KEY} attributeName The attribute name that is to be set. + * @param {KV[KEY]} attributeValue The attribute value that is to be set. + * + * @public + */ + setAttribute(attributeName, attributeValue) { + if (this.doc !== null) { + transact(this.doc, (transaction) => { + typeMapSet(transaction, this, attributeName, attributeValue); + }); + } else { + this._prelimAttrs.set(attributeName, attributeValue); + } + } + /** + * Returns an attribute value that belongs to the attribute name. + * + * @template {keyof KV & string} KEY + * + * @param {KEY} attributeName The attribute name that identifies the + * queried value. + * @return {KV[KEY]|undefined} The queried attribute value. + * + * @public + */ + getAttribute(attributeName) { + return ( + /** @type {any} */ + typeMapGet(this, attributeName) + ); + } + /** + * Returns whether an attribute exists + * + * @param {string} attributeName The attribute name to check for existence. + * @return {boolean} whether the attribute exists. + * + * @public + */ + hasAttribute(attributeName) { + return ( + /** @type {any} */ + typeMapHas(this, attributeName) + ); + } + /** + * Returns all attribute name/value pairs in a JSON Object. + * + * @param {Snapshot} [snapshot] + * @return {{ [Key in Extract]?: KV[Key]}} A JSON Object that describes the attributes. + * + * @public + */ + getAttributes(snapshot) { + return ( + /** @type {any} */ + snapshot ? typeMapGetAllSnapshot(this, snapshot) : typeMapGetAll(this) + ); + } + /** + * Creates a Dom Element that mirrors this YXmlElement. + * + * @param {Document} [_document=document] The document object (you must define + * this when calling this method in + * nodejs) + * @param {Object} [hooks={}] Optional property to customize how hooks + * are presented in the DOM + * @param {any} [binding] You should not set this property. This is + * used if DomBinding wants to create a + * association to the created DOM type. + * @return {Node} The {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element} + * + * @public + */ + toDOM(_document = document, hooks = {}, binding) { + const dom = _document.createElement(this.nodeName); + const attrs = this.getAttributes(); + for (const key in attrs) { + const value = attrs[key]; + if (typeof value === "string") { + dom.setAttribute(key, value); + } + } + typeListForEach(this, (yxml) => { + dom.appendChild(yxml.toDOM(_document, hooks, binding)); + }); + if (binding !== void 0) { + binding._createAssociation(dom, this); + } + return dom; + } + /** + * Transform the properties of this type to binary and write it to an + * BinaryEncoder. + * + * This is called when this Item is sent to a remote peer. + * + * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder The encoder to write data to. + */ + _write(encoder) { + encoder.writeTypeRef(YXmlElementRefID); + encoder.writeKey(this.nodeName); + } + }; + var readYXmlElement = (decoder) => new YXmlElement(decoder.readKey()); + var YXmlEvent = class extends YEvent { + /** + * @param {YXmlElement|YXmlText|YXmlFragment} target The target on which the event is created. + * @param {Set} subs The set of changed attributes. `null` is included if the + * child list changed. + * @param {Transaction} transaction The transaction instance with which the + * change was created. + */ + constructor(target, subs, transaction) { + super(target, transaction); + this.childListChanged = false; + this.attributesChanged = /* @__PURE__ */ new Set(); + subs.forEach((sub) => { + if (sub === null) { + this.childListChanged = true; + } else { + this.attributesChanged.add(sub); + } + }); + } + }; + var YXmlHook = class _YXmlHook extends YMap { + /** + * @param {string} hookName nodeName of the Dom Node. + */ + constructor(hookName) { + super(); + this.hookName = hookName; + } + /** + * Creates an Item with the same effect as this Item (without position effect) + */ + _copy() { + return new _YXmlHook(this.hookName); + } + /** + * Makes a copy of this data type that can be included somewhere else. + * + * Note that the content is only readable _after_ it has been included somewhere in the Ydoc. + * + * @return {YXmlHook} + */ + clone() { + const el = new _YXmlHook(this.hookName); + this.forEach((value, key) => { + el.set(key, value); + }); + return el; + } + /** + * Creates a Dom Element that mirrors this YXmlElement. + * + * @param {Document} [_document=document] The document object (you must define + * this when calling this method in + * nodejs) + * @param {Object.} [hooks] Optional property to customize how hooks + * are presented in the DOM + * @param {any} [binding] You should not set this property. This is + * used if DomBinding wants to create a + * association to the created DOM type + * @return {Element} The {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element} + * + * @public + */ + toDOM(_document = document, hooks = {}, binding) { + const hook = hooks[this.hookName]; + let dom; + if (hook !== void 0) { + dom = hook.createDom(this); + } else { + dom = document.createElement(this.hookName); + } + dom.setAttribute("data-yjs-hook", this.hookName); + if (binding !== void 0) { + binding._createAssociation(dom, this); + } + return dom; + } + /** + * Transform the properties of this type to binary and write it to an + * BinaryEncoder. + * + * This is called when this Item is sent to a remote peer. + * + * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder The encoder to write data to. + */ + _write(encoder) { + encoder.writeTypeRef(YXmlHookRefID); + encoder.writeKey(this.hookName); + } + }; + var readYXmlHook = (decoder) => new YXmlHook(decoder.readKey()); + var YXmlText = class _YXmlText extends YText { + /** + * @type {YXmlElement|YXmlText|null} + */ + get nextSibling() { + const n = this._item ? this._item.next : null; + return n ? ( + /** @type {YXmlElement|YXmlText} */ + /** @type {ContentType} */ + n.content.type + ) : null; + } + /** + * @type {YXmlElement|YXmlText|null} + */ + get prevSibling() { + const n = this._item ? this._item.prev : null; + return n ? ( + /** @type {YXmlElement|YXmlText} */ + /** @type {ContentType} */ + n.content.type + ) : null; + } + _copy() { + return new _YXmlText(); + } + /** + * Makes a copy of this data type that can be included somewhere else. + * + * Note that the content is only readable _after_ it has been included somewhere in the Ydoc. + * + * @return {YXmlText} + */ + clone() { + const text2 = new _YXmlText(); + text2.applyDelta(this.toDelta()); + return text2; + } + /** + * Creates a Dom Element that mirrors this YXmlText. + * + * @param {Document} [_document=document] The document object (you must define + * this when calling this method in + * nodejs) + * @param {Object} [hooks] Optional property to customize how hooks + * are presented in the DOM + * @param {any} [binding] You should not set this property. This is + * used if DomBinding wants to create a + * association to the created DOM type. + * @return {Text} The {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element} + * + * @public + */ + toDOM(_document = document, hooks, binding) { + const dom = _document.createTextNode(this.toString()); + if (binding !== void 0) { + binding._createAssociation(dom, this); + } + return dom; + } + toString() { + return this.toDelta().map((delta) => { + const nestedNodes = []; + for (const nodeName in delta.attributes) { + const attrs = []; + for (const key in delta.attributes[nodeName]) { + attrs.push({ key, value: delta.attributes[nodeName][key] }); + } + attrs.sort((a, b) => a.key < b.key ? -1 : 1); + nestedNodes.push({ nodeName, attrs }); + } + nestedNodes.sort((a, b) => a.nodeName < b.nodeName ? -1 : 1); + let str = ""; + for (let i = 0; i < nestedNodes.length; i++) { + const node = nestedNodes[i]; + str += `<${node.nodeName}`; + for (let j = 0; j < node.attrs.length; j++) { + const attr = node.attrs[j]; + str += ` ${attr.key}="${attr.value}"`; + } + str += ">"; + } + str += delta.insert; + for (let i = nestedNodes.length - 1; i >= 0; i--) { + str += ``; + } + return str; + }).join(""); + } + /** + * @return {string} + */ + toJSON() { + return this.toString(); + } + /** + * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder + */ + _write(encoder) { + encoder.writeTypeRef(YXmlTextRefID); + } + }; + var readYXmlText = (decoder) => new YXmlText(); + var AbstractStruct = class { + /** + * @param {ID} id + * @param {number} length + */ + constructor(id2, length3) { + this.id = id2; + this.length = length3; + } + /** + * @type {boolean} + */ + get deleted() { + throw methodUnimplemented(); + } + /** + * Merge this struct with the item to the right. + * This method is already assuming that `this.id.clock + this.length === this.id.clock`. + * Also this method does *not* remove right from StructStore! + * @param {AbstractStruct} right + * @return {boolean} whether this merged with right + */ + mergeWith(right) { + return false; + } + /** + * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder The encoder to write data to. + * @param {number} offset + * @param {number} encodingRef + */ + write(encoder, offset, encodingRef) { + throw methodUnimplemented(); + } + /** + * @param {Transaction} transaction + * @param {number} offset + */ + integrate(transaction, offset) { + throw methodUnimplemented(); + } + }; + var structGCRefNumber = 0; + var GC = class extends AbstractStruct { + get deleted() { + return true; + } + delete() { + } + /** + * @param {GC} right + * @return {boolean} + */ + mergeWith(right) { + if (this.constructor !== right.constructor) { + return false; + } + this.length += right.length; + return true; + } + /** + * @param {Transaction} transaction + * @param {number} offset + */ + integrate(transaction, offset) { + if (offset > 0) { + this.id.clock += offset; + this.length -= offset; + } + addStruct(transaction.doc.store, this); + } + /** + * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder + * @param {number} offset + */ + write(encoder, offset) { + encoder.writeInfo(structGCRefNumber); + encoder.writeLen(this.length - offset); + } + /** + * @param {Transaction} transaction + * @param {StructStore} store + * @return {null | number} + */ + getMissing(transaction, store) { + return null; + } + }; + var ContentBinary = class _ContentBinary { + /** + * @param {Uint8Array} content + */ + constructor(content) { + this.content = content; + } + /** + * @return {number} + */ + getLength() { + return 1; + } + /** + * @return {Array} + */ + getContent() { + return [this.content]; + } + /** + * @return {boolean} + */ + isCountable() { + return true; + } + /** + * @return {ContentBinary} + */ + copy() { + return new _ContentBinary(this.content); + } + /** + * @param {number} offset + * @return {ContentBinary} + */ + splice(offset) { + throw methodUnimplemented(); + } + /** + * @param {ContentBinary} right + * @return {boolean} + */ + mergeWith(right) { + return false; + } + /** + * @param {Transaction} transaction + * @param {Item} item + */ + integrate(transaction, item) { + } + /** + * @param {Transaction} transaction + */ + delete(transaction) { + } + /** + * @param {StructStore} store + */ + gc(store) { + } + /** + * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder + * @param {number} offset + */ + write(encoder, offset) { + encoder.writeBuf(this.content); + } + /** + * @return {number} + */ + getRef() { + return 3; + } + }; + var readContentBinary = (decoder) => new ContentBinary(decoder.readBuf()); + var ContentDeleted = class _ContentDeleted { + /** + * @param {number} len + */ + constructor(len) { + this.len = len; + } + /** + * @return {number} + */ + getLength() { + return this.len; + } + /** + * @return {Array} + */ + getContent() { + return []; + } + /** + * @return {boolean} + */ + isCountable() { + return false; + } + /** + * @return {ContentDeleted} + */ + copy() { + return new _ContentDeleted(this.len); + } + /** + * @param {number} offset + * @return {ContentDeleted} + */ + splice(offset) { + const right = new _ContentDeleted(this.len - offset); + this.len = offset; + return right; + } + /** + * @param {ContentDeleted} right + * @return {boolean} + */ + mergeWith(right) { + this.len += right.len; + return true; + } + /** + * @param {Transaction} transaction + * @param {Item} item + */ + integrate(transaction, item) { + addToDeleteSet(transaction.deleteSet, item.id.client, item.id.clock, this.len); + item.markDeleted(); + } + /** + * @param {Transaction} transaction + */ + delete(transaction) { + } + /** + * @param {StructStore} store + */ + gc(store) { + } + /** + * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder + * @param {number} offset + */ + write(encoder, offset) { + encoder.writeLen(this.len - offset); + } + /** + * @return {number} + */ + getRef() { + return 1; + } + }; + var readContentDeleted = (decoder) => new ContentDeleted(decoder.readLen()); + var createDocFromOpts = (guid, opts) => new Doc({ guid, ...opts, shouldLoad: opts.shouldLoad || opts.autoLoad || false }); + var ContentDoc = class _ContentDoc { + /** + * @param {Doc} doc + */ + constructor(doc2) { + if (doc2._item) { + console.error("This document was already integrated as a sub-document. You should create a second instance instead with the same guid."); + } + this.doc = doc2; + const opts = {}; + this.opts = opts; + if (!doc2.gc) { + opts.gc = false; + } + if (doc2.autoLoad) { + opts.autoLoad = true; + } + if (doc2.meta !== null) { + opts.meta = doc2.meta; + } + } + /** + * @return {number} + */ + getLength() { + return 1; + } + /** + * @return {Array} + */ + getContent() { + return [this.doc]; + } + /** + * @return {boolean} + */ + isCountable() { + return true; + } + /** + * @return {ContentDoc} + */ + copy() { + return new _ContentDoc(createDocFromOpts(this.doc.guid, this.opts)); + } + /** + * @param {number} offset + * @return {ContentDoc} + */ + splice(offset) { + throw methodUnimplemented(); + } + /** + * @param {ContentDoc} right + * @return {boolean} + */ + mergeWith(right) { + return false; + } + /** + * @param {Transaction} transaction + * @param {Item} item + */ + integrate(transaction, item) { + this.doc._item = item; + transaction.subdocsAdded.add(this.doc); + if (this.doc.shouldLoad) { + transaction.subdocsLoaded.add(this.doc); + } + } + /** + * @param {Transaction} transaction + */ + delete(transaction) { + if (transaction.subdocsAdded.has(this.doc)) { + transaction.subdocsAdded.delete(this.doc); + } else { + transaction.subdocsRemoved.add(this.doc); + } + } + /** + * @param {StructStore} store + */ + gc(store) { + } + /** + * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder + * @param {number} offset + */ + write(encoder, offset) { + encoder.writeString(this.doc.guid); + encoder.writeAny(this.opts); + } + /** + * @return {number} + */ + getRef() { + return 9; + } + }; + var readContentDoc = (decoder) => new ContentDoc(createDocFromOpts(decoder.readString(), decoder.readAny())); + var ContentEmbed = class _ContentEmbed { + /** + * @param {Object} embed + */ + constructor(embed) { + this.embed = embed; + } + /** + * @return {number} + */ + getLength() { + return 1; + } + /** + * @return {Array} + */ + getContent() { + return [this.embed]; + } + /** + * @return {boolean} + */ + isCountable() { + return true; + } + /** + * @return {ContentEmbed} + */ + copy() { + return new _ContentEmbed(this.embed); + } + /** + * @param {number} offset + * @return {ContentEmbed} + */ + splice(offset) { + throw methodUnimplemented(); + } + /** + * @param {ContentEmbed} right + * @return {boolean} + */ + mergeWith(right) { + return false; + } + /** + * @param {Transaction} transaction + * @param {Item} item + */ + integrate(transaction, item) { + } + /** + * @param {Transaction} transaction + */ + delete(transaction) { + } + /** + * @param {StructStore} store + */ + gc(store) { + } + /** + * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder + * @param {number} offset + */ + write(encoder, offset) { + encoder.writeJSON(this.embed); + } + /** + * @return {number} + */ + getRef() { + return 5; + } + }; + var readContentEmbed = (decoder) => new ContentEmbed(decoder.readJSON()); + var ContentFormat = class _ContentFormat { + /** + * @param {string} key + * @param {Object} value + */ + constructor(key, value) { + this.key = key; + this.value = value; + } + /** + * @return {number} + */ + getLength() { + return 1; + } + /** + * @return {Array} + */ + getContent() { + return []; + } + /** + * @return {boolean} + */ + isCountable() { + return false; + } + /** + * @return {ContentFormat} + */ + copy() { + return new _ContentFormat(this.key, this.value); + } + /** + * @param {number} _offset + * @return {ContentFormat} + */ + splice(_offset) { + throw methodUnimplemented(); + } + /** + * @param {ContentFormat} _right + * @return {boolean} + */ + mergeWith(_right) { + return false; + } + /** + * @param {Transaction} _transaction + * @param {Item} item + */ + integrate(_transaction, item) { + const p = ( + /** @type {YText} */ + item.parent + ); + p._searchMarker = null; + p._hasFormatting = true; + } + /** + * @param {Transaction} transaction + */ + delete(transaction) { + } + /** + * @param {StructStore} store + */ + gc(store) { + } + /** + * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder + * @param {number} offset + */ + write(encoder, offset) { + encoder.writeKey(this.key); + encoder.writeJSON(this.value); + } + /** + * @return {number} + */ + getRef() { + return 6; + } + }; + var readContentFormat = (decoder) => new ContentFormat(decoder.readKey(), decoder.readJSON()); + var ContentJSON = class _ContentJSON { + /** + * @param {Array} arr + */ + constructor(arr) { + this.arr = arr; + } + /** + * @return {number} + */ + getLength() { + return this.arr.length; + } + /** + * @return {Array} + */ + getContent() { + return this.arr; + } + /** + * @return {boolean} + */ + isCountable() { + return true; + } + /** + * @return {ContentJSON} + */ + copy() { + return new _ContentJSON(this.arr); + } + /** + * @param {number} offset + * @return {ContentJSON} + */ + splice(offset) { + const right = new _ContentJSON(this.arr.slice(offset)); + this.arr = this.arr.slice(0, offset); + return right; + } + /** + * @param {ContentJSON} right + * @return {boolean} + */ + mergeWith(right) { + this.arr = this.arr.concat(right.arr); + return true; + } + /** + * @param {Transaction} transaction + * @param {Item} item + */ + integrate(transaction, item) { + } + /** + * @param {Transaction} transaction + */ + delete(transaction) { + } + /** + * @param {StructStore} store + */ + gc(store) { + } + /** + * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder + * @param {number} offset + */ + write(encoder, offset) { + const len = this.arr.length; + encoder.writeLen(len - offset); + for (let i = offset; i < len; i++) { + const c = this.arr[i]; + encoder.writeString(c === void 0 ? "undefined" : JSON.stringify(c)); + } + } + /** + * @return {number} + */ + getRef() { + return 2; + } + }; + var readContentJSON = (decoder) => { + const len = decoder.readLen(); + const cs = []; + for (let i = 0; i < len; i++) { + const c = decoder.readString(); + if (c === "undefined") { + cs.push(void 0); + } else { + cs.push(JSON.parse(c)); + } + } + return new ContentJSON(cs); + }; + var isDevMode = getVariable("node_env") === "development"; + var ContentAny = class _ContentAny { + /** + * @param {Array} arr + */ + constructor(arr) { + this.arr = arr; + isDevMode && deepFreeze(arr); + } + /** + * @return {number} + */ + getLength() { + return this.arr.length; + } + /** + * @return {Array} + */ + getContent() { + return this.arr; + } + /** + * @return {boolean} + */ + isCountable() { + return true; + } + /** + * @return {ContentAny} + */ + copy() { + return new _ContentAny(this.arr); + } + /** + * @param {number} offset + * @return {ContentAny} + */ + splice(offset) { + const right = new _ContentAny(this.arr.slice(offset)); + this.arr = this.arr.slice(0, offset); + return right; + } + /** + * @param {ContentAny} right + * @return {boolean} + */ + mergeWith(right) { + this.arr = this.arr.concat(right.arr); + return true; + } + /** + * @param {Transaction} transaction + * @param {Item} item + */ + integrate(transaction, item) { + } + /** + * @param {Transaction} transaction + */ + delete(transaction) { + } + /** + * @param {StructStore} store + */ + gc(store) { + } + /** + * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder + * @param {number} offset + */ + write(encoder, offset) { + const len = this.arr.length; + encoder.writeLen(len - offset); + for (let i = offset; i < len; i++) { + const c = this.arr[i]; + encoder.writeAny(c); + } + } + /** + * @return {number} + */ + getRef() { + return 8; + } + }; + var readContentAny = (decoder) => { + const len = decoder.readLen(); + const cs = []; + for (let i = 0; i < len; i++) { + cs.push(decoder.readAny()); + } + return new ContentAny(cs); + }; + var ContentString = class _ContentString { + /** + * @param {string} str + */ + constructor(str) { + this.str = str; + } + /** + * @return {number} + */ + getLength() { + return this.str.length; + } + /** + * @return {Array} + */ + getContent() { + return this.str.split(""); + } + /** + * @return {boolean} + */ + isCountable() { + return true; + } + /** + * @return {ContentString} + */ + copy() { + return new _ContentString(this.str); + } + /** + * @param {number} offset + * @return {ContentString} + */ + splice(offset) { + const right = new _ContentString(this.str.slice(offset)); + this.str = this.str.slice(0, offset); + const firstCharCode = this.str.charCodeAt(offset - 1); + if (firstCharCode >= 55296 && firstCharCode <= 56319) { + this.str = this.str.slice(0, offset - 1) + "\uFFFD"; + right.str = "\uFFFD" + right.str.slice(1); + } + return right; + } + /** + * @param {ContentString} right + * @return {boolean} + */ + mergeWith(right) { + this.str += right.str; + return true; + } + /** + * @param {Transaction} transaction + * @param {Item} item + */ + integrate(transaction, item) { + } + /** + * @param {Transaction} transaction + */ + delete(transaction) { + } + /** + * @param {StructStore} store + */ + gc(store) { + } + /** + * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder + * @param {number} offset + */ + write(encoder, offset) { + encoder.writeString(offset === 0 ? this.str : this.str.slice(offset)); + } + /** + * @return {number} + */ + getRef() { + return 4; + } + }; + var readContentString = (decoder) => new ContentString(decoder.readString()); + var typeRefs = [ + readYArray, + readYMap, + readYText, + readYXmlElement, + readYXmlFragment, + readYXmlHook, + readYXmlText + ]; + var YArrayRefID = 0; + var YMapRefID = 1; + var YTextRefID = 2; + var YXmlElementRefID = 3; + var YXmlFragmentRefID = 4; + var YXmlHookRefID = 5; + var YXmlTextRefID = 6; + var ContentType = class _ContentType { + /** + * @param {AbstractType} type + */ + constructor(type) { + this.type = type; + } + /** + * @return {number} + */ + getLength() { + return 1; + } + /** + * @return {Array} + */ + getContent() { + return [this.type]; + } + /** + * @return {boolean} + */ + isCountable() { + return true; + } + /** + * @return {ContentType} + */ + copy() { + return new _ContentType(this.type._copy()); + } + /** + * @param {number} offset + * @return {ContentType} + */ + splice(offset) { + throw methodUnimplemented(); + } + /** + * @param {ContentType} right + * @return {boolean} + */ + mergeWith(right) { + return false; + } + /** + * @param {Transaction} transaction + * @param {Item} item + */ + integrate(transaction, item) { + this.type._integrate(transaction.doc, item); + } + /** + * @param {Transaction} transaction + */ + delete(transaction) { + let item = this.type._start; + while (item !== null) { + if (!item.deleted) { + item.delete(transaction); + } else if (item.id.clock < (transaction.beforeState.get(item.id.client) || 0)) { + transaction._mergeStructs.push(item); + } + item = item.right; + } + this.type._map.forEach((item2) => { + if (!item2.deleted) { + item2.delete(transaction); + } else if (item2.id.clock < (transaction.beforeState.get(item2.id.client) || 0)) { + transaction._mergeStructs.push(item2); + } + }); + transaction.changed.delete(this.type); + } + /** + * @param {StructStore} store + */ + gc(store) { + let item = this.type._start; + while (item !== null) { + item.gc(store, true); + item = item.right; + } + this.type._start = null; + this.type._map.forEach( + /** @param {Item | null} item */ + (item2) => { + while (item2 !== null) { + item2.gc(store, true); + item2 = item2.left; + } + } + ); + this.type._map = /* @__PURE__ */ new Map(); + } + /** + * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder + * @param {number} offset + */ + write(encoder, offset) { + this.type._write(encoder); + } + /** + * @return {number} + */ + getRef() { + return 7; + } + }; + var readContentType = (decoder) => new ContentType(typeRefs[decoder.readTypeRef()](decoder)); + var splitItem = (transaction, leftItem, diff) => { + const { client, clock } = leftItem.id; + const rightItem = new Item( + createID(client, clock + diff), + leftItem, + createID(client, clock + diff - 1), + leftItem.right, + leftItem.rightOrigin, + leftItem.parent, + leftItem.parentSub, + leftItem.content.splice(diff) + ); + if (leftItem.deleted) { + rightItem.markDeleted(); + } + if (leftItem.keep) { + rightItem.keep = true; + } + if (leftItem.redone !== null) { + rightItem.redone = createID(leftItem.redone.client, leftItem.redone.clock + diff); + } + leftItem.right = rightItem; + if (rightItem.right !== null) { + rightItem.right.left = rightItem; + } + transaction._mergeStructs.push(rightItem); + if (rightItem.parentSub !== null && rightItem.right === null) { + rightItem.parent._map.set(rightItem.parentSub, rightItem); + } + leftItem.length = diff; + return rightItem; + }; + var Item = class _Item extends AbstractStruct { + /** + * @param {ID} id + * @param {Item | null} left + * @param {ID | null} origin + * @param {Item | null} right + * @param {ID | null} rightOrigin + * @param {AbstractType|ID|null} parent Is a type if integrated, is null if it is possible to copy parent from left or right, is ID before integration to search for it. + * @param {string | null} parentSub + * @param {AbstractContent} content + */ + constructor(id2, left, origin, right, rightOrigin, parent, parentSub, content) { + super(id2, content.getLength()); + this.origin = origin; + this.left = left; + this.right = right; + this.rightOrigin = rightOrigin; + this.parent = parent; + this.parentSub = parentSub; + this.redone = null; + this.content = content; + this.info = this.content.isCountable() ? BIT2 : 0; + } + /** + * This is used to mark the item as an indexed fast-search marker + * + * @type {boolean} + */ + set marker(isMarked) { + if ((this.info & BIT4) > 0 !== isMarked) { + this.info ^= BIT4; + } + } + get marker() { + return (this.info & BIT4) > 0; + } + /** + * If true, do not garbage collect this Item. + */ + get keep() { + return (this.info & BIT1) > 0; + } + set keep(doKeep) { + if (this.keep !== doKeep) { + this.info ^= BIT1; + } + } + get countable() { + return (this.info & BIT2) > 0; + } + /** + * Whether this item was deleted or not. + * @type {Boolean} + */ + get deleted() { + return (this.info & BIT3) > 0; + } + set deleted(doDelete) { + if (this.deleted !== doDelete) { + this.info ^= BIT3; + } + } + markDeleted() { + this.info |= BIT3; + } + /** + * Return the creator clientID of the missing op or define missing items and return null. + * + * @param {Transaction} transaction + * @param {StructStore} store + * @return {null | number} + */ + getMissing(transaction, store) { + if (this.origin && this.origin.client !== this.id.client && this.origin.clock >= getState(store, this.origin.client)) { + return this.origin.client; + } + if (this.rightOrigin && this.rightOrigin.client !== this.id.client && this.rightOrigin.clock >= getState(store, this.rightOrigin.client)) { + return this.rightOrigin.client; + } + if (this.parent && this.parent.constructor === ID && this.id.client !== this.parent.client && this.parent.clock >= getState(store, this.parent.client)) { + return this.parent.client; + } + if (this.origin) { + this.left = getItemCleanEnd(transaction, store, this.origin); + this.origin = this.left.lastId; + } + if (this.rightOrigin) { + this.right = getItemCleanStart(transaction, this.rightOrigin); + this.rightOrigin = this.right.id; + } + if (this.left && this.left.constructor === GC || this.right && this.right.constructor === GC) { + this.parent = null; + } else if (!this.parent) { + if (this.left && this.left.constructor === _Item) { + this.parent = this.left.parent; + this.parentSub = this.left.parentSub; + } else if (this.right && this.right.constructor === _Item) { + this.parent = this.right.parent; + this.parentSub = this.right.parentSub; + } + } else if (this.parent.constructor === ID) { + const parentItem = getItem(store, this.parent); + if (parentItem.constructor === GC) { + this.parent = null; + } else { + this.parent = /** @type {ContentType} */ + parentItem.content.type; + } + } + return null; + } + /** + * @param {Transaction} transaction + * @param {number} offset + */ + integrate(transaction, offset) { + if (offset > 0) { + this.id.clock += offset; + this.left = getItemCleanEnd(transaction, transaction.doc.store, createID(this.id.client, this.id.clock - 1)); + this.origin = this.left.lastId; + this.content = this.content.splice(offset); + this.length -= offset; + } + if (this.parent) { + if (!this.left && (!this.right || this.right.left !== null) || this.left && this.left.right !== this.right) { + let left = this.left; + let o; + if (left !== null) { + o = left.right; + } else if (this.parentSub !== null) { + o = /** @type {AbstractType} */ + this.parent._map.get(this.parentSub) || null; + while (o !== null && o.left !== null) { + o = o.left; + } + } else { + o = /** @type {AbstractType} */ + this.parent._start; + } + const conflictingItems = /* @__PURE__ */ new Set(); + const itemsBeforeOrigin = /* @__PURE__ */ new Set(); + while (o !== null && o !== this.right) { + itemsBeforeOrigin.add(o); + conflictingItems.add(o); + if (compareIDs(this.origin, o.origin)) { + if (o.id.client < this.id.client) { + left = o; + conflictingItems.clear(); + } else if (compareIDs(this.rightOrigin, o.rightOrigin)) { + break; + } + } else if (o.origin !== null && itemsBeforeOrigin.has(getItem(transaction.doc.store, o.origin))) { + if (!conflictingItems.has(getItem(transaction.doc.store, o.origin))) { + left = o; + conflictingItems.clear(); + } + } else { + break; + } + o = o.right; + } + this.left = left; + } + if (this.left !== null) { + const right = this.left.right; + this.right = right; + this.left.right = this; + } else { + let r; + if (this.parentSub !== null) { + r = /** @type {AbstractType} */ + this.parent._map.get(this.parentSub) || null; + while (r !== null && r.left !== null) { + r = r.left; + } + } else { + r = /** @type {AbstractType} */ + this.parent._start; + this.parent._start = this; + } + this.right = r; + } + if (this.right !== null) { + this.right.left = this; + } else if (this.parentSub !== null) { + this.parent._map.set(this.parentSub, this); + if (this.left !== null) { + this.left.delete(transaction); + } + } + if (this.parentSub === null && this.countable && !this.deleted) { + this.parent._length += this.length; + } + addStruct(transaction.doc.store, this); + this.content.integrate(transaction, this); + addChangedTypeToTransaction( + transaction, + /** @type {AbstractType} */ + this.parent, + this.parentSub + ); + if ( + /** @type {AbstractType} */ + this.parent._item !== null && /** @type {AbstractType} */ + this.parent._item.deleted || this.parentSub !== null && this.right !== null + ) { + this.delete(transaction); + } + } else { + new GC(this.id, this.length).integrate(transaction, 0); + } + } + /** + * Returns the next non-deleted item + */ + get next() { + let n = this.right; + while (n !== null && n.deleted) { + n = n.right; + } + return n; + } + /** + * Returns the previous non-deleted item + */ + get prev() { + let n = this.left; + while (n !== null && n.deleted) { + n = n.left; + } + return n; + } + /** + * Computes the last content address of this Item. + */ + get lastId() { + return this.length === 1 ? this.id : createID(this.id.client, this.id.clock + this.length - 1); + } + /** + * Try to merge two items + * + * @param {Item} right + * @return {boolean} + */ + mergeWith(right) { + if (this.constructor === right.constructor && compareIDs(right.origin, this.lastId) && this.right === right && compareIDs(this.rightOrigin, right.rightOrigin) && this.id.client === right.id.client && this.id.clock + this.length === right.id.clock && this.deleted === right.deleted && this.redone === null && right.redone === null && this.content.constructor === right.content.constructor && this.content.mergeWith(right.content)) { + const searchMarker = ( + /** @type {AbstractType} */ + this.parent._searchMarker + ); + if (searchMarker) { + searchMarker.forEach((marker) => { + if (marker.p === right) { + marker.p = this; + if (!this.deleted && this.countable) { + marker.index -= this.length; + } + } + }); + } + if (right.keep) { + this.keep = true; + } + this.right = right.right; + if (this.right !== null) { + this.right.left = this; + } + this.length += right.length; + return true; + } + return false; + } + /** + * Mark this Item as deleted. + * + * @param {Transaction} transaction + */ + delete(transaction) { + if (!this.deleted) { + const parent = ( + /** @type {AbstractType} */ + this.parent + ); + if (this.countable && this.parentSub === null) { + parent._length -= this.length; + } + this.markDeleted(); + addToDeleteSet(transaction.deleteSet, this.id.client, this.id.clock, this.length); + addChangedTypeToTransaction(transaction, parent, this.parentSub); + this.content.delete(transaction); + } + } + /** + * @param {StructStore} store + * @param {boolean} parentGCd + */ + gc(store, parentGCd) { + if (!this.deleted) { + throw unexpectedCase(); + } + this.content.gc(store); + if (parentGCd) { + replaceStruct(store, this, new GC(this.id, this.length)); + } else { + this.content = new ContentDeleted(this.length); + } + } + /** + * Transform the properties of this type to binary and write it to an + * BinaryEncoder. + * + * This is called when this Item is sent to a remote peer. + * + * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder The encoder to write data to. + * @param {number} offset + */ + write(encoder, offset) { + const origin = offset > 0 ? createID(this.id.client, this.id.clock + offset - 1) : this.origin; + const rightOrigin = this.rightOrigin; + const parentSub = this.parentSub; + const info = this.content.getRef() & BITS5 | (origin === null ? 0 : BIT8) | // origin is defined + (rightOrigin === null ? 0 : BIT7) | // right origin is defined + (parentSub === null ? 0 : BIT6); + encoder.writeInfo(info); + if (origin !== null) { + encoder.writeLeftID(origin); + } + if (rightOrigin !== null) { + encoder.writeRightID(rightOrigin); + } + if (origin === null && rightOrigin === null) { + const parent = ( + /** @type {AbstractType} */ + this.parent + ); + if (parent._item !== void 0) { + const parentItem = parent._item; + if (parentItem === null) { + const ykey = findRootTypeKey(parent); + encoder.writeParentInfo(true); + encoder.writeString(ykey); + } else { + encoder.writeParentInfo(false); + encoder.writeLeftID(parentItem.id); + } + } else if (parent.constructor === String) { + encoder.writeParentInfo(true); + encoder.writeString(parent); + } else if (parent.constructor === ID) { + encoder.writeParentInfo(false); + encoder.writeLeftID(parent); + } else { + unexpectedCase(); + } + if (parentSub !== null) { + encoder.writeString(parentSub); + } + } + this.content.write(encoder, offset); + } + }; + var readItemContent = (decoder, info) => contentRefs[info & BITS5](decoder); + var contentRefs = [ + () => { + unexpectedCase(); + }, + // GC is not ItemContent + readContentDeleted, + // 1 + readContentJSON, + // 2 + readContentBinary, + // 3 + readContentString, + // 4 + readContentEmbed, + // 5 + readContentFormat, + // 6 + readContentType, + // 7 + readContentAny, + // 8 + readContentDoc, + // 9 + () => { + unexpectedCase(); + } + // 10 - Skip is not ItemContent + ]; + var structSkipRefNumber = 10; + var Skip = class extends AbstractStruct { + get deleted() { + return true; + } + delete() { + } + /** + * @param {Skip} right + * @return {boolean} + */ + mergeWith(right) { + if (this.constructor !== right.constructor) { + return false; + } + this.length += right.length; + return true; + } + /** + * @param {Transaction} transaction + * @param {number} offset + */ + integrate(transaction, offset) { + unexpectedCase(); + } + /** + * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder + * @param {number} offset + */ + write(encoder, offset) { + encoder.writeInfo(structSkipRefNumber); + writeVarUint(encoder.restEncoder, this.length - offset); + } + /** + * @param {Transaction} transaction + * @param {StructStore} store + * @return {null | number} + */ + getMissing(transaction, store) { + return null; + } + }; + var glo = ( + /** @type {any} */ + typeof globalThis !== "undefined" ? globalThis : typeof window !== "undefined" ? window : typeof global !== "undefined" ? global : {} + ); + var importIdentifier = "__ $YJS$ __"; + if (glo[importIdentifier] === true) { + console.error("Yjs was already imported. This breaks constructor checks and will lead to issues! - https://github.com/yjs/yjs/issues/438"); + } + glo[importIdentifier] = true; + + // node_modules/@hocuspocus/common/dist/hocuspocus-common.esm.js + var floor2 = Math.floor; + var min2 = (a, b) => a < b ? a : b; + var max2 = (a, b) => a > b ? a : b; + var BIT82 = 128; + var BITS72 = 127; + var MAX_SAFE_INTEGER2 = Number.MAX_SAFE_INTEGER; + var _encodeUtf8Polyfill2 = (str) => { + const encodedString = unescape(encodeURIComponent(str)); + const len = encodedString.length; + const buf = new Uint8Array(len); + for (let i = 0; i < len; i++) { + buf[i] = /** @type {number} */ + encodedString.codePointAt(i); + } + return buf; + }; + var utf8TextEncoder2 = ( + /** @type {TextEncoder} */ + typeof TextEncoder !== "undefined" ? new TextEncoder() : null + ); + var _encodeUtf8Native2 = (str) => utf8TextEncoder2.encode(str); + var encodeUtf82 = utf8TextEncoder2 ? _encodeUtf8Native2 : _encodeUtf8Polyfill2; + var utf8TextDecoder2 = typeof TextDecoder === "undefined" ? null : new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }); + if (utf8TextDecoder2 && utf8TextDecoder2.decode(new Uint8Array()).length === 1) { + utf8TextDecoder2 = null; + } + var write2 = (encoder, num) => { + const bufferLen = encoder.cbuf.length; + if (encoder.cpos === bufferLen) { + encoder.bufs.push(encoder.cbuf); + encoder.cbuf = new Uint8Array(bufferLen * 2); + encoder.cpos = 0; + } + encoder.cbuf[encoder.cpos++] = num; + }; + var writeVarUint2 = (encoder, num) => { + while (num > BITS72) { + write2(encoder, BIT82 | BITS72 & num); + num = floor2(num / 128); + } + write2(encoder, BITS72 & num); + }; + var _strBuffer2 = new Uint8Array(3e4); + var _maxStrBSize2 = _strBuffer2.length / 3; + var _writeVarStringNative2 = (encoder, str) => { + if (str.length < _maxStrBSize2) { + const written = utf8TextEncoder2.encodeInto(str, _strBuffer2).written || 0; + writeVarUint2(encoder, written); + for (let i = 0; i < written; i++) { + write2(encoder, _strBuffer2[i]); + } + } else { + writeVarUint8Array2(encoder, encodeUtf82(str)); + } + }; + var _writeVarStringPolyfill2 = (encoder, str) => { + const encodedString = unescape(encodeURIComponent(str)); + const len = encodedString.length; + writeVarUint2(encoder, len); + for (let i = 0; i < len; i++) { + write2( + encoder, + /** @type {number} */ + encodedString.codePointAt(i) + ); + } + }; + var writeVarString2 = utf8TextEncoder2 && /** @type {any} */ + utf8TextEncoder2.encodeInto ? _writeVarStringNative2 : _writeVarStringPolyfill2; + var writeUint8Array2 = (encoder, uint8Array) => { + const bufferLen = encoder.cbuf.length; + const cpos = encoder.cpos; + const leftCopyLen = min2(bufferLen - cpos, uint8Array.length); + const rightCopyLen = uint8Array.length - leftCopyLen; + encoder.cbuf.set(uint8Array.subarray(0, leftCopyLen), cpos); + encoder.cpos += leftCopyLen; + if (rightCopyLen > 0) { + encoder.bufs.push(encoder.cbuf); + encoder.cbuf = new Uint8Array(max2(bufferLen * 2, rightCopyLen)); + encoder.cbuf.set(uint8Array.subarray(leftCopyLen)); + encoder.cpos = rightCopyLen; + } + }; + var writeVarUint8Array2 = (encoder, uint8Array) => { + writeVarUint2(encoder, uint8Array.byteLength); + writeUint8Array2(encoder, uint8Array); + }; + var create7 = (s) => new Error(s); + var errorUnexpectedEndOfArray2 = create7("Unexpected end of array"); + var errorIntegerOutOfRange2 = create7("Integer out of Range"); + var readUint8Array2 = (decoder, len) => { + const view = new Uint8Array(decoder.arr.buffer, decoder.pos + decoder.arr.byteOffset, len); + decoder.pos += len; + return view; + }; + var readVarUint8Array2 = (decoder) => readUint8Array2(decoder, readVarUint2(decoder)); + var readUint82 = (decoder) => decoder.arr[decoder.pos++]; + var readVarUint2 = (decoder) => { + let num = 0; + let mult = 1; + const len = decoder.arr.length; + while (decoder.pos < len) { + const r = decoder.arr[decoder.pos++]; + num = num + (r & BITS72) * mult; + mult *= 128; + if (r < BIT82) { + return num; + } + if (num > MAX_SAFE_INTEGER2) { + throw errorIntegerOutOfRange2; + } + } + throw errorUnexpectedEndOfArray2; + }; + var _readVarStringPolyfill2 = (decoder) => { + let remainingLen = readVarUint2(decoder); + if (remainingLen === 0) { + return ""; + } else { + let encodedString = String.fromCodePoint(readUint82(decoder)); + if (--remainingLen < 100) { + while (remainingLen--) { + encodedString += String.fromCodePoint(readUint82(decoder)); + } + } else { + while (remainingLen > 0) { + const nextLen = remainingLen < 1e4 ? remainingLen : 1e4; + const bytes = decoder.arr.subarray(decoder.pos, decoder.pos + nextLen); + decoder.pos += nextLen; + encodedString += String.fromCodePoint.apply( + null, + /** @type {any} */ + bytes + ); + remainingLen -= nextLen; + } + } + return decodeURIComponent(escape(encodedString)); + } + }; + var _readVarStringNative2 = (decoder) => ( + /** @type any */ + utf8TextDecoder2.decode(readVarUint8Array2(decoder)) + ); + var readVarString2 = utf8TextDecoder2 ? _readVarStringNative2 : _readVarStringPolyfill2; + var AuthMessageType; + (function(AuthMessageType2) { + AuthMessageType2[AuthMessageType2["Token"] = 0] = "Token"; + AuthMessageType2[AuthMessageType2["PermissionDenied"] = 1] = "PermissionDenied"; + AuthMessageType2[AuthMessageType2["Authenticated"] = 2] = "Authenticated"; + })(AuthMessageType || (AuthMessageType = {})); + var writeAuthentication = (encoder, auth) => { + writeVarUint2(encoder, AuthMessageType.Token); + writeVarString2(encoder, auth); + }; + var readAuthMessage = (decoder, sendToken, permissionDeniedHandler, authenticatedHandler) => { + switch (readVarUint2(decoder)) { + case AuthMessageType.Token: { + sendToken(); + break; + } + case AuthMessageType.PermissionDenied: { + permissionDeniedHandler(readVarString2(decoder)); + break; + } + case AuthMessageType.Authenticated: { + authenticatedHandler(readVarString2(decoder)); + break; + } + } + }; + var awarenessStatesToArray = (states) => { + return Array.from(states.entries()).map(([key, value]) => { + return { + clientId: key, + ...value + }; + }); + }; + var WsReadyStates; + (function(WsReadyStates2) { + WsReadyStates2[WsReadyStates2["Connecting"] = 0] = "Connecting"; + WsReadyStates2[WsReadyStates2["Open"] = 1] = "Open"; + WsReadyStates2[WsReadyStates2["Closing"] = 2] = "Closing"; + WsReadyStates2[WsReadyStates2["Closed"] = 3] = "Closed"; + })(WsReadyStates || (WsReadyStates = {})); + + // node_modules/@lifeomic/attempt/dist/es6/src/index.js + function applyDefaults(options) { + if (!options) { + options = {}; + } + return { + delay: options.delay === void 0 ? 200 : options.delay, + initialDelay: options.initialDelay === void 0 ? 0 : options.initialDelay, + minDelay: options.minDelay === void 0 ? 0 : options.minDelay, + maxDelay: options.maxDelay === void 0 ? 0 : options.maxDelay, + factor: options.factor === void 0 ? 0 : options.factor, + maxAttempts: options.maxAttempts === void 0 ? 3 : options.maxAttempts, + timeout: options.timeout === void 0 ? 0 : options.timeout, + jitter: options.jitter === true, + initialJitter: options.initialJitter === true, + handleError: options.handleError === void 0 ? null : options.handleError, + handleTimeout: options.handleTimeout === void 0 ? null : options.handleTimeout, + beforeAttempt: options.beforeAttempt === void 0 ? null : options.beforeAttempt, + calculateDelay: options.calculateDelay === void 0 ? null : options.calculateDelay + }; + } + async function sleep(delay) { + return new Promise((resolve) => setTimeout(resolve, delay)); + } + function defaultCalculateDelay(context, options) { + let delay = options.delay; + if (delay === 0) { + return 0; + } + if (options.factor) { + delay *= Math.pow(options.factor, context.attemptNum - 1); + if (options.maxDelay !== 0) { + delay = Math.min(delay, options.maxDelay); + } + } + if (options.jitter) { + const min4 = Math.ceil(options.minDelay); + const max4 = Math.floor(delay); + delay = Math.floor(Math.random() * (max4 - min4 + 1)) + min4; + } + return Math.round(delay); + } + async function retry(attemptFunc, attemptOptions) { + const options = applyDefaults(attemptOptions); + for (const prop of [ + "delay", + "initialDelay", + "minDelay", + "maxDelay", + "maxAttempts", + "timeout" + ]) { + const value = options[prop]; + if (!Number.isInteger(value) || value < 0) { + throw new Error(`Value for ${prop} must be an integer greater than or equal to 0`); + } + } + if (options.factor.constructor !== Number || options.factor < 0) { + throw new Error(`Value for factor must be a number greater than or equal to 0`); + } + if (options.delay < options.minDelay) { + throw new Error(`delay cannot be less than minDelay (delay: ${options.delay}, minDelay: ${options.minDelay}`); + } + const context = { + attemptNum: 0, + attemptsRemaining: options.maxAttempts ? options.maxAttempts : -1, + aborted: false, + abort() { + context.aborted = true; + } + }; + const calculateDelay = options.calculateDelay || defaultCalculateDelay; + async function makeAttempt() { + if (options.beforeAttempt) { + options.beforeAttempt(context, options); + } + if (context.aborted) { + const err = new Error(`Attempt aborted`); + err.code = "ATTEMPT_ABORTED"; + throw err; + } + const onError = async (err) => { + if (options.handleError) { + await options.handleError(err, context, options); + } + if (context.aborted || context.attemptsRemaining === 0) { + throw err; + } + context.attemptNum++; + const delay = calculateDelay(context, options); + if (delay) { + await sleep(delay); + } + return makeAttempt(); + }; + if (context.attemptsRemaining > 0) { + context.attemptsRemaining--; + } + if (options.timeout) { + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + if (options.handleTimeout) { + try { + resolve(options.handleTimeout(context, options)); + } catch (e) { + reject(e); + } + } else { + const err = new Error(`Retry timeout (attemptNum: ${context.attemptNum}, timeout: ${options.timeout})`); + err.code = "ATTEMPT_TIMEOUT"; + reject(err); + } + }, options.timeout); + attemptFunc(context, options).then((result) => { + clearTimeout(timer); + resolve(result); + }).catch((err) => { + clearTimeout(timer); + onError(err).then(resolve).catch(reject); + }); + }); + } else { + return attemptFunc(context, options).catch(onError); + } + } + const initialDelay = options.calculateDelay ? options.calculateDelay(context, options) : options.initialDelay; + if (initialDelay) { + await sleep(initialDelay); + } + if (context.attemptNum < 1 && options.initialJitter) { + const delay = calculateDelay(context, options); + if (delay) { + await sleep(delay); + } + } + return makeAttempt(); + } + + // node_modules/@hocuspocus/provider/dist/hocuspocus-provider.esm.js + var floor3 = Math.floor; + var min3 = (a, b) => a < b ? a : b; + var max3 = (a, b) => a > b ? a : b; + var BIT72 = 64; + var BIT83 = 128; + var BITS62 = 63; + var BITS73 = 127; + var MAX_SAFE_INTEGER3 = Number.MAX_SAFE_INTEGER; + var create$2 = () => /* @__PURE__ */ new Set(); + var from2 = Array.from; + var _encodeUtf8Polyfill3 = (str) => { + const encodedString = unescape(encodeURIComponent(str)); + const len = encodedString.length; + const buf = new Uint8Array(len); + for (let i = 0; i < len; i++) { + buf[i] = /** @type {number} */ + encodedString.codePointAt(i); + } + return buf; + }; + var utf8TextEncoder3 = ( + /** @type {TextEncoder} */ + typeof TextEncoder !== "undefined" ? new TextEncoder() : null + ); + var _encodeUtf8Native3 = (str) => utf8TextEncoder3.encode(str); + var encodeUtf83 = utf8TextEncoder3 ? _encodeUtf8Native3 : _encodeUtf8Polyfill3; + var utf8TextDecoder3 = typeof TextDecoder === "undefined" ? null : new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }); + if (utf8TextDecoder3 && utf8TextDecoder3.decode(new Uint8Array()).length === 1) { + utf8TextDecoder3 = null; + } + var Encoder2 = class { + constructor() { + this.cpos = 0; + this.cbuf = new Uint8Array(100); + this.bufs = []; + } + }; + var createEncoder2 = () => new Encoder2(); + var length$1 = (encoder) => { + let len = encoder.cpos; + for (let i = 0; i < encoder.bufs.length; i++) { + len += encoder.bufs[i].length; + } + return len; + }; + var toUint8Array2 = (encoder) => { + const uint8arr = new Uint8Array(length$1(encoder)); + let curPos = 0; + for (let i = 0; i < encoder.bufs.length; i++) { + const d = encoder.bufs[i]; + uint8arr.set(d, curPos); + curPos += d.length; + } + uint8arr.set(new Uint8Array(encoder.cbuf.buffer, 0, encoder.cpos), curPos); + return uint8arr; + }; + var write3 = (encoder, num) => { + const bufferLen = encoder.cbuf.length; + if (encoder.cpos === bufferLen) { + encoder.bufs.push(encoder.cbuf); + encoder.cbuf = new Uint8Array(bufferLen * 2); + encoder.cpos = 0; + } + encoder.cbuf[encoder.cpos++] = num; + }; + var writeVarUint3 = (encoder, num) => { + while (num > BITS73) { + write3(encoder, BIT83 | BITS73 & num); + num = floor3(num / 128); + } + write3(encoder, BITS73 & num); + }; + var _strBuffer3 = new Uint8Array(3e4); + var _maxStrBSize3 = _strBuffer3.length / 3; + var _writeVarStringNative3 = (encoder, str) => { + if (str.length < _maxStrBSize3) { + const written = utf8TextEncoder3.encodeInto(str, _strBuffer3).written || 0; + writeVarUint3(encoder, written); + for (let i = 0; i < written; i++) { + write3(encoder, _strBuffer3[i]); + } + } else { + writeVarUint8Array3(encoder, encodeUtf83(str)); + } + }; + var _writeVarStringPolyfill3 = (encoder, str) => { + const encodedString = unescape(encodeURIComponent(str)); + const len = encodedString.length; + writeVarUint3(encoder, len); + for (let i = 0; i < len; i++) { + write3( + encoder, + /** @type {number} */ + encodedString.codePointAt(i) + ); + } + }; + var writeVarString3 = utf8TextEncoder3 && /** @type {any} */ + utf8TextEncoder3.encodeInto ? _writeVarStringNative3 : _writeVarStringPolyfill3; + var writeUint8Array3 = (encoder, uint8Array) => { + const bufferLen = encoder.cbuf.length; + const cpos = encoder.cpos; + const leftCopyLen = min3(bufferLen - cpos, uint8Array.length); + const rightCopyLen = uint8Array.length - leftCopyLen; + encoder.cbuf.set(uint8Array.subarray(0, leftCopyLen), cpos); + encoder.cpos += leftCopyLen; + if (rightCopyLen > 0) { + encoder.bufs.push(encoder.cbuf); + encoder.cbuf = new Uint8Array(max3(bufferLen * 2, rightCopyLen)); + encoder.cbuf.set(uint8Array.subarray(leftCopyLen)); + encoder.cpos = rightCopyLen; + } + }; + var writeVarUint8Array3 = (encoder, uint8Array) => { + writeVarUint3(encoder, uint8Array.byteLength); + writeUint8Array3(encoder, uint8Array); + }; + var create$1 = (s) => new Error(s); + var errorUnexpectedEndOfArray3 = create$1("Unexpected end of array"); + var errorIntegerOutOfRange3 = create$1("Integer out of Range"); + var Decoder2 = class { + /** + * @param {Uint8Array} uint8Array Binary data to decode + */ + constructor(uint8Array) { + this.arr = uint8Array; + this.pos = 0; + } + }; + var createDecoder2 = (uint8Array) => new Decoder2(uint8Array); + var readUint8Array3 = (decoder, len) => { + const view = new Uint8Array(decoder.arr.buffer, decoder.pos + decoder.arr.byteOffset, len); + decoder.pos += len; + return view; + }; + var readVarUint8Array3 = (decoder) => readUint8Array3(decoder, readVarUint3(decoder)); + var readUint83 = (decoder) => decoder.arr[decoder.pos++]; + var readVarUint3 = (decoder) => { + let num = 0; + let mult = 1; + const len = decoder.arr.length; + while (decoder.pos < len) { + const r = decoder.arr[decoder.pos++]; + num = num + (r & BITS73) * mult; + mult *= 128; + if (r < BIT83) { + return num; + } + if (num > MAX_SAFE_INTEGER3) { + throw errorIntegerOutOfRange3; + } + } + throw errorUnexpectedEndOfArray3; + }; + var readVarInt2 = (decoder) => { + let r = decoder.arr[decoder.pos++]; + let num = r & BITS62; + let mult = 64; + const sign = (r & BIT72) > 0 ? -1 : 1; + if ((r & BIT83) === 0) { + return sign * num; + } + const len = decoder.arr.length; + while (decoder.pos < len) { + r = decoder.arr[decoder.pos++]; + num = num + (r & BITS73) * mult; + mult *= 128; + if (r < BIT83) { + return sign * num; + } + if (num > MAX_SAFE_INTEGER3) { + throw errorIntegerOutOfRange3; + } + } + throw errorUnexpectedEndOfArray3; + }; + var _readVarStringPolyfill3 = (decoder) => { + let remainingLen = readVarUint3(decoder); + if (remainingLen === 0) { + return ""; + } else { + let encodedString = String.fromCodePoint(readUint83(decoder)); + if (--remainingLen < 100) { + while (remainingLen--) { + encodedString += String.fromCodePoint(readUint83(decoder)); + } + } else { + while (remainingLen > 0) { + const nextLen = remainingLen < 1e4 ? remainingLen : 1e4; + const bytes = decoder.arr.subarray(decoder.pos, decoder.pos + nextLen); + decoder.pos += nextLen; + encodedString += String.fromCodePoint.apply( + null, + /** @type {any} */ + bytes + ); + remainingLen -= nextLen; + } + } + return decodeURIComponent(escape(encodedString)); + } + }; + var _readVarStringNative3 = (decoder) => ( + /** @type any */ + utf8TextDecoder3.decode(readVarUint8Array3(decoder)) + ); + var readVarString3 = utf8TextDecoder3 ? _readVarStringNative3 : _readVarStringPolyfill3; + var peekVarString = (decoder) => { + const pos = decoder.pos; + const s = readVarString3(decoder); + decoder.pos = pos; + return s; + }; + var getUnixTime2 = Date.now; + var create8 = () => /* @__PURE__ */ new Map(); + var setIfUndefined2 = (map2, key, createT) => { + let set = map2.get(key); + if (set === void 0) { + map2.set(key, set = createT()); + } + return set; + }; + var Observable = class { + constructor() { + this._observers = create8(); + } + /** + * @param {N} name + * @param {function} f + */ + on(name, f) { + setIfUndefined2(this._observers, name, create$2).add(f); + } + /** + * @param {N} name + * @param {function} f + */ + once(name, f) { + const _f = (...args2) => { + this.off(name, _f); + f(...args2); + }; + this.on(name, _f); + } + /** + * @param {N} name + * @param {function} f + */ + off(name, f) { + const observers = this._observers.get(name); + if (observers !== void 0) { + observers.delete(f); + if (observers.size === 0) { + this._observers.delete(name); + } + } + } + /** + * Emit a named event. All registered event listeners that listen to the + * specified name will receive the event. + * + * @todo This should catch exceptions + * + * @param {N} name The event name. + * @param {Array} args The arguments that are applied to the event listener. + */ + emit(name, args2) { + return from2((this._observers.get(name) || create8()).values()).forEach((f) => f(...args2)); + } + destroy() { + this._observers = create8(); + } + }; + var keys2 = Object.keys; + var length2 = (obj) => keys2(obj).length; + var hasProperty2 = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key); + var equalityStrict = (a, b) => a === b; + var equalityDeep2 = (a, b) => { + if (a == null || b == null) { + return equalityStrict(a, b); + } + if (a.constructor !== b.constructor) { + return false; + } + if (a === b) { + return true; + } + switch (a.constructor) { + case ArrayBuffer: + a = new Uint8Array(a); + b = new Uint8Array(b); + // eslint-disable-next-line no-fallthrough + case Uint8Array: { + if (a.byteLength !== b.byteLength) { + return false; + } + for (let i = 0; i < a.length; i++) { + if (a[i] !== b[i]) { + return false; + } + } + break; + } + case Set: { + if (a.size !== b.size) { + return false; + } + for (const value of a) { + if (!b.has(value)) { + return false; + } + } + break; + } + case Map: { + if (a.size !== b.size) { + return false; + } + for (const key of a.keys()) { + if (!b.has(key) || !equalityDeep2(a.get(key), b.get(key))) { + return false; + } + } + break; + } + case Object: + if (length2(a) !== length2(b)) { + return false; + } + for (const key in a) { + if (!hasProperty2(a, key) || !equalityDeep2(a[key], b[key])) { + return false; + } + } + break; + case Array: + if (a.length !== b.length) { + return false; + } + for (let i = 0; i < a.length; i++) { + if (!equalityDeep2(a[i], b[i])) { + return false; + } + } + break; + default: + return false; + } + return true; + }; + var outdatedTimeout = 3e4; + var Awareness = class extends Observable { + /** + * @param {Y.Doc} doc + */ + constructor(doc2) { + super(); + this.doc = doc2; + this.clientID = doc2.clientID; + this.states = /* @__PURE__ */ new Map(); + this.meta = /* @__PURE__ */ new Map(); + this._checkInterval = /** @type {any} */ + setInterval(() => { + const now = getUnixTime2(); + if (this.getLocalState() !== null && outdatedTimeout / 2 <= now - /** @type {{lastUpdated:number}} */ + this.meta.get(this.clientID).lastUpdated) { + this.setLocalState(this.getLocalState()); + } + const remove = []; + this.meta.forEach((meta, clientid) => { + if (clientid !== this.clientID && outdatedTimeout <= now - meta.lastUpdated && this.states.has(clientid)) { + remove.push(clientid); + } + }); + if (remove.length > 0) { + removeAwarenessStates(this, remove, "timeout"); + } + }, floor3(outdatedTimeout / 10)); + doc2.on("destroy", () => { + this.destroy(); + }); + this.setLocalState({}); + } + destroy() { + this.emit("destroy", [this]); + this.setLocalState(null); + super.destroy(); + clearInterval(this._checkInterval); + } + /** + * @return {Object|null} + */ + getLocalState() { + return this.states.get(this.clientID) || null; + } + /** + * @param {Object|null} state + */ + setLocalState(state) { + const clientID = this.clientID; + const currLocalMeta = this.meta.get(clientID); + const clock = currLocalMeta === void 0 ? 0 : currLocalMeta.clock + 1; + const prevState = this.states.get(clientID); + if (state === null) { + this.states.delete(clientID); + } else { + this.states.set(clientID, state); + } + this.meta.set(clientID, { + clock, + lastUpdated: getUnixTime2() + }); + const added = []; + const updated = []; + const filteredUpdated = []; + const removed = []; + if (state === null) { + removed.push(clientID); + } else if (prevState == null) { + if (state != null) { + added.push(clientID); + } + } else { + updated.push(clientID); + if (!equalityDeep2(prevState, state)) { + filteredUpdated.push(clientID); + } + } + if (added.length > 0 || filteredUpdated.length > 0 || removed.length > 0) { + this.emit("change", [{ added, updated: filteredUpdated, removed }, "local"]); + } + this.emit("update", [{ added, updated, removed }, "local"]); + } + /** + * @param {string} field + * @param {any} value + */ + setLocalStateField(field, value) { + const state = this.getLocalState(); + if (state !== null) { + this.setLocalState({ + ...state, + [field]: value + }); + } + } + /** + * @return {Map>} + */ + getStates() { + return this.states; + } + }; + var removeAwarenessStates = (awareness, clients, origin) => { + const removed = []; + for (let i = 0; i < clients.length; i++) { + const clientID = clients[i]; + if (awareness.states.has(clientID)) { + awareness.states.delete(clientID); + if (clientID === awareness.clientID) { + const curMeta = ( + /** @type {MetaClientState} */ + awareness.meta.get(clientID) + ); + awareness.meta.set(clientID, { + clock: curMeta.clock + 1, + lastUpdated: getUnixTime2() + }); + } + removed.push(clientID); + } + } + if (removed.length > 0) { + awareness.emit("change", [{ added: [], updated: [], removed }, origin]); + awareness.emit("update", [{ added: [], updated: [], removed }, origin]); + } + }; + var encodeAwarenessUpdate = (awareness, clients, states = awareness.states) => { + const len = clients.length; + const encoder = createEncoder2(); + writeVarUint3(encoder, len); + for (let i = 0; i < len; i++) { + const clientID = clients[i]; + const state = states.get(clientID) || null; + const clock = ( + /** @type {MetaClientState} */ + awareness.meta.get(clientID).clock + ); + writeVarUint3(encoder, clientID); + writeVarUint3(encoder, clock); + writeVarString3(encoder, JSON.stringify(state)); + } + return toUint8Array2(encoder); + }; + var applyAwarenessUpdate = (awareness, update, origin) => { + const decoder = createDecoder2(update); + const timestamp = getUnixTime2(); + const added = []; + const updated = []; + const filteredUpdated = []; + const removed = []; + const len = readVarUint3(decoder); + for (let i = 0; i < len; i++) { + const clientID = readVarUint3(decoder); + let clock = readVarUint3(decoder); + const state = JSON.parse(readVarString3(decoder)); + const clientMeta = awareness.meta.get(clientID); + const prevState = awareness.states.get(clientID); + const currClock = clientMeta === void 0 ? 0 : clientMeta.clock; + if (currClock < clock || currClock === clock && state === null && awareness.states.has(clientID)) { + if (state === null) { + if (clientID === awareness.clientID && awareness.getLocalState() != null) { + clock++; + } else { + awareness.states.delete(clientID); + } + } else { + awareness.states.set(clientID, state); + } + awareness.meta.set(clientID, { + clock, + lastUpdated: timestamp + }); + if (clientMeta === void 0 && state !== null) { + added.push(clientID); + } else if (clientMeta !== void 0 && state === null) { + removed.push(clientID); + } else if (state !== null) { + if (!equalityDeep2(state, prevState)) { + filteredUpdated.push(clientID); + } + updated.push(clientID); + } + } + } + if (added.length > 0 || filteredUpdated.length > 0 || removed.length > 0) { + awareness.emit("change", [{ + added, + updated: filteredUpdated, + removed + }, origin]); + } + if (added.length > 0 || updated.length > 0 || removed.length > 0) { + awareness.emit("update", [{ + added, + updated, + removed + }, origin]); + } + }; + var EventEmitter = class { + constructor() { + this.callbacks = {}; + } + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type + on(event, fn) { + if (!this.callbacks[event]) { + this.callbacks[event] = []; + } + this.callbacks[event].push(fn); + return this; + } + emit(event, ...args2) { + const callbacks = this.callbacks[event]; + if (callbacks) { + callbacks.forEach((callback) => callback.apply(this, args2)); + } + return this; + } + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type + off(event, fn) { + const callbacks = this.callbacks[event]; + if (callbacks) { + if (fn) { + this.callbacks[event] = callbacks.filter((callback) => callback !== fn); + } else { + delete this.callbacks[event]; + } + } + return this; + } + removeAllListeners() { + this.callbacks = {}; + } + }; + var IncomingMessage = class { + constructor(data) { + this.data = data; + this.encoder = createEncoder2(); + this.decoder = createDecoder2(new Uint8Array(this.data)); + } + peekVarString() { + return peekVarString(this.decoder); + } + readVarUint() { + return readVarUint3(this.decoder); + } + readVarString() { + return readVarString3(this.decoder); + } + readVarUint8Array() { + return readVarUint8Array3(this.decoder); + } + writeVarUint(type) { + return writeVarUint3(this.encoder, type); + } + writeVarString(string) { + return writeVarString3(this.encoder, string); + } + writeVarUint8Array(data) { + return writeVarUint8Array3(this.encoder, data); + } + length() { + return length$1(this.encoder); + } + }; + var MessageType; + (function(MessageType2) { + MessageType2[MessageType2["Sync"] = 0] = "Sync"; + MessageType2[MessageType2["Awareness"] = 1] = "Awareness"; + MessageType2[MessageType2["Auth"] = 2] = "Auth"; + MessageType2[MessageType2["QueryAwareness"] = 3] = "QueryAwareness"; + MessageType2[MessageType2["Stateless"] = 5] = "Stateless"; + MessageType2[MessageType2["CLOSE"] = 7] = "CLOSE"; + MessageType2[MessageType2["SyncStatus"] = 8] = "SyncStatus"; + })(MessageType || (MessageType = {})); + var WebSocketStatus; + (function(WebSocketStatus2) { + WebSocketStatus2["Connecting"] = "connecting"; + WebSocketStatus2["Connected"] = "connected"; + WebSocketStatus2["Disconnected"] = "disconnected"; + })(WebSocketStatus || (WebSocketStatus = {})); + var OutgoingMessage = class { + constructor() { + this.encoder = createEncoder2(); + } + get(args2) { + return args2.encoder; + } + toUint8Array() { + return toUint8Array2(this.encoder); + } + }; + var CloseMessage = class extends OutgoingMessage { + constructor() { + super(...arguments); + this.type = MessageType.CLOSE; + this.description = "Ask the server to close the connection"; + } + get(args2) { + writeVarString3(this.encoder, args2.documentName); + writeVarUint3(this.encoder, this.type); + return this.encoder; + } + }; + var HocuspocusProviderWebsocket = class extends EventEmitter { + constructor(configuration) { + super(); + this.messageQueue = []; + this.configuration = { + url: "", + autoConnect: true, + preserveTrailingSlash: false, + // @ts-ignore + document: void 0, + WebSocketPolyfill: void 0, + // TODO: this should depend on awareness.outdatedTime + messageReconnectTimeout: 3e4, + // 1 second + delay: 1e3, + // instant + initialDelay: 0, + // double the delay each time + factor: 2, + // unlimited retries + maxAttempts: 0, + // wait at least 1 second + minDelay: 1e3, + // at least every 30 seconds + maxDelay: 3e4, + // randomize + jitter: true, + // retry forever + timeout: 0, + onOpen: () => null, + onConnect: () => null, + onMessage: () => null, + onOutgoingMessage: () => null, + onStatus: () => null, + onDisconnect: () => null, + onClose: () => null, + onDestroy: () => null, + onAwarenessUpdate: () => null, + onAwarenessChange: () => null, + handleTimeout: null, + providerMap: /* @__PURE__ */ new Map() + }; + this.webSocket = null; + this.webSocketHandlers = {}; + this.shouldConnect = true; + this.status = WebSocketStatus.Disconnected; + this.lastMessageReceived = 0; + this.identifier = 0; + this.intervals = { + connectionChecker: null + }; + this.connectionAttempt = null; + this.receivedOnOpenPayload = void 0; + this.closeTries = 0; + this.setConfiguration(configuration); + this.configuration.WebSocketPolyfill = configuration.WebSocketPolyfill ? configuration.WebSocketPolyfill : WebSocket; + this.on("open", this.configuration.onOpen); + this.on("open", this.onOpen.bind(this)); + this.on("connect", this.configuration.onConnect); + this.on("message", this.configuration.onMessage); + this.on("outgoingMessage", this.configuration.onOutgoingMessage); + this.on("status", this.configuration.onStatus); + this.on("disconnect", this.configuration.onDisconnect); + this.on("close", this.configuration.onClose); + this.on("destroy", this.configuration.onDestroy); + this.on("awarenessUpdate", this.configuration.onAwarenessUpdate); + this.on("awarenessChange", this.configuration.onAwarenessChange); + this.on("close", this.onClose.bind(this)); + this.on("message", this.onMessage.bind(this)); + this.intervals.connectionChecker = setInterval(this.checkConnection.bind(this), this.configuration.messageReconnectTimeout / 10); + if (this.shouldConnect) { + this.connect(); + } + } + async onOpen(event) { + this.status = WebSocketStatus.Connected; + this.emit("status", { status: WebSocketStatus.Connected }); + this.cancelWebsocketRetry = void 0; + this.receivedOnOpenPayload = event; + } + attach(provider) { + this.configuration.providerMap.set(provider.configuration.name, provider); + if (this.status === WebSocketStatus.Disconnected && this.shouldConnect) { + this.connect(); + } + if (this.receivedOnOpenPayload && this.status === WebSocketStatus.Connected) { + provider.onOpen(this.receivedOnOpenPayload); + } + } + detach(provider) { + if (this.configuration.providerMap.has(provider.configuration.name)) { + provider.send(CloseMessage, { + documentName: provider.configuration.name + }); + this.configuration.providerMap.delete(provider.configuration.name); + } + } + setConfiguration(configuration = {}) { + this.configuration = { ...this.configuration, ...configuration }; + if (!this.configuration.autoConnect) { + this.shouldConnect = false; + } + } + async connect() { + if (this.status === WebSocketStatus.Connected) { + return; + } + if (this.cancelWebsocketRetry) { + this.cancelWebsocketRetry(); + this.cancelWebsocketRetry = void 0; + } + this.receivedOnOpenPayload = void 0; + this.shouldConnect = true; + const abortableRetry = () => { + let cancelAttempt = false; + const retryPromise2 = retry(this.createWebSocketConnection.bind(this), { + delay: this.configuration.delay, + initialDelay: this.configuration.initialDelay, + factor: this.configuration.factor, + maxAttempts: this.configuration.maxAttempts, + minDelay: this.configuration.minDelay, + maxDelay: this.configuration.maxDelay, + jitter: this.configuration.jitter, + timeout: this.configuration.timeout, + handleTimeout: this.configuration.handleTimeout, + beforeAttempt: (context) => { + if (!this.shouldConnect || cancelAttempt) { + context.abort(); + } + } + }).catch((error) => { + if (error && error.code !== "ATTEMPT_ABORTED") { + throw error; + } + }); + return { + retryPromise: retryPromise2, + cancelFunc: () => { + cancelAttempt = true; + } + }; + }; + const { retryPromise, cancelFunc } = abortableRetry(); + this.cancelWebsocketRetry = cancelFunc; + return retryPromise; + } + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type + attachWebSocketListeners(ws, reject) { + const { identifier } = ws; + const onMessageHandler = (payload) => this.emit("message", payload); + const onCloseHandler = (payload) => this.emit("close", { event: payload }); + const onOpenHandler = (payload) => this.emit("open", payload); + const onErrorHandler = (err) => { + reject(err); + }; + this.webSocketHandlers[identifier] = { + message: onMessageHandler, + close: onCloseHandler, + open: onOpenHandler, + error: onErrorHandler + }; + const handlers = this.webSocketHandlers[ws.identifier]; + Object.keys(handlers).forEach((name) => { + ws.addEventListener(name, handlers[name]); + }); + } + cleanupWebSocket() { + if (!this.webSocket) { + return; + } + const { identifier } = this.webSocket; + const handlers = this.webSocketHandlers[identifier]; + Object.keys(handlers).forEach((name) => { + var _a; + (_a = this.webSocket) === null || _a === void 0 ? void 0 : _a.removeEventListener(name, handlers[name]); + delete this.webSocketHandlers[identifier]; + }); + this.webSocket.close(); + this.webSocket = null; + } + createWebSocketConnection() { + return new Promise((resolve, reject) => { + if (this.webSocket) { + this.messageQueue = []; + this.cleanupWebSocket(); + } + this.lastMessageReceived = 0; + this.identifier += 1; + const ws = new this.configuration.WebSocketPolyfill(this.url); + ws.binaryType = "arraybuffer"; + ws.identifier = this.identifier; + this.attachWebSocketListeners(ws, reject); + this.webSocket = ws; + this.status = WebSocketStatus.Connecting; + this.emit("status", { status: WebSocketStatus.Connecting }); + this.connectionAttempt = { + resolve, + reject + }; + }); + } + onMessage(event) { + var _a; + this.resolveConnectionAttempt(); + this.lastMessageReceived = getUnixTime2(); + const message = new IncomingMessage(event.data); + const documentName = message.peekVarString(); + (_a = this.configuration.providerMap.get(documentName)) === null || _a === void 0 ? void 0 : _a.onMessage(event); + } + resolveConnectionAttempt() { + if (this.connectionAttempt) { + this.connectionAttempt.resolve(); + this.connectionAttempt = null; + this.status = WebSocketStatus.Connected; + this.emit("status", { status: WebSocketStatus.Connected }); + this.emit("connect"); + this.messageQueue.forEach((message) => this.send(message)); + this.messageQueue = []; + } + } + stopConnectionAttempt() { + this.connectionAttempt = null; + } + rejectConnectionAttempt() { + var _a; + (_a = this.connectionAttempt) === null || _a === void 0 ? void 0 : _a.reject(); + this.connectionAttempt = null; + } + checkConnection() { + var _a; + if (this.status !== WebSocketStatus.Connected) { + return; + } + if (!this.lastMessageReceived) { + return; + } + if (this.configuration.messageReconnectTimeout >= getUnixTime2() - this.lastMessageReceived) { + return; + } + this.closeTries += 1; + if (this.closeTries > 2) { + this.onClose({ + event: { + code: 4408, + reason: "forced" + } + }); + this.closeTries = 0; + } else { + (_a = this.webSocket) === null || _a === void 0 ? void 0 : _a.close(); + this.messageQueue = []; + } + } + get serverUrl() { + if (this.configuration.preserveTrailingSlash) { + return this.configuration.url; + } + let url = this.configuration.url; + while (url[url.length - 1] === "/") { + url = url.slice(0, url.length - 1); + } + return url; + } + get url() { + return this.serverUrl; + } + disconnect() { + this.shouldConnect = false; + if (this.webSocket === null) { + return; + } + try { + this.webSocket.close(); + this.messageQueue = []; + } catch (e) { + console.error(e); + } + } + send(message) { + var _a; + if (((_a = this.webSocket) === null || _a === void 0 ? void 0 : _a.readyState) === WsReadyStates.Open) { + this.webSocket.send(message); + } else { + this.messageQueue.push(message); + } + } + onClose({ event }) { + this.closeTries = 0; + this.cleanupWebSocket(); + if (this.connectionAttempt) { + this.rejectConnectionAttempt(); + } + this.status = WebSocketStatus.Disconnected; + this.emit("status", { status: WebSocketStatus.Disconnected }); + this.emit("disconnect", { event }); + if (!this.cancelWebsocketRetry && this.shouldConnect) { + setTimeout(() => { + this.connect(); + }, this.configuration.delay); + } + } + destroy() { + this.emit("destroy"); + clearInterval(this.intervals.connectionChecker); + this.stopConnectionAttempt(); + this.disconnect(); + this.removeAllListeners(); + this.cleanupWebSocket(); + } + }; + var messageYjsSyncStep1 = 0; + var messageYjsSyncStep2 = 1; + var messageYjsUpdate = 2; + var writeSyncStep1 = (encoder, doc2) => { + writeVarUint3(encoder, messageYjsSyncStep1); + const sv = encodeStateVector(doc2); + writeVarUint8Array3(encoder, sv); + }; + var writeSyncStep2 = (encoder, doc2, encodedStateVector) => { + writeVarUint3(encoder, messageYjsSyncStep2); + writeVarUint8Array3(encoder, encodeStateAsUpdate(doc2, encodedStateVector)); + }; + var readSyncStep1 = (decoder, encoder, doc2) => writeSyncStep2(encoder, doc2, readVarUint8Array3(decoder)); + var readSyncStep2 = (decoder, doc2, transactionOrigin) => { + try { + applyUpdate(doc2, readVarUint8Array3(decoder), transactionOrigin); + } catch (error) { + console.error("Caught error while handling a Yjs update", error); + } + }; + var writeUpdate = (encoder, update) => { + writeVarUint3(encoder, messageYjsUpdate); + writeVarUint8Array3(encoder, update); + }; + var readUpdate = readSyncStep2; + var readSyncMessage = (decoder, encoder, doc2, transactionOrigin) => { + const messageType = readVarUint3(decoder); + switch (messageType) { + case messageYjsSyncStep1: + readSyncStep1(decoder, encoder, doc2); + break; + case messageYjsSyncStep2: + readSyncStep2(decoder, doc2, transactionOrigin); + break; + case messageYjsUpdate: + readUpdate(decoder, doc2, transactionOrigin); + break; + default: + throw new Error("Unknown message type"); + } + return messageType; + }; + var MessageReceiver = class { + constructor(message) { + this.message = message; + } + apply(provider, emitSynced) { + const { message } = this; + const type = message.readVarUint(); + const emptyMessageLength = message.length(); + switch (type) { + case MessageType.Sync: + this.applySyncMessage(provider, emitSynced); + break; + case MessageType.Awareness: + this.applyAwarenessMessage(provider); + break; + case MessageType.Auth: + this.applyAuthMessage(provider); + break; + case MessageType.QueryAwareness: + this.applyQueryAwarenessMessage(provider); + break; + case MessageType.Stateless: + provider.receiveStateless(readVarString3(message.decoder)); + break; + case MessageType.SyncStatus: + this.applySyncStatusMessage(provider, readVarInt2(message.decoder) === 1); + break; + case MessageType.CLOSE: + const event = { + code: 1e3, + reason: readVarString3(message.decoder), + // @ts-ignore + target: provider.configuration.websocketProvider.webSocket, + type: "close" + }; + provider.onClose(); + provider.configuration.onClose({ event }); + provider.forwardClose({ event }); + break; + default: + throw new Error(`Can\u2019t apply message of unknown type: ${type}`); + } + if (message.length() > emptyMessageLength + 1) { + provider.send(OutgoingMessage, { encoder: message.encoder }); + } + } + applySyncMessage(provider, emitSynced) { + const { message } = this; + message.writeVarUint(MessageType.Sync); + const syncMessageType = readSyncMessage(message.decoder, message.encoder, provider.document, provider); + if (emitSynced && syncMessageType === messageYjsSyncStep2) { + provider.synced = true; + } + } + applySyncStatusMessage(provider, applied) { + if (applied) { + provider.decrementUnsyncedChanges(); + } + } + applyAwarenessMessage(provider) { + if (!provider.awareness) + return; + const { message } = this; + applyAwarenessUpdate(provider.awareness, message.readVarUint8Array(), provider); + } + applyAuthMessage(provider) { + const { message } = this; + readAuthMessage(message.decoder, provider.sendToken.bind(provider), provider.permissionDeniedHandler.bind(provider), provider.authenticatedHandler.bind(provider)); + } + applyQueryAwarenessMessage(provider) { + if (!provider.awareness) + return; + const { message } = this; + message.writeVarUint(MessageType.Awareness); + message.writeVarUint8Array(encodeAwarenessUpdate(provider.awareness, Array.from(provider.awareness.getStates().keys()))); + } + }; + var MessageSender = class { + constructor(Message, args2 = {}) { + this.message = new Message(); + this.encoder = this.message.get(args2); + } + create() { + return toUint8Array2(this.encoder); + } + send(webSocket) { + webSocket === null || webSocket === void 0 ? void 0 : webSocket.send(this.create()); + } + }; + var AuthenticationMessage = class extends OutgoingMessage { + constructor() { + super(...arguments); + this.type = MessageType.Auth; + this.description = "Authentication"; + } + get(args2) { + if (typeof args2.token === "undefined") { + throw new Error("The authentication message requires `token` as an argument."); + } + writeVarString3(this.encoder, args2.documentName); + writeVarUint3(this.encoder, this.type); + writeAuthentication(this.encoder, args2.token); + return this.encoder; + } + }; + var AwarenessMessage = class extends OutgoingMessage { + constructor() { + super(...arguments); + this.type = MessageType.Awareness; + this.description = "Awareness states update"; + } + get(args2) { + if (typeof args2.awareness === "undefined") { + throw new Error("The awareness message requires awareness as an argument"); + } + if (typeof args2.clients === "undefined") { + throw new Error("The awareness message requires clients as an argument"); + } + writeVarString3(this.encoder, args2.documentName); + writeVarUint3(this.encoder, this.type); + let awarenessUpdate; + if (args2.states === void 0) { + awarenessUpdate = encodeAwarenessUpdate(args2.awareness, args2.clients); + } else { + awarenessUpdate = encodeAwarenessUpdate(args2.awareness, args2.clients, args2.states); + } + writeVarUint8Array3(this.encoder, awarenessUpdate); + return this.encoder; + } + }; + var StatelessMessage = class extends OutgoingMessage { + constructor() { + super(...arguments); + this.type = MessageType.Stateless; + this.description = "A stateless message"; + } + get(args2) { + var _a; + writeVarString3(this.encoder, args2.documentName); + writeVarUint3(this.encoder, this.type); + writeVarString3(this.encoder, (_a = args2.payload) !== null && _a !== void 0 ? _a : ""); + return this.encoder; + } + }; + var SyncStepOneMessage = class extends OutgoingMessage { + constructor() { + super(...arguments); + this.type = MessageType.Sync; + this.description = "First sync step"; + } + get(args2) { + if (typeof args2.document === "undefined") { + throw new Error("The sync step one message requires document as an argument"); + } + writeVarString3(this.encoder, args2.documentName); + writeVarUint3(this.encoder, this.type); + writeSyncStep1(this.encoder, args2.document); + return this.encoder; + } + }; + var UpdateMessage = class extends OutgoingMessage { + constructor() { + super(...arguments); + this.type = MessageType.Sync; + this.description = "A document update"; + } + get(args2) { + writeVarString3(this.encoder, args2.documentName); + writeVarUint3(this.encoder, this.type); + writeUpdate(this.encoder, args2.update); + return this.encoder; + } + }; + var AwarenessError = class extends Error { + constructor() { + super(...arguments); + this.code = 1001; + } + }; + var HocuspocusProvider = class extends EventEmitter { + constructor(configuration) { + var _a, _b, _c; + super(); + this.configuration = { + name: "", + // @ts-ignore + document: void 0, + // @ts-ignore + awareness: void 0, + token: null, + forceSyncInterval: false, + onAuthenticated: () => null, + onAuthenticationFailed: () => null, + onOpen: () => null, + onConnect: () => null, + onMessage: () => null, + onOutgoingMessage: () => null, + onSynced: () => null, + onStatus: () => null, + onDisconnect: () => null, + onClose: () => null, + onDestroy: () => null, + onAwarenessUpdate: () => null, + onAwarenessChange: () => null, + onStateless: () => null, + onUnsyncedChanges: () => null + }; + this.isSynced = false; + this.unsyncedChanges = 0; + this.isAuthenticated = false; + this.authorizedScope = void 0; + this.manageSocket = false; + this._isAttached = false; + this.intervals = { + forceSync: null + }; + this.boundDocumentUpdateHandler = this.documentUpdateHandler.bind(this); + this.boundAwarenessUpdateHandler = this.awarenessUpdateHandler.bind(this); + this.boundPageHide = this.pageHide.bind(this); + this.boundOnOpen = this.onOpen.bind(this); + this.boundOnClose = this.onClose.bind(this); + this.forwardConnect = () => this.emit("connect"); + this.forwardStatus = (e) => this.emit("status", e); + this.forwardClose = (e) => this.emit("close", e); + this.forwardDisconnect = (e) => this.emit("disconnect", e); + this.forwardDestroy = () => this.emit("destroy"); + this.setConfiguration(configuration); + this.configuration.document = configuration.document ? configuration.document : new Doc(); + this.configuration.awareness = configuration.awareness !== void 0 ? configuration.awareness : new Awareness(this.document); + this.on("open", this.configuration.onOpen); + this.on("message", this.configuration.onMessage); + this.on("outgoingMessage", this.configuration.onOutgoingMessage); + this.on("synced", this.configuration.onSynced); + this.on("destroy", this.configuration.onDestroy); + this.on("awarenessUpdate", this.configuration.onAwarenessUpdate); + this.on("awarenessChange", this.configuration.onAwarenessChange); + this.on("stateless", this.configuration.onStateless); + this.on("unsyncedChanges", this.configuration.onUnsyncedChanges); + this.on("authenticated", this.configuration.onAuthenticated); + this.on("authenticationFailed", this.configuration.onAuthenticationFailed); + (_a = this.awareness) === null || _a === void 0 ? void 0 : _a.on("update", () => { + this.emit("awarenessUpdate", { + states: awarenessStatesToArray(this.awareness.getStates()) + }); + }); + (_b = this.awareness) === null || _b === void 0 ? void 0 : _b.on("change", () => { + this.emit("awarenessChange", { + states: awarenessStatesToArray(this.awareness.getStates()) + }); + }); + this.document.on("update", this.boundDocumentUpdateHandler); + (_c = this.awareness) === null || _c === void 0 ? void 0 : _c.on("update", this.boundAwarenessUpdateHandler); + this.registerEventListeners(); + if (this.configuration.forceSyncInterval && typeof this.configuration.forceSyncInterval === "number") { + this.intervals.forceSync = setInterval(this.forceSync.bind(this), this.configuration.forceSyncInterval); + } + if (this.manageSocket) { + this.attach(); + } + } + setConfiguration(configuration = {}) { + if (!configuration.websocketProvider) { + this.manageSocket = true; + this.configuration.websocketProvider = new HocuspocusProviderWebsocket(configuration); + } + this.configuration = { ...this.configuration, ...configuration }; + } + get document() { + return this.configuration.document; + } + get isAttached() { + return this._isAttached; + } + get awareness() { + return this.configuration.awareness; + } + get hasUnsyncedChanges() { + return this.unsyncedChanges > 0; + } + resetUnsyncedChanges() { + this.unsyncedChanges = 1; + this.emit("unsyncedChanges", { number: this.unsyncedChanges }); + } + incrementUnsyncedChanges() { + this.unsyncedChanges += 1; + this.emit("unsyncedChanges", { number: this.unsyncedChanges }); + } + decrementUnsyncedChanges() { + if (this.unsyncedChanges > 0) { + this.unsyncedChanges -= 1; + } + if (this.unsyncedChanges === 0) { + this.synced = true; + } + this.emit("unsyncedChanges", { number: this.unsyncedChanges }); + } + forceSync() { + this.resetUnsyncedChanges(); + this.send(SyncStepOneMessage, { + document: this.document, + documentName: this.configuration.name + }); + } + pageHide() { + if (this.awareness) { + removeAwarenessStates(this.awareness, [this.document.clientID], "page hide"); + } + } + registerEventListeners() { + if (typeof window === "undefined" || !("addEventListener" in window)) { + return; + } + window.addEventListener("pagehide", this.boundPageHide); + } + sendStateless(payload) { + this.send(StatelessMessage, { + documentName: this.configuration.name, + payload + }); + } + async sendToken() { + let token; + try { + token = await this.getToken(); + } catch (error) { + this.permissionDeniedHandler(`Failed to get token during sendToken(): ${error}`); + return; + } + this.send(AuthenticationMessage, { + token: token !== null && token !== void 0 ? token : "", + documentName: this.configuration.name + }); + } + documentUpdateHandler(update, origin) { + if (origin === this) { + return; + } + this.incrementUnsyncedChanges(); + this.send(UpdateMessage, { update, documentName: this.configuration.name }); + } + awarenessUpdateHandler({ added, updated, removed }, origin) { + const changedClients = added.concat(updated).concat(removed); + this.send(AwarenessMessage, { + awareness: this.awareness, + clients: changedClients, + documentName: this.configuration.name + }); + } + /** + * Indicates whether a first handshake with the server has been established + * + * Note: this does not mean all updates from the client have been persisted to the backend. For this, + * use `hasUnsyncedChanges`. + */ + get synced() { + return this.isSynced; + } + set synced(state) { + if (this.isSynced === state) { + return; + } + this.isSynced = state; + if (state) { + this.emit("synced", { state }); + } + } + receiveStateless(payload) { + this.emit("stateless", { payload }); + } + // not needed, but provides backward compatibility with e.g. lexical/yjs + async connect() { + if (this.manageSocket) { + return this.configuration.websocketProvider.connect(); + } + console.warn("HocuspocusProvider::connect() is deprecated and does not do anything. Please connect/disconnect on the websocketProvider, or attach/deattach providers."); + } + disconnect() { + if (this.manageSocket) { + return this.configuration.websocketProvider.disconnect(); + } + console.warn("HocuspocusProvider::disconnect() is deprecated and does not do anything. Please connect/disconnect on the websocketProvider, or attach/deattach providers."); + } + async onOpen(event) { + this.isAuthenticated = false; + this.emit("open", { event }); + await this.sendToken(); + this.startSync(); + } + async getToken() { + if (typeof this.configuration.token === "function") { + const token = await this.configuration.token(); + return token; + } + return this.configuration.token; + } + startSync() { + this.resetUnsyncedChanges(); + this.send(SyncStepOneMessage, { + document: this.document, + documentName: this.configuration.name + }); + if (this.awareness && this.awareness.getLocalState() !== null) { + this.send(AwarenessMessage, { + awareness: this.awareness, + clients: [this.document.clientID], + documentName: this.configuration.name + }); + } + } + send(message, args2) { + if (!this._isAttached) + return; + const messageSender = new MessageSender(message, args2); + this.emit("outgoingMessage", { message: messageSender.message }); + messageSender.send(this.configuration.websocketProvider); + } + onMessage(event) { + const message = new IncomingMessage(event.data); + const documentName = message.readVarString(); + message.writeVarString(documentName); + this.emit("message", { event, message: new IncomingMessage(event.data) }); + new MessageReceiver(message).apply(this, true); + } + onClose() { + this.isAuthenticated = false; + this.synced = false; + if (this.awareness) { + removeAwarenessStates(this.awareness, Array.from(this.awareness.getStates().keys()).filter((client) => client !== this.document.clientID), this); + } + } + destroy() { + this.emit("destroy"); + if (this.intervals.forceSync) { + clearInterval(this.intervals.forceSync); + } + if (this.awareness) { + removeAwarenessStates(this.awareness, [this.document.clientID], "provider destroy"); + this.awareness.off("update", this.boundAwarenessUpdateHandler); + this.awareness.destroy(); + } + this.document.off("update", this.boundDocumentUpdateHandler); + this.removeAllListeners(); + this.detach(); + if (this.manageSocket) { + this.configuration.websocketProvider.destroy(); + } + if (typeof window === "undefined" || !("removeEventListener" in window)) { + return; + } + window.removeEventListener("pagehide", this.boundPageHide); + } + detach() { + this.configuration.websocketProvider.off("connect", this.configuration.onConnect); + this.configuration.websocketProvider.off("connect", this.forwardConnect); + this.configuration.websocketProvider.off("status", this.forwardStatus); + this.configuration.websocketProvider.off("status", this.configuration.onStatus); + this.configuration.websocketProvider.off("open", this.boundOnOpen); + this.configuration.websocketProvider.off("close", this.boundOnClose); + this.configuration.websocketProvider.off("close", this.configuration.onClose); + this.configuration.websocketProvider.off("close", this.forwardClose); + this.configuration.websocketProvider.off("disconnect", this.configuration.onDisconnect); + this.configuration.websocketProvider.off("disconnect", this.forwardDisconnect); + this.configuration.websocketProvider.off("destroy", this.configuration.onDestroy); + this.configuration.websocketProvider.off("destroy", this.forwardDestroy); + this.configuration.websocketProvider.detach(this); + this._isAttached = false; + } + attach() { + if (this._isAttached) + return; + this.configuration.websocketProvider.on("connect", this.configuration.onConnect); + this.configuration.websocketProvider.on("connect", this.forwardConnect); + this.configuration.websocketProvider.on("status", this.configuration.onStatus); + this.configuration.websocketProvider.on("status", this.forwardStatus); + this.configuration.websocketProvider.on("open", this.boundOnOpen); + this.configuration.websocketProvider.on("close", this.boundOnClose); + this.configuration.websocketProvider.on("close", this.configuration.onClose); + this.configuration.websocketProvider.on("close", this.forwardClose); + this.configuration.websocketProvider.on("disconnect", this.configuration.onDisconnect); + this.configuration.websocketProvider.on("disconnect", this.forwardDisconnect); + this.configuration.websocketProvider.on("destroy", this.configuration.onDestroy); + this.configuration.websocketProvider.on("destroy", this.forwardDestroy); + this.configuration.websocketProvider.attach(this); + this._isAttached = true; + } + permissionDeniedHandler(reason) { + this.emit("authenticationFailed", { reason }); + this.isAuthenticated = false; + } + authenticatedHandler(scope) { + this.isAuthenticated = true; + this.authorizedScope = scope; + this.emit("authenticated", { scope }); + } + setAwarenessField(key, value) { + if (!this.awareness) { + throw new AwarenessError(`Cannot set awareness field "${key}" to ${JSON.stringify(value)}. You have disabled Awareness for this provider by explicitly passing awareness: null in the provider configuration.`); + } + this.awareness.setLocalStateField(key, value); + } + }; + + // src/bridge/cursor-presence/userColor.ts + var CURSOR_PALETTE = [ + "#E53935", + "#1E88E5", + "#43A047", + "#FB8C00", + "#8E24AA", + "#00ACC1", + "#F4511E", + "#3949AB", + "#7CB342", + "#D81B60", + "#6D4C41", + "#546E7A" + ]; + function hashString(input) { + let hash = 2166136261; + for (let i = 0; i < input.length; i += 1) { + hash ^= input.charCodeAt(i); + hash = Math.imul(hash, 16777619); + } + return hash >>> 0; + } + function getUserColor(userId) { + const normalized = userId.trim() || "anonymous"; + return CURSOR_PALETTE[hashString(normalized) % CURSOR_PALETTE.length]; + } + + // src/bridge/cursor-presence/CursorPresenceProvider.ts + var DEFAULT_THROTTLE_MS = 33; + var CursorPresenceProvider = class { + constructor(provider, user, onRemoteChange, throttleMs = DEFAULT_THROTTLE_MS) { + this.provider = provider; + this.lastBroadcastAt = 0; + this.destroyed = false; + this.boundAwarenessChange = () => this.onRemoteChange(); + this.onRemoteChange = onRemoteChange; + this.throttleMs = throttleMs; + this.provider.setAwarenessField("user", user); + this.provider.setAwarenessField("cursor", null); + this.provider.awareness?.on("change", this.boundAwarenessChange); + } + /** + * Broadcast typing caret to peers via awareness. + * Does not create any local DOM — peers render it; the typist does not. + * Passing `null` clears immediately (no throttle) so inactive cursors vanish. + */ + setLocalCursor(cursor) { + if (this.destroyed) return; + if (cursor === null) { + window.clearTimeout(this.throttleTimer); + this.pendingCursor = null; + this.flushBroadcast(); + return; + } + this.pendingCursor = cursor; + const elapsed = performance.now() - this.lastBroadcastAt; + if (elapsed >= this.throttleMs) { + this.flushBroadcast(); + return; + } + window.clearTimeout(this.throttleTimer); + this.throttleTimer = window.setTimeout(() => this.flushBroadcast(), this.throttleMs - elapsed); + } + flushBroadcast() { + if (this.destroyed || this.pendingCursor === void 0) return; + this.lastBroadcastAt = performance.now(); + this.provider.setAwarenessField("cursor", this.pendingCursor); + this.pendingCursor = void 0; + } + syncOverlayFromAwareness(sync) { + const awareness = this.provider.awareness; + if (awareness) sync(awareness); + } + destroy() { + if (this.destroyed) return; + this.destroyed = true; + window.clearTimeout(this.throttleTimer); + this.provider.setAwarenessField("cursor", null); + this.provider.awareness?.off("change", this.boundAwarenessChange); + } + }; + + // src/bridge/cursor-presence/caretMetrics.ts + var MIRROR_PROPERTIES = [ + "direction", + "boxSizing", + "width", + "height", + "overflowX", + "overflowY", + "borderTopWidth", + "borderRightWidth", + "borderBottomWidth", + "borderLeftWidth", + "paddingTop", + "paddingRight", + "paddingBottom", + "paddingLeft", + "fontStyle", + "fontVariant", + "fontWeight", + "fontStretch", + "fontSize", + "fontSizeAdjust", + "lineHeight", + "fontFamily", + "textAlign", + "textTransform", + "textIndent", + "textDecoration", + "letterSpacing", + "wordSpacing", + "tabSize", + "whiteSpace", + "wordWrap", + "wordBreak" + ]; + var mirrorDiv = null; + function getMirrorDiv() { + if (!mirrorDiv) { + mirrorDiv = document.createElement("div"); + mirrorDiv.id = "lowcoder-cursor-mirror"; + mirrorDiv.setAttribute("aria-hidden", "true"); + mirrorDiv.style.cssText = "position:absolute;visibility:hidden;white-space:pre-wrap;word-wrap:break-word;top:0;left:-9999px;"; + document.body.appendChild(mirrorDiv); + } + return mirrorDiv; + } + function toKebabCase(prop) { + return prop.replace(/([A-Z])/g, "-$1").toLowerCase(); + } + function copyInputStyles(element2, div) { + const computed = window.getComputedStyle(element2); + for (const prop of MIRROR_PROPERTIES) { + const kebab = toKebabCase(prop); + div.style.setProperty(kebab, computed.getPropertyValue(kebab)); + } + div.style.width = `${element2.clientWidth}px`; + div.style.whiteSpace = element2 instanceof HTMLTextAreaElement ? "pre-wrap" : "nowrap"; + } + function fieldLineHeight(field) { + const style = window.getComputedStyle(field); + return parseFloat(style.lineHeight) || parseFloat(style.fontSize) * 1.2 || 20; + } + function getFieldFallbackCaret(field) { + const rect = field.getBoundingClientRect(); + const height = fieldLineHeight(field); + const style = window.getComputedStyle(field); + const padL = parseFloat(style.paddingLeft || "0"); + const padT = parseFloat(style.paddingTop || "0"); + return { + left: rect.left + padL + 4, + top: rect.top + padT + 2, + height + }; + } + function getContentEditableCaret(field) { + const sel = window.getSelection(); + if (!sel || sel.rangeCount === 0) return getFieldFallbackCaret(field); + const range = sel.getRangeAt(0); + if (!field.contains(range.startContainer)) return getFieldFallbackCaret(field); + const collapsed = range.cloneRange(); + collapsed.collapse(true); + const rects = collapsed.getClientRects(); + const rect = rects.length > 0 ? rects[0] : collapsed.getBoundingClientRect(); + if (rect.width === 0 && rect.height === 0) return getFieldFallbackCaret(field); + return { left: rect.left, top: rect.top, height: Math.max(rect.height, fieldLineHeight(field)) }; + } + function getCaretCoordinatesForField(field, position) { + if (field instanceof HTMLElement && field.isContentEditable && !(field instanceof HTMLInputElement) && !(field instanceof HTMLTextAreaElement)) { + return getContentEditableCaret(field); + } + if (position == null) return getFieldFallbackCaret(field); + const exact = getCaretCoordinates(field, position); + return exact ?? getFieldFallbackCaret(field); + } + function getCaretCoordinates(element2, position) { + if (!element2.isConnected) return null; + const div = getMirrorDiv(); + copyInputStyles(element2, div); + const value = element2.value; + const clamped = Math.max(0, Math.min(position, value.length)); + const before = value.slice(0, clamped); + const after = value.slice(clamped) || "."; + div.textContent = before; + const span = document.createElement("span"); + span.textContent = after; + div.appendChild(span); + const elementRect = element2.getBoundingClientRect(); + const spanRect = span.getBoundingClientRect(); + const divRect = div.getBoundingClientRect(); + const style = window.getComputedStyle(element2); + const lineHeight = parseFloat(style.lineHeight) || parseFloat(style.fontSize) * 1.2; + const left = elementRect.left - element2.scrollLeft + (spanRect.left - divRect.left) + parseFloat(style.borderLeftWidth || "0") + parseFloat(style.paddingLeft || "0"); + const top = elementRect.top - element2.scrollTop + (spanRect.top - divRect.top) + parseFloat(style.borderTopWidth || "0") + parseFloat(style.paddingTop || "0"); + div.textContent = ""; + const coords = { left, top, height: lineHeight }; + if (!Number.isFinite(coords.left) || !Number.isFinite(coords.top)) { + return null; + } + return coords; + } + function getSelectionRectsForField(field, anchor, head) { + if (field instanceof HTMLElement && field.isContentEditable && !(field instanceof HTMLInputElement) && !(field instanceof HTMLTextAreaElement)) { + const sel = window.getSelection(); + if (!sel || sel.rangeCount === 0 || anchor === head) return []; + const range = sel.getRangeAt(0); + if (!field.contains(range.startContainer)) return []; + const rects = []; + for (const r of Array.from(range.getClientRects())) { + rects.push({ left: r.left, top: r.top, width: r.width, height: r.height }); + } + return rects; + } + return getSelectionRects(field, anchor, head); + } + function getSelectionRects(element2, anchor, head) { + const start = Math.min(anchor, head); + const end = Math.max(anchor, head); + if (start === end) return []; + const startCoords = getCaretCoordinates(element2, start); + const endCoords = getCaretCoordinates(element2, end); + if (!startCoords || !endCoords) return []; + const height = startCoords.height; + if (Math.abs(startCoords.top - endCoords.top) < height * 0.5) { + return [{ + left: startCoords.left, + top: startCoords.top, + width: Math.max(2, endCoords.left - startCoords.left), + height + }]; + } + const value = element2.value; + const lineStart = value.lastIndexOf("\n", start) + 1; + const lineEnd = value.indexOf("\n", end); + const lineEndIndex = lineEnd === -1 ? value.length : lineEnd; + const lineEndCoords = getCaretCoordinates(element2, lineEndIndex); + const lineStartCoords = getCaretCoordinates(element2, lineStart); + const rects = []; + if (lineEndCoords) { + rects.push({ + left: startCoords.left, + top: startCoords.top, + width: Math.max(2, lineEndCoords.left - startCoords.left), + height + }); + } + if (lineStartCoords) { + rects.push({ + left: lineStartCoords.left, + top: endCoords.top, + width: Math.max(2, endCoords.left - lineStartCoords.left), + height + }); + } + return rects; + } + function destroyCaretMirror() { + mirrorDiv?.remove(); + mirrorDiv = null; + } + + // src/bridge/cursor-presence/textField.ts + var IGNORED_INPUT_TYPES = /* @__PURE__ */ new Set([ + "hidden", + "checkbox", + "radio", + "button", + "submit", + "file", + "password" + ]); + function isTextFieldElement(el) { + if (!el) return false; + if (el instanceof HTMLTextAreaElement) return true; + if (el instanceof HTMLInputElement) { + const type = (el.getAttribute("type") || el.type || "text").toLowerCase(); + return !IGNORED_INPUT_TYPES.has(type); + } + if (el instanceof HTMLElement && el.isContentEditable) return true; + return false; + } + function getFocusedTextField() { + const el = document.activeElement; + if (isTextFieldElement(el)) return el; + if (el instanceof HTMLElement) { + const inner = el.querySelector( + 'input:not([type="hidden"]):not([type="checkbox"]):not([type="radio"]), textarea, [contenteditable="true"]' + ); + if (isTextFieldElement(inner)) return inner; + } + return null; + } + function listEditableFields(container = document) { + const nodes = container.querySelectorAll( + [ + 'input[type="text"]', + 'input[type="email"]', + 'input[type="number"]', + 'input[type="tel"]', + 'input[type="url"]', + 'input[type="search"]', + 'input[type="short_text"]', + 'input[type="long_text"]', + 'input[type="phone_number"]', + "input[name]", + "input:not([type])", + "textarea", + '[contenteditable="true"]', + '[role="textbox"]' + ].join(", ") + ); + return Array.from(nodes).filter((field) => { + if (!isTextFieldElement(field)) return false; + const rect = field.getBoundingClientRect(); + return rect.width > 0 && rect.height > 0; + }); + } + function getFieldText(field) { + if (field instanceof HTMLInputElement || field instanceof HTMLTextAreaElement) { + return field.value; + } + return field.textContent ?? ""; + } + function getFieldSelection(field) { + if (field instanceof HTMLInputElement || field instanceof HTMLTextAreaElement) { + return { + anchor: field.selectionStart ?? 0, + head: field.selectionEnd ?? 0 + }; + } + const sel = window.getSelection(); + if (!sel || sel.rangeCount === 0) { + const len = getFieldText(field).length; + return { anchor: len, head: len }; + } + const range = sel.getRangeAt(0); + if (!field.contains(range.startContainer)) { + const len = getFieldText(field).length; + return { anchor: len, head: len }; + } + const pre = range.cloneRange(); + pre.selectNodeContents(field); + pre.setEnd(range.startContainer, range.startOffset); + const anchor = pre.toString().length; + pre.setEnd(range.endContainer, range.endOffset); + const head = pre.toString().length; + return { anchor, head }; + } + function extractQuestionUuid(value) { + const match2 = value.match( + /([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i + ); + return match2?.[1] ?? null; + } + function getCursorFieldKey(field, step, bridgeGetFieldKey) { + if (bridgeGetFieldKey && (field instanceof HTMLInputElement || field instanceof HTMLTextAreaElement)) { + return bridgeGetFieldKey(field); + } + const name = field.getAttribute("name")?.trim(); + if (name) return `name:${name}`; + const labelledBy = field.getAttribute("aria-labelledby") || ""; + const fromLabel = extractQuestionUuid(labelledBy); + if (fromLabel) return `qid:${fromLabel}`; + const id2 = field.getAttribute("id") || ""; + const fromId = extractQuestionUuid(id2); + if (fromId) return `qid:${fromId}`; + const qa = field.getAttribute("data-qa"); + if (qa) return `qa:${qa}:step:${step}`; + return `ce:step:${step}`; + } + function findFieldByCursorKey(key, step, bridgeFindFieldByKey) { + if (bridgeFindFieldByKey) { + const bridged = bridgeFindFieldByKey(key); + if (bridged) return bridged; + } + for (const field of listEditableFields()) { + if (getCursorFieldKey(field, step) === key) return field; + } + if (key.startsWith("name:")) { + const name = key.slice("name:".length); + const el = document.querySelector(`input[name="${CSS.escape(name)}"], textarea[name="${CSS.escape(name)}"]`); + if (isTextFieldElement(el)) return el; + } + if (key.startsWith("qid:")) { + const qid = key.slice("qid:".length); + const sel = `[aria-labelledby*="${CSS.escape(qid)}"], [id*="${CSS.escape(qid)}"]`; + for (const el of document.querySelectorAll(sel)) { + if (isTextFieldElement(el)) return el; + } + } + return null; + } + + // src/bridge/cursor-presence/RemoteCursor.ts + var CURSOR_CLASS = "lowcoder-remote-cursor"; + var LABEL_CLASS = "lowcoder-remote-cursor-label"; + var CARET_CLASS = "lowcoder-remote-cursor-caret"; + var SELECTION_CLASS = "lowcoder-remote-cursor-selection"; + var CARET_VERTICAL_OFFSET_PX = -6; + var RemoteCursor = class { + constructor(clientId) { + this.selectionHighlights = []; + this.selectionKey = ""; + this.visible = false; + this.clientId = clientId; + this.root = document.createElement("div"); + this.root.className = CURSOR_CLASS; + this.root.dataset.clientId = String(clientId); + this.root.style.cssText = "position:fixed;pointer-events:none;z-index:2147483646;transition:opacity 120ms ease;"; + this.label = document.createElement("div"); + this.label.className = LABEL_CLASS; + this.label.style.cssText = "position:absolute;transform:translate(-2px,calc(-100% - 4px));padding:1px 6px;border-radius:3px;font:500 11px/16px system-ui,sans-serif;color:#fff;white-space:nowrap;max-width:160px;overflow:hidden;text-overflow:ellipsis;"; + this.caret = document.createElement("div"); + this.caret.className = CARET_CLASS; + this.caret.style.cssText = "position:absolute;width:2px;border-radius:1px;transform:translateX(-1px);"; + this.root.append(this.caret, this.label); + this.hide(); + } + mount(container) { + if (!this.root.isConnected) container.appendChild(this.root); + } + update(state, overlayContainer) { + if (!state.online || !state.cursor?.typing) { + this.hide(); + return; + } + this.visible = true; + this.root.style.opacity = "1"; + this.root.style.display = "block"; + const { user, x, y, height, selectionRects } = state; + this.root.style.transform = `translate(${x}px, ${y}px)`; + this.label.textContent = user.name; + this.label.style.backgroundColor = user.color; + this.caret.style.backgroundColor = user.color; + this.caret.style.height = `${Math.max(6, height)}px`; + this.caret.style.top = `${CARET_VERTICAL_OFFSET_PX}px`; + this.renderSelectionHighlights(user.color, selectionRects, overlayContainer); + } + updatePosition(x, y) { + if (!this.visible) return; + this.root.style.transform = `translate(${x}px, ${y}px)`; + } + hide() { + this.visible = false; + this.root.style.opacity = "0"; + this.root.style.display = "none"; + this.selectionKey = ""; + this.clearSelectionHighlights(); + } + destroy() { + this.clearSelectionHighlights(); + this.root.remove(); + } + renderSelectionHighlights(color, selectionRects, overlayContainer) { + const key = selectionRects.map((r) => `${r.left},${r.top},${r.width},${r.height}`).join("|"); + if (key === this.selectionKey) return; + this.selectionKey = key; + this.clearSelectionHighlights(); + for (const rect of selectionRects) { + const highlight = document.createElement("div"); + highlight.className = SELECTION_CLASS; + highlight.style.cssText = `position:fixed;left:${rect.left}px;top:${rect.top}px;width:${rect.width}px;height:${rect.height}px;background:${color};opacity:0.28;border-radius:2px;pointer-events:none;z-index:2147483644;`; + overlayContainer.appendChild(highlight); + this.selectionHighlights.push(highlight); + } + } + clearSelectionHighlights() { + for (const el of this.selectionHighlights) el.remove(); + this.selectionHighlights = []; + } + }; + function ensureCursorStyles() { + if (document.getElementById("lowcoder-cursor-presence-styles")) return; + const style = document.createElement("style"); + style.id = "lowcoder-cursor-presence-styles"; + style.textContent = ` + .${CURSOR_CLASS} { contain: layout style; } + .${LABEL_CLASS} { box-shadow: 0 1px 3px rgba(0,0,0,0.25); } + .${CARET_CLASS} { animation: lowcoder-cursor-blink 1s step-end infinite; } + @keyframes lowcoder-cursor-blink { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } } + `; + document.head.appendChild(style); + } + + // src/bridge/cursor-presence/CursorOverlay.ts + var LERP_FACTOR = 0.35; + var CursorOverlay = class { + constructor(options) { + this.options = options; + this.cursors = /* @__PURE__ */ new Map(); + this.renderStates = /* @__PURE__ */ new Map(); + this.rafId = null; + this.destroyed = false; + ensureCursorStyles(); + this.container = document.createElement("div"); + this.container.id = "lowcoder-cursor-overlay"; + this.container.style.cssText = "position:fixed;inset:0;pointer-events:none;z-index:2147483647;overflow:visible;"; + document.documentElement.appendChild(this.container); + this.startAnimationLoop(); + } + /** + * Render cursors for other connected users only. + * The local typist never sees their own collaborative caret/label. + * Null / inactive remote cursors are hidden (no time-based timeout). + */ + syncFromAwareness(awareness) { + const localClientId = awareness.clientID; + const localUserId = this.options.localUserId; + const active = /* @__PURE__ */ new Set(); + awareness.getStates().forEach((rawState, clientId) => { + if (clientId === localClientId) return; + const state = rawState; + if (!state?.user) return; + if (state.user.id === localUserId) return; + if (!this.isActiveRemoteCursor(state.cursor)) { + this.removeRemote(clientId); + return; + } + active.add(clientId); + this.upsertRemoteState(clientId, state.user, state.cursor); + }); + for (const clientId of this.cursors.keys()) { + if (!active.has(clientId)) this.removeRemote(clientId); + } + this.renderAll(); + } + isActiveRemoteCursor(cursor) { + return cursor != null && cursor.typing === true; + } + upsertRemoteState(clientId, user, cursor) { + const existing = this.renderStates.get(clientId); + const metrics = this.resolveCursorMetrics(cursor); + const hasCursor = cursor?.typing === true && metrics != null; + this.renderStates.set(clientId, { + clientId, + user, + cursor, + x: existing?.x ?? metrics?.x ?? 0, + y: existing?.y ?? metrics?.y ?? 0, + targetX: metrics?.x ?? existing?.targetX ?? 0, + targetY: metrics?.y ?? existing?.targetY ?? 0, + height: metrics?.height ?? existing?.height ?? 16, + selectionRects: metrics?.selectionRects ?? [], + online: hasCursor + }); + if (!this.cursors.has(clientId)) { + const remoteCursor = new RemoteCursor(clientId); + remoteCursor.mount(this.container); + this.cursors.set(clientId, remoteCursor); + } + } + removeRemote(clientId) { + this.renderStates.delete(clientId); + this.cursors.get(clientId)?.destroy(); + this.cursors.delete(clientId); + } + resolveField(key) { + return findFieldByCursorKey( + key, + this.options.getCurrentStep(), + this.options.findFieldByKey + ); + } + resolveCursorMetrics(cursor) { + if (!this.isActiveRemoteCursor(cursor)) return null; + if (cursor.step !== this.options.getCurrentStep()) return null; + const field = this.resolveField(cursor.fieldKey); + if (!field?.isConnected) return null; + let caret = getCaretCoordinatesForField(field, cursor.selection.head) ?? getFieldFallbackCaret(field); + if (!caret || !Number.isFinite(caret.left)) { + caret = getFieldFallbackCaret(field); + } + return { + x: caret.left, + y: caret.top, + height: caret.height, + selectionRects: getSelectionRectsForField( + field, + cursor.selection.anchor, + cursor.selection.head + ) + }; + } + renderAll() { + for (const state of this.renderStates.values()) { + this.cursors.get(state.clientId)?.update(state, this.container); + } + } + startAnimationLoop() { + const tick = () => { + if (this.destroyed) return; + for (const state of this.renderStates.values()) { + const dx = state.targetX - state.x; + const dy = state.targetY - state.y; + if (Math.abs(dx) > 0.5 || Math.abs(dy) > 0.5) { + state.x += dx * LERP_FACTOR; + state.y += dy * LERP_FACTOR; + } else { + state.x = state.targetX; + state.y = state.targetY; + } + this.cursors.get(state.clientId)?.updatePosition(state.x, state.y); + } + this.rafId = window.requestAnimationFrame(tick); + }; + this.rafId = window.requestAnimationFrame(tick); + } + destroy() { + if (this.destroyed) return; + this.destroyed = true; + if (this.rafId != null) window.cancelAnimationFrame(this.rafId); + for (const cursor of this.cursors.values()) cursor.destroy(); + this.cursors.clear(); + this.renderStates.clear(); + this.container.remove(); + destroyCaretMirror(); + } + }; + + // src/bridge/cursor-presence/initTypeformCursorPresence.ts + var TYPING_IDLE_MS = 2500; + function readUserName(editorId) { + const params2 = new URLSearchParams(window.location.search); + return params2.get("username") || document.documentElement.getAttribute("data-lowcoder-username") || editorId; + } + function isRealUserActivity(event) { + return event.isTrusted === true; + } + function initTypeformCursorPresence(config) { + const userName = readUserName(config.editorId); + const user = { + id: config.editorId, + name: userName, + color: getUserColor(config.editorId), + role: config.role + }; + const canBroadcast = () => !config.isWelcomeScreen() && !(config.isSyncing?.() ?? false); + const overlay = new CursorOverlay({ + findFieldByKey: config.findFieldByKey, + getCurrentStep: config.getCurrentStep, + localUserId: config.editorId + }); + const presence = new CursorPresenceProvider( + config.provider, + user, + () => { + presence.syncOverlayFromAwareness((awareness) => overlay.syncFromAwareness(awareness)); + }, + 33 + ); + let isActive = false; + let idleTimer; + const clearCursor = () => { + isActive = false; + window.clearTimeout(idleTimer); + idleTimer = void 0; + presence.setLocalCursor(null); + }; + const syncOverlay = () => { + presence.syncOverlayFromAwareness((awareness) => overlay.syncFromAwareness(awareness)); + }; + const scheduleIdleClear = () => { + window.clearTimeout(idleTimer); + idleTimer = window.setTimeout(() => { + clearCursor(); + syncOverlay(); + }, TYPING_IDLE_MS); + }; + const publishCursor = () => { + if (!isActive) return; + if (!canBroadcast()) return; + const field = getFocusedTextField(); + if (!field) return; + const step = config.getCurrentStep(); + presence.setLocalCursor({ + fieldKey: getCursorFieldKey(field, step, config.getFieldKey), + step, + selection: getFieldSelection(field), + typing: true, + updatedAt: Date.now() + }); + }; + const activateCursor = (event) => { + if (!isRealUserActivity(event)) return; + if (!canBroadcast()) return; + const target = event.target; + if (target instanceof Element && !isTextFieldElement(target) && !getFocusedTextField()) { + return; + } + if (!getFocusedTextField()) return; + isActive = true; + publishCursor(); + scheduleIdleClear(); + }; + const listenerOpts = { capture: true, passive: true }; + const onInput = (event) => activateCursor(event); + const onCompositionUpdate = (event) => activateCursor(event); + const onKeyDown = (event) => { + if (!isRealUserActivity(event)) return; + if (!getFocusedTextField()) return; + activateCursor(event); + }; + const onSelectionChange = () => { + if (!isActive) return; + publishCursor(); + }; + const onFocusOut = () => { + window.setTimeout(() => { + if (!getFocusedTextField()) clearCursor(); + }, 0); + }; + const onScroll = () => { + if (isActive) publishCursor(); + syncOverlay(); + }; + const onResize = () => syncOverlay(); + document.addEventListener("input", onInput, listenerOpts); + document.addEventListener("compositionupdate", onCompositionUpdate, listenerOpts); + document.addEventListener("keydown", onKeyDown, listenerOpts); + document.addEventListener("selectionchange", onSelectionChange); + document.addEventListener("focusout", onFocusOut, listenerOpts); + document.addEventListener("scroll", onScroll, listenerOpts); + window.addEventListener("resize", onResize, { passive: true }); + let layoutTimer; + const domObserver = new MutationObserver(() => { + window.clearTimeout(layoutTimer); + layoutTimer = window.setTimeout(() => { + if (isActive) publishCursor(); + syncOverlay(); + }, 100); + }); + domObserver.observe(document.documentElement, { + childList: true, + subtree: true, + attributes: true + }); + const pollTimer = window.setInterval(() => { + if (isActive) publishCursor(); + syncOverlay(); + }, 100); + const onProviderStatus = () => syncOverlay(); + config.provider.on("synced", onProviderStatus); + presence.setLocalCursor(null); + syncOverlay(); + if (config.debug) { + console.log("[typeform-cursor-presence] started (idle-clear, no sync-clear)", { + userName, + editorId: config.editorId + }); + } + const destroy = () => { + window.clearInterval(pollTimer); + window.clearTimeout(layoutTimer); + window.clearTimeout(idleTimer); + config.provider.off("synced", onProviderStatus); + document.removeEventListener("input", onInput, listenerOpts); + document.removeEventListener("compositionupdate", onCompositionUpdate, listenerOpts); + document.removeEventListener("keydown", onKeyDown, listenerOpts); + document.removeEventListener("selectionchange", onSelectionChange); + document.removeEventListener("focusout", onFocusOut, listenerOpts); + document.removeEventListener("scroll", onScroll, listenerOpts); + window.removeEventListener("resize", onResize); + domObserver.disconnect(); + clearCursor(); + presence.destroy(); + overlay.destroy(); + }; + window.addEventListener("beforeunload", destroy, { once: true }); + return destroy; + } + + // src/bridge/google-forms/proxyRuntime.ts + var URL_ATTRIBUTES = ["src", "href", "action", "data-src", "poster"]; + var SKIPPED_LINK_RELS = /* @__PURE__ */ new Set(["preconnect", "dns-prefetch"]); + function installGoogleFormsProxyRuntime() { + const config = window.__LOWCODER_GOOGLE_PROXY__; + if (!config || window.__LOWCODER_GOOGLE_PROXY_READY__) return; + window.__LOWCODER_GOOGLE_PROXY_READY__ = true; + let upstreamBase; + try { + upstreamBase = new URL(config.upstreamUrl); + } catch { + return; + } + const rootPaths = new Set(config.rootPaths); + function isGoogleHost(hostname) { + const host = hostname.toLowerCase(); + return config.hosts.some( + (allowed) => allowed.startsWith(".") ? host.endsWith(allowed) : host === allowed + ); + } + function isGoogleRootPath(pathname) { + const segment = pathname.split("/")[1] ?? ""; + return rootPaths.has(segment); + } + function toProxiedUrl(absoluteUrl) { + const params2 = new URLSearchParams(); + params2.set("target", absoluteUrl); + Object.entries(config.params).forEach(([key, value]) => { + if (value) params2.set(key, value); + }); + return `${config.prefix}?${params2.toString()}`; + } + function rewrite(rawUrl) { + if (!rawUrl || /^(data|blob|javascript|about|mailto|tel):/i.test(rawUrl)) return null; + let resolved; + try { + resolved = new URL(rawUrl, window.location.href); + } catch { + return null; + } + if (resolved.pathname.startsWith(config.prefix)) return null; + if (resolved.protocol !== "http:" && resolved.protocol !== "https:") return null; + if (resolved.origin === window.location.origin) { + if (!isGoogleRootPath(resolved.pathname)) return null; + const upstream = new URL( + `${resolved.pathname}${resolved.search}${resolved.hash}`, + upstreamBase + ); + return toProxiedUrl(upstream.toString()); + } + if (!isGoogleHost(resolved.hostname)) return null; + resolved.protocol = "https:"; + return toProxiedUrl(resolved.toString()); + } + patchFetch(rewrite); + patchXhr(rewrite); + patchSendBeacon(rewrite); + patchElementUrls(rewrite); + } + function patchFetch(rewrite) { + const originalFetch = typeof window.fetch === "function" ? window.fetch.bind(window) : null; + if (!originalFetch) return; + window.fetch = ((input, init) => { + try { + if (typeof input === "string" || input instanceof URL) { + const next = rewrite(String(input)); + if (next) return originalFetch(next, init); + } else if (typeof Request !== "undefined" && input instanceof Request) { + const next = rewrite(input.url); + if (next) return originalFetch(new Request(next, input), init); + } + } catch { + } + return originalFetch(input, init); + }); + } + function patchXhr(rewrite) { + const originalOpen = XMLHttpRequest.prototype.open; + XMLHttpRequest.prototype.open = function open(method, url, ...rest) { + let target = url; + try { + target = rewrite(String(url)) ?? url; + } catch { + target = url; + } + return originalOpen.call( + this, + method, + target, + ...rest + ); + }; + } + function patchSendBeacon(rewrite) { + const originalSendBeacon = navigator.sendBeacon?.bind(navigator); + if (!originalSendBeacon) return; + navigator.sendBeacon = ((url, data) => { + try { + const next = rewrite(String(url)); + if (next) return originalSendBeacon(next, data); + } catch { + } + return originalSendBeacon(url, data); + }); + } + function patchElementUrls(rewrite) { + function shouldSkip(element2) { + if (!(element2 instanceof HTMLLinkElement)) return false; + const rel = (element2.getAttribute("rel") || "").toLowerCase(); + return SKIPPED_LINK_RELS.has(rel); + } + function patchProperty(prototype, property) { + const descriptor = Object.getOwnPropertyDescriptor(prototype, property); + if (!descriptor?.set || !descriptor.get) return; + Object.defineProperty(prototype, property, { + ...descriptor, + set(value) { + let next = null; + try { + if (!shouldSkip(this)) next = rewrite(String(value)); + } catch { + next = null; + } + descriptor.set.call(this, next ?? value); + } + }); + } + patchProperty(HTMLScriptElement.prototype, "src"); + patchProperty(HTMLLinkElement.prototype, "href"); + patchProperty(HTMLImageElement.prototype, "src"); + patchProperty(HTMLIFrameElement.prototype, "src"); + patchProperty(HTMLFormElement.prototype, "action"); + const originalSetAttribute = Element.prototype.setAttribute; + Element.prototype.setAttribute = function setAttribute(name, value) { + let next = null; + try { + if (URL_ATTRIBUTES.includes(name.toLowerCase()) && !shouldSkip(this)) { + next = rewrite(String(value)); + } + } catch { + next = null; + } + return originalSetAttribute.call(this, name, next ?? value); + }; + const observer = new MutationObserver((records) => { + records.forEach((record) => { + record.addedNodes.forEach((node) => { + if (!(node instanceof Element)) return; + rewriteElement(node); + node.querySelectorAll?.(URL_ATTRIBUTES.map((attr) => `[${attr}]`).join(",")).forEach((child) => rewriteElement(child)); + }); + }); + }); + function rewriteElement(element2) { + if (shouldSkip(element2)) return; + URL_ATTRIBUTES.forEach((attribute) => { + const value = element2.getAttribute(attribute); + if (!value) return; + const next = rewrite(value); + if (next && next !== value) originalSetAttribute.call(element2, attribute, next); + }); + } + observer.observe(document.documentElement, { childList: true, subtree: true }); + } + + // src/bridge/google-forms-bridge.ts + installGoogleFormsProxyRuntime(); + (() => { + const params2 = new URLSearchParams(window.location.search); + const root = document.documentElement; + const roomId = params2.get("roomId") || root.dataset.lowcoderRoomId || ""; + const role = params2.get("role") || root.dataset.lowcoderRole || "driver"; + const editorId = params2.get("editorId") || root.dataset.lowcoderEditorId || "local"; + const collabId = params2.get("collab") || root.dataset.lowcoderCollabId || ""; + const debug = params2.get("debug") === "1"; + const peerId = `${editorId}|${role}|${Math.random().toString(36).slice(2, 10)}`; + if (!roomId || !collabId) { + console.error( + "[google-forms-bridge] Missing roomId/collab. Load the form through /proxy/google-forms and create a session with createGoogleFormsProxySession before Fill Together." + ); + return; + } + if (!window.location.pathname.includes("/proxy/google-forms")) { + console.error( + "[google-forms-bridge] Form left the Lowcoder proxy (often after Google sign-in on an /edit URL). Use the published .../viewform responder URL instead of webViewLink." + ); + return; + } + const hocuspocusConfig = window.__LOWCODER_HOCUSPOCUS__ ?? {}; + const hocuspocusUrl = hocuspocusConfig.url || root.dataset.lowcoderHocuspocusUrl || "ws://localhost:3006"; + const hocuspocusToken = hocuspocusConfig.token || root.dataset.lowcoderHocuspocusToken || ""; + const documentName = `googleform_${roomId}_${collabId}`; + let providerReady = false; + let isApplyingRemoteState = false; + let lastNavigationId = ""; + let navigationTimer; + const doc2 = new Doc(); + const fields = doc2.getMap("fields"); + const state = doc2.getMap("state"); + const provider = new HocuspocusProvider({ + url: hocuspocusUrl, + name: documentName, + document: doc2, + token: hocuspocusToken || void 0, + onAuthenticationFailed: (data) => { + console.error("[google-forms-bridge] Hocuspocus auth failed", data); + } + }); + function log(...args2) { + if (debug) console.log("[google-forms-bridge]", role, ...args2); + } + function listControls() { + return Array.from( + document.querySelectorAll("input, textarea, select") + ).filter((control) => { + if (control.disabled) return false; + if (control.getAttribute("name") === "g-recaptcha-response") return false; + if (control instanceof HTMLInputElement) { + const type = (control.type || "text").toLowerCase(); + return !["hidden", "button", "submit", "reset", "file", "password", "image"].includes(type); + } + const rect = control.getBoundingClientRect(); + return rect.width > 0 && rect.height > 0; + }); + } + function listChoiceWidgets() { + return Array.from( + document.querySelectorAll( + '[role="radio"], [role="checkbox"], [role="listbox"]' + ) + ).filter((widget) => widget.getAttribute("aria-disabled") !== "true"); + } + function stableHash(value) { + let hash = 5381; + for (let index = 0; index < value.length; index += 1) { + hash = hash * 33 ^ value.charCodeAt(index); + } + return (hash >>> 0).toString(36); + } + function widgetGroup(widget) { + return widget.closest('[role="radiogroup"], [role="group"]') || widget.closest('[role="listitem"], [data-params]') || widget.parentElement || widget; + } + function widgetIdentity(widget) { + const group = widgetGroup(widget); + const question = widget.closest("[data-params]"); + const dataParams = question?.getAttribute("data-params"); + if (dataParams) { + const groups = Array.from( + question.querySelectorAll('[role="radiogroup"], [role="group"], [role="listbox"]') + ); + return `question:${stableHash(dataParams)}:group:${Math.max(0, groups.indexOf(group))}`; + } + const listItems = Array.from(document.querySelectorAll('[role="listitem"]')); + const listItem = widget.closest('[role="listitem"]'); + if (listItem) return `listitem:${Math.max(0, listItems.indexOf(listItem))}`; + return `widget:${Math.max(0, listChoiceWidgets().indexOf(widget))}`; + } + function widgetKey(widget) { + const identity = widgetIdentity(widget); + if (widget.getAttribute("role") === "checkbox") { + return `widget-checkbox:${identity}:${widget.dataset.value || widget.getAttribute("aria-label") || ""}`; + } + return `widget-${widget.getAttribute("role")}:${identity}`; + } + function widgetValue(widget) { + const role2 = widget.getAttribute("role"); + if (role2 === "checkbox") return widget.getAttribute("aria-checked") === "true" ? "1" : "0"; + if (role2 === "radio") { + const selected2 = widgetGroup(widget).querySelector( + '[role="radio"][aria-checked="true"]' + ); + return selected2?.dataset.value || selected2?.getAttribute("aria-label") || ""; + } + const selected = widget.querySelector('[role="option"][aria-selected="true"]'); + return selected?.dataset.value || selected?.textContent?.trim() || ""; + } + function controlIdentity(control) { + const name = control.getAttribute("name")?.trim(); + if (name) return `name:${name}`; + const id2 = control.id?.trim(); + if (id2) return `id:${id2}`; + const ariaLabel = control.getAttribute("aria-label")?.trim(); + if (ariaLabel) return `aria:${ariaLabel}`; + const all2 = listControls(); + return `index:${all2.indexOf(control)}`; + } + function controlValue(control) { + if (control instanceof HTMLInputElement && control.type === "checkbox") { + return control.checked ? "1" : "0"; + } + if (control instanceof HTMLInputElement && control.type === "radio") { + const group = listControls().filter( + (candidate) => candidate instanceof HTMLInputElement && candidate.type === "radio" && controlIdentity(candidate) === controlIdentity(control) + ); + const checked = group.find((candidate) => candidate.checked); + return checked ? optionValue(checked) : ""; + } + if (control instanceof HTMLSelectElement && control.multiple) { + return JSON.stringify(Array.from(control.selectedOptions).map((option) => option.value)); + } + return control.value; + } + function optionValue(input) { + return input.closest("[data-value]")?.dataset.value || input.getAttribute("data-value") || input.value; + } + function controlKey(control) { + const identity = controlIdentity(control); + if (control instanceof HTMLInputElement && control.type === "radio") { + return `radio:${identity}`; + } + if (control instanceof HTMLInputElement && control.type === "checkbox") { + const peers = listControls().filter( + (candidate) => candidate instanceof HTMLInputElement && candidate.type === "checkbox" && controlIdentity(candidate) === identity + ); + const sameValueIndex = peers.filter((candidate) => optionValue(candidate) === optionValue(control)).indexOf(control); + return `checkbox:${identity}:${optionValue(control)}:${Math.max(0, sameValueIndex)}`; + } + const sameIdentity = listControls().filter( + (candidate) => !(candidate instanceof HTMLInputElement && ["radio", "checkbox"].includes(candidate.type)) && controlIdentity(candidate) === identity + ); + return `field:${identity}:${Math.max(0, sameIdentity.indexOf(control))}`; + } + function findControl(key) { + return listControls().find((control) => controlKey(control) === key) ?? null; + } + function setNativeValue(control, value) { + if (control instanceof HTMLInputElement && control.type === "radio") { + const target = listControls().find( + (candidate) => candidate instanceof HTMLInputElement && candidate.type === "radio" && controlKey(candidate) === controlKey(control) && optionValue(candidate) === value + ); + if (target && !target.checked) target.click(); + return; + } + if (control instanceof HTMLInputElement && control.type === "checkbox") { + const checked = value === "1"; + if (control.checked !== checked) control.click(); + return; + } + if (control instanceof HTMLSelectElement) { + if (control.multiple) { + let selected = []; + try { + selected = JSON.parse(value); + } catch { + selected = []; + } + Array.from(control.options).forEach((option) => { + option.selected = selected.includes(option.value); + }); + } else { + control.value = value; + } + control.dispatchEvent(new Event("change", { bubbles: true })); + return; + } + if (control.value === value) return; + const prototype = control instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype; + const setter = Object.getOwnPropertyDescriptor(prototype, "value")?.set; + if (setter) setter.call(control, value); + else control.value = value; + control.dispatchEvent(new InputEvent("input", { bubbles: true, inputType: "insertText" })); + control.dispatchEvent(new Event("change", { bubbles: true })); + } + function publishControl(control) { + if (isApplyingRemoteState) return; + const key = controlKey(control); + const value = controlValue(control); + if (fields.get(key) === value) return; + doc2.transact(() => fields.set(key, value), peerId); + log("published field", key); + } + function publishAllControls(onlyMissing = false) { + doc2.transact(() => { + listControls().forEach((control) => { + const key = controlKey(control); + if (onlyMissing && fields.has(key)) return; + fields.set(key, controlValue(control)); + }); + }, peerId); + } + function publishWidget(widget) { + if (isApplyingRemoteState) return; + const key = widgetKey(widget); + const value = widgetValue(widget); + if (fields.get(key) === value) return; + doc2.transact(() => fields.set(key, value), peerId); + log("published widget", key); + } + function publishAllWidgets(onlyMissing = false) { + const seen = /* @__PURE__ */ new Set(); + doc2.transact(() => { + listChoiceWidgets().forEach((widget) => { + const key = widgetKey(widget); + if (seen.has(key) || onlyMissing && fields.has(key)) return; + seen.add(key); + fields.set(key, widgetValue(widget)); + }); + }, peerId); + } + function findWidget(key) { + return listChoiceWidgets().find((widget) => widgetKey(widget) === key) ?? null; + } + function applyWidget(key) { + const widget = findWidget(key); + const value = fields.get(key); + if (!widget || typeof value !== "string" || widgetValue(widget) === value) return; + let target = null; + const role2 = widget.getAttribute("role"); + if (role2 === "checkbox") { + target = widget; + } else if (role2 === "radio") { + target = Array.from(widgetGroup(widget).querySelectorAll('[role="radio"]')).find( + (option) => (option.dataset.value || option.getAttribute("aria-label") || "") === value + ) ?? null; + } else { + target = Array.from(widget.querySelectorAll('[role="option"]')).find( + (option) => (option.dataset.value || option.textContent?.trim() || "") === value + ) ?? null; + } + if (!target) return; + isApplyingRemoteState = true; + try { + target.click(); + log("applied widget", key); + } finally { + window.setTimeout(() => { + isApplyingRemoteState = false; + }, 0); + } + } + function applyField(key) { + if (key.startsWith("widget-")) { + applyWidget(key); + return; + } + const control = findControl(key); + const value = fields.get(key); + if (!control || typeof value !== "string" || controlValue(control) === value) return; + isApplyingRemoteState = true; + try { + setNativeValue(control, value); + log("applied field", key); + } finally { + isApplyingRemoteState = false; + } + } + function applyAllFields() { + fields.forEach((_value, key) => applyField(key)); + } + function pageMarker() { + const history = document.querySelector( + 'input[name="pageHistory"], input[name="pagehistory"]' + )?.value; + if (history) return history; + const visibleQuestion = Array.from( + document.querySelectorAll('[role="listitem"], [data-params]') + ).find((element2) => { + const rect = element2.getBoundingClientRect(); + return rect.width > 0 && rect.height > 0; + }); + return visibleQuestion?.getAttribute("data-params")?.slice(0, 200) || window.location.pathname; + } + function navigationAction(target) { + const button = target.closest( + 'button, input[type="submit"], [role="button"], [jsname]' + ); + if (!button) return null; + const jsname = button.getAttribute("jsname") || ""; + const text2 = (button.textContent || button.value || "").trim().toLowerCase(); + if (jsname === "e19J0b" || /^(back|previous)$/.test(text2)) return "back"; + if (jsname === "OCpkoe" || /^(next|continue)$/.test(text2)) return "next"; + if (jsname === "M2UYVd" || /^(submit|send)$/.test(text2)) return "submit"; + return null; + } + function findNavigationButton(action) { + const jsname = action === "next" ? "OCpkoe" : "e19J0b"; + const byJsName = document.querySelector(`[jsname="${jsname}"]`); + if (byJsName) return byJsName; + return Array.from( + document.querySelectorAll('button, input[type="submit"], [role="button"]') + ).find((button) => navigationAction(button) === action) ?? null; + } + function publishNavigation(action) { + publishAllControls(); + publishAllWidgets(); + const command = { + id: `${peerId}:${Date.now()}:${Math.random().toString(36).slice(2, 7)}`, + action, + fromPage: pageMarker(), + editorId: peerId + }; + lastNavigationId = command.id; + doc2.transact(() => state.set("navigationJson", JSON.stringify(command)), peerId); + log("published navigation", command); + } + function applyRemoteNavigation() { + const raw = state.get("navigationJson"); + if (typeof raw !== "string" || !raw) return; + let command; + try { + command = JSON.parse(raw); + } catch { + return; + } + if (!command.id || command.id === lastNavigationId || command.editorId === peerId || command.action === "submit" || command.fromPage !== pageMarker()) { + return; + } + lastNavigationId = command.id; + window.clearTimeout(navigationTimer); + navigationTimer = window.setTimeout(() => { + applyAllFields(); + const button = findNavigationButton(command.action); + if (!button) { + log("navigation button not found", command.action); + return; + } + isApplyingRemoteState = true; + button.click(); + window.setTimeout(() => { + isApplyingRemoteState = false; + }, 500); + log("applied navigation", command.action); + }, 100); + } + fields.observe((event) => { + if (event.transaction.origin === peerId) return; + event.keysChanged.forEach((key) => applyField(key)); + }); + state.observe((event) => { + if (event.transaction.origin === peerId) return; + if (event.keysChanged.has("navigationJson")) applyRemoteNavigation(); + }); + function onProviderReady() { + if (!providerReady) { + providerReady = true; + publishAllControls(true); + publishAllWidgets(true); + } + applyAllFields(); + applyRemoteNavigation(); + } + provider.on("status", ({ status }) => { + if (status === WebSocketStatus.Connected) onProviderReady(); + }); + provider.on("synced", onProviderReady); + document.addEventListener( + "input", + (event) => { + if (!event.isTrusted || isApplyingRemoteState) return; + if (event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement || event.target instanceof HTMLSelectElement) { + publishControl(event.target); + } + }, + true + ); + document.addEventListener( + "change", + (event) => { + if (!event.isTrusted || isApplyingRemoteState) return; + if (event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement || event.target instanceof HTMLSelectElement) { + publishControl(event.target); + } + }, + true + ); + document.addEventListener( + "click", + (event) => { + if (!event.isTrusted || isApplyingRemoteState || !(event.target instanceof Element)) return; + const action = navigationAction(event.target); + if (action) { + publishNavigation(action); + return; + } + const widget = event.target.closest( + '[role="radio"], [role="checkbox"], [role="listbox"], [role="option"]' + ); + if (widget) { + const root2 = widget.getAttribute("role") === "option" ? widget.closest('[role="listbox"]') : widget; + if (root2) window.setTimeout(() => publishWidget(root2), 0); + } + }, + true + ); + let mutationTimer; + const observer = new MutationObserver(() => { + window.clearTimeout(mutationTimer); + mutationTimer = window.setTimeout(() => { + if (!isApplyingRemoteState) { + publishAllControls(true); + publishAllWidgets(true); + applyAllFields(); + } + }, 100); + }); + observer.observe(document.documentElement, { childList: true, subtree: true }); + initTypeformCursorPresence({ + provider, + editorId, + role, + debug, + getFieldKey: (field) => controlKey(field), + findFieldByKey: (key) => { + const field = findControl(key); + return field instanceof HTMLInputElement || field instanceof HTMLTextAreaElement ? field : null; + }, + getCurrentStep: () => { + const marker = pageMarker(); + const last2 = marker.split(",").pop(); + return Number(last2) || 0; + }, + getSessionStarted: () => true, + isWelcomeScreen: () => false, + isSyncing: () => isApplyingRemoteState + }); + console.info("[google-forms-bridge] ready", { roomId, collabId, role, editorId, documentName }); + log("ready", { roomId, collabId, role, editorId, documentName }); + window.addEventListener("beforeunload", () => { + window.clearTimeout(mutationTimer); + window.clearTimeout(navigationTimer); + observer.disconnect(); + provider.destroy(); + doc2.destroy(); + }); + })(); +})(); diff --git a/server/proxy-service/build/bridge/typeform-bridge.js b/server/proxy-service/build/bridge/typeform-bridge.js new file mode 100644 index 0000000000..9cc6294690 --- /dev/null +++ b/server/proxy-service/build/bridge/typeform-bridge.js @@ -0,0 +1,11842 @@ +(() => { + var __defProp = Object.defineProperty; + var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; + var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); + + // node_modules/lib0/map.js + var create = () => /* @__PURE__ */ new Map(); + var copy = (m) => { + const r = create(); + m.forEach((v, k) => { + r.set(k, v); + }); + return r; + }; + var setIfUndefined = (map2, key, createT) => { + let set = map2.get(key); + if (set === void 0) { + map2.set(key, set = createT()); + } + return set; + }; + var map = (m, f) => { + const res = []; + for (const [key, value] of m) { + res.push(f(value, key)); + } + return res; + }; + var any = (m, f) => { + for (const [key, value] of m) { + if (f(value, key)) { + return true; + } + } + return false; + }; + + // node_modules/lib0/set.js + var create2 = () => /* @__PURE__ */ new Set(); + + // node_modules/lib0/array.js + var last = (arr) => arr[arr.length - 1]; + var appendTo = (dest, src) => { + for (let i = 0; i < src.length; i++) { + dest.push(src[i]); + } + }; + var from = Array.from; + var every = (arr, f) => { + for (let i = 0; i < arr.length; i++) { + if (!f(arr[i], i, arr)) { + return false; + } + } + return true; + }; + var some = (arr, f) => { + for (let i = 0; i < arr.length; i++) { + if (f(arr[i], i, arr)) { + return true; + } + } + return false; + }; + var unfold = (len, f) => { + const array = new Array(len); + for (let i = 0; i < len; i++) { + array[i] = f(i, array); + } + return array; + }; + var isArray = Array.isArray; + + // node_modules/lib0/observable.js + var ObservableV2 = class { + constructor() { + this._observers = create(); + } + /** + * @template {keyof EVENTS & string} NAME + * @param {NAME} name + * @param {EVENTS[NAME]} f + */ + on(name, f) { + setIfUndefined( + this._observers, + /** @type {string} */ + name, + create2 + ).add(f); + return f; + } + /** + * @template {keyof EVENTS & string} NAME + * @param {NAME} name + * @param {EVENTS[NAME]} f + */ + once(name, f) { + const _f = (...args2) => { + this.off( + name, + /** @type {any} */ + _f + ); + f(...args2); + }; + this.on( + name, + /** @type {any} */ + _f + ); + } + /** + * @template {keyof EVENTS & string} NAME + * @param {NAME} name + * @param {EVENTS[NAME]} f + */ + off(name, f) { + const observers = this._observers.get(name); + if (observers !== void 0) { + observers.delete(f); + if (observers.size === 0) { + this._observers.delete(name); + } + } + } + /** + * Emit a named event. All registered event listeners that listen to the + * specified name will receive the event. + * + * @todo This should catch exceptions + * + * @template {keyof EVENTS & string} NAME + * @param {NAME} name The event name. + * @param {Parameters} args The arguments that are applied to the event listener. + */ + emit(name, args2) { + return from((this._observers.get(name) || create()).values()).forEach((f) => f(...args2)); + } + destroy() { + this._observers = create(); + } + }; + + // node_modules/lib0/math.js + var floor = Math.floor; + var abs = Math.abs; + var min = (a, b) => a < b ? a : b; + var max = (a, b) => a > b ? a : b; + var isNaN = Number.isNaN; + var isNegativeZero = (n) => n !== 0 ? n < 0 : 1 / n < 0; + + // node_modules/lib0/binary.js + var BIT1 = 1; + var BIT2 = 2; + var BIT3 = 4; + var BIT4 = 8; + var BIT6 = 32; + var BIT7 = 64; + var BIT8 = 128; + var BIT18 = 1 << 17; + var BIT19 = 1 << 18; + var BIT20 = 1 << 19; + var BIT21 = 1 << 20; + var BIT22 = 1 << 21; + var BIT23 = 1 << 22; + var BIT24 = 1 << 23; + var BIT25 = 1 << 24; + var BIT26 = 1 << 25; + var BIT27 = 1 << 26; + var BIT28 = 1 << 27; + var BIT29 = 1 << 28; + var BIT30 = 1 << 29; + var BIT31 = 1 << 30; + var BIT32 = 1 << 31; + var BITS5 = 31; + var BITS6 = 63; + var BITS7 = 127; + var BITS17 = BIT18 - 1; + var BITS18 = BIT19 - 1; + var BITS19 = BIT20 - 1; + var BITS20 = BIT21 - 1; + var BITS21 = BIT22 - 1; + var BITS22 = BIT23 - 1; + var BITS23 = BIT24 - 1; + var BITS24 = BIT25 - 1; + var BITS25 = BIT26 - 1; + var BITS26 = BIT27 - 1; + var BITS27 = BIT28 - 1; + var BITS28 = BIT29 - 1; + var BITS29 = BIT30 - 1; + var BITS30 = BIT31 - 1; + var BITS31 = 2147483647; + + // node_modules/lib0/number.js + var MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER; + var MIN_SAFE_INTEGER = Number.MIN_SAFE_INTEGER; + var LOWEST_INT32 = 1 << 31; + var isInteger = Number.isInteger || ((num) => typeof num === "number" && isFinite(num) && floor(num) === num); + var isNaN2 = Number.isNaN; + var parseInt = Number.parseInt; + + // node_modules/lib0/string.js + var fromCharCode = String.fromCharCode; + var fromCodePoint = String.fromCodePoint; + var MAX_UTF16_CHARACTER = fromCharCode(65535); + var toLowerCase = (s) => s.toLowerCase(); + var trimLeftRegex = /^\s*/g; + var trimLeft = (s) => s.replace(trimLeftRegex, ""); + var fromCamelCaseRegex = /([A-Z])/g; + var fromCamelCase = (s, separator) => trimLeft(s.replace(fromCamelCaseRegex, (match2) => `${separator}${toLowerCase(match2)}`)); + var _encodeUtf8Polyfill = (str) => { + const encodedString = unescape(encodeURIComponent(str)); + const len = encodedString.length; + const buf = new Uint8Array(len); + for (let i = 0; i < len; i++) { + buf[i] = /** @type {number} */ + encodedString.codePointAt(i); + } + return buf; + }; + var utf8TextEncoder = ( + /** @type {TextEncoder} */ + typeof TextEncoder !== "undefined" ? new TextEncoder() : null + ); + var _encodeUtf8Native = (str) => utf8TextEncoder.encode(str); + var encodeUtf8 = utf8TextEncoder ? _encodeUtf8Native : _encodeUtf8Polyfill; + var utf8TextDecoder = typeof TextDecoder === "undefined" ? null : new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }); + if (utf8TextDecoder && utf8TextDecoder.decode(new Uint8Array()).length === 1) { + utf8TextDecoder = null; + } + var repeat = (source, n) => unfold(n, () => source).join(""); + + // node_modules/lib0/encoding.js + var Encoder = class { + constructor() { + this.cpos = 0; + this.cbuf = new Uint8Array(100); + this.bufs = []; + } + }; + var createEncoder = () => new Encoder(); + var length = (encoder) => { + let len = encoder.cpos; + for (let i = 0; i < encoder.bufs.length; i++) { + len += encoder.bufs[i].length; + } + return len; + }; + var toUint8Array = (encoder) => { + const uint8arr = new Uint8Array(length(encoder)); + let curPos = 0; + for (let i = 0; i < encoder.bufs.length; i++) { + const d = encoder.bufs[i]; + uint8arr.set(d, curPos); + curPos += d.length; + } + uint8arr.set(new Uint8Array(encoder.cbuf.buffer, 0, encoder.cpos), curPos); + return uint8arr; + }; + var verifyLen = (encoder, len) => { + const bufferLen = encoder.cbuf.length; + if (bufferLen - encoder.cpos < len) { + encoder.bufs.push(new Uint8Array(encoder.cbuf.buffer, 0, encoder.cpos)); + encoder.cbuf = new Uint8Array(max(bufferLen, len) * 2); + encoder.cpos = 0; + } + }; + var write = (encoder, num) => { + const bufferLen = encoder.cbuf.length; + if (encoder.cpos === bufferLen) { + encoder.bufs.push(encoder.cbuf); + encoder.cbuf = new Uint8Array(bufferLen * 2); + encoder.cpos = 0; + } + encoder.cbuf[encoder.cpos++] = num; + }; + var writeUint8 = write; + var writeVarUint = (encoder, num) => { + while (num > BITS7) { + write(encoder, BIT8 | BITS7 & num); + num = floor(num / 128); + } + write(encoder, BITS7 & num); + }; + var writeVarInt = (encoder, num) => { + const isNegative = isNegativeZero(num); + if (isNegative) { + num = -num; + } + write(encoder, (num > BITS6 ? BIT8 : 0) | (isNegative ? BIT7 : 0) | BITS6 & num); + num = floor(num / 64); + while (num > 0) { + write(encoder, (num > BITS7 ? BIT8 : 0) | BITS7 & num); + num = floor(num / 128); + } + }; + var _strBuffer = new Uint8Array(3e4); + var _maxStrBSize = _strBuffer.length / 3; + var _writeVarStringNative = (encoder, str) => { + if (str.length < _maxStrBSize) { + const written = utf8TextEncoder.encodeInto(str, _strBuffer).written || 0; + writeVarUint(encoder, written); + for (let i = 0; i < written; i++) { + write(encoder, _strBuffer[i]); + } + } else { + writeVarUint8Array(encoder, encodeUtf8(str)); + } + }; + var _writeVarStringPolyfill = (encoder, str) => { + const encodedString = unescape(encodeURIComponent(str)); + const len = encodedString.length; + writeVarUint(encoder, len); + for (let i = 0; i < len; i++) { + write( + encoder, + /** @type {number} */ + encodedString.codePointAt(i) + ); + } + }; + var writeVarString = utf8TextEncoder && /** @type {any} */ + utf8TextEncoder.encodeInto ? _writeVarStringNative : _writeVarStringPolyfill; + var writeUint8Array = (encoder, uint8Array) => { + const bufferLen = encoder.cbuf.length; + const cpos = encoder.cpos; + const leftCopyLen = min(bufferLen - cpos, uint8Array.length); + const rightCopyLen = uint8Array.length - leftCopyLen; + encoder.cbuf.set(uint8Array.subarray(0, leftCopyLen), cpos); + encoder.cpos += leftCopyLen; + if (rightCopyLen > 0) { + encoder.bufs.push(encoder.cbuf); + encoder.cbuf = new Uint8Array(max(bufferLen * 2, rightCopyLen)); + encoder.cbuf.set(uint8Array.subarray(leftCopyLen)); + encoder.cpos = rightCopyLen; + } + }; + var writeVarUint8Array = (encoder, uint8Array) => { + writeVarUint(encoder, uint8Array.byteLength); + writeUint8Array(encoder, uint8Array); + }; + var writeOnDataView = (encoder, len) => { + verifyLen(encoder, len); + const dview = new DataView(encoder.cbuf.buffer, encoder.cpos, len); + encoder.cpos += len; + return dview; + }; + var writeFloat32 = (encoder, num) => writeOnDataView(encoder, 4).setFloat32(0, num, false); + var writeFloat64 = (encoder, num) => writeOnDataView(encoder, 8).setFloat64(0, num, false); + var writeBigInt64 = (encoder, num) => ( + /** @type {any} */ + writeOnDataView(encoder, 8).setBigInt64(0, num, false) + ); + var floatTestBed = new DataView(new ArrayBuffer(4)); + var isFloat32 = (num) => { + floatTestBed.setFloat32(0, num); + return floatTestBed.getFloat32(0) === num; + }; + var writeAny = (encoder, data) => { + switch (typeof data) { + case "string": + write(encoder, 119); + writeVarString(encoder, data); + break; + case "number": + if (isInteger(data) && abs(data) <= BITS31) { + write(encoder, 125); + writeVarInt(encoder, data); + } else if (isFloat32(data)) { + write(encoder, 124); + writeFloat32(encoder, data); + } else { + write(encoder, 123); + writeFloat64(encoder, data); + } + break; + case "bigint": + write(encoder, 122); + writeBigInt64(encoder, data); + break; + case "object": + if (data === null) { + write(encoder, 126); + } else if (isArray(data)) { + write(encoder, 117); + writeVarUint(encoder, data.length); + for (let i = 0; i < data.length; i++) { + writeAny(encoder, data[i]); + } + } else if (data instanceof Uint8Array) { + write(encoder, 116); + writeVarUint8Array(encoder, data); + } else { + write(encoder, 118); + const keys3 = Object.keys(data); + writeVarUint(encoder, keys3.length); + for (let i = 0; i < keys3.length; i++) { + const key = keys3[i]; + writeVarString(encoder, key); + writeAny(encoder, data[key]); + } + } + break; + case "boolean": + write(encoder, data ? 120 : 121); + break; + default: + write(encoder, 127); + } + }; + var RleEncoder = class extends Encoder { + /** + * @param {function(Encoder, T):void} writer + */ + constructor(writer) { + super(); + this.w = writer; + this.s = null; + this.count = 0; + } + /** + * @param {T} v + */ + write(v) { + if (this.s === v) { + this.count++; + } else { + if (this.count > 0) { + writeVarUint(this, this.count - 1); + } + this.count = 1; + this.w(this, v); + this.s = v; + } + } + }; + var flushUintOptRleEncoder = (encoder) => { + if (encoder.count > 0) { + writeVarInt(encoder.encoder, encoder.count === 1 ? encoder.s : -encoder.s); + if (encoder.count > 1) { + writeVarUint(encoder.encoder, encoder.count - 2); + } + } + }; + var UintOptRleEncoder = class { + constructor() { + this.encoder = new Encoder(); + this.s = 0; + this.count = 0; + } + /** + * @param {number} v + */ + write(v) { + if (this.s === v) { + this.count++; + } else { + flushUintOptRleEncoder(this); + this.count = 1; + this.s = v; + } + } + /** + * Flush the encoded state and transform this to a Uint8Array. + * + * Note that this should only be called once. + */ + toUint8Array() { + flushUintOptRleEncoder(this); + return toUint8Array(this.encoder); + } + }; + var flushIntDiffOptRleEncoder = (encoder) => { + if (encoder.count > 0) { + const encodedDiff = encoder.diff * 2 + (encoder.count === 1 ? 0 : 1); + writeVarInt(encoder.encoder, encodedDiff); + if (encoder.count > 1) { + writeVarUint(encoder.encoder, encoder.count - 2); + } + } + }; + var IntDiffOptRleEncoder = class { + constructor() { + this.encoder = new Encoder(); + this.s = 0; + this.count = 0; + this.diff = 0; + } + /** + * @param {number} v + */ + write(v) { + if (this.diff === v - this.s) { + this.s = v; + this.count++; + } else { + flushIntDiffOptRleEncoder(this); + this.count = 1; + this.diff = v - this.s; + this.s = v; + } + } + /** + * Flush the encoded state and transform this to a Uint8Array. + * + * Note that this should only be called once. + */ + toUint8Array() { + flushIntDiffOptRleEncoder(this); + return toUint8Array(this.encoder); + } + }; + var StringEncoder = class { + constructor() { + this.sarr = []; + this.s = ""; + this.lensE = new UintOptRleEncoder(); + } + /** + * @param {string} string + */ + write(string) { + this.s += string; + if (this.s.length > 19) { + this.sarr.push(this.s); + this.s = ""; + } + this.lensE.write(string.length); + } + toUint8Array() { + const encoder = new Encoder(); + this.sarr.push(this.s); + this.s = ""; + writeVarString(encoder, this.sarr.join("")); + writeUint8Array(encoder, this.lensE.toUint8Array()); + return toUint8Array(encoder); + } + }; + + // node_modules/lib0/error.js + var create3 = (s) => new Error(s); + var methodUnimplemented = () => { + throw create3("Method unimplemented"); + }; + var unexpectedCase = () => { + throw create3("Unexpected case"); + }; + + // node_modules/lib0/decoding.js + var errorUnexpectedEndOfArray = create3("Unexpected end of array"); + var errorIntegerOutOfRange = create3("Integer out of Range"); + var Decoder = class { + /** + * @param {Uint8Array} uint8Array Binary data to decode + */ + constructor(uint8Array) { + this.arr = uint8Array; + this.pos = 0; + } + }; + var createDecoder = (uint8Array) => new Decoder(uint8Array); + var hasContent = (decoder) => decoder.pos !== decoder.arr.length; + var readUint8Array = (decoder, len) => { + const view = new Uint8Array(decoder.arr.buffer, decoder.pos + decoder.arr.byteOffset, len); + decoder.pos += len; + return view; + }; + var readVarUint8Array = (decoder) => readUint8Array(decoder, readVarUint(decoder)); + var readUint8 = (decoder) => decoder.arr[decoder.pos++]; + var readVarUint = (decoder) => { + let num = 0; + let mult = 1; + const len = decoder.arr.length; + while (decoder.pos < len) { + const r = decoder.arr[decoder.pos++]; + num = num + (r & BITS7) * mult; + mult *= 128; + if (r < BIT8) { + return num; + } + if (num > MAX_SAFE_INTEGER) { + throw errorIntegerOutOfRange; + } + } + throw errorUnexpectedEndOfArray; + }; + var readVarInt = (decoder) => { + let r = decoder.arr[decoder.pos++]; + let num = r & BITS6; + let mult = 64; + const sign = (r & BIT7) > 0 ? -1 : 1; + if ((r & BIT8) === 0) { + return sign * num; + } + const len = decoder.arr.length; + while (decoder.pos < len) { + r = decoder.arr[decoder.pos++]; + num = num + (r & BITS7) * mult; + mult *= 128; + if (r < BIT8) { + return sign * num; + } + if (num > MAX_SAFE_INTEGER) { + throw errorIntegerOutOfRange; + } + } + throw errorUnexpectedEndOfArray; + }; + var _readVarStringPolyfill = (decoder) => { + let remainingLen = readVarUint(decoder); + if (remainingLen === 0) { + return ""; + } else { + let encodedString = String.fromCodePoint(readUint8(decoder)); + if (--remainingLen < 100) { + while (remainingLen--) { + encodedString += String.fromCodePoint(readUint8(decoder)); + } + } else { + while (remainingLen > 0) { + const nextLen = remainingLen < 1e4 ? remainingLen : 1e4; + const bytes = decoder.arr.subarray(decoder.pos, decoder.pos + nextLen); + decoder.pos += nextLen; + encodedString += String.fromCodePoint.apply( + null, + /** @type {any} */ + bytes + ); + remainingLen -= nextLen; + } + } + return decodeURIComponent(escape(encodedString)); + } + }; + var _readVarStringNative = (decoder) => ( + /** @type any */ + utf8TextDecoder.decode(readVarUint8Array(decoder)) + ); + var readVarString = utf8TextDecoder ? _readVarStringNative : _readVarStringPolyfill; + var readFromDataView = (decoder, len) => { + const dv = new DataView(decoder.arr.buffer, decoder.arr.byteOffset + decoder.pos, len); + decoder.pos += len; + return dv; + }; + var readFloat32 = (decoder) => readFromDataView(decoder, 4).getFloat32(0, false); + var readFloat64 = (decoder) => readFromDataView(decoder, 8).getFloat64(0, false); + var readBigInt64 = (decoder) => ( + /** @type {any} */ + readFromDataView(decoder, 8).getBigInt64(0, false) + ); + var readAnyLookupTable = [ + (decoder) => void 0, + // CASE 127: undefined + (decoder) => null, + // CASE 126: null + readVarInt, + // CASE 125: integer + readFloat32, + // CASE 124: float32 + readFloat64, + // CASE 123: float64 + readBigInt64, + // CASE 122: bigint + (decoder) => false, + // CASE 121: boolean (false) + (decoder) => true, + // CASE 120: boolean (true) + readVarString, + // CASE 119: string + (decoder) => { + const len = readVarUint(decoder); + const obj = {}; + for (let i = 0; i < len; i++) { + const key = readVarString(decoder); + obj[key] = readAny(decoder); + } + return obj; + }, + (decoder) => { + const len = readVarUint(decoder); + const arr = []; + for (let i = 0; i < len; i++) { + arr.push(readAny(decoder)); + } + return arr; + }, + readVarUint8Array + // CASE 116: Uint8Array + ]; + var readAny = (decoder) => readAnyLookupTable[127 - readUint8(decoder)](decoder); + var RleDecoder = class extends Decoder { + /** + * @param {Uint8Array} uint8Array + * @param {function(Decoder):T} reader + */ + constructor(uint8Array, reader) { + super(uint8Array); + this.reader = reader; + this.s = null; + this.count = 0; + } + read() { + if (this.count === 0) { + this.s = this.reader(this); + if (hasContent(this)) { + this.count = readVarUint(this) + 1; + } else { + this.count = -1; + } + } + this.count--; + return ( + /** @type {T} */ + this.s + ); + } + }; + var UintOptRleDecoder = class extends Decoder { + /** + * @param {Uint8Array} uint8Array + */ + constructor(uint8Array) { + super(uint8Array); + this.s = 0; + this.count = 0; + } + read() { + if (this.count === 0) { + this.s = readVarInt(this); + const isNegative = isNegativeZero(this.s); + this.count = 1; + if (isNegative) { + this.s = -this.s; + this.count = readVarUint(this) + 2; + } + } + this.count--; + return ( + /** @type {number} */ + this.s + ); + } + }; + var IntDiffOptRleDecoder = class extends Decoder { + /** + * @param {Uint8Array} uint8Array + */ + constructor(uint8Array) { + super(uint8Array); + this.s = 0; + this.count = 0; + this.diff = 0; + } + /** + * @return {number} + */ + read() { + if (this.count === 0) { + const diff = readVarInt(this); + const hasCount = diff & 1; + this.diff = floor(diff / 2); + this.count = 1; + if (hasCount) { + this.count = readVarUint(this) + 2; + } + } + this.s += this.diff; + this.count--; + return this.s; + } + }; + var StringDecoder = class { + /** + * @param {Uint8Array} uint8Array + */ + constructor(uint8Array) { + this.decoder = new UintOptRleDecoder(uint8Array); + this.str = readVarString(this.decoder); + this.spos = 0; + } + /** + * @return {string} + */ + read() { + const end = this.spos + this.decoder.read(); + const res = this.str.slice(this.spos, end); + this.spos = end; + return res; + } + }; + + // node_modules/lib0/webcrypto.js + var subtle = crypto.subtle; + var getRandomValues = crypto.getRandomValues.bind(crypto); + + // node_modules/lib0/random.js + var uint32 = () => getRandomValues(new Uint32Array(1))[0]; + var uuidv4Template = "10000000-1000-4000-8000" + -1e11; + var uuidv4 = () => uuidv4Template.replace( + /[018]/g, + /** @param {number} c */ + (c) => (c ^ uint32() & 15 >> c / 4).toString(16) + ); + + // node_modules/lib0/time.js + var getUnixTime = Date.now; + + // node_modules/lib0/promise.js + var create4 = (f) => ( + /** @type {Promise} */ + new Promise(f) + ); + var all = Promise.all.bind(Promise); + + // node_modules/lib0/conditions.js + var undefinedToNull = (v) => v === void 0 ? null : v; + + // node_modules/lib0/storage.js + var VarStoragePolyfill = class { + constructor() { + this.map = /* @__PURE__ */ new Map(); + } + /** + * @param {string} key + * @param {any} newValue + */ + setItem(key, newValue) { + this.map.set(key, newValue); + } + /** + * @param {string} key + */ + getItem(key) { + return this.map.get(key); + } + }; + var _localStorage = new VarStoragePolyfill(); + var usePolyfill = true; + try { + if (typeof localStorage !== "undefined" && localStorage) { + _localStorage = localStorage; + usePolyfill = false; + } + } catch (e) { + } + var varStorage = _localStorage; + + // node_modules/lib0/trait/equality.js + var EqualityTraitSymbol = Symbol("Equality"); + var equals = (a, b) => a === b || !!a?.[EqualityTraitSymbol]?.(b) || false; + + // node_modules/lib0/object.js + var isObject = (o) => typeof o === "object"; + var assign = Object.assign; + var keys = Object.keys; + var forEach = (obj, f) => { + for (const key in obj) { + f(obj[key], key); + } + }; + var size = (obj) => keys(obj).length; + var isEmpty = (obj) => { + for (const _k in obj) { + return false; + } + return true; + }; + var every2 = (obj, f) => { + for (const key in obj) { + if (!f(obj[key], key)) { + return false; + } + } + return true; + }; + var hasProperty = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key); + var equalFlat = (a, b) => a === b || size(a) === size(b) && every2(a, (val, key) => (val !== void 0 || hasProperty(b, key)) && equals(b[key], val)); + var freeze = Object.freeze; + var deepFreeze = (o) => { + for (const key in o) { + const c = o[key]; + if (typeof c === "object" || typeof c === "function") { + deepFreeze(o[key]); + } + } + return freeze(o); + }; + + // node_modules/lib0/function.js + var callAll = (fs, args2, i = 0) => { + try { + for (; i < fs.length; i++) { + fs[i](...args2); + } + } finally { + if (i < fs.length) { + callAll(fs, args2, i + 1); + } + } + }; + var id = (a) => a; + var equalityDeep = (a, b) => { + if (a === b) { + return true; + } + if (a == null || b == null || a.constructor !== b.constructor && (a.constructor || Object) !== (b.constructor || Object)) { + return false; + } + if (a[EqualityTraitSymbol] != null) { + return a[EqualityTraitSymbol](b); + } + switch (a.constructor) { + case ArrayBuffer: + a = new Uint8Array(a); + b = new Uint8Array(b); + // eslint-disable-next-line no-fallthrough + case Uint8Array: { + if (a.byteLength !== b.byteLength) { + return false; + } + for (let i = 0; i < a.length; i++) { + if (a[i] !== b[i]) { + return false; + } + } + break; + } + case Set: { + if (a.size !== b.size) { + return false; + } + for (const value of a) { + if (!b.has(value)) { + return false; + } + } + break; + } + case Map: { + if (a.size !== b.size) { + return false; + } + for (const key of a.keys()) { + if (!b.has(key) || !equalityDeep(a.get(key), b.get(key))) { + return false; + } + } + break; + } + case void 0: + case Object: + if (size(a) !== size(b)) { + return false; + } + for (const key in a) { + if (!hasProperty(a, key) || !equalityDeep(a[key], b[key])) { + return false; + } + } + break; + case Array: + if (a.length !== b.length) { + return false; + } + for (let i = 0; i < a.length; i++) { + if (!equalityDeep(a[i], b[i])) { + return false; + } + } + break; + default: + return false; + } + return true; + }; + var isOneOf = (value, options) => options.includes(value); + + // node_modules/lib0/environment.js + var isNode = typeof process !== "undefined" && process.release && /node|io\.js/.test(process.release.name) && Object.prototype.toString.call(typeof process !== "undefined" ? process : 0) === "[object process]"; + var isMac = typeof navigator !== "undefined" ? /Mac/.test(navigator.platform) : false; + var params; + var args = []; + var computeParams = () => { + if (params === void 0) { + if (isNode) { + params = create(); + const pargs = process.argv; + let currParamName = null; + for (let i = 0; i < pargs.length; i++) { + const parg = pargs[i]; + if (parg[0] === "-") { + if (currParamName !== null) { + params.set(currParamName, ""); + } + currParamName = parg; + } else { + if (currParamName !== null) { + params.set(currParamName, parg); + currParamName = null; + } else { + args.push(parg); + } + } + } + if (currParamName !== null) { + params.set(currParamName, ""); + } + } else if (typeof location === "object") { + params = create(); + (location.search || "?").slice(1).split("&").forEach((kv) => { + if (kv.length !== 0) { + const [key, value] = kv.split("="); + params.set(`--${fromCamelCase(key, "-")}`, value); + params.set(`-${fromCamelCase(key, "-")}`, value); + } + }); + } else { + params = create(); + } + } + return params; + }; + var hasParam = (name) => computeParams().has(name); + var getVariable = (name) => isNode ? undefinedToNull(process.env[name.toUpperCase().replaceAll("-", "_")]) : undefinedToNull(varStorage.getItem(name)); + var hasConf = (name) => hasParam("--" + name) || getVariable(name) !== null; + var production = hasConf("production"); + var forceColor = isNode && isOneOf(process.env.FORCE_COLOR, ["true", "1", "2"]); + var supportsColor = forceColor || !hasParam("--no-colors") && // @todo deprecate --no-colors + !hasConf("no-color") && (!isNode || process.stdout.isTTY) && (!isNode || hasParam("--color") || getVariable("COLORTERM") !== null || (getVariable("TERM") || "").includes("color")); + + // node_modules/lib0/buffer.js + var createUint8ArrayFromLen = (len) => new Uint8Array(len); + var copyUint8Array = (uint8Array) => { + const newBuf = createUint8ArrayFromLen(uint8Array.byteLength); + newBuf.set(uint8Array); + return newBuf; + }; + + // node_modules/lib0/pair.js + var Pair = class { + /** + * @param {L} left + * @param {R} right + */ + constructor(left, right) { + this.left = left; + this.right = right; + } + }; + var create5 = (left, right) => new Pair(left, right); + + // node_modules/lib0/prng.js + var bool = (gen) => gen.next() >= 0.5; + var int53 = (gen, min4, max4) => floor(gen.next() * (max4 + 1 - min4) + min4); + var int32 = (gen, min4, max4) => floor(gen.next() * (max4 + 1 - min4) + min4); + var int31 = (gen, min4, max4) => int32(gen, min4, max4); + var letter = (gen) => fromCharCode(int31(gen, 97, 122)); + var word = (gen, minLen = 0, maxLen = 20) => { + const len = int31(gen, minLen, maxLen); + let str = ""; + for (let i = 0; i < len; i++) { + str += letter(gen); + } + return str; + }; + var oneOf = (gen, array) => array[int31(gen, 0, array.length - 1)]; + + // node_modules/lib0/schema.js + var schemaSymbol = Symbol("0schema"); + var ValidationError = class { + constructor() { + this._rerrs = []; + } + /** + * @param {string?} path + * @param {string} expected + * @param {string} has + * @param {string?} message + */ + extend(path, expected, has, message = null) { + this._rerrs.push({ path, expected, has, message }); + } + toString() { + const s = []; + for (let i = this._rerrs.length - 1; i > 0; i--) { + const r = this._rerrs[i]; + s.push(repeat(" ", (this._rerrs.length - i) * 2) + `${r.path != null ? `[${r.path}] ` : ""}${r.has} doesn't match ${r.expected}. ${r.message}`); + } + return s.join("\n"); + } + }; + var shapeExtends = (a, b) => { + if (a === b) return true; + if (a == null || b == null || a.constructor !== b.constructor) return false; + if (a[EqualityTraitSymbol]) return equals(a, b); + if (isArray(a)) { + return every( + a, + (aitem) => some(b, (bitem) => shapeExtends(aitem, bitem)) + ); + } else if (isObject(a)) { + return every2( + a, + (aitem, akey) => shapeExtends(aitem, b[akey]) + ); + } + return false; + }; + var Schema = class { + /** + * @param {Schema} other + */ + extends(other) { + let [a, b] = [ + /** @type {any} */ + this.shape, + /** @type {any} */ + other.shape + ]; + if ( + /** @type {typeof Schema} */ + this.constructor._dilutes + ) [b, a] = [a, b]; + return shapeExtends(a, b); + } + /** + * Overwrite this when necessary. By default, we only check the `shape` property which every shape + * should have. + * @param {Schema} other + */ + equals(other) { + return this.constructor === other.constructor && equalityDeep(this.shape, other.shape); + } + [schemaSymbol]() { + return true; + } + /** + * @param {object} other + */ + [EqualityTraitSymbol](other) { + return this.equals( + /** @type {any} */ + other + ); + } + /** + * Use `schema.validate(obj)` with a typed parameter that is already of typed to be an instance of + * Schema. Validate will check the structure of the parameter and return true iff the instance + * really is an instance of Schema. + * + * @param {T} o + * @return {boolean} + */ + validate(o) { + return this.check(o); + } + /* c8 ignore start */ + /** + * Similar to validate, but this method accepts untyped parameters. + * + * @param {any} _o + * @param {ValidationError} [_err] + * @return {_o is T} + */ + check(_o, _err) { + methodUnimplemented(); + } + /* c8 ignore stop */ + /** + * @type {Schema} + */ + get nullable() { + return $union(this, $null); + } + /** + * @type {$Optional>} + */ + get optional() { + return new $Optional( + /** @type {Schema} */ + this + ); + } + /** + * Cast a variable to a specific type. Returns the casted value, or throws an exception otherwise. + * Use this if you know that the type is of a specific type and you just want to convince the type + * system. + * + * **Do not rely on these error messages!** + * Performs an assertion check only if not in a production environment. + * + * @template OO + * @param {OO} o + * @return {Extract extends never ? T : (OO extends Array ? T : Extract)} + */ + cast(o) { + assert(o, this); + return ( + /** @type {any} */ + o + ); + } + /** + * EXPECTO PATRONUM!! 🪄 + * This function protects against type errors. Though it may not work in the real world. + * + * "After all this time?" + * "Always." - Snape, talking about type safety + * + * Ensures that a variable is a a specific type. Returns the value, or throws an exception if the assertion check failed. + * Use this if you know that the type is of a specific type and you just want to convince the type + * system. + * + * Can be useful when defining lambdas: `s.lambda(s.$number, s.$void).expect((n) => n + 1)` + * + * **Do not rely on these error messages!** + * Performs an assertion check if not in a production environment. + * + * @param {T} o + * @return {o extends T ? T : never} + */ + expect(o) { + assert(o, this); + return o; + } + }; + // this.shape must not be defined on Schema. Otherwise typecheck on metatypes (e.g. $$object) won't work as expected anymore + /** + * If true, the more things are added to the shape the more objects this schema will accept (e.g. + * union). By default, the more objects are added, the the fewer objects this schema will accept. + * @protected + */ + __publicField(Schema, "_dilutes", false); + var $ConstructedBy = class extends Schema { + /** + * @param {C} c + * @param {((o:Instance)=>boolean)|null} check + */ + constructor(c, check) { + super(); + this.shape = c; + this._c = check; + } + /** + * @param {any} o + * @param {ValidationError} [err] + * @return {o is C extends ((...args:any[]) => infer T) ? T : (C extends (new (...args:any[]) => any) ? InstanceType : never)} o + */ + check(o, err = void 0) { + const c = o?.constructor === this.shape && (this._c == null || this._c(o)); + !c && err?.extend(null, this.shape.name, o?.constructor.name, o?.constructor !== this.shape ? "Constructor match failed" : "Check failed"); + return c; + } + }; + var $constructedBy = (c, check = null) => new $ConstructedBy(c, check); + var $$constructedBy = $constructedBy($ConstructedBy); + var $Custom = class extends Schema { + /** + * @param {(o:any) => boolean} check + */ + constructor(check) { + super(); + this.shape = check; + } + /** + * @param {any} o + * @param {ValidationError} err + * @return {o is any} + */ + check(o, err) { + const c = this.shape(o); + !c && err?.extend(null, "custom prop", o?.constructor.name, "failed to check custom prop"); + return c; + } + }; + var $custom = (check) => new $Custom(check); + var $$custom = $constructedBy($Custom); + var $Literal = class extends Schema { + /** + * @param {Array} literals + */ + constructor(literals) { + super(); + this.shape = literals; + } + /** + * + * @param {any} o + * @param {ValidationError} [err] + * @return {o is T} + */ + check(o, err) { + const c = this.shape.some((a) => a === o); + !c && err?.extend(null, this.shape.join(" | "), o.toString()); + return c; + } + }; + var $literal = (...literals) => new $Literal(literals); + var $$literal = $constructedBy($Literal); + var _regexEscape = ( + /** @type {any} */ + RegExp.escape || /** @type {(str:string) => string} */ + ((str) => str.replace(/[().|&,$^[\]]/g, (s) => "\\" + s)) + ); + var _schemaStringTemplateToRegex = (s) => { + if ($string.check(s)) { + return [_regexEscape(s)]; + } + if ($$literal.check(s)) { + return ( + /** @type {Array} */ + s.shape.map((v) => v + "") + ); + } + if ($$number.check(s)) { + return ["[+-]?\\d+.?\\d*"]; + } + if ($$string.check(s)) { + return [".*"]; + } + if ($$union.check(s)) { + return s.shape.map(_schemaStringTemplateToRegex).flat(1); + } + unexpectedCase(); + }; + var $StringTemplate = class extends Schema { + /** + * @param {T} shape + */ + constructor(shape) { + super(); + this.shape = shape; + this._r = new RegExp("^" + shape.map(_schemaStringTemplateToRegex).map((opts) => `(${opts.join("|")})`).join("") + "$"); + } + /** + * @param {any} o + * @param {ValidationError} [err] + * @return {o is CastStringTemplateArgsToTemplate} + */ + check(o, err) { + const c = this._r.exec(o) != null; + !c && err?.extend(null, this._r.toString(), o.toString(), "String doesn't match string template."); + return c; + } + }; + var $$stringTemplate = $constructedBy($StringTemplate); + var isOptionalSymbol = Symbol("optional"); + var $Optional = class extends Schema { + /** + * @param {S} shape + */ + constructor(shape) { + super(); + this.shape = shape; + } + /** + * @param {any} o + * @param {ValidationError} [err] + * @return {o is (Unwrap|undefined)} + */ + check(o, err) { + const c = o === void 0 || this.shape.check(o); + !c && err?.extend(null, "undefined (optional)", "()"); + return c; + } + get [isOptionalSymbol]() { + return true; + } + }; + var $$optional = $constructedBy($Optional); + var $Never = class extends Schema { + /** + * @param {any} _o + * @param {ValidationError} [err] + * @return {_o is never} + */ + check(_o, err) { + err?.extend(null, "never", typeof _o); + return false; + } + }; + var $never = new $Never(); + var $$never = $constructedBy($Never); + var _$Object = class _$Object extends Schema { + /** + * @param {S} shape + * @param {boolean} partial + */ + constructor(shape, partial = false) { + super(); + this.shape = shape; + this._isPartial = partial; + } + /** + * @type {Schema>>} + */ + get partial() { + return new _$Object(this.shape, true); + } + /** + * @param {any} o + * @param {ValidationError} err + * @return {o is $ObjectToType} + */ + check(o, err) { + if (o == null) { + err?.extend(null, "object", "null"); + return false; + } + return every2(this.shape, (vv, vk) => { + const c = this._isPartial && !hasProperty(o, vk) || vv.check(o[vk], err); + !c && err?.extend(vk.toString(), vv.toString(), typeof o[vk], "Object property does not match"); + return c; + }); + } + }; + __publicField(_$Object, "_dilutes", true); + var $Object = _$Object; + var $object = (def) => ( + /** @type {any} */ + new $Object(def) + ); + var $$object = $constructedBy($Object); + var $objectAny = $custom((o) => o != null && (o.constructor === Object || o.constructor == null)); + var $Record = class extends Schema { + /** + * @param {Keys} keys + * @param {Values} values + */ + constructor(keys3, values) { + super(); + this.shape = { + keys: keys3, + values + }; + } + /** + * @param {any} o + * @param {ValidationError} err + * @return {o is { [key in Unwrap]: Unwrap }} + */ + check(o, err) { + return o != null && every2(o, (vv, vk) => { + const ck = this.shape.keys.check(vk, err); + !ck && err?.extend(vk + "", "Record", typeof o, ck ? "Key doesn't match schema" : "Value doesn't match value"); + return ck && this.shape.values.check(vv, err); + }); + } + }; + var $record = (keys3, values) => new $Record(keys3, values); + var $$record = $constructedBy($Record); + var $Tuple = class extends Schema { + /** + * @param {S} shape + */ + constructor(shape) { + super(); + this.shape = shape; + } + /** + * @param {any} o + * @param {ValidationError} err + * @return {o is { [K in keyof S]: S[K] extends Schema ? Type : never }} + */ + check(o, err) { + return o != null && every2(this.shape, (vv, vk) => { + const c = ( + /** @type {Schema} */ + vv.check(o[vk], err) + ); + !c && err?.extend(vk.toString(), "Tuple", typeof vv); + return c; + }); + } + }; + var $tuple = (...def) => new $Tuple(def); + var $$tuple = $constructedBy($Tuple); + var $Array = class extends Schema { + /** + * @param {Array} v + */ + constructor(v) { + super(); + this.shape = v.length === 1 ? v[0] : new $Union(v); + } + /** + * @param {any} o + * @param {ValidationError} [err] + * @return {o is Array ? T : never>} o + */ + check(o, err) { + const c = isArray(o) && every(o, (oi) => this.shape.check(oi)); + !c && err?.extend(null, "Array", ""); + return c; + } + }; + var $array = (...def) => new $Array(def); + var $$array = $constructedBy($Array); + var $arrayAny = $custom((o) => isArray(o)); + var $InstanceOf = class extends Schema { + /** + * @param {new (...args:any) => T} constructor + * @param {((o:T) => boolean)|null} check + */ + constructor(constructor, check) { + super(); + this.shape = constructor; + this._c = check; + } + /** + * @param {any} o + * @param {ValidationError} err + * @return {o is T} + */ + check(o, err) { + const c = o instanceof this.shape && (this._c == null || this._c(o)); + !c && err?.extend(null, this.shape.name, o?.constructor.name); + return c; + } + }; + var $instanceOf = (c, check = null) => new $InstanceOf(c, check); + var $$instanceOf = $constructedBy($InstanceOf); + var $$schema = $instanceOf(Schema); + var $Lambda = class extends Schema { + /** + * @param {Args} args + */ + constructor(args2) { + super(); + this.len = args2.length - 1; + this.args = $tuple(...args2.slice(-1)); + this.res = args2[this.len]; + } + /** + * @param {any} f + * @param {ValidationError} err + * @return {f is _LArgsToLambdaDef} + */ + check(f, err) { + const c = f.constructor === Function && f.length <= this.len; + !c && err?.extend(null, "function", typeof f); + return c; + } + }; + var $$lambda = $constructedBy($Lambda); + var $function = $custom((o) => typeof o === "function"); + var $Intersection = class extends Schema { + /** + * @param {T} v + */ + constructor(v) { + super(); + this.shape = v; + } + /** + * @param {any} o + * @param {ValidationError} [err] + * @return {o is Intersect>} + */ + check(o, err) { + const c = every(this.shape, (check) => check.check(o, err)); + !c && err?.extend(null, "Intersectinon", typeof o); + return c; + } + }; + var $$intersect = $constructedBy($Intersection, (o) => o.shape.length > 0); + var $Union = class extends Schema { + /** + * @param {Array>} v + */ + constructor(v) { + super(); + this.shape = v; + } + /** + * @param {any} o + * @param {ValidationError} [err] + * @return {o is S} + */ + check(o, err) { + const c = some(this.shape, (vv) => vv.check(o, err)); + err?.extend(null, "Union", typeof o); + return c; + } + }; + __publicField($Union, "_dilutes", true); + var $union = (...schemas) => schemas.findIndex(($s) => $$union.check($s)) >= 0 ? $union(...schemas.map(($s) => $($s)).map(($s) => $$union.check($s) ? $s.shape : [$s]).flat(1)) : schemas.length === 1 ? schemas[0] : new $Union(schemas); + var $$union = ( + /** @type {Schema<$Union>} */ + $constructedBy($Union) + ); + var _t = () => true; + var $any = $custom(_t); + var $$any = ( + /** @type {Schema>} */ + $constructedBy($Custom, (o) => o.shape === _t) + ); + var $bigint = $custom((o) => typeof o === "bigint"); + var $$bigint = ( + /** @type {Schema>} */ + $custom((o) => o === $bigint) + ); + var $symbol = $custom((o) => typeof o === "symbol"); + var $$symbol = ( + /** @type {Schema>} */ + $custom((o) => o === $symbol) + ); + var $number = $custom((o) => typeof o === "number"); + var $$number = ( + /** @type {Schema>} */ + $custom((o) => o === $number) + ); + var $string = $custom((o) => typeof o === "string"); + var $$string = ( + /** @type {Schema>} */ + $custom((o) => o === $string) + ); + var $boolean = $custom((o) => typeof o === "boolean"); + var $$boolean = ( + /** @type {Schema>} */ + $custom((o) => o === $boolean) + ); + var $undefined = $literal(void 0); + var $$undefined = ( + /** @type {Schema>} */ + $constructedBy($Literal, (o) => o.shape.length === 1 && o.shape[0] === void 0) + ); + var $void = $literal(void 0); + var $null = $literal(null); + var $$null = ( + /** @type {Schema>} */ + $constructedBy($Literal, (o) => o.shape.length === 1 && o.shape[0] === null) + ); + var $uint8Array = $constructedBy(Uint8Array); + var $$uint8Array = ( + /** @type {Schema>} */ + $constructedBy($ConstructedBy, (o) => o.shape === Uint8Array) + ); + var $primitive = $union($number, $string, $null, $undefined, $bigint, $boolean, $symbol); + var $json = (() => { + const $jsonArr = ( + /** @type {$Array<$any>} */ + $array($any) + ); + const $jsonRecord = ( + /** @type {$Record<$string,$any>} */ + $record($string, $any) + ); + const $json2 = $union($number, $string, $null, $boolean, $jsonArr, $jsonRecord); + $jsonArr.shape = $json2; + $jsonRecord.shape.values = $json2; + return $json2; + })(); + var $ = (o) => { + if ($$schema.check(o)) { + return ( + /** @type {any} */ + o + ); + } else if ($objectAny.check(o)) { + const o2 = {}; + for (const k in o) { + o2[k] = $(o[k]); + } + return ( + /** @type {any} */ + $object(o2) + ); + } else if ($arrayAny.check(o)) { + return ( + /** @type {any} */ + $union(...o.map($)) + ); + } else if ($primitive.check(o)) { + return ( + /** @type {any} */ + $literal(o) + ); + } else if ($function.check(o)) { + return ( + /** @type {any} */ + $constructedBy( + /** @type {any} */ + o + ) + ); + } + unexpectedCase(); + }; + var assert = production ? () => { + } : (o, schema) => { + const err = new ValidationError(); + if (!schema.check(o, err)) { + throw create3(`Expected value to be of type ${schema.constructor.name}. +${err.toString()}`); + } + }; + var PatternMatcher = class { + /** + * @param {Schema} [$state] + */ + constructor($state) { + this.patterns = []; + this.$state = $state; + } + /** + * @template P + * @template R + * @param {P} pattern + * @param {(o:NoInfer>>,s:State)=>R} handler + * @return {PatternMatcher>,R>>} + */ + if(pattern, handler) { + this.patterns.push({ if: $(pattern), h: handler }); + return this; + } + /** + * @template R + * @param {(o:any,s:State)=>R} h + */ + else(h) { + return this.if($any, h); + } + /** + * @return {State extends undefined + * ? >(o:In,state?:undefined)=>PatternMatchResult + * : >(o:In,state:State)=>PatternMatchResult} + */ + done() { + return ( + /** @type {any} */ + (o, s) => { + for (let i = 0; i < this.patterns.length; i++) { + const p = this.patterns[i]; + if (p.if.check(o)) { + return p.h(o, s); + } + } + throw create3("Unhandled pattern"); + } + ); + } + }; + var match = (state) => new PatternMatcher( + /** @type {any} */ + state + ); + var _random = ( + /** @type {any} */ + match( + /** @type {Schema} */ + $any + ).if($$number, (_o, gen) => int53(gen, MIN_SAFE_INTEGER, MAX_SAFE_INTEGER)).if($$string, (_o, gen) => word(gen)).if($$boolean, (_o, gen) => bool(gen)).if($$bigint, (_o, gen) => BigInt(int53(gen, MIN_SAFE_INTEGER, MAX_SAFE_INTEGER))).if($$union, (o, gen) => random(gen, oneOf(gen, o.shape))).if($$object, (o, gen) => { + const res = {}; + for (const k in o.shape) { + let prop = o.shape[k]; + if ($$optional.check(prop)) { + if (bool(gen)) { + continue; + } + prop = prop.shape; + } + res[k] = _random(prop, gen); + } + return res; + }).if($$array, (o, gen) => { + const arr = []; + const n = int32(gen, 0, 42); + for (let i = 0; i < n; i++) { + arr.push(random(gen, o.shape)); + } + return arr; + }).if($$literal, (o, gen) => { + return oneOf(gen, o.shape); + }).if($$null, (o, gen) => { + return null; + }).if($$lambda, (o, gen) => { + const res = random(gen, o.res); + return () => res; + }).if($$any, (o, gen) => random(gen, oneOf(gen, [ + $number, + $string, + $null, + $undefined, + $bigint, + $boolean, + $array($number), + $record($union("a", "b", "c"), $number) + ]))).if($$record, (o, gen) => { + const res = {}; + const keysN = int53(gen, 0, 3); + for (let i = 0; i < keysN; i++) { + const key = random(gen, o.shape.keys); + const val = random(gen, o.shape.values); + res[key] = val; + } + return res; + }).done() + ); + var random = (gen, schema) => ( + /** @type {any} */ + _random($(schema), gen) + ); + + // node_modules/lib0/dom.js + var doc = ( + /** @type {Document} */ + typeof document !== "undefined" ? document : {} + ); + var $fragment = $custom((el) => el.nodeType === DOCUMENT_FRAGMENT_NODE); + var domParser = ( + /** @type {DOMParser} */ + typeof DOMParser !== "undefined" ? new DOMParser() : null + ); + var $element = $custom((el) => el.nodeType === ELEMENT_NODE); + var $text = $custom((el) => el.nodeType === TEXT_NODE); + var mapToStyleString = (m) => map(m, (value, key) => `${key}:${value};`).join(""); + var ELEMENT_NODE = doc.ELEMENT_NODE; + var TEXT_NODE = doc.TEXT_NODE; + var CDATA_SECTION_NODE = doc.CDATA_SECTION_NODE; + var COMMENT_NODE = doc.COMMENT_NODE; + var DOCUMENT_NODE = doc.DOCUMENT_NODE; + var DOCUMENT_TYPE_NODE = doc.DOCUMENT_TYPE_NODE; + var DOCUMENT_FRAGMENT_NODE = doc.DOCUMENT_FRAGMENT_NODE; + var $node = $custom((el) => el.nodeType === DOCUMENT_NODE); + + // node_modules/lib0/symbol.js + var create6 = Symbol; + + // node_modules/lib0/logging.common.js + var BOLD = create6(); + var UNBOLD = create6(); + var BLUE = create6(); + var GREY = create6(); + var GREEN = create6(); + var RED = create6(); + var PURPLE = create6(); + var ORANGE = create6(); + var UNCOLOR = create6(); + var computeNoColorLoggingArgs = (args2) => { + if (args2.length === 1 && args2[0]?.constructor === Function) { + args2 = /** @type {Array} */ + /** @type {[function]} */ + args2[0](); + } + const strBuilder = []; + const logArgs = []; + let i = 0; + for (; i < args2.length; i++) { + const arg = args2[i]; + if (arg === void 0) { + break; + } else if (arg.constructor === String || arg.constructor === Number) { + strBuilder.push(arg); + } else if (arg.constructor === Object) { + break; + } + } + if (i > 0) { + logArgs.push(strBuilder.join("")); + } + for (; i < args2.length; i++) { + const arg = args2[i]; + if (!(arg instanceof Symbol)) { + logArgs.push(arg); + } + } + return logArgs; + }; + var lastLoggingTime = getUnixTime(); + + // node_modules/lib0/logging.js + var _browserStyleMap = { + [BOLD]: create5("font-weight", "bold"), + [UNBOLD]: create5("font-weight", "normal"), + [BLUE]: create5("color", "blue"), + [GREEN]: create5("color", "green"), + [GREY]: create5("color", "grey"), + [RED]: create5("color", "red"), + [PURPLE]: create5("color", "purple"), + [ORANGE]: create5("color", "orange"), + // not well supported in chrome when debugging node with inspector - TODO: deprecate + [UNCOLOR]: create5("color", "black") + }; + var computeBrowserLoggingArgs = (args2) => { + if (args2.length === 1 && args2[0]?.constructor === Function) { + args2 = /** @type {Array} */ + /** @type {[function]} */ + args2[0](); + } + const strBuilder = []; + const styles = []; + const currentStyle = create(); + let logArgs = []; + let i = 0; + for (; i < args2.length; i++) { + const arg = args2[i]; + const style = _browserStyleMap[arg]; + if (style !== void 0) { + currentStyle.set(style.left, style.right); + } else { + if (arg === void 0) { + break; + } + if (arg.constructor === String || arg.constructor === Number) { + const style2 = mapToStyleString(currentStyle); + if (i > 0 || style2.length > 0) { + strBuilder.push("%c" + arg); + styles.push(style2); + } else { + strBuilder.push(arg); + } + } else { + break; + } + } + } + if (i > 0) { + logArgs = styles; + logArgs.unshift(strBuilder.join("")); + } + for (; i < args2.length; i++) { + const arg = args2[i]; + if (!(arg instanceof Symbol)) { + logArgs.push(arg); + } + } + return logArgs; + }; + var computeLoggingArgs = supportsColor ? computeBrowserLoggingArgs : computeNoColorLoggingArgs; + var print = (...args2) => { + console.log(...computeLoggingArgs(args2)); + vconsoles.forEach((vc) => vc.print(args2)); + }; + var warn = (...args2) => { + console.warn(...computeLoggingArgs(args2)); + args2.unshift(ORANGE); + vconsoles.forEach((vc) => vc.print(args2)); + }; + var vconsoles = create2(); + + // node_modules/lib0/iterator.js + var createIterator = (next) => ({ + /** + * @return {IterableIterator} + */ + [Symbol.iterator]() { + return this; + }, + // @ts-ignore + next + }); + var iteratorFilter = (iterator, filter) => createIterator(() => { + let res; + do { + res = iterator.next(); + } while (!res.done && !filter(res.value)); + return res; + }); + var iteratorMap = (iterator, fmap) => createIterator(() => { + const { done, value } = iterator.next(); + return { done, value: done ? void 0 : fmap(value) }; + }); + + // node_modules/yjs/dist/yjs.mjs + var DeleteItem = class { + /** + * @param {number} clock + * @param {number} len + */ + constructor(clock, len) { + this.clock = clock; + this.len = len; + } + }; + var DeleteSet = class { + constructor() { + this.clients = /* @__PURE__ */ new Map(); + } + }; + var iterateDeletedStructs = (transaction, ds, f) => ds.clients.forEach((deletes, clientid) => { + const structs = ( + /** @type {Array} */ + transaction.doc.store.clients.get(clientid) + ); + if (structs != null) { + const lastStruct = structs[structs.length - 1]; + const clockState = lastStruct.id.clock + lastStruct.length; + for (let i = 0, del = deletes[i]; i < deletes.length && del.clock < clockState; del = deletes[++i]) { + iterateStructs(transaction, structs, del.clock, del.len, f); + } + } + }); + var findIndexDS = (dis, clock) => { + let left = 0; + let right = dis.length - 1; + while (left <= right) { + const midindex = floor((left + right) / 2); + const mid = dis[midindex]; + const midclock = mid.clock; + if (midclock <= clock) { + if (clock < midclock + mid.len) { + return midindex; + } + left = midindex + 1; + } else { + right = midindex - 1; + } + } + return null; + }; + var isDeleted = (ds, id2) => { + const dis = ds.clients.get(id2.client); + return dis !== void 0 && findIndexDS(dis, id2.clock) !== null; + }; + var sortAndMergeDeleteSet = (ds) => { + ds.clients.forEach((dels) => { + dels.sort((a, b) => a.clock - b.clock); + let i, j; + for (i = 1, j = 1; i < dels.length; i++) { + const left = dels[j - 1]; + const right = dels[i]; + if (left.clock + left.len >= right.clock) { + dels[j - 1] = new DeleteItem(left.clock, max(left.len, right.clock + right.len - left.clock)); + } else { + if (j < i) { + dels[j] = right; + } + j++; + } + } + dels.length = j; + }); + }; + var mergeDeleteSets = (dss) => { + const merged = new DeleteSet(); + for (let dssI = 0; dssI < dss.length; dssI++) { + dss[dssI].clients.forEach((delsLeft, client) => { + if (!merged.clients.has(client)) { + const dels = delsLeft.slice(); + for (let i = dssI + 1; i < dss.length; i++) { + appendTo(dels, dss[i].clients.get(client) || []); + } + merged.clients.set(client, dels); + } + }); + } + sortAndMergeDeleteSet(merged); + return merged; + }; + var addToDeleteSet = (ds, client, clock, length3) => { + setIfUndefined(ds.clients, client, () => ( + /** @type {Array} */ + [] + )).push(new DeleteItem(clock, length3)); + }; + var createDeleteSet = () => new DeleteSet(); + var createDeleteSetFromStructStore = (ss) => { + const ds = createDeleteSet(); + ss.clients.forEach((structs, client) => { + const dsitems = []; + for (let i = 0; i < structs.length; i++) { + const struct = structs[i]; + if (struct.deleted) { + const clock = struct.id.clock; + let len = struct.length; + if (i + 1 < structs.length) { + for (let next = structs[i + 1]; i + 1 < structs.length && next.deleted; next = structs[++i + 1]) { + len += next.length; + } + } + dsitems.push(new DeleteItem(clock, len)); + } + } + if (dsitems.length > 0) { + ds.clients.set(client, dsitems); + } + }); + return ds; + }; + var writeDeleteSet = (encoder, ds) => { + writeVarUint(encoder.restEncoder, ds.clients.size); + from(ds.clients.entries()).sort((a, b) => b[0] - a[0]).forEach(([client, dsitems]) => { + encoder.resetDsCurVal(); + writeVarUint(encoder.restEncoder, client); + const len = dsitems.length; + writeVarUint(encoder.restEncoder, len); + for (let i = 0; i < len; i++) { + const item = dsitems[i]; + encoder.writeDsClock(item.clock); + encoder.writeDsLen(item.len); + } + }); + }; + var readDeleteSet = (decoder) => { + const ds = new DeleteSet(); + const numClients = readVarUint(decoder.restDecoder); + for (let i = 0; i < numClients; i++) { + decoder.resetDsCurVal(); + const client = readVarUint(decoder.restDecoder); + const numberOfDeletes = readVarUint(decoder.restDecoder); + if (numberOfDeletes > 0) { + const dsField = setIfUndefined(ds.clients, client, () => ( + /** @type {Array} */ + [] + )); + for (let i2 = 0; i2 < numberOfDeletes; i2++) { + dsField.push(new DeleteItem(decoder.readDsClock(), decoder.readDsLen())); + } + } + } + return ds; + }; + var readAndApplyDeleteSet = (decoder, transaction, store) => { + const unappliedDS = new DeleteSet(); + const numClients = readVarUint(decoder.restDecoder); + for (let i = 0; i < numClients; i++) { + decoder.resetDsCurVal(); + const client = readVarUint(decoder.restDecoder); + const numberOfDeletes = readVarUint(decoder.restDecoder); + const structs = store.clients.get(client) || []; + const state = getState(store, client); + for (let i2 = 0; i2 < numberOfDeletes; i2++) { + const clock = decoder.readDsClock(); + const clockEnd = clock + decoder.readDsLen(); + if (clock < state) { + if (state < clockEnd) { + addToDeleteSet(unappliedDS, client, state, clockEnd - state); + } + let index = findIndexSS(structs, clock); + let struct = structs[index]; + if (!struct.deleted && struct.id.clock < clock) { + structs.splice(index + 1, 0, splitItem(transaction, struct, clock - struct.id.clock)); + index++; + } + while (index < structs.length) { + struct = structs[index++]; + if (struct.id.clock < clockEnd) { + if (!struct.deleted) { + if (clockEnd < struct.id.clock + struct.length) { + structs.splice(index, 0, splitItem(transaction, struct, clockEnd - struct.id.clock)); + } + struct.delete(transaction); + } + } else { + break; + } + } + } else { + addToDeleteSet(unappliedDS, client, clock, clockEnd - clock); + } + } + } + if (unappliedDS.clients.size > 0) { + const ds = new UpdateEncoderV2(); + writeVarUint(ds.restEncoder, 0); + writeDeleteSet(ds, unappliedDS); + return ds.toUint8Array(); + } + return null; + }; + var generateNewClientId = uint32; + var Doc = class _Doc extends ObservableV2 { + /** + * @param {DocOpts} opts configuration + */ + constructor({ guid = uuidv4(), collectionid = null, gc = true, gcFilter = () => true, meta = null, autoLoad = false, shouldLoad = true } = {}) { + super(); + this.gc = gc; + this.gcFilter = gcFilter; + this.clientID = generateNewClientId(); + this.guid = guid; + this.collectionid = collectionid; + this.share = /* @__PURE__ */ new Map(); + this.store = new StructStore(); + this._transaction = null; + this._transactionCleanups = []; + this.subdocs = /* @__PURE__ */ new Set(); + this._item = null; + this.shouldLoad = shouldLoad; + this.autoLoad = autoLoad; + this.meta = meta; + this.isLoaded = false; + this.isSynced = false; + this.isDestroyed = false; + this.whenLoaded = create4((resolve) => { + this.on("load", () => { + this.isLoaded = true; + resolve(this); + }); + }); + const provideSyncedPromise = () => create4((resolve) => { + const eventHandler = (isSynced) => { + if (isSynced === void 0 || isSynced === true) { + this.off("sync", eventHandler); + resolve(); + } + }; + this.on("sync", eventHandler); + }); + this.on("sync", (isSynced) => { + if (isSynced === false && this.isSynced) { + this.whenSynced = provideSyncedPromise(); + } + this.isSynced = isSynced === void 0 || isSynced === true; + if (this.isSynced && !this.isLoaded) { + this.emit("load", [this]); + } + }); + this.whenSynced = provideSyncedPromise(); + } + /** + * Notify the parent document that you request to load data into this subdocument (if it is a subdocument). + * + * `load()` might be used in the future to request any provider to load the most current data. + * + * It is safe to call `load()` multiple times. + */ + load() { + const item = this._item; + if (item !== null && !this.shouldLoad) { + transact( + /** @type {any} */ + item.parent.doc, + (transaction) => { + transaction.subdocsLoaded.add(this); + }, + null, + true + ); + } + this.shouldLoad = true; + } + getSubdocs() { + return this.subdocs; + } + getSubdocGuids() { + return new Set(from(this.subdocs).map((doc2) => doc2.guid)); + } + /** + * Changes that happen inside of a transaction are bundled. This means that + * the observer fires _after_ the transaction is finished and that all changes + * that happened inside of the transaction are sent as one message to the + * other peers. + * + * @template T + * @param {function(Transaction):T} f The function that should be executed as a transaction + * @param {any} [origin] Origin of who started the transaction. Will be stored on transaction.origin + * @return T + * + * @public + */ + transact(f, origin = null) { + return transact(this, f, origin); + } + /** + * Define a shared data type. + * + * Multiple calls of `ydoc.get(name, TypeConstructor)` yield the same result + * and do not overwrite each other. I.e. + * `ydoc.get(name, Y.Array) === ydoc.get(name, Y.Array)` + * + * After this method is called, the type is also available on `ydoc.share.get(name)`. + * + * *Best Practices:* + * Define all types right after the Y.Doc instance is created and store them in a separate object. + * Also use the typed methods `getText(name)`, `getArray(name)`, .. + * + * @template {typeof AbstractType} Type + * @example + * const ydoc = new Y.Doc(..) + * const appState = { + * document: ydoc.getText('document') + * comments: ydoc.getArray('comments') + * } + * + * @param {string} name + * @param {Type} TypeConstructor The constructor of the type definition. E.g. Y.Text, Y.Array, Y.Map, ... + * @return {InstanceType} The created type. Constructed with TypeConstructor + * + * @public + */ + get(name, TypeConstructor = ( + /** @type {any} */ + AbstractType + )) { + const type = setIfUndefined(this.share, name, () => { + const t = new TypeConstructor(); + t._integrate(this, null); + return t; + }); + const Constr = type.constructor; + if (TypeConstructor !== AbstractType && Constr !== TypeConstructor) { + if (Constr === AbstractType) { + const t = new TypeConstructor(); + t._map = type._map; + type._map.forEach( + /** @param {Item?} n */ + (n) => { + for (; n !== null; n = n.left) { + n.parent = t; + } + } + ); + t._start = type._start; + for (let n = t._start; n !== null; n = n.right) { + n.parent = t; + } + t._length = type._length; + this.share.set(name, t); + t._integrate(this, null); + return ( + /** @type {InstanceType} */ + t + ); + } else { + throw new Error(`Type with the name ${name} has already been defined with a different constructor`); + } + } + return ( + /** @type {InstanceType} */ + type + ); + } + /** + * @template T + * @param {string} [name] + * @return {YArray} + * + * @public + */ + getArray(name = "") { + return ( + /** @type {YArray} */ + this.get(name, YArray) + ); + } + /** + * @param {string} [name] + * @return {YText} + * + * @public + */ + getText(name = "") { + return this.get(name, YText); + } + /** + * @template T + * @param {string} [name] + * @return {YMap} + * + * @public + */ + getMap(name = "") { + return ( + /** @type {YMap} */ + this.get(name, YMap) + ); + } + /** + * @param {string} [name] + * @return {YXmlElement} + * + * @public + */ + getXmlElement(name = "") { + return ( + /** @type {YXmlElement<{[key:string]:string}>} */ + this.get(name, YXmlElement) + ); + } + /** + * @param {string} [name] + * @return {YXmlFragment} + * + * @public + */ + getXmlFragment(name = "") { + return this.get(name, YXmlFragment); + } + /** + * Converts the entire document into a js object, recursively traversing each yjs type + * Doesn't log types that have not been defined (using ydoc.getType(..)). + * + * @deprecated Do not use this method and rather call toJSON directly on the shared types. + * + * @return {Object} + */ + toJSON() { + const doc2 = {}; + this.share.forEach((value, key) => { + doc2[key] = value.toJSON(); + }); + return doc2; + } + /** + * Emit `destroy` event and unregister all event handlers. + */ + destroy() { + this.isDestroyed = true; + from(this.subdocs).forEach((subdoc) => subdoc.destroy()); + const item = this._item; + if (item !== null) { + this._item = null; + const content = ( + /** @type {ContentDoc} */ + item.content + ); + content.doc = new _Doc({ guid: this.guid, ...content.opts, shouldLoad: false }); + content.doc._item = item; + transact( + /** @type {any} */ + item.parent.doc, + (transaction) => { + const doc2 = content.doc; + if (!item.deleted) { + transaction.subdocsAdded.add(doc2); + } + transaction.subdocsRemoved.add(this); + }, + null, + true + ); + } + this.emit("destroyed", [true]); + this.emit("destroy", [this]); + super.destroy(); + } + }; + var DSDecoderV1 = class { + /** + * @param {decoding.Decoder} decoder + */ + constructor(decoder) { + this.restDecoder = decoder; + } + resetDsCurVal() { + } + /** + * @return {number} + */ + readDsClock() { + return readVarUint(this.restDecoder); + } + /** + * @return {number} + */ + readDsLen() { + return readVarUint(this.restDecoder); + } + }; + var UpdateDecoderV1 = class extends DSDecoderV1 { + /** + * @return {ID} + */ + readLeftID() { + return createID(readVarUint(this.restDecoder), readVarUint(this.restDecoder)); + } + /** + * @return {ID} + */ + readRightID() { + return createID(readVarUint(this.restDecoder), readVarUint(this.restDecoder)); + } + /** + * Read the next client id. + * Use this in favor of readID whenever possible to reduce the number of objects created. + */ + readClient() { + return readVarUint(this.restDecoder); + } + /** + * @return {number} info An unsigned 8-bit integer + */ + readInfo() { + return readUint8(this.restDecoder); + } + /** + * @return {string} + */ + readString() { + return readVarString(this.restDecoder); + } + /** + * @return {boolean} isKey + */ + readParentInfo() { + return readVarUint(this.restDecoder) === 1; + } + /** + * @return {number} info An unsigned 8-bit integer + */ + readTypeRef() { + return readVarUint(this.restDecoder); + } + /** + * Write len of a struct - well suited for Opt RLE encoder. + * + * @return {number} len + */ + readLen() { + return readVarUint(this.restDecoder); + } + /** + * @return {any} + */ + readAny() { + return readAny(this.restDecoder); + } + /** + * @return {Uint8Array} + */ + readBuf() { + return copyUint8Array(readVarUint8Array(this.restDecoder)); + } + /** + * Legacy implementation uses JSON parse. We use any-decoding in v2. + * + * @return {any} + */ + readJSON() { + return JSON.parse(readVarString(this.restDecoder)); + } + /** + * @return {string} + */ + readKey() { + return readVarString(this.restDecoder); + } + }; + var DSDecoderV2 = class { + /** + * @param {decoding.Decoder} decoder + */ + constructor(decoder) { + this.dsCurrVal = 0; + this.restDecoder = decoder; + } + resetDsCurVal() { + this.dsCurrVal = 0; + } + /** + * @return {number} + */ + readDsClock() { + this.dsCurrVal += readVarUint(this.restDecoder); + return this.dsCurrVal; + } + /** + * @return {number} + */ + readDsLen() { + const diff = readVarUint(this.restDecoder) + 1; + this.dsCurrVal += diff; + return diff; + } + }; + var UpdateDecoderV2 = class extends DSDecoderV2 { + /** + * @param {decoding.Decoder} decoder + */ + constructor(decoder) { + super(decoder); + this.keys = []; + readVarUint(decoder); + this.keyClockDecoder = new IntDiffOptRleDecoder(readVarUint8Array(decoder)); + this.clientDecoder = new UintOptRleDecoder(readVarUint8Array(decoder)); + this.leftClockDecoder = new IntDiffOptRleDecoder(readVarUint8Array(decoder)); + this.rightClockDecoder = new IntDiffOptRleDecoder(readVarUint8Array(decoder)); + this.infoDecoder = new RleDecoder(readVarUint8Array(decoder), readUint8); + this.stringDecoder = new StringDecoder(readVarUint8Array(decoder)); + this.parentInfoDecoder = new RleDecoder(readVarUint8Array(decoder), readUint8); + this.typeRefDecoder = new UintOptRleDecoder(readVarUint8Array(decoder)); + this.lenDecoder = new UintOptRleDecoder(readVarUint8Array(decoder)); + } + /** + * @return {ID} + */ + readLeftID() { + return new ID(this.clientDecoder.read(), this.leftClockDecoder.read()); + } + /** + * @return {ID} + */ + readRightID() { + return new ID(this.clientDecoder.read(), this.rightClockDecoder.read()); + } + /** + * Read the next client id. + * Use this in favor of readID whenever possible to reduce the number of objects created. + */ + readClient() { + return this.clientDecoder.read(); + } + /** + * @return {number} info An unsigned 8-bit integer + */ + readInfo() { + return ( + /** @type {number} */ + this.infoDecoder.read() + ); + } + /** + * @return {string} + */ + readString() { + return this.stringDecoder.read(); + } + /** + * @return {boolean} + */ + readParentInfo() { + return this.parentInfoDecoder.read() === 1; + } + /** + * @return {number} An unsigned 8-bit integer + */ + readTypeRef() { + return this.typeRefDecoder.read(); + } + /** + * Write len of a struct - well suited for Opt RLE encoder. + * + * @return {number} + */ + readLen() { + return this.lenDecoder.read(); + } + /** + * @return {any} + */ + readAny() { + return readAny(this.restDecoder); + } + /** + * @return {Uint8Array} + */ + readBuf() { + return readVarUint8Array(this.restDecoder); + } + /** + * This is mainly here for legacy purposes. + * + * Initial we incoded objects using JSON. Now we use the much faster lib0/any-encoder. This method mainly exists for legacy purposes for the v1 encoder. + * + * @return {any} + */ + readJSON() { + return readAny(this.restDecoder); + } + /** + * @return {string} + */ + readKey() { + const keyClock = this.keyClockDecoder.read(); + if (keyClock < this.keys.length) { + return this.keys[keyClock]; + } else { + const key = this.stringDecoder.read(); + this.keys.push(key); + return key; + } + } + }; + var DSEncoderV1 = class { + constructor() { + this.restEncoder = createEncoder(); + } + toUint8Array() { + return toUint8Array(this.restEncoder); + } + resetDsCurVal() { + } + /** + * @param {number} clock + */ + writeDsClock(clock) { + writeVarUint(this.restEncoder, clock); + } + /** + * @param {number} len + */ + writeDsLen(len) { + writeVarUint(this.restEncoder, len); + } + }; + var UpdateEncoderV1 = class extends DSEncoderV1 { + /** + * @param {ID} id + */ + writeLeftID(id2) { + writeVarUint(this.restEncoder, id2.client); + writeVarUint(this.restEncoder, id2.clock); + } + /** + * @param {ID} id + */ + writeRightID(id2) { + writeVarUint(this.restEncoder, id2.client); + writeVarUint(this.restEncoder, id2.clock); + } + /** + * Use writeClient and writeClock instead of writeID if possible. + * @param {number} client + */ + writeClient(client) { + writeVarUint(this.restEncoder, client); + } + /** + * @param {number} info An unsigned 8-bit integer + */ + writeInfo(info) { + writeUint8(this.restEncoder, info); + } + /** + * @param {string} s + */ + writeString(s) { + writeVarString(this.restEncoder, s); + } + /** + * @param {boolean} isYKey + */ + writeParentInfo(isYKey) { + writeVarUint(this.restEncoder, isYKey ? 1 : 0); + } + /** + * @param {number} info An unsigned 8-bit integer + */ + writeTypeRef(info) { + writeVarUint(this.restEncoder, info); + } + /** + * Write len of a struct - well suited for Opt RLE encoder. + * + * @param {number} len + */ + writeLen(len) { + writeVarUint(this.restEncoder, len); + } + /** + * @param {any} any + */ + writeAny(any2) { + writeAny(this.restEncoder, any2); + } + /** + * @param {Uint8Array} buf + */ + writeBuf(buf) { + writeVarUint8Array(this.restEncoder, buf); + } + /** + * @param {any} embed + */ + writeJSON(embed) { + writeVarString(this.restEncoder, JSON.stringify(embed)); + } + /** + * @param {string} key + */ + writeKey(key) { + writeVarString(this.restEncoder, key); + } + }; + var DSEncoderV2 = class { + constructor() { + this.restEncoder = createEncoder(); + this.dsCurrVal = 0; + } + toUint8Array() { + return toUint8Array(this.restEncoder); + } + resetDsCurVal() { + this.dsCurrVal = 0; + } + /** + * @param {number} clock + */ + writeDsClock(clock) { + const diff = clock - this.dsCurrVal; + this.dsCurrVal = clock; + writeVarUint(this.restEncoder, diff); + } + /** + * @param {number} len + */ + writeDsLen(len) { + if (len === 0) { + unexpectedCase(); + } + writeVarUint(this.restEncoder, len - 1); + this.dsCurrVal += len; + } + }; + var UpdateEncoderV2 = class extends DSEncoderV2 { + constructor() { + super(); + this.keyMap = /* @__PURE__ */ new Map(); + this.keyClock = 0; + this.keyClockEncoder = new IntDiffOptRleEncoder(); + this.clientEncoder = new UintOptRleEncoder(); + this.leftClockEncoder = new IntDiffOptRleEncoder(); + this.rightClockEncoder = new IntDiffOptRleEncoder(); + this.infoEncoder = new RleEncoder(writeUint8); + this.stringEncoder = new StringEncoder(); + this.parentInfoEncoder = new RleEncoder(writeUint8); + this.typeRefEncoder = new UintOptRleEncoder(); + this.lenEncoder = new UintOptRleEncoder(); + } + toUint8Array() { + const encoder = createEncoder(); + writeVarUint(encoder, 0); + writeVarUint8Array(encoder, this.keyClockEncoder.toUint8Array()); + writeVarUint8Array(encoder, this.clientEncoder.toUint8Array()); + writeVarUint8Array(encoder, this.leftClockEncoder.toUint8Array()); + writeVarUint8Array(encoder, this.rightClockEncoder.toUint8Array()); + writeVarUint8Array(encoder, toUint8Array(this.infoEncoder)); + writeVarUint8Array(encoder, this.stringEncoder.toUint8Array()); + writeVarUint8Array(encoder, toUint8Array(this.parentInfoEncoder)); + writeVarUint8Array(encoder, this.typeRefEncoder.toUint8Array()); + writeVarUint8Array(encoder, this.lenEncoder.toUint8Array()); + writeUint8Array(encoder, toUint8Array(this.restEncoder)); + return toUint8Array(encoder); + } + /** + * @param {ID} id + */ + writeLeftID(id2) { + this.clientEncoder.write(id2.client); + this.leftClockEncoder.write(id2.clock); + } + /** + * @param {ID} id + */ + writeRightID(id2) { + this.clientEncoder.write(id2.client); + this.rightClockEncoder.write(id2.clock); + } + /** + * @param {number} client + */ + writeClient(client) { + this.clientEncoder.write(client); + } + /** + * @param {number} info An unsigned 8-bit integer + */ + writeInfo(info) { + this.infoEncoder.write(info); + } + /** + * @param {string} s + */ + writeString(s) { + this.stringEncoder.write(s); + } + /** + * @param {boolean} isYKey + */ + writeParentInfo(isYKey) { + this.parentInfoEncoder.write(isYKey ? 1 : 0); + } + /** + * @param {number} info An unsigned 8-bit integer + */ + writeTypeRef(info) { + this.typeRefEncoder.write(info); + } + /** + * Write len of a struct - well suited for Opt RLE encoder. + * + * @param {number} len + */ + writeLen(len) { + this.lenEncoder.write(len); + } + /** + * @param {any} any + */ + writeAny(any2) { + writeAny(this.restEncoder, any2); + } + /** + * @param {Uint8Array} buf + */ + writeBuf(buf) { + writeVarUint8Array(this.restEncoder, buf); + } + /** + * This is mainly here for legacy purposes. + * + * Initial we incoded objects using JSON. Now we use the much faster lib0/any-encoder. This method mainly exists for legacy purposes for the v1 encoder. + * + * @param {any} embed + */ + writeJSON(embed) { + writeAny(this.restEncoder, embed); + } + /** + * Property keys are often reused. For example, in y-prosemirror the key `bold` might + * occur very often. For a 3d application, the key `position` might occur very often. + * + * We cache these keys in a Map and refer to them via a unique number. + * + * @param {string} key + */ + writeKey(key) { + const clock = this.keyMap.get(key); + if (clock === void 0) { + this.keyClockEncoder.write(this.keyClock++); + this.stringEncoder.write(key); + } else { + this.keyClockEncoder.write(clock); + } + } + }; + var writeStructs = (encoder, structs, client, clock) => { + clock = max(clock, structs[0].id.clock); + const startNewStructs = findIndexSS(structs, clock); + writeVarUint(encoder.restEncoder, structs.length - startNewStructs); + encoder.writeClient(client); + writeVarUint(encoder.restEncoder, clock); + const firstStruct = structs[startNewStructs]; + firstStruct.write(encoder, clock - firstStruct.id.clock); + for (let i = startNewStructs + 1; i < structs.length; i++) { + structs[i].write(encoder, 0); + } + }; + var writeClientsStructs = (encoder, store, _sm) => { + const sm = /* @__PURE__ */ new Map(); + _sm.forEach((clock, client) => { + if (getState(store, client) > clock) { + sm.set(client, clock); + } + }); + getStateVector(store).forEach((_clock, client) => { + if (!_sm.has(client)) { + sm.set(client, 0); + } + }); + writeVarUint(encoder.restEncoder, sm.size); + from(sm.entries()).sort((a, b) => b[0] - a[0]).forEach(([client, clock]) => { + writeStructs( + encoder, + /** @type {Array} */ + store.clients.get(client), + client, + clock + ); + }); + }; + var readClientsStructRefs = (decoder, doc2) => { + const clientRefs = create(); + const numOfStateUpdates = readVarUint(decoder.restDecoder); + for (let i = 0; i < numOfStateUpdates; i++) { + const numberOfStructs = readVarUint(decoder.restDecoder); + const refs = new Array(numberOfStructs); + const client = decoder.readClient(); + let clock = readVarUint(decoder.restDecoder); + clientRefs.set(client, { i: 0, refs }); + for (let i2 = 0; i2 < numberOfStructs; i2++) { + const info = decoder.readInfo(); + switch (BITS5 & info) { + case 0: { + const len = decoder.readLen(); + refs[i2] = new GC(createID(client, clock), len); + clock += len; + break; + } + case 10: { + const len = readVarUint(decoder.restDecoder); + refs[i2] = new Skip(createID(client, clock), len); + clock += len; + break; + } + default: { + const cantCopyParentInfo = (info & (BIT7 | BIT8)) === 0; + const struct = new Item( + createID(client, clock), + null, + // left + (info & BIT8) === BIT8 ? decoder.readLeftID() : null, + // origin + null, + // right + (info & BIT7) === BIT7 ? decoder.readRightID() : null, + // right origin + cantCopyParentInfo ? decoder.readParentInfo() ? doc2.get(decoder.readString()) : decoder.readLeftID() : null, + // parent + cantCopyParentInfo && (info & BIT6) === BIT6 ? decoder.readString() : null, + // parentSub + readItemContent(decoder, info) + // item content + ); + refs[i2] = struct; + clock += struct.length; + } + } + } + } + return clientRefs; + }; + var integrateStructs = (transaction, store, clientsStructRefs) => { + const stack = []; + let clientsStructRefsIds = from(clientsStructRefs.keys()).sort((a, b) => a - b); + if (clientsStructRefsIds.length === 0) { + return null; + } + const getNextStructTarget = () => { + if (clientsStructRefsIds.length === 0) { + return null; + } + let nextStructsTarget = ( + /** @type {{i:number,refs:Array}} */ + clientsStructRefs.get(clientsStructRefsIds[clientsStructRefsIds.length - 1]) + ); + while (nextStructsTarget.refs.length === nextStructsTarget.i) { + clientsStructRefsIds.pop(); + if (clientsStructRefsIds.length > 0) { + nextStructsTarget = /** @type {{i:number,refs:Array}} */ + clientsStructRefs.get(clientsStructRefsIds[clientsStructRefsIds.length - 1]); + } else { + return null; + } + } + return nextStructsTarget; + }; + let curStructsTarget = getNextStructTarget(); + if (curStructsTarget === null) { + return null; + } + const restStructs = new StructStore(); + const missingSV = /* @__PURE__ */ new Map(); + const updateMissingSv = (client, clock) => { + const mclock = missingSV.get(client); + if (mclock == null || mclock > clock) { + missingSV.set(client, clock); + } + }; + let stackHead = ( + /** @type {any} */ + curStructsTarget.refs[ + /** @type {any} */ + curStructsTarget.i++ + ] + ); + const state = /* @__PURE__ */ new Map(); + const addStackToRestSS = () => { + for (const item of stack) { + const client = item.id.client; + const inapplicableItems = clientsStructRefs.get(client); + if (inapplicableItems) { + inapplicableItems.i--; + restStructs.clients.set(client, inapplicableItems.refs.slice(inapplicableItems.i)); + clientsStructRefs.delete(client); + inapplicableItems.i = 0; + inapplicableItems.refs = []; + } else { + restStructs.clients.set(client, [item]); + } + clientsStructRefsIds = clientsStructRefsIds.filter((c) => c !== client); + } + stack.length = 0; + }; + while (true) { + if (stackHead.constructor !== Skip) { + const localClock = setIfUndefined(state, stackHead.id.client, () => getState(store, stackHead.id.client)); + const offset = localClock - stackHead.id.clock; + if (offset < 0) { + stack.push(stackHead); + updateMissingSv(stackHead.id.client, stackHead.id.clock - 1); + addStackToRestSS(); + } else { + const missing = stackHead.getMissing(transaction, store); + if (missing !== null) { + stack.push(stackHead); + const structRefs = clientsStructRefs.get( + /** @type {number} */ + missing + ) || { refs: [], i: 0 }; + if (structRefs.refs.length === structRefs.i) { + updateMissingSv( + /** @type {number} */ + missing, + getState(store, missing) + ); + addStackToRestSS(); + } else { + stackHead = structRefs.refs[structRefs.i++]; + continue; + } + } else if (offset === 0 || offset < stackHead.length) { + stackHead.integrate(transaction, offset); + state.set(stackHead.id.client, stackHead.id.clock + stackHead.length); + } + } + } + if (stack.length > 0) { + stackHead = /** @type {GC|Item} */ + stack.pop(); + } else if (curStructsTarget !== null && curStructsTarget.i < curStructsTarget.refs.length) { + stackHead = /** @type {GC|Item} */ + curStructsTarget.refs[curStructsTarget.i++]; + } else { + curStructsTarget = getNextStructTarget(); + if (curStructsTarget === null) { + break; + } else { + stackHead = /** @type {GC|Item} */ + curStructsTarget.refs[curStructsTarget.i++]; + } + } + } + if (restStructs.clients.size > 0) { + const encoder = new UpdateEncoderV2(); + writeClientsStructs(encoder, restStructs, /* @__PURE__ */ new Map()); + writeVarUint(encoder.restEncoder, 0); + return { missing: missingSV, update: encoder.toUint8Array() }; + } + return null; + }; + var writeStructsFromTransaction = (encoder, transaction) => writeClientsStructs(encoder, transaction.doc.store, transaction.beforeState); + var readUpdateV2 = (decoder, ydoc, transactionOrigin, structDecoder = new UpdateDecoderV2(decoder)) => transact(ydoc, (transaction) => { + transaction.local = false; + let retry2 = false; + const doc2 = transaction.doc; + const store = doc2.store; + const ss = readClientsStructRefs(structDecoder, doc2); + const restStructs = integrateStructs(transaction, store, ss); + const pending = store.pendingStructs; + if (pending) { + for (const [client, clock] of pending.missing) { + if (clock < getState(store, client)) { + retry2 = true; + break; + } + } + if (restStructs) { + for (const [client, clock] of restStructs.missing) { + const mclock = pending.missing.get(client); + if (mclock == null || mclock > clock) { + pending.missing.set(client, clock); + } + } + pending.update = mergeUpdatesV2([pending.update, restStructs.update]); + } + } else { + store.pendingStructs = restStructs; + } + const dsRest = readAndApplyDeleteSet(structDecoder, transaction, store); + if (store.pendingDs) { + const pendingDSUpdate = new UpdateDecoderV2(createDecoder(store.pendingDs)); + readVarUint(pendingDSUpdate.restDecoder); + const dsRest2 = readAndApplyDeleteSet(pendingDSUpdate, transaction, store); + if (dsRest && dsRest2) { + store.pendingDs = mergeUpdatesV2([dsRest, dsRest2]); + } else { + store.pendingDs = dsRest || dsRest2; + } + } else { + store.pendingDs = dsRest; + } + if (retry2) { + const update = ( + /** @type {{update: Uint8Array}} */ + store.pendingStructs.update + ); + store.pendingStructs = null; + applyUpdateV2(transaction.doc, update); + } + }, transactionOrigin, false); + var applyUpdateV2 = (ydoc, update, transactionOrigin, YDecoder = UpdateDecoderV2) => { + const decoder = createDecoder(update); + readUpdateV2(decoder, ydoc, transactionOrigin, new YDecoder(decoder)); + }; + var applyUpdate = (ydoc, update, transactionOrigin) => applyUpdateV2(ydoc, update, transactionOrigin, UpdateDecoderV1); + var writeStateAsUpdate = (encoder, doc2, targetStateVector = /* @__PURE__ */ new Map()) => { + writeClientsStructs(encoder, doc2.store, targetStateVector); + writeDeleteSet(encoder, createDeleteSetFromStructStore(doc2.store)); + }; + var encodeStateAsUpdateV2 = (doc2, encodedTargetStateVector = new Uint8Array([0]), encoder = new UpdateEncoderV2()) => { + const targetStateVector = decodeStateVector(encodedTargetStateVector); + writeStateAsUpdate(encoder, doc2, targetStateVector); + const updates = [encoder.toUint8Array()]; + if (doc2.store.pendingDs) { + updates.push(doc2.store.pendingDs); + } + if (doc2.store.pendingStructs) { + updates.push(diffUpdateV2(doc2.store.pendingStructs.update, encodedTargetStateVector)); + } + if (updates.length > 1) { + if (encoder.constructor === UpdateEncoderV1) { + return mergeUpdates(updates.map((update, i) => i === 0 ? update : convertUpdateFormatV2ToV1(update))); + } else if (encoder.constructor === UpdateEncoderV2) { + return mergeUpdatesV2(updates); + } + } + return updates[0]; + }; + var encodeStateAsUpdate = (doc2, encodedTargetStateVector) => encodeStateAsUpdateV2(doc2, encodedTargetStateVector, new UpdateEncoderV1()); + var readStateVector = (decoder) => { + const ss = /* @__PURE__ */ new Map(); + const ssLength = readVarUint(decoder.restDecoder); + for (let i = 0; i < ssLength; i++) { + const client = readVarUint(decoder.restDecoder); + const clock = readVarUint(decoder.restDecoder); + ss.set(client, clock); + } + return ss; + }; + var decodeStateVector = (decodedState) => readStateVector(new DSDecoderV1(createDecoder(decodedState))); + var writeStateVector = (encoder, sv) => { + writeVarUint(encoder.restEncoder, sv.size); + from(sv.entries()).sort((a, b) => b[0] - a[0]).forEach(([client, clock]) => { + writeVarUint(encoder.restEncoder, client); + writeVarUint(encoder.restEncoder, clock); + }); + return encoder; + }; + var writeDocumentStateVector = (encoder, doc2) => writeStateVector(encoder, getStateVector(doc2.store)); + var encodeStateVectorV2 = (doc2, encoder = new DSEncoderV2()) => { + if (doc2 instanceof Map) { + writeStateVector(encoder, doc2); + } else { + writeDocumentStateVector(encoder, doc2); + } + return encoder.toUint8Array(); + }; + var encodeStateVector = (doc2) => encodeStateVectorV2(doc2, new DSEncoderV1()); + var EventHandler = class { + constructor() { + this.l = []; + } + }; + var createEventHandler = () => new EventHandler(); + var addEventHandlerListener = (eventHandler, f) => eventHandler.l.push(f); + var removeEventHandlerListener = (eventHandler, f) => { + const l = eventHandler.l; + const len = l.length; + eventHandler.l = l.filter((g) => f !== g); + if (len === eventHandler.l.length) { + console.error("[yjs] Tried to remove event handler that doesn't exist."); + } + }; + var callEventHandlerListeners = (eventHandler, arg0, arg1) => callAll(eventHandler.l, [arg0, arg1]); + var ID = class { + /** + * @param {number} client client id + * @param {number} clock unique per client id, continuous number + */ + constructor(client, clock) { + this.client = client; + this.clock = clock; + } + }; + var compareIDs = (a, b) => a === b || a !== null && b !== null && a.client === b.client && a.clock === b.clock; + var createID = (client, clock) => new ID(client, clock); + var findRootTypeKey = (type) => { + for (const [key, value] of type.doc.share.entries()) { + if (value === type) { + return key; + } + } + throw unexpectedCase(); + }; + var Snapshot = class { + /** + * @param {DeleteSet} ds + * @param {Map} sv state map + */ + constructor(ds, sv) { + this.ds = ds; + this.sv = sv; + } + }; + var createSnapshot = (ds, sm) => new Snapshot(ds, sm); + var emptySnapshot = createSnapshot(createDeleteSet(), /* @__PURE__ */ new Map()); + var isVisible = (item, snapshot) => snapshot === void 0 ? !item.deleted : snapshot.sv.has(item.id.client) && (snapshot.sv.get(item.id.client) || 0) > item.id.clock && !isDeleted(snapshot.ds, item.id); + var splitSnapshotAffectedStructs = (transaction, snapshot) => { + const meta = setIfUndefined(transaction.meta, splitSnapshotAffectedStructs, create2); + const store = transaction.doc.store; + if (!meta.has(snapshot)) { + snapshot.sv.forEach((clock, client) => { + if (clock < getState(store, client)) { + getItemCleanStart(transaction, createID(client, clock)); + } + }); + iterateDeletedStructs(transaction, snapshot.ds, (_item) => { + }); + meta.add(snapshot); + } + }; + var StructStore = class { + constructor() { + this.clients = /* @__PURE__ */ new Map(); + this.pendingStructs = null; + this.pendingDs = null; + } + }; + var getStateVector = (store) => { + const sm = /* @__PURE__ */ new Map(); + store.clients.forEach((structs, client) => { + const struct = structs[structs.length - 1]; + sm.set(client, struct.id.clock + struct.length); + }); + return sm; + }; + var getState = (store, client) => { + const structs = store.clients.get(client); + if (structs === void 0) { + return 0; + } + const lastStruct = structs[structs.length - 1]; + return lastStruct.id.clock + lastStruct.length; + }; + var addStruct = (store, struct) => { + let structs = store.clients.get(struct.id.client); + if (structs === void 0) { + structs = []; + store.clients.set(struct.id.client, structs); + } else { + const lastStruct = structs[structs.length - 1]; + if (lastStruct.id.clock + lastStruct.length !== struct.id.clock) { + throw unexpectedCase(); + } + } + structs.push(struct); + }; + var findIndexSS = (structs, clock) => { + let left = 0; + let right = structs.length - 1; + let mid = structs[right]; + let midclock = mid.id.clock; + if (midclock === clock) { + return right; + } + let midindex = floor(clock / (midclock + mid.length - 1) * right); + while (left <= right) { + mid = structs[midindex]; + midclock = mid.id.clock; + if (midclock <= clock) { + if (clock < midclock + mid.length) { + return midindex; + } + left = midindex + 1; + } else { + right = midindex - 1; + } + midindex = floor((left + right) / 2); + } + throw unexpectedCase(); + }; + var find = (store, id2) => { + const structs = store.clients.get(id2.client); + return structs[findIndexSS(structs, id2.clock)]; + }; + var getItem = ( + /** @type {function(StructStore,ID):Item} */ + find + ); + var findIndexCleanStart = (transaction, structs, clock) => { + const index = findIndexSS(structs, clock); + const struct = structs[index]; + if (struct.id.clock < clock && struct instanceof Item) { + structs.splice(index + 1, 0, splitItem(transaction, struct, clock - struct.id.clock)); + return index + 1; + } + return index; + }; + var getItemCleanStart = (transaction, id2) => { + const structs = ( + /** @type {Array} */ + transaction.doc.store.clients.get(id2.client) + ); + return structs[findIndexCleanStart(transaction, structs, id2.clock)]; + }; + var getItemCleanEnd = (transaction, store, id2) => { + const structs = store.clients.get(id2.client); + const index = findIndexSS(structs, id2.clock); + const struct = structs[index]; + if (id2.clock !== struct.id.clock + struct.length - 1 && struct.constructor !== GC) { + structs.splice(index + 1, 0, splitItem(transaction, struct, id2.clock - struct.id.clock + 1)); + } + return struct; + }; + var replaceStruct = (store, struct, newStruct) => { + const structs = ( + /** @type {Array} */ + store.clients.get(struct.id.client) + ); + structs[findIndexSS(structs, struct.id.clock)] = newStruct; + }; + var iterateStructs = (transaction, structs, clockStart, len, f) => { + if (len === 0) { + return; + } + const clockEnd = clockStart + len; + let index = findIndexCleanStart(transaction, structs, clockStart); + let struct; + do { + struct = structs[index++]; + if (clockEnd < struct.id.clock + struct.length) { + findIndexCleanStart(transaction, structs, clockEnd); + } + f(struct); + } while (index < structs.length && structs[index].id.clock < clockEnd); + }; + var Transaction = class { + /** + * @param {Doc} doc + * @param {any} origin + * @param {boolean} local + */ + constructor(doc2, origin, local) { + this.doc = doc2; + this.deleteSet = new DeleteSet(); + this.beforeState = getStateVector(doc2.store); + this.afterState = /* @__PURE__ */ new Map(); + this.changed = /* @__PURE__ */ new Map(); + this.changedParentTypes = /* @__PURE__ */ new Map(); + this._mergeStructs = []; + this.origin = origin; + this.meta = /* @__PURE__ */ new Map(); + this.local = local; + this.subdocsAdded = /* @__PURE__ */ new Set(); + this.subdocsRemoved = /* @__PURE__ */ new Set(); + this.subdocsLoaded = /* @__PURE__ */ new Set(); + this._needFormattingCleanup = false; + } + }; + var writeUpdateMessageFromTransaction = (encoder, transaction) => { + if (transaction.deleteSet.clients.size === 0 && !any(transaction.afterState, (clock, client) => transaction.beforeState.get(client) !== clock)) { + return false; + } + sortAndMergeDeleteSet(transaction.deleteSet); + writeStructsFromTransaction(encoder, transaction); + writeDeleteSet(encoder, transaction.deleteSet); + return true; + }; + var addChangedTypeToTransaction = (transaction, type, parentSub) => { + const item = type._item; + if (item === null || item.id.clock < (transaction.beforeState.get(item.id.client) || 0) && !item.deleted) { + setIfUndefined(transaction.changed, type, create2).add(parentSub); + } + }; + var tryToMergeWithLefts = (structs, pos) => { + let right = structs[pos]; + let left = structs[pos - 1]; + let i = pos; + for (; i > 0; right = left, left = structs[--i - 1]) { + if (left.deleted === right.deleted && left.constructor === right.constructor) { + if (left.mergeWith(right)) { + if (right instanceof Item && right.parentSub !== null && /** @type {AbstractType} */ + right.parent._map.get(right.parentSub) === right) { + right.parent._map.set( + right.parentSub, + /** @type {Item} */ + left + ); + } + continue; + } + } + break; + } + const merged = pos - i; + if (merged) { + structs.splice(pos + 1 - merged, merged); + } + return merged; + }; + var tryGcDeleteSet = (ds, store, gcFilter) => { + for (const [client, deleteItems] of ds.clients.entries()) { + const structs = ( + /** @type {Array} */ + store.clients.get(client) + ); + for (let di = deleteItems.length - 1; di >= 0; di--) { + const deleteItem = deleteItems[di]; + const endDeleteItemClock = deleteItem.clock + deleteItem.len; + for (let si = findIndexSS(structs, deleteItem.clock), struct = structs[si]; si < structs.length && struct.id.clock < endDeleteItemClock; struct = structs[++si]) { + const struct2 = structs[si]; + if (deleteItem.clock + deleteItem.len <= struct2.id.clock) { + break; + } + if (struct2 instanceof Item && struct2.deleted && !struct2.keep && gcFilter(struct2)) { + struct2.gc(store, false); + } + } + } + } + }; + var tryMergeDeleteSet = (ds, store) => { + ds.clients.forEach((deleteItems, client) => { + const structs = ( + /** @type {Array} */ + store.clients.get(client) + ); + for (let di = deleteItems.length - 1; di >= 0; di--) { + const deleteItem = deleteItems[di]; + const mostRightIndexToCheck = min(structs.length - 1, 1 + findIndexSS(structs, deleteItem.clock + deleteItem.len - 1)); + for (let si = mostRightIndexToCheck, struct = structs[si]; si > 0 && struct.id.clock >= deleteItem.clock; struct = structs[si]) { + si -= 1 + tryToMergeWithLefts(structs, si); + } + } + }); + }; + var cleanupTransactions = (transactionCleanups, i) => { + if (i < transactionCleanups.length) { + const transaction = transactionCleanups[i]; + const doc2 = transaction.doc; + const store = doc2.store; + const ds = transaction.deleteSet; + const mergeStructs = transaction._mergeStructs; + try { + sortAndMergeDeleteSet(ds); + transaction.afterState = getStateVector(transaction.doc.store); + doc2.emit("beforeObserverCalls", [transaction, doc2]); + const fs = []; + transaction.changed.forEach( + (subs, itemtype) => fs.push(() => { + if (itemtype._item === null || !itemtype._item.deleted) { + itemtype._callObserver(transaction, subs); + } + }) + ); + fs.push(() => { + transaction.changedParentTypes.forEach((events, type) => { + if (type._dEH.l.length > 0 && (type._item === null || !type._item.deleted)) { + events = events.filter( + (event) => event.target._item === null || !event.target._item.deleted + ); + events.forEach((event) => { + event.currentTarget = type; + event._path = null; + }); + events.sort((event1, event2) => event1.path.length - event2.path.length); + fs.push(() => { + callEventHandlerListeners(type._dEH, events, transaction); + }); + } + }); + fs.push(() => doc2.emit("afterTransaction", [transaction, doc2])); + fs.push(() => { + if (transaction._needFormattingCleanup) { + cleanupYTextAfterTransaction(transaction); + } + }); + }); + callAll(fs, []); + } finally { + if (doc2.gc) { + tryGcDeleteSet(ds, store, doc2.gcFilter); + } + tryMergeDeleteSet(ds, store); + transaction.afterState.forEach((clock, client) => { + const beforeClock = transaction.beforeState.get(client) || 0; + if (beforeClock !== clock) { + const structs = ( + /** @type {Array} */ + store.clients.get(client) + ); + const firstChangePos = max(findIndexSS(structs, beforeClock), 1); + for (let i2 = structs.length - 1; i2 >= firstChangePos; ) { + i2 -= 1 + tryToMergeWithLefts(structs, i2); + } + } + }); + for (let i2 = mergeStructs.length - 1; i2 >= 0; i2--) { + const { client, clock } = mergeStructs[i2].id; + const structs = ( + /** @type {Array} */ + store.clients.get(client) + ); + const replacedStructPos = findIndexSS(structs, clock); + if (replacedStructPos + 1 < structs.length) { + if (tryToMergeWithLefts(structs, replacedStructPos + 1) > 1) { + continue; + } + } + if (replacedStructPos > 0) { + tryToMergeWithLefts(structs, replacedStructPos); + } + } + if (!transaction.local && transaction.afterState.get(doc2.clientID) !== transaction.beforeState.get(doc2.clientID)) { + print(ORANGE, BOLD, "[yjs] ", UNBOLD, RED, "Changed the client-id because another client seems to be using it."); + doc2.clientID = generateNewClientId(); + } + doc2.emit("afterTransactionCleanup", [transaction, doc2]); + if (doc2._observers.has("update")) { + const encoder = new UpdateEncoderV1(); + const hasContent2 = writeUpdateMessageFromTransaction(encoder, transaction); + if (hasContent2) { + doc2.emit("update", [encoder.toUint8Array(), transaction.origin, doc2, transaction]); + } + } + if (doc2._observers.has("updateV2")) { + const encoder = new UpdateEncoderV2(); + const hasContent2 = writeUpdateMessageFromTransaction(encoder, transaction); + if (hasContent2) { + doc2.emit("updateV2", [encoder.toUint8Array(), transaction.origin, doc2, transaction]); + } + } + const { subdocsAdded, subdocsLoaded, subdocsRemoved } = transaction; + if (subdocsAdded.size > 0 || subdocsRemoved.size > 0 || subdocsLoaded.size > 0) { + subdocsAdded.forEach((subdoc) => { + subdoc.clientID = doc2.clientID; + if (subdoc.collectionid == null) { + subdoc.collectionid = doc2.collectionid; + } + doc2.subdocs.add(subdoc); + }); + subdocsRemoved.forEach((subdoc) => doc2.subdocs.delete(subdoc)); + doc2.emit("subdocs", [{ loaded: subdocsLoaded, added: subdocsAdded, removed: subdocsRemoved }, doc2, transaction]); + subdocsRemoved.forEach((subdoc) => subdoc.destroy()); + } + if (transactionCleanups.length <= i + 1) { + doc2._transactionCleanups = []; + doc2.emit("afterAllTransactions", [doc2, transactionCleanups]); + } else { + cleanupTransactions(transactionCleanups, i + 1); + } + } + } + }; + var transact = (doc2, f, origin = null, local = true) => { + const transactionCleanups = doc2._transactionCleanups; + let initialCall = false; + let result = null; + if (doc2._transaction === null) { + initialCall = true; + doc2._transaction = new Transaction(doc2, origin, local); + transactionCleanups.push(doc2._transaction); + if (transactionCleanups.length === 1) { + doc2.emit("beforeAllTransactions", [doc2]); + } + doc2.emit("beforeTransaction", [doc2._transaction, doc2]); + } + try { + result = f(doc2._transaction); + } finally { + if (initialCall) { + const finishCleanup = doc2._transaction === transactionCleanups[0]; + doc2._transaction = null; + if (finishCleanup) { + cleanupTransactions(transactionCleanups, 0); + } + } + } + return result; + }; + function* lazyStructReaderGenerator(decoder) { + const numOfStateUpdates = readVarUint(decoder.restDecoder); + for (let i = 0; i < numOfStateUpdates; i++) { + const numberOfStructs = readVarUint(decoder.restDecoder); + const client = decoder.readClient(); + let clock = readVarUint(decoder.restDecoder); + for (let i2 = 0; i2 < numberOfStructs; i2++) { + const info = decoder.readInfo(); + if (info === 10) { + const len = readVarUint(decoder.restDecoder); + yield new Skip(createID(client, clock), len); + clock += len; + } else if ((BITS5 & info) !== 0) { + const cantCopyParentInfo = (info & (BIT7 | BIT8)) === 0; + const struct = new Item( + createID(client, clock), + null, + // left + (info & BIT8) === BIT8 ? decoder.readLeftID() : null, + // origin + null, + // right + (info & BIT7) === BIT7 ? decoder.readRightID() : null, + // right origin + // @ts-ignore Force writing a string here. + cantCopyParentInfo ? decoder.readParentInfo() ? decoder.readString() : decoder.readLeftID() : null, + // parent + cantCopyParentInfo && (info & BIT6) === BIT6 ? decoder.readString() : null, + // parentSub + readItemContent(decoder, info) + // item content + ); + yield struct; + clock += struct.length; + } else { + const len = decoder.readLen(); + yield new GC(createID(client, clock), len); + clock += len; + } + } + } + } + var LazyStructReader = class { + /** + * @param {UpdateDecoderV1 | UpdateDecoderV2} decoder + * @param {boolean} filterSkips + */ + constructor(decoder, filterSkips) { + this.gen = lazyStructReaderGenerator(decoder); + this.curr = null; + this.done = false; + this.filterSkips = filterSkips; + this.next(); + } + /** + * @return {Item | GC | Skip |null} + */ + next() { + do { + this.curr = this.gen.next().value || null; + } while (this.filterSkips && this.curr !== null && this.curr.constructor === Skip); + return this.curr; + } + }; + var LazyStructWriter = class { + /** + * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder + */ + constructor(encoder) { + this.currClient = 0; + this.startClock = 0; + this.written = 0; + this.encoder = encoder; + this.clientStructs = []; + } + }; + var mergeUpdates = (updates) => mergeUpdatesV2(updates, UpdateDecoderV1, UpdateEncoderV1); + var sliceStruct = (left, diff) => { + if (left.constructor === GC) { + const { client, clock } = left.id; + return new GC(createID(client, clock + diff), left.length - diff); + } else if (left.constructor === Skip) { + const { client, clock } = left.id; + return new Skip(createID(client, clock + diff), left.length - diff); + } else { + const leftItem = ( + /** @type {Item} */ + left + ); + const { client, clock } = leftItem.id; + return new Item( + createID(client, clock + diff), + null, + createID(client, clock + diff - 1), + null, + leftItem.rightOrigin, + leftItem.parent, + leftItem.parentSub, + leftItem.content.splice(diff) + ); + } + }; + var mergeUpdatesV2 = (updates, YDecoder = UpdateDecoderV2, YEncoder = UpdateEncoderV2) => { + if (updates.length === 1) { + return updates[0]; + } + const updateDecoders = updates.map((update) => new YDecoder(createDecoder(update))); + let lazyStructDecoders = updateDecoders.map((decoder) => new LazyStructReader(decoder, true)); + let currWrite = null; + const updateEncoder = new YEncoder(); + const lazyStructEncoder = new LazyStructWriter(updateEncoder); + while (true) { + lazyStructDecoders = lazyStructDecoders.filter((dec) => dec.curr !== null); + lazyStructDecoders.sort( + /** @type {function(any,any):number} */ + (dec1, dec2) => { + if (dec1.curr.id.client === dec2.curr.id.client) { + const clockDiff = dec1.curr.id.clock - dec2.curr.id.clock; + if (clockDiff === 0) { + return dec1.curr.constructor === dec2.curr.constructor ? 0 : dec1.curr.constructor === Skip ? 1 : -1; + } else { + return clockDiff; + } + } else { + return dec2.curr.id.client - dec1.curr.id.client; + } + } + ); + if (lazyStructDecoders.length === 0) { + break; + } + const currDecoder = lazyStructDecoders[0]; + const firstClient = ( + /** @type {Item | GC} */ + currDecoder.curr.id.client + ); + if (currWrite !== null) { + let curr = ( + /** @type {Item | GC | null} */ + currDecoder.curr + ); + let iterated = false; + while (curr !== null && curr.id.clock + curr.length <= currWrite.struct.id.clock + currWrite.struct.length && curr.id.client >= currWrite.struct.id.client) { + curr = currDecoder.next(); + iterated = true; + } + if (curr === null || // current decoder is empty + curr.id.client !== firstClient || // check whether there is another decoder that has has updates from `firstClient` + iterated && curr.id.clock > currWrite.struct.id.clock + currWrite.struct.length) { + continue; + } + if (firstClient !== currWrite.struct.id.client) { + writeStructToLazyStructWriter(lazyStructEncoder, currWrite.struct, currWrite.offset); + currWrite = { struct: curr, offset: 0 }; + currDecoder.next(); + } else { + if (currWrite.struct.id.clock + currWrite.struct.length < curr.id.clock) { + if (currWrite.struct.constructor === Skip) { + currWrite.struct.length = curr.id.clock + curr.length - currWrite.struct.id.clock; + } else { + writeStructToLazyStructWriter(lazyStructEncoder, currWrite.struct, currWrite.offset); + const diff = curr.id.clock - currWrite.struct.id.clock - currWrite.struct.length; + const struct = new Skip(createID(firstClient, currWrite.struct.id.clock + currWrite.struct.length), diff); + currWrite = { struct, offset: 0 }; + } + } else { + const diff = currWrite.struct.id.clock + currWrite.struct.length - curr.id.clock; + if (diff > 0) { + if (currWrite.struct.constructor === Skip) { + currWrite.struct.length -= diff; + } else { + curr = sliceStruct(curr, diff); + } + } + if (!currWrite.struct.mergeWith( + /** @type {any} */ + curr + )) { + writeStructToLazyStructWriter(lazyStructEncoder, currWrite.struct, currWrite.offset); + currWrite = { struct: curr, offset: 0 }; + currDecoder.next(); + } + } + } + } else { + currWrite = { struct: ( + /** @type {Item | GC} */ + currDecoder.curr + ), offset: 0 }; + currDecoder.next(); + } + for (let next = currDecoder.curr; next !== null && next.id.client === firstClient && next.id.clock === currWrite.struct.id.clock + currWrite.struct.length && next.constructor !== Skip; next = currDecoder.next()) { + writeStructToLazyStructWriter(lazyStructEncoder, currWrite.struct, currWrite.offset); + currWrite = { struct: next, offset: 0 }; + } + } + if (currWrite !== null) { + writeStructToLazyStructWriter(lazyStructEncoder, currWrite.struct, currWrite.offset); + currWrite = null; + } + finishLazyStructWriting(lazyStructEncoder); + const dss = updateDecoders.map((decoder) => readDeleteSet(decoder)); + const ds = mergeDeleteSets(dss); + writeDeleteSet(updateEncoder, ds); + return updateEncoder.toUint8Array(); + }; + var diffUpdateV2 = (update, sv, YDecoder = UpdateDecoderV2, YEncoder = UpdateEncoderV2) => { + const state = decodeStateVector(sv); + const encoder = new YEncoder(); + const lazyStructWriter = new LazyStructWriter(encoder); + const decoder = new YDecoder(createDecoder(update)); + const reader = new LazyStructReader(decoder, false); + while (reader.curr) { + const curr = reader.curr; + const currClient = curr.id.client; + const svClock = state.get(currClient) || 0; + if (reader.curr.constructor === Skip) { + reader.next(); + continue; + } + if (curr.id.clock + curr.length > svClock) { + writeStructToLazyStructWriter(lazyStructWriter, curr, max(svClock - curr.id.clock, 0)); + reader.next(); + while (reader.curr && reader.curr.id.client === currClient) { + writeStructToLazyStructWriter(lazyStructWriter, reader.curr, 0); + reader.next(); + } + } else { + while (reader.curr && reader.curr.id.client === currClient && reader.curr.id.clock + reader.curr.length <= svClock) { + reader.next(); + } + } + } + finishLazyStructWriting(lazyStructWriter); + const ds = readDeleteSet(decoder); + writeDeleteSet(encoder, ds); + return encoder.toUint8Array(); + }; + var flushLazyStructWriter = (lazyWriter) => { + if (lazyWriter.written > 0) { + lazyWriter.clientStructs.push({ written: lazyWriter.written, restEncoder: toUint8Array(lazyWriter.encoder.restEncoder) }); + lazyWriter.encoder.restEncoder = createEncoder(); + lazyWriter.written = 0; + } + }; + var writeStructToLazyStructWriter = (lazyWriter, struct, offset) => { + if (lazyWriter.written > 0 && lazyWriter.currClient !== struct.id.client) { + flushLazyStructWriter(lazyWriter); + } + if (lazyWriter.written === 0) { + lazyWriter.currClient = struct.id.client; + lazyWriter.encoder.writeClient(struct.id.client); + writeVarUint(lazyWriter.encoder.restEncoder, struct.id.clock + offset); + } + struct.write(lazyWriter.encoder, offset); + lazyWriter.written++; + }; + var finishLazyStructWriting = (lazyWriter) => { + flushLazyStructWriter(lazyWriter); + const restEncoder = lazyWriter.encoder.restEncoder; + writeVarUint(restEncoder, lazyWriter.clientStructs.length); + for (let i = 0; i < lazyWriter.clientStructs.length; i++) { + const partStructs = lazyWriter.clientStructs[i]; + writeVarUint(restEncoder, partStructs.written); + writeUint8Array(restEncoder, partStructs.restEncoder); + } + }; + var convertUpdateFormat = (update, blockTransformer, YDecoder, YEncoder) => { + const updateDecoder = new YDecoder(createDecoder(update)); + const lazyDecoder = new LazyStructReader(updateDecoder, false); + const updateEncoder = new YEncoder(); + const lazyWriter = new LazyStructWriter(updateEncoder); + for (let curr = lazyDecoder.curr; curr !== null; curr = lazyDecoder.next()) { + writeStructToLazyStructWriter(lazyWriter, blockTransformer(curr), 0); + } + finishLazyStructWriting(lazyWriter); + const ds = readDeleteSet(updateDecoder); + writeDeleteSet(updateEncoder, ds); + return updateEncoder.toUint8Array(); + }; + var convertUpdateFormatV2ToV1 = (update) => convertUpdateFormat(update, id, UpdateDecoderV2, UpdateEncoderV1); + var errorComputeChanges = "You must not compute changes after the event-handler fired."; + var YEvent = class { + /** + * @param {T} target The changed type. + * @param {Transaction} transaction + */ + constructor(target, transaction) { + this.target = target; + this.currentTarget = target; + this.transaction = transaction; + this._changes = null; + this._keys = null; + this._delta = null; + this._path = null; + } + /** + * Computes the path from `y` to the changed type. + * + * @todo v14 should standardize on path: Array<{parent, index}> because that is easier to work with. + * + * The following property holds: + * @example + * let type = y + * event.path.forEach(dir => { + * type = type.get(dir) + * }) + * type === event.target // => true + */ + get path() { + return this._path || (this._path = getPathTo(this.currentTarget, this.target)); + } + /** + * Check if a struct is deleted by this event. + * + * In contrast to change.deleted, this method also returns true if the struct was added and then deleted. + * + * @param {AbstractStruct} struct + * @return {boolean} + */ + deletes(struct) { + return isDeleted(this.transaction.deleteSet, struct.id); + } + /** + * @type {Map} + */ + get keys() { + if (this._keys === null) { + if (this.transaction.doc._transactionCleanups.length === 0) { + throw create3(errorComputeChanges); + } + const keys3 = /* @__PURE__ */ new Map(); + const target = this.target; + const changed = ( + /** @type Set */ + this.transaction.changed.get(target) + ); + changed.forEach((key) => { + if (key !== null) { + const item = ( + /** @type {Item} */ + target._map.get(key) + ); + let action; + let oldValue; + if (this.adds(item)) { + let prev = item.left; + while (prev !== null && this.adds(prev)) { + prev = prev.left; + } + if (this.deletes(item)) { + if (prev !== null && this.deletes(prev)) { + action = "delete"; + oldValue = last(prev.content.getContent()); + } else { + return; + } + } else { + if (prev !== null && this.deletes(prev)) { + action = "update"; + oldValue = last(prev.content.getContent()); + } else { + action = "add"; + oldValue = void 0; + } + } + } else { + if (this.deletes(item)) { + action = "delete"; + oldValue = last( + /** @type {Item} */ + item.content.getContent() + ); + } else { + return; + } + } + keys3.set(key, { action, oldValue }); + } + }); + this._keys = keys3; + } + return this._keys; + } + /** + * This is a computed property. Note that this can only be safely computed during the + * event call. Computing this property after other changes happened might result in + * unexpected behavior (incorrect computation of deltas). A safe way to collect changes + * is to store the `changes` or the `delta` object. Avoid storing the `transaction` object. + * + * @type {Array<{insert?: string | Array | object | AbstractType, retain?: number, delete?: number, attributes?: Object}>} + */ + get delta() { + return this.changes.delta; + } + /** + * Check if a struct is added by this event. + * + * In contrast to change.deleted, this method also returns true if the struct was added and then deleted. + * + * @param {AbstractStruct} struct + * @return {boolean} + */ + adds(struct) { + return struct.id.clock >= (this.transaction.beforeState.get(struct.id.client) || 0); + } + /** + * This is a computed property. Note that this can only be safely computed during the + * event call. Computing this property after other changes happened might result in + * unexpected behavior (incorrect computation of deltas). A safe way to collect changes + * is to store the `changes` or the `delta` object. Avoid storing the `transaction` object. + * + * @type {{added:Set,deleted:Set,keys:Map,delta:Array<{insert?:Array|string, delete?:number, retain?:number}>}} + */ + get changes() { + let changes = this._changes; + if (changes === null) { + if (this.transaction.doc._transactionCleanups.length === 0) { + throw create3(errorComputeChanges); + } + const target = this.target; + const added = create2(); + const deleted = create2(); + const delta = []; + changes = { + added, + deleted, + delta, + keys: this.keys + }; + const changed = ( + /** @type Set */ + this.transaction.changed.get(target) + ); + if (changed.has(null)) { + let lastOp = null; + const packOp = () => { + if (lastOp) { + delta.push(lastOp); + } + }; + for (let item = target._start; item !== null; item = item.right) { + if (item.deleted) { + if (this.deletes(item) && !this.adds(item)) { + if (lastOp === null || lastOp.delete === void 0) { + packOp(); + lastOp = { delete: 0 }; + } + lastOp.delete += item.length; + deleted.add(item); + } + } else { + if (this.adds(item)) { + if (lastOp === null || lastOp.insert === void 0) { + packOp(); + lastOp = { insert: [] }; + } + lastOp.insert = lastOp.insert.concat(item.content.getContent()); + added.add(item); + } else { + if (lastOp === null || lastOp.retain === void 0) { + packOp(); + lastOp = { retain: 0 }; + } + lastOp.retain += item.length; + } + } + } + if (lastOp !== null && lastOp.retain === void 0) { + packOp(); + } + } + this._changes = changes; + } + return ( + /** @type {any} */ + changes + ); + } + }; + var getPathTo = (parent, child) => { + const path = []; + while (child._item !== null && child !== parent) { + if (child._item.parentSub !== null) { + path.unshift(child._item.parentSub); + } else { + let i = 0; + let c = ( + /** @type {AbstractType} */ + child._item.parent._start + ); + while (c !== child._item && c !== null) { + if (!c.deleted && c.countable) { + i += c.length; + } + c = c.right; + } + path.unshift(i); + } + child = /** @type {AbstractType} */ + child._item.parent; + } + return path; + }; + var warnPrematureAccess = () => { + warn("Invalid access: Add Yjs type to a document before reading data."); + }; + var maxSearchMarker = 80; + var globalSearchMarkerTimestamp = 0; + var ArraySearchMarker = class { + /** + * @param {Item} p + * @param {number} index + */ + constructor(p, index) { + p.marker = true; + this.p = p; + this.index = index; + this.timestamp = globalSearchMarkerTimestamp++; + } + }; + var refreshMarkerTimestamp = (marker) => { + marker.timestamp = globalSearchMarkerTimestamp++; + }; + var overwriteMarker = (marker, p, index) => { + marker.p.marker = false; + marker.p = p; + p.marker = true; + marker.index = index; + marker.timestamp = globalSearchMarkerTimestamp++; + }; + var markPosition = (searchMarker, p, index) => { + if (searchMarker.length >= maxSearchMarker) { + const marker = searchMarker.reduce((a, b) => a.timestamp < b.timestamp ? a : b); + overwriteMarker(marker, p, index); + return marker; + } else { + const pm = new ArraySearchMarker(p, index); + searchMarker.push(pm); + return pm; + } + }; + var findMarker = (yarray, index) => { + if (yarray._start === null || index === 0 || yarray._searchMarker === null) { + return null; + } + const marker = yarray._searchMarker.length === 0 ? null : yarray._searchMarker.reduce((a, b) => abs(index - a.index) < abs(index - b.index) ? a : b); + let p = yarray._start; + let pindex = 0; + if (marker !== null) { + p = marker.p; + pindex = marker.index; + refreshMarkerTimestamp(marker); + } + while (p.right !== null && pindex < index) { + if (!p.deleted && p.countable) { + if (index < pindex + p.length) { + break; + } + pindex += p.length; + } + p = p.right; + } + while (p.left !== null && pindex > index) { + p = p.left; + if (!p.deleted && p.countable) { + pindex -= p.length; + } + } + while (p.left !== null && p.left.id.client === p.id.client && p.left.id.clock + p.left.length === p.id.clock) { + p = p.left; + if (!p.deleted && p.countable) { + pindex -= p.length; + } + } + if (marker !== null && abs(marker.index - pindex) < /** @type {YText|YArray} */ + p.parent.length / maxSearchMarker) { + overwriteMarker(marker, p, pindex); + return marker; + } else { + return markPosition(yarray._searchMarker, p, pindex); + } + }; + var updateMarkerChanges = (searchMarker, index, len) => { + for (let i = searchMarker.length - 1; i >= 0; i--) { + const m = searchMarker[i]; + if (len > 0) { + let p = m.p; + p.marker = false; + while (p && (p.deleted || !p.countable)) { + p = p.left; + if (p && !p.deleted && p.countable) { + m.index -= p.length; + } + } + if (p === null || p.marker === true) { + searchMarker.splice(i, 1); + continue; + } + m.p = p; + p.marker = true; + } + if (index < m.index || len > 0 && index === m.index) { + m.index = max(index, m.index + len); + } + } + }; + var callTypeObservers = (type, transaction, event) => { + const changedType = type; + const changedParentTypes = transaction.changedParentTypes; + while (true) { + setIfUndefined(changedParentTypes, type, () => []).push(event); + if (type._item === null) { + break; + } + type = /** @type {AbstractType} */ + type._item.parent; + } + callEventHandlerListeners(changedType._eH, event, transaction); + }; + var AbstractType = class { + constructor() { + this._item = null; + this._map = /* @__PURE__ */ new Map(); + this._start = null; + this.doc = null; + this._length = 0; + this._eH = createEventHandler(); + this._dEH = createEventHandler(); + this._searchMarker = null; + } + /** + * @return {AbstractType|null} + */ + get parent() { + return this._item ? ( + /** @type {AbstractType} */ + this._item.parent + ) : null; + } + /** + * Integrate this type into the Yjs instance. + * + * * Save this struct in the os + * * This type is sent to other client + * * Observer functions are fired + * + * @param {Doc} y The Yjs instance + * @param {Item|null} item + */ + _integrate(y, item) { + this.doc = y; + this._item = item; + } + /** + * @return {AbstractType} + */ + _copy() { + throw methodUnimplemented(); + } + /** + * Makes a copy of this data type that can be included somewhere else. + * + * Note that the content is only readable _after_ it has been included somewhere in the Ydoc. + * + * @return {AbstractType} + */ + clone() { + throw methodUnimplemented(); + } + /** + * @param {UpdateEncoderV1 | UpdateEncoderV2} _encoder + */ + _write(_encoder) { + } + /** + * The first non-deleted item + */ + get _first() { + let n = this._start; + while (n !== null && n.deleted) { + n = n.right; + } + return n; + } + /** + * Creates YEvent and calls all type observers. + * Must be implemented by each type. + * + * @param {Transaction} transaction + * @param {Set} _parentSubs Keys changed on this type. `null` if list was modified. + */ + _callObserver(transaction, _parentSubs) { + if (!transaction.local && this._searchMarker) { + this._searchMarker.length = 0; + } + } + /** + * Observe all events that are created on this type. + * + * @param {function(EventType, Transaction):void} f Observer function + */ + observe(f) { + addEventHandlerListener(this._eH, f); + } + /** + * Observe all events that are created by this type and its children. + * + * @param {function(Array>,Transaction):void} f Observer function + */ + observeDeep(f) { + addEventHandlerListener(this._dEH, f); + } + /** + * Unregister an observer function. + * + * @param {function(EventType,Transaction):void} f Observer function + */ + unobserve(f) { + removeEventHandlerListener(this._eH, f); + } + /** + * Unregister an observer function. + * + * @param {function(Array>,Transaction):void} f Observer function + */ + unobserveDeep(f) { + removeEventHandlerListener(this._dEH, f); + } + /** + * @abstract + * @return {any} + */ + toJSON() { + } + }; + var typeListSlice = (type, start, end) => { + type.doc ?? warnPrematureAccess(); + if (start < 0) { + start = type._length + start; + } + if (end < 0) { + end = type._length + end; + } + let len = end - start; + const cs = []; + let n = type._start; + while (n !== null && len > 0) { + if (n.countable && !n.deleted) { + const c = n.content.getContent(); + if (c.length <= start) { + start -= c.length; + } else { + for (let i = start; i < c.length && len > 0; i++) { + cs.push(c[i]); + len--; + } + start = 0; + } + } + n = n.right; + } + return cs; + }; + var typeListToArray = (type) => { + type.doc ?? warnPrematureAccess(); + const cs = []; + let n = type._start; + while (n !== null) { + if (n.countable && !n.deleted) { + const c = n.content.getContent(); + for (let i = 0; i < c.length; i++) { + cs.push(c[i]); + } + } + n = n.right; + } + return cs; + }; + var typeListForEach = (type, f) => { + let index = 0; + let n = type._start; + type.doc ?? warnPrematureAccess(); + while (n !== null) { + if (n.countable && !n.deleted) { + const c = n.content.getContent(); + for (let i = 0; i < c.length; i++) { + f(c[i], index++, type); + } + } + n = n.right; + } + }; + var typeListMap = (type, f) => { + const result = []; + typeListForEach(type, (c, i) => { + result.push(f(c, i, type)); + }); + return result; + }; + var typeListCreateIterator = (type) => { + let n = type._start; + let currentContent = null; + let currentContentIndex = 0; + return { + [Symbol.iterator]() { + return this; + }, + next: () => { + if (currentContent === null) { + while (n !== null && n.deleted) { + n = n.right; + } + if (n === null) { + return { + done: true, + value: void 0 + }; + } + currentContent = n.content.getContent(); + currentContentIndex = 0; + n = n.right; + } + const value = currentContent[currentContentIndex++]; + if (currentContent.length <= currentContentIndex) { + currentContent = null; + } + return { + done: false, + value + }; + } + }; + }; + var typeListGet = (type, index) => { + type.doc ?? warnPrematureAccess(); + const marker = findMarker(type, index); + let n = type._start; + if (marker !== null) { + n = marker.p; + index -= marker.index; + } + for (; n !== null; n = n.right) { + if (!n.deleted && n.countable) { + if (index < n.length) { + return n.content.getContent()[index]; + } + index -= n.length; + } + } + }; + var typeListInsertGenericsAfter = (transaction, parent, referenceItem, content) => { + let left = referenceItem; + const doc2 = transaction.doc; + const ownClientId = doc2.clientID; + const store = doc2.store; + const right = referenceItem === null ? parent._start : referenceItem.right; + let jsonContent = []; + const packJsonContent = () => { + if (jsonContent.length > 0) { + left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentAny(jsonContent)); + left.integrate(transaction, 0); + jsonContent = []; + } + }; + content.forEach((c) => { + if (c === null) { + jsonContent.push(c); + } else { + switch (c.constructor) { + case Number: + case Object: + case Boolean: + case Array: + case String: + jsonContent.push(c); + break; + default: + packJsonContent(); + switch (c.constructor) { + case Uint8Array: + case ArrayBuffer: + left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentBinary(new Uint8Array( + /** @type {Uint8Array} */ + c + ))); + left.integrate(transaction, 0); + break; + case Doc: + left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentDoc( + /** @type {Doc} */ + c + )); + left.integrate(transaction, 0); + break; + default: + if (c instanceof AbstractType) { + left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentType(c)); + left.integrate(transaction, 0); + } else { + throw new Error("Unexpected content type in insert operation"); + } + } + } + } + }); + packJsonContent(); + }; + var lengthExceeded = () => create3("Length exceeded!"); + var typeListInsertGenerics = (transaction, parent, index, content) => { + if (index > parent._length) { + throw lengthExceeded(); + } + if (index === 0) { + if (parent._searchMarker) { + updateMarkerChanges(parent._searchMarker, index, content.length); + } + return typeListInsertGenericsAfter(transaction, parent, null, content); + } + const startIndex = index; + const marker = findMarker(parent, index); + let n = parent._start; + if (marker !== null) { + n = marker.p; + index -= marker.index; + if (index === 0) { + n = n.prev; + index += n && n.countable && !n.deleted ? n.length : 0; + } + } + for (; n !== null; n = n.right) { + if (!n.deleted && n.countable) { + if (index <= n.length) { + if (index < n.length) { + getItemCleanStart(transaction, createID(n.id.client, n.id.clock + index)); + } + break; + } + index -= n.length; + } + } + if (parent._searchMarker) { + updateMarkerChanges(parent._searchMarker, startIndex, content.length); + } + return typeListInsertGenericsAfter(transaction, parent, n, content); + }; + var typeListPushGenerics = (transaction, parent, content) => { + const marker = (parent._searchMarker || []).reduce((maxMarker, currMarker) => currMarker.index > maxMarker.index ? currMarker : maxMarker, { index: 0, p: parent._start }); + let n = marker.p; + if (n) { + while (n.right) { + n = n.right; + } + } + return typeListInsertGenericsAfter(transaction, parent, n, content); + }; + var typeListDelete = (transaction, parent, index, length3) => { + if (length3 === 0) { + return; + } + const startIndex = index; + const startLength = length3; + const marker = findMarker(parent, index); + let n = parent._start; + if (marker !== null) { + n = marker.p; + index -= marker.index; + } + for (; n !== null && index > 0; n = n.right) { + if (!n.deleted && n.countable) { + if (index < n.length) { + getItemCleanStart(transaction, createID(n.id.client, n.id.clock + index)); + } + index -= n.length; + } + } + while (length3 > 0 && n !== null) { + if (!n.deleted) { + if (length3 < n.length) { + getItemCleanStart(transaction, createID(n.id.client, n.id.clock + length3)); + } + n.delete(transaction); + length3 -= n.length; + } + n = n.right; + } + if (length3 > 0) { + throw lengthExceeded(); + } + if (parent._searchMarker) { + updateMarkerChanges( + parent._searchMarker, + startIndex, + -startLength + length3 + /* in case we remove the above exception */ + ); + } + }; + var typeMapDelete = (transaction, parent, key) => { + const c = parent._map.get(key); + if (c !== void 0) { + c.delete(transaction); + } + }; + var typeMapSet = (transaction, parent, key, value) => { + const left = parent._map.get(key) || null; + const doc2 = transaction.doc; + const ownClientId = doc2.clientID; + let content; + if (value == null) { + content = new ContentAny([value]); + } else { + switch (value.constructor) { + case Number: + case Object: + case Boolean: + case Array: + case String: + case Date: + case BigInt: + content = new ContentAny([value]); + break; + case Uint8Array: + content = new ContentBinary( + /** @type {Uint8Array} */ + value + ); + break; + case Doc: + content = new ContentDoc( + /** @type {Doc} */ + value + ); + break; + default: + if (value instanceof AbstractType) { + content = new ContentType(value); + } else { + throw new Error("Unexpected content type"); + } + } + } + new Item(createID(ownClientId, getState(doc2.store, ownClientId)), left, left && left.lastId, null, null, parent, key, content).integrate(transaction, 0); + }; + var typeMapGet = (parent, key) => { + parent.doc ?? warnPrematureAccess(); + const val = parent._map.get(key); + return val !== void 0 && !val.deleted ? val.content.getContent()[val.length - 1] : void 0; + }; + var typeMapGetAll = (parent) => { + const res = {}; + parent.doc ?? warnPrematureAccess(); + parent._map.forEach((value, key) => { + if (!value.deleted) { + res[key] = value.content.getContent()[value.length - 1]; + } + }); + return res; + }; + var typeMapHas = (parent, key) => { + parent.doc ?? warnPrematureAccess(); + const val = parent._map.get(key); + return val !== void 0 && !val.deleted; + }; + var typeMapGetAllSnapshot = (parent, snapshot) => { + const res = {}; + parent._map.forEach((value, key) => { + let v = value; + while (v !== null && (!snapshot.sv.has(v.id.client) || v.id.clock >= (snapshot.sv.get(v.id.client) || 0))) { + v = v.left; + } + if (v !== null && isVisible(v, snapshot)) { + res[key] = v.content.getContent()[v.length - 1]; + } + }); + return res; + }; + var createMapIterator = (type) => { + type.doc ?? warnPrematureAccess(); + return iteratorFilter( + type._map.entries(), + /** @param {any} entry */ + (entry) => !entry[1].deleted + ); + }; + var YArrayEvent = class extends YEvent { + }; + var YArray = class _YArray extends AbstractType { + constructor() { + super(); + this._prelimContent = []; + this._searchMarker = []; + } + /** + * Construct a new YArray containing the specified items. + * @template {Object|Array|number|null|string|Uint8Array} T + * @param {Array} items + * @return {YArray} + */ + static from(items) { + const a = new _YArray(); + a.push(items); + return a; + } + /** + * Integrate this type into the Yjs instance. + * + * * Save this struct in the os + * * This type is sent to other client + * * Observer functions are fired + * + * @param {Doc} y The Yjs instance + * @param {Item} item + */ + _integrate(y, item) { + super._integrate(y, item); + this.insert( + 0, + /** @type {Array} */ + this._prelimContent + ); + this._prelimContent = null; + } + /** + * @return {YArray} + */ + _copy() { + return new _YArray(); + } + /** + * Makes a copy of this data type that can be included somewhere else. + * + * Note that the content is only readable _after_ it has been included somewhere in the Ydoc. + * + * @return {YArray} + */ + clone() { + const arr = new _YArray(); + arr.insert(0, this.toArray().map( + (el) => el instanceof AbstractType ? ( + /** @type {typeof el} */ + el.clone() + ) : el + )); + return arr; + } + get length() { + this.doc ?? warnPrematureAccess(); + return this._length; + } + /** + * Creates YArrayEvent and calls observers. + * + * @param {Transaction} transaction + * @param {Set} parentSubs Keys changed on this type. `null` if list was modified. + */ + _callObserver(transaction, parentSubs) { + super._callObserver(transaction, parentSubs); + callTypeObservers(this, transaction, new YArrayEvent(this, transaction)); + } + /** + * Inserts new content at an index. + * + * Important: This function expects an array of content. Not just a content + * object. The reason for this "weirdness" is that inserting several elements + * is very efficient when it is done as a single operation. + * + * @example + * // Insert character 'a' at position 0 + * yarray.insert(0, ['a']) + * // Insert numbers 1, 2 at position 1 + * yarray.insert(1, [1, 2]) + * + * @param {number} index The index to insert content at. + * @param {Array} content The array of content + */ + insert(index, content) { + if (this.doc !== null) { + transact(this.doc, (transaction) => { + typeListInsertGenerics( + transaction, + this, + index, + /** @type {any} */ + content + ); + }); + } else { + this._prelimContent.splice(index, 0, ...content); + } + } + /** + * Appends content to this YArray. + * + * @param {Array} content Array of content to append. + * + * @todo Use the following implementation in all types. + */ + push(content) { + if (this.doc !== null) { + transact(this.doc, (transaction) => { + typeListPushGenerics( + transaction, + this, + /** @type {any} */ + content + ); + }); + } else { + this._prelimContent.push(...content); + } + } + /** + * Prepends content to this YArray. + * + * @param {Array} content Array of content to prepend. + */ + unshift(content) { + this.insert(0, content); + } + /** + * Deletes elements starting from an index. + * + * @param {number} index Index at which to start deleting elements + * @param {number} length The number of elements to remove. Defaults to 1. + */ + delete(index, length3 = 1) { + if (this.doc !== null) { + transact(this.doc, (transaction) => { + typeListDelete(transaction, this, index, length3); + }); + } else { + this._prelimContent.splice(index, length3); + } + } + /** + * Returns the i-th element from a YArray. + * + * @param {number} index The index of the element to return from the YArray + * @return {T} + */ + get(index) { + return typeListGet(this, index); + } + /** + * Transforms this YArray to a JavaScript Array. + * + * @return {Array} + */ + toArray() { + return typeListToArray(this); + } + /** + * Returns a portion of this YArray into a JavaScript Array selected + * from start to end (end not included). + * + * @param {number} [start] + * @param {number} [end] + * @return {Array} + */ + slice(start = 0, end = this.length) { + return typeListSlice(this, start, end); + } + /** + * Transforms this Shared Type to a JSON object. + * + * @return {Array} + */ + toJSON() { + return this.map((c) => c instanceof AbstractType ? c.toJSON() : c); + } + /** + * Returns an Array with the result of calling a provided function on every + * element of this YArray. + * + * @template M + * @param {function(T,number,YArray):M} f Function that produces an element of the new Array + * @return {Array} A new array with each element being the result of the + * callback function + */ + map(f) { + return typeListMap( + this, + /** @type {any} */ + f + ); + } + /** + * Executes a provided function once on every element of this YArray. + * + * @param {function(T,number,YArray):void} f A function to execute on every element of this YArray. + */ + forEach(f) { + typeListForEach(this, f); + } + /** + * @return {IterableIterator} + */ + [Symbol.iterator]() { + return typeListCreateIterator(this); + } + /** + * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder + */ + _write(encoder) { + encoder.writeTypeRef(YArrayRefID); + } + }; + var readYArray = (_decoder) => new YArray(); + var YMapEvent = class extends YEvent { + /** + * @param {YMap} ymap The YArray that changed. + * @param {Transaction} transaction + * @param {Set} subs The keys that changed. + */ + constructor(ymap, transaction, subs) { + super(ymap, transaction); + this.keysChanged = subs; + } + }; + var YMap = class _YMap extends AbstractType { + /** + * + * @param {Iterable=} entries - an optional iterable to initialize the YMap + */ + constructor(entries) { + super(); + this._prelimContent = null; + if (entries === void 0) { + this._prelimContent = /* @__PURE__ */ new Map(); + } else { + this._prelimContent = new Map(entries); + } + } + /** + * Integrate this type into the Yjs instance. + * + * * Save this struct in the os + * * This type is sent to other client + * * Observer functions are fired + * + * @param {Doc} y The Yjs instance + * @param {Item} item + */ + _integrate(y, item) { + super._integrate(y, item); + this._prelimContent.forEach((value, key) => { + this.set(key, value); + }); + this._prelimContent = null; + } + /** + * @return {YMap} + */ + _copy() { + return new _YMap(); + } + /** + * Makes a copy of this data type that can be included somewhere else. + * + * Note that the content is only readable _after_ it has been included somewhere in the Ydoc. + * + * @return {YMap} + */ + clone() { + const map2 = new _YMap(); + this.forEach((value, key) => { + map2.set(key, value instanceof AbstractType ? ( + /** @type {typeof value} */ + value.clone() + ) : value); + }); + return map2; + } + /** + * Creates YMapEvent and calls observers. + * + * @param {Transaction} transaction + * @param {Set} parentSubs Keys changed on this type. `null` if list was modified. + */ + _callObserver(transaction, parentSubs) { + callTypeObservers(this, transaction, new YMapEvent(this, transaction, parentSubs)); + } + /** + * Transforms this Shared Type to a JSON object. + * + * @return {Object} + */ + toJSON() { + this.doc ?? warnPrematureAccess(); + const map2 = {}; + this._map.forEach((item, key) => { + if (!item.deleted) { + const v = item.content.getContent()[item.length - 1]; + map2[key] = v instanceof AbstractType ? v.toJSON() : v; + } + }); + return map2; + } + /** + * Returns the size of the YMap (count of key/value pairs) + * + * @return {number} + */ + get size() { + return [...createMapIterator(this)].length; + } + /** + * Returns the keys for each element in the YMap Type. + * + * @return {IterableIterator} + */ + keys() { + return iteratorMap( + createMapIterator(this), + /** @param {any} v */ + (v) => v[0] + ); + } + /** + * Returns the values for each element in the YMap Type. + * + * @return {IterableIterator} + */ + values() { + return iteratorMap( + createMapIterator(this), + /** @param {any} v */ + (v) => v[1].content.getContent()[v[1].length - 1] + ); + } + /** + * Returns an Iterator of [key, value] pairs + * + * @return {IterableIterator<[string, MapType]>} + */ + entries() { + return iteratorMap( + createMapIterator(this), + /** @param {any} v */ + (v) => ( + /** @type {any} */ + [v[0], v[1].content.getContent()[v[1].length - 1]] + ) + ); + } + /** + * Executes a provided function on once on every key-value pair. + * + * @param {function(MapType,string,YMap):void} f A function to execute on every element of this YArray. + */ + forEach(f) { + this.doc ?? warnPrematureAccess(); + this._map.forEach((item, key) => { + if (!item.deleted) { + f(item.content.getContent()[item.length - 1], key, this); + } + }); + } + /** + * Returns an Iterator of [key, value] pairs + * + * @return {IterableIterator<[string, MapType]>} + */ + [Symbol.iterator]() { + return this.entries(); + } + /** + * Remove a specified element from this YMap. + * + * @param {string} key The key of the element to remove. + */ + delete(key) { + if (this.doc !== null) { + transact(this.doc, (transaction) => { + typeMapDelete(transaction, this, key); + }); + } else { + this._prelimContent.delete(key); + } + } + /** + * Adds or updates an element with a specified key and value. + * @template {MapType} VAL + * + * @param {string} key The key of the element to add to this YMap + * @param {VAL} value The value of the element to add + * @return {VAL} + */ + set(key, value) { + if (this.doc !== null) { + transact(this.doc, (transaction) => { + typeMapSet( + transaction, + this, + key, + /** @type {any} */ + value + ); + }); + } else { + this._prelimContent.set(key, value); + } + return value; + } + /** + * Returns a specified element from this YMap. + * + * @param {string} key + * @return {MapType|undefined} + */ + get(key) { + return ( + /** @type {any} */ + typeMapGet(this, key) + ); + } + /** + * Returns a boolean indicating whether the specified key exists or not. + * + * @param {string} key The key to test. + * @return {boolean} + */ + has(key) { + return typeMapHas(this, key); + } + /** + * Removes all elements from this YMap. + */ + clear() { + if (this.doc !== null) { + transact(this.doc, (transaction) => { + this.forEach(function(_value, key, map2) { + typeMapDelete(transaction, map2, key); + }); + }); + } else { + this._prelimContent.clear(); + } + } + /** + * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder + */ + _write(encoder) { + encoder.writeTypeRef(YMapRefID); + } + }; + var readYMap = (_decoder) => new YMap(); + var equalAttrs = (a, b) => a === b || typeof a === "object" && typeof b === "object" && a && b && equalFlat(a, b); + var ItemTextListPosition = class { + /** + * @param {Item|null} left + * @param {Item|null} right + * @param {number} index + * @param {Map} currentAttributes + */ + constructor(left, right, index, currentAttributes) { + this.left = left; + this.right = right; + this.index = index; + this.currentAttributes = currentAttributes; + } + /** + * Only call this if you know that this.right is defined + */ + forward() { + if (this.right === null) { + unexpectedCase(); + } + switch (this.right.content.constructor) { + case ContentFormat: + if (!this.right.deleted) { + updateCurrentAttributes( + this.currentAttributes, + /** @type {ContentFormat} */ + this.right.content + ); + } + break; + default: + if (!this.right.deleted) { + this.index += this.right.length; + } + break; + } + this.left = this.right; + this.right = this.right.right; + } + }; + var findNextPosition = (transaction, pos, count) => { + while (pos.right !== null && count > 0) { + switch (pos.right.content.constructor) { + case ContentFormat: + if (!pos.right.deleted) { + updateCurrentAttributes( + pos.currentAttributes, + /** @type {ContentFormat} */ + pos.right.content + ); + } + break; + default: + if (!pos.right.deleted) { + if (count < pos.right.length) { + getItemCleanStart(transaction, createID(pos.right.id.client, pos.right.id.clock + count)); + } + pos.index += pos.right.length; + count -= pos.right.length; + } + break; + } + pos.left = pos.right; + pos.right = pos.right.right; + } + return pos; + }; + var findPosition = (transaction, parent, index, useSearchMarker) => { + const currentAttributes = /* @__PURE__ */ new Map(); + const marker = useSearchMarker ? findMarker(parent, index) : null; + if (marker) { + const pos = new ItemTextListPosition(marker.p.left, marker.p, marker.index, currentAttributes); + return findNextPosition(transaction, pos, index - marker.index); + } else { + const pos = new ItemTextListPosition(null, parent._start, 0, currentAttributes); + return findNextPosition(transaction, pos, index); + } + }; + var insertNegatedAttributes = (transaction, parent, currPos, negatedAttributes) => { + while (currPos.right !== null && (currPos.right.deleted === true || currPos.right.content.constructor === ContentFormat && equalAttrs( + negatedAttributes.get( + /** @type {ContentFormat} */ + currPos.right.content.key + ), + /** @type {ContentFormat} */ + currPos.right.content.value + ))) { + if (!currPos.right.deleted) { + negatedAttributes.delete( + /** @type {ContentFormat} */ + currPos.right.content.key + ); + } + currPos.forward(); + } + const doc2 = transaction.doc; + const ownClientId = doc2.clientID; + negatedAttributes.forEach((val, key) => { + const left = currPos.left; + const right = currPos.right; + const nextFormat = new Item(createID(ownClientId, getState(doc2.store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentFormat(key, val)); + nextFormat.integrate(transaction, 0); + currPos.right = nextFormat; + currPos.forward(); + }); + }; + var updateCurrentAttributes = (currentAttributes, format) => { + const { key, value } = format; + if (value === null) { + currentAttributes.delete(key); + } else { + currentAttributes.set(key, value); + } + }; + var minimizeAttributeChanges = (currPos, attributes) => { + while (true) { + if (currPos.right === null) { + break; + } else if (currPos.right.deleted || currPos.right.content.constructor === ContentFormat && equalAttrs( + attributes[ + /** @type {ContentFormat} */ + currPos.right.content.key + ] ?? null, + /** @type {ContentFormat} */ + currPos.right.content.value + )) ; + else { + break; + } + currPos.forward(); + } + }; + var insertAttributes = (transaction, parent, currPos, attributes) => { + const doc2 = transaction.doc; + const ownClientId = doc2.clientID; + const negatedAttributes = /* @__PURE__ */ new Map(); + for (const key in attributes) { + const val = attributes[key]; + const currentVal = currPos.currentAttributes.get(key) ?? null; + if (!equalAttrs(currentVal, val)) { + negatedAttributes.set(key, currentVal); + const { left, right } = currPos; + currPos.right = new Item(createID(ownClientId, getState(doc2.store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentFormat(key, val)); + currPos.right.integrate(transaction, 0); + currPos.forward(); + } + } + return negatedAttributes; + }; + var insertText = (transaction, parent, currPos, text2, attributes) => { + currPos.currentAttributes.forEach((_val, key) => { + if (attributes[key] === void 0) { + attributes[key] = null; + } + }); + const doc2 = transaction.doc; + const ownClientId = doc2.clientID; + minimizeAttributeChanges(currPos, attributes); + const negatedAttributes = insertAttributes(transaction, parent, currPos, attributes); + const content = text2.constructor === String ? new ContentString( + /** @type {string} */ + text2 + ) : text2 instanceof AbstractType ? new ContentType(text2) : new ContentEmbed(text2); + let { left, right, index } = currPos; + if (parent._searchMarker) { + updateMarkerChanges(parent._searchMarker, currPos.index, content.getLength()); + } + right = new Item(createID(ownClientId, getState(doc2.store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, content); + right.integrate(transaction, 0); + currPos.right = right; + currPos.index = index; + currPos.forward(); + insertNegatedAttributes(transaction, parent, currPos, negatedAttributes); + }; + var formatText = (transaction, parent, currPos, length3, attributes) => { + const doc2 = transaction.doc; + const ownClientId = doc2.clientID; + minimizeAttributeChanges(currPos, attributes); + const negatedAttributes = insertAttributes(transaction, parent, currPos, attributes); + iterationLoop: while (currPos.right !== null && (length3 > 0 || negatedAttributes.size > 0 && (currPos.right.deleted || currPos.right.content.constructor === ContentFormat))) { + if (!currPos.right.deleted) { + switch (currPos.right.content.constructor) { + case ContentFormat: { + const { key, value } = ( + /** @type {ContentFormat} */ + currPos.right.content + ); + const attr = attributes[key]; + if (attr !== void 0) { + if (equalAttrs(attr, value)) { + negatedAttributes.delete(key); + } else { + if (length3 === 0) { + break iterationLoop; + } + negatedAttributes.set(key, value); + } + currPos.right.delete(transaction); + } else { + currPos.currentAttributes.set(key, value); + } + break; + } + default: + if (length3 < currPos.right.length) { + getItemCleanStart(transaction, createID(currPos.right.id.client, currPos.right.id.clock + length3)); + } + length3 -= currPos.right.length; + break; + } + } + currPos.forward(); + } + if (length3 > 0) { + let newlines = ""; + for (; length3 > 0; length3--) { + newlines += "\n"; + } + currPos.right = new Item(createID(ownClientId, getState(doc2.store, ownClientId)), currPos.left, currPos.left && currPos.left.lastId, currPos.right, currPos.right && currPos.right.id, parent, null, new ContentString(newlines)); + currPos.right.integrate(transaction, 0); + currPos.forward(); + } + insertNegatedAttributes(transaction, parent, currPos, negatedAttributes); + }; + var cleanupFormattingGap = (transaction, start, curr, startAttributes, currAttributes) => { + let end = start; + const endFormats = create(); + while (end && (!end.countable || end.deleted)) { + if (!end.deleted && end.content.constructor === ContentFormat) { + const cf = ( + /** @type {ContentFormat} */ + end.content + ); + endFormats.set(cf.key, cf); + } + end = end.right; + } + let cleanups = 0; + let reachedCurr = false; + while (start !== end) { + if (curr === start) { + reachedCurr = true; + } + if (!start.deleted) { + const content = start.content; + switch (content.constructor) { + case ContentFormat: { + const { key, value } = ( + /** @type {ContentFormat} */ + content + ); + const startAttrValue = startAttributes.get(key) ?? null; + if (endFormats.get(key) !== content || startAttrValue === value) { + start.delete(transaction); + cleanups++; + if (!reachedCurr && (currAttributes.get(key) ?? null) === value && startAttrValue !== value) { + if (startAttrValue === null) { + currAttributes.delete(key); + } else { + currAttributes.set(key, startAttrValue); + } + } + } + if (!reachedCurr && !start.deleted) { + updateCurrentAttributes( + currAttributes, + /** @type {ContentFormat} */ + content + ); + } + break; + } + } + } + start = /** @type {Item} */ + start.right; + } + return cleanups; + }; + var cleanupContextlessFormattingGap = (transaction, item) => { + while (item && item.right && (item.right.deleted || !item.right.countable)) { + item = item.right; + } + const attrs = /* @__PURE__ */ new Set(); + while (item && (item.deleted || !item.countable)) { + if (!item.deleted && item.content.constructor === ContentFormat) { + const key = ( + /** @type {ContentFormat} */ + item.content.key + ); + if (attrs.has(key)) { + item.delete(transaction); + } else { + attrs.add(key); + } + } + item = item.left; + } + }; + var cleanupYTextFormatting = (type) => { + let res = 0; + transact( + /** @type {Doc} */ + type.doc, + (transaction) => { + let start = ( + /** @type {Item} */ + type._start + ); + let end = type._start; + let startAttributes = create(); + const currentAttributes = copy(startAttributes); + while (end) { + if (end.deleted === false) { + switch (end.content.constructor) { + case ContentFormat: + updateCurrentAttributes( + currentAttributes, + /** @type {ContentFormat} */ + end.content + ); + break; + default: + res += cleanupFormattingGap(transaction, start, end, startAttributes, currentAttributes); + startAttributes = copy(currentAttributes); + start = end; + break; + } + } + end = end.right; + } + } + ); + return res; + }; + var cleanupYTextAfterTransaction = (transaction) => { + const needFullCleanup = /* @__PURE__ */ new Set(); + const doc2 = transaction.doc; + for (const [client, afterClock] of transaction.afterState.entries()) { + const clock = transaction.beforeState.get(client) || 0; + if (afterClock === clock) { + continue; + } + iterateStructs( + transaction, + /** @type {Array} */ + doc2.store.clients.get(client), + clock, + afterClock, + (item) => { + if (!item.deleted && /** @type {Item} */ + item.content.constructor === ContentFormat && item.constructor !== GC) { + needFullCleanup.add( + /** @type {any} */ + item.parent + ); + } + } + ); + } + transact(doc2, (t) => { + iterateDeletedStructs(transaction, transaction.deleteSet, (item) => { + if (item instanceof GC || !/** @type {YText} */ + item.parent._hasFormatting || needFullCleanup.has( + /** @type {YText} */ + item.parent + )) { + return; + } + const parent = ( + /** @type {YText} */ + item.parent + ); + if (item.content.constructor === ContentFormat) { + needFullCleanup.add(parent); + } else { + cleanupContextlessFormattingGap(t, item); + } + }); + for (const yText of needFullCleanup) { + cleanupYTextFormatting(yText); + } + }); + }; + var deleteText = (transaction, currPos, length3) => { + const startLength = length3; + const startAttrs = copy(currPos.currentAttributes); + const start = currPos.right; + while (length3 > 0 && currPos.right !== null) { + if (currPos.right.deleted === false) { + switch (currPos.right.content.constructor) { + case ContentType: + case ContentEmbed: + case ContentString: + if (length3 < currPos.right.length) { + getItemCleanStart(transaction, createID(currPos.right.id.client, currPos.right.id.clock + length3)); + } + length3 -= currPos.right.length; + currPos.right.delete(transaction); + break; + } + } + currPos.forward(); + } + if (start) { + cleanupFormattingGap(transaction, start, currPos.right, startAttrs, currPos.currentAttributes); + } + const parent = ( + /** @type {AbstractType} */ + /** @type {Item} */ + (currPos.left || currPos.right).parent + ); + if (parent._searchMarker) { + updateMarkerChanges(parent._searchMarker, currPos.index, -startLength + length3); + } + return currPos; + }; + var YTextEvent = class extends YEvent { + /** + * @param {YText} ytext + * @param {Transaction} transaction + * @param {Set} subs The keys that changed + */ + constructor(ytext, transaction, subs) { + super(ytext, transaction); + this.childListChanged = false; + this.keysChanged = /* @__PURE__ */ new Set(); + subs.forEach((sub) => { + if (sub === null) { + this.childListChanged = true; + } else { + this.keysChanged.add(sub); + } + }); + } + /** + * @type {{added:Set,deleted:Set,keys:Map,delta:Array<{insert?:Array|string, delete?:number, retain?:number}>}} + */ + get changes() { + if (this._changes === null) { + const changes = { + keys: this.keys, + delta: this.delta, + added: /* @__PURE__ */ new Set(), + deleted: /* @__PURE__ */ new Set() + }; + this._changes = changes; + } + return ( + /** @type {any} */ + this._changes + ); + } + /** + * Compute the changes in the delta format. + * A {@link https://quilljs.com/docs/delta/|Quill Delta}) that represents the changes on the document. + * + * @type {Array<{insert?:string|object|AbstractType, delete?:number, retain?:number, attributes?: Object}>} + * + * @public + */ + get delta() { + if (this._delta === null) { + const y = ( + /** @type {Doc} */ + this.target.doc + ); + const delta = []; + transact(y, (transaction) => { + const currentAttributes = /* @__PURE__ */ new Map(); + const oldAttributes = /* @__PURE__ */ new Map(); + let item = this.target._start; + let action = null; + const attributes = {}; + let insert = ""; + let retain = 0; + let deleteLen = 0; + const addOp = () => { + if (action !== null) { + let op = null; + switch (action) { + case "delete": + if (deleteLen > 0) { + op = { delete: deleteLen }; + } + deleteLen = 0; + break; + case "insert": + if (typeof insert === "object" || insert.length > 0) { + op = { insert }; + if (currentAttributes.size > 0) { + op.attributes = {}; + currentAttributes.forEach((value, key) => { + if (value !== null) { + op.attributes[key] = value; + } + }); + } + } + insert = ""; + break; + case "retain": + if (retain > 0) { + op = { retain }; + if (!isEmpty(attributes)) { + op.attributes = assign({}, attributes); + } + } + retain = 0; + break; + } + if (op) delta.push(op); + action = null; + } + }; + while (item !== null) { + switch (item.content.constructor) { + case ContentType: + case ContentEmbed: + if (this.adds(item)) { + if (!this.deletes(item)) { + addOp(); + action = "insert"; + insert = item.content.getContent()[0]; + addOp(); + } + } else if (this.deletes(item)) { + if (action !== "delete") { + addOp(); + action = "delete"; + } + deleteLen += 1; + } else if (!item.deleted) { + if (action !== "retain") { + addOp(); + action = "retain"; + } + retain += 1; + } + break; + case ContentString: + if (this.adds(item)) { + if (!this.deletes(item)) { + if (action !== "insert") { + addOp(); + action = "insert"; + } + insert += /** @type {ContentString} */ + item.content.str; + } + } else if (this.deletes(item)) { + if (action !== "delete") { + addOp(); + action = "delete"; + } + deleteLen += item.length; + } else if (!item.deleted) { + if (action !== "retain") { + addOp(); + action = "retain"; + } + retain += item.length; + } + break; + case ContentFormat: { + const { key, value } = ( + /** @type {ContentFormat} */ + item.content + ); + if (this.adds(item)) { + if (!this.deletes(item)) { + const curVal = currentAttributes.get(key) ?? null; + if (!equalAttrs(curVal, value)) { + if (action === "retain") { + addOp(); + } + if (equalAttrs(value, oldAttributes.get(key) ?? null)) { + delete attributes[key]; + } else { + attributes[key] = value; + } + } else if (value !== null) { + item.delete(transaction); + } + } + } else if (this.deletes(item)) { + oldAttributes.set(key, value); + const curVal = currentAttributes.get(key) ?? null; + if (!equalAttrs(curVal, value)) { + if (action === "retain") { + addOp(); + } + attributes[key] = curVal; + } + } else if (!item.deleted) { + oldAttributes.set(key, value); + const attr = attributes[key]; + if (attr !== void 0) { + if (!equalAttrs(attr, value)) { + if (action === "retain") { + addOp(); + } + if (value === null) { + delete attributes[key]; + } else { + attributes[key] = value; + } + } else if (attr !== null) { + item.delete(transaction); + } + } + } + if (!item.deleted) { + if (action === "insert") { + addOp(); + } + updateCurrentAttributes( + currentAttributes, + /** @type {ContentFormat} */ + item.content + ); + } + break; + } + } + item = item.right; + } + addOp(); + while (delta.length > 0) { + const lastOp = delta[delta.length - 1]; + if (lastOp.retain !== void 0 && lastOp.attributes === void 0) { + delta.pop(); + } else { + break; + } + } + }); + this._delta = delta; + } + return ( + /** @type {any} */ + this._delta + ); + } + }; + var YText = class _YText extends AbstractType { + /** + * @param {String} [string] The initial value of the YText. + */ + constructor(string) { + super(); + this._pending = string !== void 0 ? [() => this.insert(0, string)] : []; + this._searchMarker = []; + this._hasFormatting = false; + } + /** + * Number of characters of this text type. + * + * @type {number} + */ + get length() { + this.doc ?? warnPrematureAccess(); + return this._length; + } + /** + * @param {Doc} y + * @param {Item} item + */ + _integrate(y, item) { + super._integrate(y, item); + try { + this._pending.forEach((f) => f()); + } catch (e) { + console.error(e); + } + this._pending = null; + } + _copy() { + return new _YText(); + } + /** + * Makes a copy of this data type that can be included somewhere else. + * + * Note that the content is only readable _after_ it has been included somewhere in the Ydoc. + * + * @return {YText} + */ + clone() { + const text2 = new _YText(); + text2.applyDelta(this.toDelta()); + return text2; + } + /** + * Creates YTextEvent and calls observers. + * + * @param {Transaction} transaction + * @param {Set} parentSubs Keys changed on this type. `null` if list was modified. + */ + _callObserver(transaction, parentSubs) { + super._callObserver(transaction, parentSubs); + const event = new YTextEvent(this, transaction, parentSubs); + callTypeObservers(this, transaction, event); + if (!transaction.local && this._hasFormatting) { + transaction._needFormattingCleanup = true; + } + } + /** + * Returns the unformatted string representation of this YText type. + * + * @public + */ + toString() { + this.doc ?? warnPrematureAccess(); + let str = ""; + let n = this._start; + while (n !== null) { + if (!n.deleted && n.countable && n.content.constructor === ContentString) { + str += /** @type {ContentString} */ + n.content.str; + } + n = n.right; + } + return str; + } + /** + * Returns the unformatted string representation of this YText type. + * + * @return {string} + * @public + */ + toJSON() { + return this.toString(); + } + /** + * Apply a {@link Delta} on this shared YText type. + * + * @param {Array} delta The changes to apply on this element. + * @param {object} opts + * @param {boolean} [opts.sanitize] Sanitize input delta. Removes ending newlines if set to true. + * + * + * @public + */ + applyDelta(delta, { sanitize = true } = {}) { + if (this.doc !== null) { + transact(this.doc, (transaction) => { + const currPos = new ItemTextListPosition(null, this._start, 0, /* @__PURE__ */ new Map()); + for (let i = 0; i < delta.length; i++) { + const op = delta[i]; + if (op.insert !== void 0) { + const ins = !sanitize && typeof op.insert === "string" && i === delta.length - 1 && currPos.right === null && op.insert.slice(-1) === "\n" ? op.insert.slice(0, -1) : op.insert; + if (typeof ins !== "string" || ins.length > 0) { + insertText(transaction, this, currPos, ins, op.attributes || {}); + } + } else if (op.retain !== void 0) { + formatText(transaction, this, currPos, op.retain, op.attributes || {}); + } else if (op.delete !== void 0) { + deleteText(transaction, currPos, op.delete); + } + } + }); + } else { + this._pending.push(() => this.applyDelta(delta)); + } + } + /** + * Returns the Delta representation of this YText type. + * + * @param {Snapshot} [snapshot] + * @param {Snapshot} [prevSnapshot] + * @param {function('removed' | 'added', ID):any} [computeYChange] + * @return {any} The Delta representation of this type. + * + * @public + */ + toDelta(snapshot, prevSnapshot, computeYChange) { + this.doc ?? warnPrematureAccess(); + const ops = []; + const currentAttributes = /* @__PURE__ */ new Map(); + const doc2 = ( + /** @type {Doc} */ + this.doc + ); + let str = ""; + let n = this._start; + function packStr() { + if (str.length > 0) { + const attributes = {}; + let addAttributes = false; + currentAttributes.forEach((value, key) => { + addAttributes = true; + attributes[key] = value; + }); + const op = { insert: str }; + if (addAttributes) { + op.attributes = attributes; + } + ops.push(op); + str = ""; + } + } + const computeDelta = () => { + while (n !== null) { + if (isVisible(n, snapshot) || prevSnapshot !== void 0 && isVisible(n, prevSnapshot)) { + switch (n.content.constructor) { + case ContentString: { + const cur = currentAttributes.get("ychange"); + if (snapshot !== void 0 && !isVisible(n, snapshot)) { + if (cur === void 0 || cur.user !== n.id.client || cur.type !== "removed") { + packStr(); + currentAttributes.set("ychange", computeYChange ? computeYChange("removed", n.id) : { type: "removed" }); + } + } else if (prevSnapshot !== void 0 && !isVisible(n, prevSnapshot)) { + if (cur === void 0 || cur.user !== n.id.client || cur.type !== "added") { + packStr(); + currentAttributes.set("ychange", computeYChange ? computeYChange("added", n.id) : { type: "added" }); + } + } else if (cur !== void 0) { + packStr(); + currentAttributes.delete("ychange"); + } + str += /** @type {ContentString} */ + n.content.str; + break; + } + case ContentType: + case ContentEmbed: { + packStr(); + const op = { + insert: n.content.getContent()[0] + }; + if (currentAttributes.size > 0) { + const attrs = ( + /** @type {Object} */ + {} + ); + op.attributes = attrs; + currentAttributes.forEach((value, key) => { + attrs[key] = value; + }); + } + ops.push(op); + break; + } + case ContentFormat: + if (isVisible(n, snapshot)) { + packStr(); + updateCurrentAttributes( + currentAttributes, + /** @type {ContentFormat} */ + n.content + ); + } + break; + } + } + n = n.right; + } + packStr(); + }; + if (snapshot || prevSnapshot) { + transact(doc2, (transaction) => { + if (snapshot) { + splitSnapshotAffectedStructs(transaction, snapshot); + } + if (prevSnapshot) { + splitSnapshotAffectedStructs(transaction, prevSnapshot); + } + computeDelta(); + }, "cleanup"); + } else { + computeDelta(); + } + return ops; + } + /** + * Insert text at a given index. + * + * @param {number} index The index at which to start inserting. + * @param {String} text The text to insert at the specified position. + * @param {TextAttributes} [attributes] Optionally define some formatting + * information to apply on the inserted + * Text. + * @public + */ + insert(index, text2, attributes) { + if (text2.length <= 0) { + return; + } + const y = this.doc; + if (y !== null) { + transact(y, (transaction) => { + const pos = findPosition(transaction, this, index, !attributes); + if (!attributes) { + attributes = {}; + pos.currentAttributes.forEach((v, k) => { + attributes[k] = v; + }); + } + insertText(transaction, this, pos, text2, attributes); + }); + } else { + this._pending.push(() => this.insert(index, text2, attributes)); + } + } + /** + * Inserts an embed at a index. + * + * @param {number} index The index to insert the embed at. + * @param {Object | AbstractType} embed The Object that represents the embed. + * @param {TextAttributes} [attributes] Attribute information to apply on the + * embed + * + * @public + */ + insertEmbed(index, embed, attributes) { + const y = this.doc; + if (y !== null) { + transact(y, (transaction) => { + const pos = findPosition(transaction, this, index, !attributes); + insertText(transaction, this, pos, embed, attributes || {}); + }); + } else { + this._pending.push(() => this.insertEmbed(index, embed, attributes || {})); + } + } + /** + * Deletes text starting from an index. + * + * @param {number} index Index at which to start deleting. + * @param {number} length The number of characters to remove. Defaults to 1. + * + * @public + */ + delete(index, length3) { + if (length3 === 0) { + return; + } + const y = this.doc; + if (y !== null) { + transact(y, (transaction) => { + deleteText(transaction, findPosition(transaction, this, index, true), length3); + }); + } else { + this._pending.push(() => this.delete(index, length3)); + } + } + /** + * Assigns properties to a range of text. + * + * @param {number} index The position where to start formatting. + * @param {number} length The amount of characters to assign properties to. + * @param {TextAttributes} attributes Attribute information to apply on the + * text. + * + * @public + */ + format(index, length3, attributes) { + if (length3 === 0) { + return; + } + const y = this.doc; + if (y !== null) { + transact(y, (transaction) => { + const pos = findPosition(transaction, this, index, false); + if (pos.right === null) { + return; + } + formatText(transaction, this, pos, length3, attributes); + }); + } else { + this._pending.push(() => this.format(index, length3, attributes)); + } + } + /** + * Removes an attribute. + * + * @note Xml-Text nodes don't have attributes. You can use this feature to assign properties to complete text-blocks. + * + * @param {String} attributeName The attribute name that is to be removed. + * + * @public + */ + removeAttribute(attributeName) { + if (this.doc !== null) { + transact(this.doc, (transaction) => { + typeMapDelete(transaction, this, attributeName); + }); + } else { + this._pending.push(() => this.removeAttribute(attributeName)); + } + } + /** + * Sets or updates an attribute. + * + * @note Xml-Text nodes don't have attributes. You can use this feature to assign properties to complete text-blocks. + * + * @param {String} attributeName The attribute name that is to be set. + * @param {any} attributeValue The attribute value that is to be set. + * + * @public + */ + setAttribute(attributeName, attributeValue) { + if (this.doc !== null) { + transact(this.doc, (transaction) => { + typeMapSet(transaction, this, attributeName, attributeValue); + }); + } else { + this._pending.push(() => this.setAttribute(attributeName, attributeValue)); + } + } + /** + * Returns an attribute value that belongs to the attribute name. + * + * @note Xml-Text nodes don't have attributes. You can use this feature to assign properties to complete text-blocks. + * + * @param {String} attributeName The attribute name that identifies the + * queried value. + * @return {any} The queried attribute value. + * + * @public + */ + getAttribute(attributeName) { + return ( + /** @type {any} */ + typeMapGet(this, attributeName) + ); + } + /** + * Returns all attribute name/value pairs in a JSON Object. + * + * @note Xml-Text nodes don't have attributes. You can use this feature to assign properties to complete text-blocks. + * + * @return {Object} A JSON Object that describes the attributes. + * + * @public + */ + getAttributes() { + return typeMapGetAll(this); + } + /** + * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder + */ + _write(encoder) { + encoder.writeTypeRef(YTextRefID); + } + }; + var readYText = (_decoder) => new YText(); + var YXmlTreeWalker = class { + /** + * @param {YXmlFragment | YXmlElement} root + * @param {function(AbstractType):boolean} [f] + */ + constructor(root, f = () => true) { + this._filter = f; + this._root = root; + this._currentNode = /** @type {Item} */ + root._start; + this._firstCall = true; + root.doc ?? warnPrematureAccess(); + } + [Symbol.iterator]() { + return this; + } + /** + * Get the next node. + * + * @return {IteratorResult} The next node. + * + * @public + */ + next() { + let n = this._currentNode; + let type = n && n.content && /** @type {any} */ + n.content.type; + if (n !== null && (!this._firstCall || n.deleted || !this._filter(type))) { + do { + type = /** @type {any} */ + n.content.type; + if (!n.deleted && (type.constructor === YXmlElement || type.constructor === YXmlFragment) && type._start !== null) { + n = type._start; + } else { + while (n !== null) { + const nxt = n.next; + if (nxt !== null) { + n = nxt; + break; + } else if (n.parent === this._root) { + n = null; + } else { + n = /** @type {AbstractType} */ + n.parent._item; + } + } + } + } while (n !== null && (n.deleted || !this._filter( + /** @type {ContentType} */ + n.content.type + ))); + } + this._firstCall = false; + if (n === null) { + return { value: void 0, done: true }; + } + this._currentNode = n; + return { value: ( + /** @type {any} */ + n.content.type + ), done: false }; + } + }; + var YXmlFragment = class _YXmlFragment extends AbstractType { + constructor() { + super(); + this._prelimContent = []; + } + /** + * @type {YXmlElement|YXmlText|null} + */ + get firstChild() { + const first = this._first; + return first ? first.content.getContent()[0] : null; + } + /** + * Integrate this type into the Yjs instance. + * + * * Save this struct in the os + * * This type is sent to other client + * * Observer functions are fired + * + * @param {Doc} y The Yjs instance + * @param {Item} item + */ + _integrate(y, item) { + super._integrate(y, item); + this.insert( + 0, + /** @type {Array} */ + this._prelimContent + ); + this._prelimContent = null; + } + _copy() { + return new _YXmlFragment(); + } + /** + * Makes a copy of this data type that can be included somewhere else. + * + * Note that the content is only readable _after_ it has been included somewhere in the Ydoc. + * + * @return {YXmlFragment} + */ + clone() { + const el = new _YXmlFragment(); + el.insert(0, this.toArray().map((item) => item instanceof AbstractType ? item.clone() : item)); + return el; + } + get length() { + this.doc ?? warnPrematureAccess(); + return this._prelimContent === null ? this._length : this._prelimContent.length; + } + /** + * Create a subtree of childNodes. + * + * @example + * const walker = elem.createTreeWalker(dom => dom.nodeName === 'div') + * for (let node in walker) { + * // `node` is a div node + * nop(node) + * } + * + * @param {function(AbstractType):boolean} filter Function that is called on each child element and + * returns a Boolean indicating whether the child + * is to be included in the subtree. + * @return {YXmlTreeWalker} A subtree and a position within it. + * + * @public + */ + createTreeWalker(filter) { + return new YXmlTreeWalker(this, filter); + } + /** + * Returns the first YXmlElement that matches the query. + * Similar to DOM's {@link querySelector}. + * + * Query support: + * - tagname + * TODO: + * - id + * - attribute + * + * @param {CSS_Selector} query The query on the children. + * @return {YXmlElement|YXmlText|YXmlHook|null} The first element that matches the query or null. + * + * @public + */ + querySelector(query) { + query = query.toUpperCase(); + const iterator = new YXmlTreeWalker(this, (element2) => element2.nodeName && element2.nodeName.toUpperCase() === query); + const next = iterator.next(); + if (next.done) { + return null; + } else { + return next.value; + } + } + /** + * Returns all YXmlElements that match the query. + * Similar to Dom's {@link querySelectorAll}. + * + * @todo Does not yet support all queries. Currently only query by tagName. + * + * @param {CSS_Selector} query The query on the children + * @return {Array} The elements that match this query. + * + * @public + */ + querySelectorAll(query) { + query = query.toUpperCase(); + return from(new YXmlTreeWalker(this, (element2) => element2.nodeName && element2.nodeName.toUpperCase() === query)); + } + /** + * Creates YXmlEvent and calls observers. + * + * @param {Transaction} transaction + * @param {Set} parentSubs Keys changed on this type. `null` if list was modified. + */ + _callObserver(transaction, parentSubs) { + callTypeObservers(this, transaction, new YXmlEvent(this, parentSubs, transaction)); + } + /** + * Get the string representation of all the children of this YXmlFragment. + * + * @return {string} The string representation of all children. + */ + toString() { + return typeListMap(this, (xml) => xml.toString()).join(""); + } + /** + * @return {string} + */ + toJSON() { + return this.toString(); + } + /** + * Creates a Dom Element that mirrors this YXmlElement. + * + * @param {Document} [_document=document] The document object (you must define + * this when calling this method in + * nodejs) + * @param {Object} [hooks={}] Optional property to customize how hooks + * are presented in the DOM + * @param {any} [binding] You should not set this property. This is + * used if DomBinding wants to create a + * association to the created DOM type. + * @return {Node} The {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element} + * + * @public + */ + toDOM(_document = document, hooks = {}, binding) { + const fragment = _document.createDocumentFragment(); + if (binding !== void 0) { + binding._createAssociation(fragment, this); + } + typeListForEach(this, (xmlType) => { + fragment.insertBefore(xmlType.toDOM(_document, hooks, binding), null); + }); + return fragment; + } + /** + * Inserts new content at an index. + * + * @example + * // Insert character 'a' at position 0 + * xml.insert(0, [new Y.XmlText('text')]) + * + * @param {number} index The index to insert content at + * @param {Array} content The array of content + */ + insert(index, content) { + if (this.doc !== null) { + transact(this.doc, (transaction) => { + typeListInsertGenerics(transaction, this, index, content); + }); + } else { + this._prelimContent.splice(index, 0, ...content); + } + } + /** + * Inserts new content at an index. + * + * @example + * // Insert character 'a' at position 0 + * xml.insert(0, [new Y.XmlText('text')]) + * + * @param {null|Item|YXmlElement|YXmlText} ref The index to insert content at + * @param {Array} content The array of content + */ + insertAfter(ref, content) { + if (this.doc !== null) { + transact(this.doc, (transaction) => { + const refItem = ref && ref instanceof AbstractType ? ref._item : ref; + typeListInsertGenericsAfter(transaction, this, refItem, content); + }); + } else { + const pc = ( + /** @type {Array} */ + this._prelimContent + ); + const index = ref === null ? 0 : pc.findIndex((el) => el === ref) + 1; + if (index === 0 && ref !== null) { + throw create3("Reference item not found"); + } + pc.splice(index, 0, ...content); + } + } + /** + * Deletes elements starting from an index. + * + * @param {number} index Index at which to start deleting elements + * @param {number} [length=1] The number of elements to remove. Defaults to 1. + */ + delete(index, length3 = 1) { + if (this.doc !== null) { + transact(this.doc, (transaction) => { + typeListDelete(transaction, this, index, length3); + }); + } else { + this._prelimContent.splice(index, length3); + } + } + /** + * Transforms this YArray to a JavaScript Array. + * + * @return {Array} + */ + toArray() { + return typeListToArray(this); + } + /** + * Appends content to this YArray. + * + * @param {Array} content Array of content to append. + */ + push(content) { + this.insert(this.length, content); + } + /** + * Prepends content to this YArray. + * + * @param {Array} content Array of content to prepend. + */ + unshift(content) { + this.insert(0, content); + } + /** + * Returns the i-th element from a YArray. + * + * @param {number} index The index of the element to return from the YArray + * @return {YXmlElement|YXmlText} + */ + get(index) { + return typeListGet(this, index); + } + /** + * Returns a portion of this YXmlFragment into a JavaScript Array selected + * from start to end (end not included). + * + * @param {number} [start] + * @param {number} [end] + * @return {Array} + */ + slice(start = 0, end = this.length) { + return typeListSlice(this, start, end); + } + /** + * Executes a provided function on once on every child element. + * + * @param {function(YXmlElement|YXmlText,number, typeof self):void} f A function to execute on every element of this YArray. + */ + forEach(f) { + typeListForEach(this, f); + } + /** + * Transform the properties of this type to binary and write it to an + * BinaryEncoder. + * + * This is called when this Item is sent to a remote peer. + * + * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder The encoder to write data to. + */ + _write(encoder) { + encoder.writeTypeRef(YXmlFragmentRefID); + } + }; + var readYXmlFragment = (_decoder) => new YXmlFragment(); + var YXmlElement = class _YXmlElement extends YXmlFragment { + constructor(nodeName = "UNDEFINED") { + super(); + this.nodeName = nodeName; + this._prelimAttrs = /* @__PURE__ */ new Map(); + } + /** + * @type {YXmlElement|YXmlText|null} + */ + get nextSibling() { + const n = this._item ? this._item.next : null; + return n ? ( + /** @type {YXmlElement|YXmlText} */ + /** @type {ContentType} */ + n.content.type + ) : null; + } + /** + * @type {YXmlElement|YXmlText|null} + */ + get prevSibling() { + const n = this._item ? this._item.prev : null; + return n ? ( + /** @type {YXmlElement|YXmlText} */ + /** @type {ContentType} */ + n.content.type + ) : null; + } + /** + * Integrate this type into the Yjs instance. + * + * * Save this struct in the os + * * This type is sent to other client + * * Observer functions are fired + * + * @param {Doc} y The Yjs instance + * @param {Item} item + */ + _integrate(y, item) { + super._integrate(y, item); + /** @type {Map} */ + this._prelimAttrs.forEach((value, key) => { + this.setAttribute(key, value); + }); + this._prelimAttrs = null; + } + /** + * Creates an Item with the same effect as this Item (without position effect) + * + * @return {YXmlElement} + */ + _copy() { + return new _YXmlElement(this.nodeName); + } + /** + * Makes a copy of this data type that can be included somewhere else. + * + * Note that the content is only readable _after_ it has been included somewhere in the Ydoc. + * + * @return {YXmlElement} + */ + clone() { + const el = new _YXmlElement(this.nodeName); + const attrs = this.getAttributes(); + forEach(attrs, (value, key) => { + el.setAttribute( + key, + /** @type {any} */ + value + ); + }); + el.insert(0, this.toArray().map((v) => v instanceof AbstractType ? v.clone() : v)); + return el; + } + /** + * Returns the XML serialization of this YXmlElement. + * The attributes are ordered by attribute-name, so you can easily use this + * method to compare YXmlElements + * + * @return {string} The string representation of this type. + * + * @public + */ + toString() { + const attrs = this.getAttributes(); + const stringBuilder = []; + const keys3 = []; + for (const key in attrs) { + keys3.push(key); + } + keys3.sort(); + const keysLen = keys3.length; + for (let i = 0; i < keysLen; i++) { + const key = keys3[i]; + stringBuilder.push(key + '="' + attrs[key] + '"'); + } + const nodeName = this.nodeName.toLocaleLowerCase(); + const attrsString = stringBuilder.length > 0 ? " " + stringBuilder.join(" ") : ""; + return `<${nodeName}${attrsString}>${super.toString()}`; + } + /** + * Removes an attribute from this YXmlElement. + * + * @param {string} attributeName The attribute name that is to be removed. + * + * @public + */ + removeAttribute(attributeName) { + if (this.doc !== null) { + transact(this.doc, (transaction) => { + typeMapDelete(transaction, this, attributeName); + }); + } else { + this._prelimAttrs.delete(attributeName); + } + } + /** + * Sets or updates an attribute. + * + * @template {keyof KV & string} KEY + * + * @param {KEY} attributeName The attribute name that is to be set. + * @param {KV[KEY]} attributeValue The attribute value that is to be set. + * + * @public + */ + setAttribute(attributeName, attributeValue) { + if (this.doc !== null) { + transact(this.doc, (transaction) => { + typeMapSet(transaction, this, attributeName, attributeValue); + }); + } else { + this._prelimAttrs.set(attributeName, attributeValue); + } + } + /** + * Returns an attribute value that belongs to the attribute name. + * + * @template {keyof KV & string} KEY + * + * @param {KEY} attributeName The attribute name that identifies the + * queried value. + * @return {KV[KEY]|undefined} The queried attribute value. + * + * @public + */ + getAttribute(attributeName) { + return ( + /** @type {any} */ + typeMapGet(this, attributeName) + ); + } + /** + * Returns whether an attribute exists + * + * @param {string} attributeName The attribute name to check for existence. + * @return {boolean} whether the attribute exists. + * + * @public + */ + hasAttribute(attributeName) { + return ( + /** @type {any} */ + typeMapHas(this, attributeName) + ); + } + /** + * Returns all attribute name/value pairs in a JSON Object. + * + * @param {Snapshot} [snapshot] + * @return {{ [Key in Extract]?: KV[Key]}} A JSON Object that describes the attributes. + * + * @public + */ + getAttributes(snapshot) { + return ( + /** @type {any} */ + snapshot ? typeMapGetAllSnapshot(this, snapshot) : typeMapGetAll(this) + ); + } + /** + * Creates a Dom Element that mirrors this YXmlElement. + * + * @param {Document} [_document=document] The document object (you must define + * this when calling this method in + * nodejs) + * @param {Object} [hooks={}] Optional property to customize how hooks + * are presented in the DOM + * @param {any} [binding] You should not set this property. This is + * used if DomBinding wants to create a + * association to the created DOM type. + * @return {Node} The {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element} + * + * @public + */ + toDOM(_document = document, hooks = {}, binding) { + const dom = _document.createElement(this.nodeName); + const attrs = this.getAttributes(); + for (const key in attrs) { + const value = attrs[key]; + if (typeof value === "string") { + dom.setAttribute(key, value); + } + } + typeListForEach(this, (yxml) => { + dom.appendChild(yxml.toDOM(_document, hooks, binding)); + }); + if (binding !== void 0) { + binding._createAssociation(dom, this); + } + return dom; + } + /** + * Transform the properties of this type to binary and write it to an + * BinaryEncoder. + * + * This is called when this Item is sent to a remote peer. + * + * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder The encoder to write data to. + */ + _write(encoder) { + encoder.writeTypeRef(YXmlElementRefID); + encoder.writeKey(this.nodeName); + } + }; + var readYXmlElement = (decoder) => new YXmlElement(decoder.readKey()); + var YXmlEvent = class extends YEvent { + /** + * @param {YXmlElement|YXmlText|YXmlFragment} target The target on which the event is created. + * @param {Set} subs The set of changed attributes. `null` is included if the + * child list changed. + * @param {Transaction} transaction The transaction instance with which the + * change was created. + */ + constructor(target, subs, transaction) { + super(target, transaction); + this.childListChanged = false; + this.attributesChanged = /* @__PURE__ */ new Set(); + subs.forEach((sub) => { + if (sub === null) { + this.childListChanged = true; + } else { + this.attributesChanged.add(sub); + } + }); + } + }; + var YXmlHook = class _YXmlHook extends YMap { + /** + * @param {string} hookName nodeName of the Dom Node. + */ + constructor(hookName) { + super(); + this.hookName = hookName; + } + /** + * Creates an Item with the same effect as this Item (without position effect) + */ + _copy() { + return new _YXmlHook(this.hookName); + } + /** + * Makes a copy of this data type that can be included somewhere else. + * + * Note that the content is only readable _after_ it has been included somewhere in the Ydoc. + * + * @return {YXmlHook} + */ + clone() { + const el = new _YXmlHook(this.hookName); + this.forEach((value, key) => { + el.set(key, value); + }); + return el; + } + /** + * Creates a Dom Element that mirrors this YXmlElement. + * + * @param {Document} [_document=document] The document object (you must define + * this when calling this method in + * nodejs) + * @param {Object.} [hooks] Optional property to customize how hooks + * are presented in the DOM + * @param {any} [binding] You should not set this property. This is + * used if DomBinding wants to create a + * association to the created DOM type + * @return {Element} The {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element} + * + * @public + */ + toDOM(_document = document, hooks = {}, binding) { + const hook = hooks[this.hookName]; + let dom; + if (hook !== void 0) { + dom = hook.createDom(this); + } else { + dom = document.createElement(this.hookName); + } + dom.setAttribute("data-yjs-hook", this.hookName); + if (binding !== void 0) { + binding._createAssociation(dom, this); + } + return dom; + } + /** + * Transform the properties of this type to binary and write it to an + * BinaryEncoder. + * + * This is called when this Item is sent to a remote peer. + * + * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder The encoder to write data to. + */ + _write(encoder) { + encoder.writeTypeRef(YXmlHookRefID); + encoder.writeKey(this.hookName); + } + }; + var readYXmlHook = (decoder) => new YXmlHook(decoder.readKey()); + var YXmlText = class _YXmlText extends YText { + /** + * @type {YXmlElement|YXmlText|null} + */ + get nextSibling() { + const n = this._item ? this._item.next : null; + return n ? ( + /** @type {YXmlElement|YXmlText} */ + /** @type {ContentType} */ + n.content.type + ) : null; + } + /** + * @type {YXmlElement|YXmlText|null} + */ + get prevSibling() { + const n = this._item ? this._item.prev : null; + return n ? ( + /** @type {YXmlElement|YXmlText} */ + /** @type {ContentType} */ + n.content.type + ) : null; + } + _copy() { + return new _YXmlText(); + } + /** + * Makes a copy of this data type that can be included somewhere else. + * + * Note that the content is only readable _after_ it has been included somewhere in the Ydoc. + * + * @return {YXmlText} + */ + clone() { + const text2 = new _YXmlText(); + text2.applyDelta(this.toDelta()); + return text2; + } + /** + * Creates a Dom Element that mirrors this YXmlText. + * + * @param {Document} [_document=document] The document object (you must define + * this when calling this method in + * nodejs) + * @param {Object} [hooks] Optional property to customize how hooks + * are presented in the DOM + * @param {any} [binding] You should not set this property. This is + * used if DomBinding wants to create a + * association to the created DOM type. + * @return {Text} The {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element} + * + * @public + */ + toDOM(_document = document, hooks, binding) { + const dom = _document.createTextNode(this.toString()); + if (binding !== void 0) { + binding._createAssociation(dom, this); + } + return dom; + } + toString() { + return this.toDelta().map((delta) => { + const nestedNodes = []; + for (const nodeName in delta.attributes) { + const attrs = []; + for (const key in delta.attributes[nodeName]) { + attrs.push({ key, value: delta.attributes[nodeName][key] }); + } + attrs.sort((a, b) => a.key < b.key ? -1 : 1); + nestedNodes.push({ nodeName, attrs }); + } + nestedNodes.sort((a, b) => a.nodeName < b.nodeName ? -1 : 1); + let str = ""; + for (let i = 0; i < nestedNodes.length; i++) { + const node = nestedNodes[i]; + str += `<${node.nodeName}`; + for (let j = 0; j < node.attrs.length; j++) { + const attr = node.attrs[j]; + str += ` ${attr.key}="${attr.value}"`; + } + str += ">"; + } + str += delta.insert; + for (let i = nestedNodes.length - 1; i >= 0; i--) { + str += ``; + } + return str; + }).join(""); + } + /** + * @return {string} + */ + toJSON() { + return this.toString(); + } + /** + * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder + */ + _write(encoder) { + encoder.writeTypeRef(YXmlTextRefID); + } + }; + var readYXmlText = (decoder) => new YXmlText(); + var AbstractStruct = class { + /** + * @param {ID} id + * @param {number} length + */ + constructor(id2, length3) { + this.id = id2; + this.length = length3; + } + /** + * @type {boolean} + */ + get deleted() { + throw methodUnimplemented(); + } + /** + * Merge this struct with the item to the right. + * This method is already assuming that `this.id.clock + this.length === this.id.clock`. + * Also this method does *not* remove right from StructStore! + * @param {AbstractStruct} right + * @return {boolean} whether this merged with right + */ + mergeWith(right) { + return false; + } + /** + * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder The encoder to write data to. + * @param {number} offset + * @param {number} encodingRef + */ + write(encoder, offset, encodingRef) { + throw methodUnimplemented(); + } + /** + * @param {Transaction} transaction + * @param {number} offset + */ + integrate(transaction, offset) { + throw methodUnimplemented(); + } + }; + var structGCRefNumber = 0; + var GC = class extends AbstractStruct { + get deleted() { + return true; + } + delete() { + } + /** + * @param {GC} right + * @return {boolean} + */ + mergeWith(right) { + if (this.constructor !== right.constructor) { + return false; + } + this.length += right.length; + return true; + } + /** + * @param {Transaction} transaction + * @param {number} offset + */ + integrate(transaction, offset) { + if (offset > 0) { + this.id.clock += offset; + this.length -= offset; + } + addStruct(transaction.doc.store, this); + } + /** + * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder + * @param {number} offset + */ + write(encoder, offset) { + encoder.writeInfo(structGCRefNumber); + encoder.writeLen(this.length - offset); + } + /** + * @param {Transaction} transaction + * @param {StructStore} store + * @return {null | number} + */ + getMissing(transaction, store) { + return null; + } + }; + var ContentBinary = class _ContentBinary { + /** + * @param {Uint8Array} content + */ + constructor(content) { + this.content = content; + } + /** + * @return {number} + */ + getLength() { + return 1; + } + /** + * @return {Array} + */ + getContent() { + return [this.content]; + } + /** + * @return {boolean} + */ + isCountable() { + return true; + } + /** + * @return {ContentBinary} + */ + copy() { + return new _ContentBinary(this.content); + } + /** + * @param {number} offset + * @return {ContentBinary} + */ + splice(offset) { + throw methodUnimplemented(); + } + /** + * @param {ContentBinary} right + * @return {boolean} + */ + mergeWith(right) { + return false; + } + /** + * @param {Transaction} transaction + * @param {Item} item + */ + integrate(transaction, item) { + } + /** + * @param {Transaction} transaction + */ + delete(transaction) { + } + /** + * @param {StructStore} store + */ + gc(store) { + } + /** + * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder + * @param {number} offset + */ + write(encoder, offset) { + encoder.writeBuf(this.content); + } + /** + * @return {number} + */ + getRef() { + return 3; + } + }; + var readContentBinary = (decoder) => new ContentBinary(decoder.readBuf()); + var ContentDeleted = class _ContentDeleted { + /** + * @param {number} len + */ + constructor(len) { + this.len = len; + } + /** + * @return {number} + */ + getLength() { + return this.len; + } + /** + * @return {Array} + */ + getContent() { + return []; + } + /** + * @return {boolean} + */ + isCountable() { + return false; + } + /** + * @return {ContentDeleted} + */ + copy() { + return new _ContentDeleted(this.len); + } + /** + * @param {number} offset + * @return {ContentDeleted} + */ + splice(offset) { + const right = new _ContentDeleted(this.len - offset); + this.len = offset; + return right; + } + /** + * @param {ContentDeleted} right + * @return {boolean} + */ + mergeWith(right) { + this.len += right.len; + return true; + } + /** + * @param {Transaction} transaction + * @param {Item} item + */ + integrate(transaction, item) { + addToDeleteSet(transaction.deleteSet, item.id.client, item.id.clock, this.len); + item.markDeleted(); + } + /** + * @param {Transaction} transaction + */ + delete(transaction) { + } + /** + * @param {StructStore} store + */ + gc(store) { + } + /** + * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder + * @param {number} offset + */ + write(encoder, offset) { + encoder.writeLen(this.len - offset); + } + /** + * @return {number} + */ + getRef() { + return 1; + } + }; + var readContentDeleted = (decoder) => new ContentDeleted(decoder.readLen()); + var createDocFromOpts = (guid, opts) => new Doc({ guid, ...opts, shouldLoad: opts.shouldLoad || opts.autoLoad || false }); + var ContentDoc = class _ContentDoc { + /** + * @param {Doc} doc + */ + constructor(doc2) { + if (doc2._item) { + console.error("This document was already integrated as a sub-document. You should create a second instance instead with the same guid."); + } + this.doc = doc2; + const opts = {}; + this.opts = opts; + if (!doc2.gc) { + opts.gc = false; + } + if (doc2.autoLoad) { + opts.autoLoad = true; + } + if (doc2.meta !== null) { + opts.meta = doc2.meta; + } + } + /** + * @return {number} + */ + getLength() { + return 1; + } + /** + * @return {Array} + */ + getContent() { + return [this.doc]; + } + /** + * @return {boolean} + */ + isCountable() { + return true; + } + /** + * @return {ContentDoc} + */ + copy() { + return new _ContentDoc(createDocFromOpts(this.doc.guid, this.opts)); + } + /** + * @param {number} offset + * @return {ContentDoc} + */ + splice(offset) { + throw methodUnimplemented(); + } + /** + * @param {ContentDoc} right + * @return {boolean} + */ + mergeWith(right) { + return false; + } + /** + * @param {Transaction} transaction + * @param {Item} item + */ + integrate(transaction, item) { + this.doc._item = item; + transaction.subdocsAdded.add(this.doc); + if (this.doc.shouldLoad) { + transaction.subdocsLoaded.add(this.doc); + } + } + /** + * @param {Transaction} transaction + */ + delete(transaction) { + if (transaction.subdocsAdded.has(this.doc)) { + transaction.subdocsAdded.delete(this.doc); + } else { + transaction.subdocsRemoved.add(this.doc); + } + } + /** + * @param {StructStore} store + */ + gc(store) { + } + /** + * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder + * @param {number} offset + */ + write(encoder, offset) { + encoder.writeString(this.doc.guid); + encoder.writeAny(this.opts); + } + /** + * @return {number} + */ + getRef() { + return 9; + } + }; + var readContentDoc = (decoder) => new ContentDoc(createDocFromOpts(decoder.readString(), decoder.readAny())); + var ContentEmbed = class _ContentEmbed { + /** + * @param {Object} embed + */ + constructor(embed) { + this.embed = embed; + } + /** + * @return {number} + */ + getLength() { + return 1; + } + /** + * @return {Array} + */ + getContent() { + return [this.embed]; + } + /** + * @return {boolean} + */ + isCountable() { + return true; + } + /** + * @return {ContentEmbed} + */ + copy() { + return new _ContentEmbed(this.embed); + } + /** + * @param {number} offset + * @return {ContentEmbed} + */ + splice(offset) { + throw methodUnimplemented(); + } + /** + * @param {ContentEmbed} right + * @return {boolean} + */ + mergeWith(right) { + return false; + } + /** + * @param {Transaction} transaction + * @param {Item} item + */ + integrate(transaction, item) { + } + /** + * @param {Transaction} transaction + */ + delete(transaction) { + } + /** + * @param {StructStore} store + */ + gc(store) { + } + /** + * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder + * @param {number} offset + */ + write(encoder, offset) { + encoder.writeJSON(this.embed); + } + /** + * @return {number} + */ + getRef() { + return 5; + } + }; + var readContentEmbed = (decoder) => new ContentEmbed(decoder.readJSON()); + var ContentFormat = class _ContentFormat { + /** + * @param {string} key + * @param {Object} value + */ + constructor(key, value) { + this.key = key; + this.value = value; + } + /** + * @return {number} + */ + getLength() { + return 1; + } + /** + * @return {Array} + */ + getContent() { + return []; + } + /** + * @return {boolean} + */ + isCountable() { + return false; + } + /** + * @return {ContentFormat} + */ + copy() { + return new _ContentFormat(this.key, this.value); + } + /** + * @param {number} _offset + * @return {ContentFormat} + */ + splice(_offset) { + throw methodUnimplemented(); + } + /** + * @param {ContentFormat} _right + * @return {boolean} + */ + mergeWith(_right) { + return false; + } + /** + * @param {Transaction} _transaction + * @param {Item} item + */ + integrate(_transaction, item) { + const p = ( + /** @type {YText} */ + item.parent + ); + p._searchMarker = null; + p._hasFormatting = true; + } + /** + * @param {Transaction} transaction + */ + delete(transaction) { + } + /** + * @param {StructStore} store + */ + gc(store) { + } + /** + * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder + * @param {number} offset + */ + write(encoder, offset) { + encoder.writeKey(this.key); + encoder.writeJSON(this.value); + } + /** + * @return {number} + */ + getRef() { + return 6; + } + }; + var readContentFormat = (decoder) => new ContentFormat(decoder.readKey(), decoder.readJSON()); + var ContentJSON = class _ContentJSON { + /** + * @param {Array} arr + */ + constructor(arr) { + this.arr = arr; + } + /** + * @return {number} + */ + getLength() { + return this.arr.length; + } + /** + * @return {Array} + */ + getContent() { + return this.arr; + } + /** + * @return {boolean} + */ + isCountable() { + return true; + } + /** + * @return {ContentJSON} + */ + copy() { + return new _ContentJSON(this.arr); + } + /** + * @param {number} offset + * @return {ContentJSON} + */ + splice(offset) { + const right = new _ContentJSON(this.arr.slice(offset)); + this.arr = this.arr.slice(0, offset); + return right; + } + /** + * @param {ContentJSON} right + * @return {boolean} + */ + mergeWith(right) { + this.arr = this.arr.concat(right.arr); + return true; + } + /** + * @param {Transaction} transaction + * @param {Item} item + */ + integrate(transaction, item) { + } + /** + * @param {Transaction} transaction + */ + delete(transaction) { + } + /** + * @param {StructStore} store + */ + gc(store) { + } + /** + * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder + * @param {number} offset + */ + write(encoder, offset) { + const len = this.arr.length; + encoder.writeLen(len - offset); + for (let i = offset; i < len; i++) { + const c = this.arr[i]; + encoder.writeString(c === void 0 ? "undefined" : JSON.stringify(c)); + } + } + /** + * @return {number} + */ + getRef() { + return 2; + } + }; + var readContentJSON = (decoder) => { + const len = decoder.readLen(); + const cs = []; + for (let i = 0; i < len; i++) { + const c = decoder.readString(); + if (c === "undefined") { + cs.push(void 0); + } else { + cs.push(JSON.parse(c)); + } + } + return new ContentJSON(cs); + }; + var isDevMode = getVariable("node_env") === "development"; + var ContentAny = class _ContentAny { + /** + * @param {Array} arr + */ + constructor(arr) { + this.arr = arr; + isDevMode && deepFreeze(arr); + } + /** + * @return {number} + */ + getLength() { + return this.arr.length; + } + /** + * @return {Array} + */ + getContent() { + return this.arr; + } + /** + * @return {boolean} + */ + isCountable() { + return true; + } + /** + * @return {ContentAny} + */ + copy() { + return new _ContentAny(this.arr); + } + /** + * @param {number} offset + * @return {ContentAny} + */ + splice(offset) { + const right = new _ContentAny(this.arr.slice(offset)); + this.arr = this.arr.slice(0, offset); + return right; + } + /** + * @param {ContentAny} right + * @return {boolean} + */ + mergeWith(right) { + this.arr = this.arr.concat(right.arr); + return true; + } + /** + * @param {Transaction} transaction + * @param {Item} item + */ + integrate(transaction, item) { + } + /** + * @param {Transaction} transaction + */ + delete(transaction) { + } + /** + * @param {StructStore} store + */ + gc(store) { + } + /** + * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder + * @param {number} offset + */ + write(encoder, offset) { + const len = this.arr.length; + encoder.writeLen(len - offset); + for (let i = offset; i < len; i++) { + const c = this.arr[i]; + encoder.writeAny(c); + } + } + /** + * @return {number} + */ + getRef() { + return 8; + } + }; + var readContentAny = (decoder) => { + const len = decoder.readLen(); + const cs = []; + for (let i = 0; i < len; i++) { + cs.push(decoder.readAny()); + } + return new ContentAny(cs); + }; + var ContentString = class _ContentString { + /** + * @param {string} str + */ + constructor(str) { + this.str = str; + } + /** + * @return {number} + */ + getLength() { + return this.str.length; + } + /** + * @return {Array} + */ + getContent() { + return this.str.split(""); + } + /** + * @return {boolean} + */ + isCountable() { + return true; + } + /** + * @return {ContentString} + */ + copy() { + return new _ContentString(this.str); + } + /** + * @param {number} offset + * @return {ContentString} + */ + splice(offset) { + const right = new _ContentString(this.str.slice(offset)); + this.str = this.str.slice(0, offset); + const firstCharCode = this.str.charCodeAt(offset - 1); + if (firstCharCode >= 55296 && firstCharCode <= 56319) { + this.str = this.str.slice(0, offset - 1) + "\uFFFD"; + right.str = "\uFFFD" + right.str.slice(1); + } + return right; + } + /** + * @param {ContentString} right + * @return {boolean} + */ + mergeWith(right) { + this.str += right.str; + return true; + } + /** + * @param {Transaction} transaction + * @param {Item} item + */ + integrate(transaction, item) { + } + /** + * @param {Transaction} transaction + */ + delete(transaction) { + } + /** + * @param {StructStore} store + */ + gc(store) { + } + /** + * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder + * @param {number} offset + */ + write(encoder, offset) { + encoder.writeString(offset === 0 ? this.str : this.str.slice(offset)); + } + /** + * @return {number} + */ + getRef() { + return 4; + } + }; + var readContentString = (decoder) => new ContentString(decoder.readString()); + var typeRefs = [ + readYArray, + readYMap, + readYText, + readYXmlElement, + readYXmlFragment, + readYXmlHook, + readYXmlText + ]; + var YArrayRefID = 0; + var YMapRefID = 1; + var YTextRefID = 2; + var YXmlElementRefID = 3; + var YXmlFragmentRefID = 4; + var YXmlHookRefID = 5; + var YXmlTextRefID = 6; + var ContentType = class _ContentType { + /** + * @param {AbstractType} type + */ + constructor(type) { + this.type = type; + } + /** + * @return {number} + */ + getLength() { + return 1; + } + /** + * @return {Array} + */ + getContent() { + return [this.type]; + } + /** + * @return {boolean} + */ + isCountable() { + return true; + } + /** + * @return {ContentType} + */ + copy() { + return new _ContentType(this.type._copy()); + } + /** + * @param {number} offset + * @return {ContentType} + */ + splice(offset) { + throw methodUnimplemented(); + } + /** + * @param {ContentType} right + * @return {boolean} + */ + mergeWith(right) { + return false; + } + /** + * @param {Transaction} transaction + * @param {Item} item + */ + integrate(transaction, item) { + this.type._integrate(transaction.doc, item); + } + /** + * @param {Transaction} transaction + */ + delete(transaction) { + let item = this.type._start; + while (item !== null) { + if (!item.deleted) { + item.delete(transaction); + } else if (item.id.clock < (transaction.beforeState.get(item.id.client) || 0)) { + transaction._mergeStructs.push(item); + } + item = item.right; + } + this.type._map.forEach((item2) => { + if (!item2.deleted) { + item2.delete(transaction); + } else if (item2.id.clock < (transaction.beforeState.get(item2.id.client) || 0)) { + transaction._mergeStructs.push(item2); + } + }); + transaction.changed.delete(this.type); + } + /** + * @param {StructStore} store + */ + gc(store) { + let item = this.type._start; + while (item !== null) { + item.gc(store, true); + item = item.right; + } + this.type._start = null; + this.type._map.forEach( + /** @param {Item | null} item */ + (item2) => { + while (item2 !== null) { + item2.gc(store, true); + item2 = item2.left; + } + } + ); + this.type._map = /* @__PURE__ */ new Map(); + } + /** + * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder + * @param {number} offset + */ + write(encoder, offset) { + this.type._write(encoder); + } + /** + * @return {number} + */ + getRef() { + return 7; + } + }; + var readContentType = (decoder) => new ContentType(typeRefs[decoder.readTypeRef()](decoder)); + var splitItem = (transaction, leftItem, diff) => { + const { client, clock } = leftItem.id; + const rightItem = new Item( + createID(client, clock + diff), + leftItem, + createID(client, clock + diff - 1), + leftItem.right, + leftItem.rightOrigin, + leftItem.parent, + leftItem.parentSub, + leftItem.content.splice(diff) + ); + if (leftItem.deleted) { + rightItem.markDeleted(); + } + if (leftItem.keep) { + rightItem.keep = true; + } + if (leftItem.redone !== null) { + rightItem.redone = createID(leftItem.redone.client, leftItem.redone.clock + diff); + } + leftItem.right = rightItem; + if (rightItem.right !== null) { + rightItem.right.left = rightItem; + } + transaction._mergeStructs.push(rightItem); + if (rightItem.parentSub !== null && rightItem.right === null) { + rightItem.parent._map.set(rightItem.parentSub, rightItem); + } + leftItem.length = diff; + return rightItem; + }; + var Item = class _Item extends AbstractStruct { + /** + * @param {ID} id + * @param {Item | null} left + * @param {ID | null} origin + * @param {Item | null} right + * @param {ID | null} rightOrigin + * @param {AbstractType|ID|null} parent Is a type if integrated, is null if it is possible to copy parent from left or right, is ID before integration to search for it. + * @param {string | null} parentSub + * @param {AbstractContent} content + */ + constructor(id2, left, origin, right, rightOrigin, parent, parentSub, content) { + super(id2, content.getLength()); + this.origin = origin; + this.left = left; + this.right = right; + this.rightOrigin = rightOrigin; + this.parent = parent; + this.parentSub = parentSub; + this.redone = null; + this.content = content; + this.info = this.content.isCountable() ? BIT2 : 0; + } + /** + * This is used to mark the item as an indexed fast-search marker + * + * @type {boolean} + */ + set marker(isMarked) { + if ((this.info & BIT4) > 0 !== isMarked) { + this.info ^= BIT4; + } + } + get marker() { + return (this.info & BIT4) > 0; + } + /** + * If true, do not garbage collect this Item. + */ + get keep() { + return (this.info & BIT1) > 0; + } + set keep(doKeep) { + if (this.keep !== doKeep) { + this.info ^= BIT1; + } + } + get countable() { + return (this.info & BIT2) > 0; + } + /** + * Whether this item was deleted or not. + * @type {Boolean} + */ + get deleted() { + return (this.info & BIT3) > 0; + } + set deleted(doDelete) { + if (this.deleted !== doDelete) { + this.info ^= BIT3; + } + } + markDeleted() { + this.info |= BIT3; + } + /** + * Return the creator clientID of the missing op or define missing items and return null. + * + * @param {Transaction} transaction + * @param {StructStore} store + * @return {null | number} + */ + getMissing(transaction, store) { + if (this.origin && this.origin.client !== this.id.client && this.origin.clock >= getState(store, this.origin.client)) { + return this.origin.client; + } + if (this.rightOrigin && this.rightOrigin.client !== this.id.client && this.rightOrigin.clock >= getState(store, this.rightOrigin.client)) { + return this.rightOrigin.client; + } + if (this.parent && this.parent.constructor === ID && this.id.client !== this.parent.client && this.parent.clock >= getState(store, this.parent.client)) { + return this.parent.client; + } + if (this.origin) { + this.left = getItemCleanEnd(transaction, store, this.origin); + this.origin = this.left.lastId; + } + if (this.rightOrigin) { + this.right = getItemCleanStart(transaction, this.rightOrigin); + this.rightOrigin = this.right.id; + } + if (this.left && this.left.constructor === GC || this.right && this.right.constructor === GC) { + this.parent = null; + } else if (!this.parent) { + if (this.left && this.left.constructor === _Item) { + this.parent = this.left.parent; + this.parentSub = this.left.parentSub; + } else if (this.right && this.right.constructor === _Item) { + this.parent = this.right.parent; + this.parentSub = this.right.parentSub; + } + } else if (this.parent.constructor === ID) { + const parentItem = getItem(store, this.parent); + if (parentItem.constructor === GC) { + this.parent = null; + } else { + this.parent = /** @type {ContentType} */ + parentItem.content.type; + } + } + return null; + } + /** + * @param {Transaction} transaction + * @param {number} offset + */ + integrate(transaction, offset) { + if (offset > 0) { + this.id.clock += offset; + this.left = getItemCleanEnd(transaction, transaction.doc.store, createID(this.id.client, this.id.clock - 1)); + this.origin = this.left.lastId; + this.content = this.content.splice(offset); + this.length -= offset; + } + if (this.parent) { + if (!this.left && (!this.right || this.right.left !== null) || this.left && this.left.right !== this.right) { + let left = this.left; + let o; + if (left !== null) { + o = left.right; + } else if (this.parentSub !== null) { + o = /** @type {AbstractType} */ + this.parent._map.get(this.parentSub) || null; + while (o !== null && o.left !== null) { + o = o.left; + } + } else { + o = /** @type {AbstractType} */ + this.parent._start; + } + const conflictingItems = /* @__PURE__ */ new Set(); + const itemsBeforeOrigin = /* @__PURE__ */ new Set(); + while (o !== null && o !== this.right) { + itemsBeforeOrigin.add(o); + conflictingItems.add(o); + if (compareIDs(this.origin, o.origin)) { + if (o.id.client < this.id.client) { + left = o; + conflictingItems.clear(); + } else if (compareIDs(this.rightOrigin, o.rightOrigin)) { + break; + } + } else if (o.origin !== null && itemsBeforeOrigin.has(getItem(transaction.doc.store, o.origin))) { + if (!conflictingItems.has(getItem(transaction.doc.store, o.origin))) { + left = o; + conflictingItems.clear(); + } + } else { + break; + } + o = o.right; + } + this.left = left; + } + if (this.left !== null) { + const right = this.left.right; + this.right = right; + this.left.right = this; + } else { + let r; + if (this.parentSub !== null) { + r = /** @type {AbstractType} */ + this.parent._map.get(this.parentSub) || null; + while (r !== null && r.left !== null) { + r = r.left; + } + } else { + r = /** @type {AbstractType} */ + this.parent._start; + this.parent._start = this; + } + this.right = r; + } + if (this.right !== null) { + this.right.left = this; + } else if (this.parentSub !== null) { + this.parent._map.set(this.parentSub, this); + if (this.left !== null) { + this.left.delete(transaction); + } + } + if (this.parentSub === null && this.countable && !this.deleted) { + this.parent._length += this.length; + } + addStruct(transaction.doc.store, this); + this.content.integrate(transaction, this); + addChangedTypeToTransaction( + transaction, + /** @type {AbstractType} */ + this.parent, + this.parentSub + ); + if ( + /** @type {AbstractType} */ + this.parent._item !== null && /** @type {AbstractType} */ + this.parent._item.deleted || this.parentSub !== null && this.right !== null + ) { + this.delete(transaction); + } + } else { + new GC(this.id, this.length).integrate(transaction, 0); + } + } + /** + * Returns the next non-deleted item + */ + get next() { + let n = this.right; + while (n !== null && n.deleted) { + n = n.right; + } + return n; + } + /** + * Returns the previous non-deleted item + */ + get prev() { + let n = this.left; + while (n !== null && n.deleted) { + n = n.left; + } + return n; + } + /** + * Computes the last content address of this Item. + */ + get lastId() { + return this.length === 1 ? this.id : createID(this.id.client, this.id.clock + this.length - 1); + } + /** + * Try to merge two items + * + * @param {Item} right + * @return {boolean} + */ + mergeWith(right) { + if (this.constructor === right.constructor && compareIDs(right.origin, this.lastId) && this.right === right && compareIDs(this.rightOrigin, right.rightOrigin) && this.id.client === right.id.client && this.id.clock + this.length === right.id.clock && this.deleted === right.deleted && this.redone === null && right.redone === null && this.content.constructor === right.content.constructor && this.content.mergeWith(right.content)) { + const searchMarker = ( + /** @type {AbstractType} */ + this.parent._searchMarker + ); + if (searchMarker) { + searchMarker.forEach((marker) => { + if (marker.p === right) { + marker.p = this; + if (!this.deleted && this.countable) { + marker.index -= this.length; + } + } + }); + } + if (right.keep) { + this.keep = true; + } + this.right = right.right; + if (this.right !== null) { + this.right.left = this; + } + this.length += right.length; + return true; + } + return false; + } + /** + * Mark this Item as deleted. + * + * @param {Transaction} transaction + */ + delete(transaction) { + if (!this.deleted) { + const parent = ( + /** @type {AbstractType} */ + this.parent + ); + if (this.countable && this.parentSub === null) { + parent._length -= this.length; + } + this.markDeleted(); + addToDeleteSet(transaction.deleteSet, this.id.client, this.id.clock, this.length); + addChangedTypeToTransaction(transaction, parent, this.parentSub); + this.content.delete(transaction); + } + } + /** + * @param {StructStore} store + * @param {boolean} parentGCd + */ + gc(store, parentGCd) { + if (!this.deleted) { + throw unexpectedCase(); + } + this.content.gc(store); + if (parentGCd) { + replaceStruct(store, this, new GC(this.id, this.length)); + } else { + this.content = new ContentDeleted(this.length); + } + } + /** + * Transform the properties of this type to binary and write it to an + * BinaryEncoder. + * + * This is called when this Item is sent to a remote peer. + * + * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder The encoder to write data to. + * @param {number} offset + */ + write(encoder, offset) { + const origin = offset > 0 ? createID(this.id.client, this.id.clock + offset - 1) : this.origin; + const rightOrigin = this.rightOrigin; + const parentSub = this.parentSub; + const info = this.content.getRef() & BITS5 | (origin === null ? 0 : BIT8) | // origin is defined + (rightOrigin === null ? 0 : BIT7) | // right origin is defined + (parentSub === null ? 0 : BIT6); + encoder.writeInfo(info); + if (origin !== null) { + encoder.writeLeftID(origin); + } + if (rightOrigin !== null) { + encoder.writeRightID(rightOrigin); + } + if (origin === null && rightOrigin === null) { + const parent = ( + /** @type {AbstractType} */ + this.parent + ); + if (parent._item !== void 0) { + const parentItem = parent._item; + if (parentItem === null) { + const ykey = findRootTypeKey(parent); + encoder.writeParentInfo(true); + encoder.writeString(ykey); + } else { + encoder.writeParentInfo(false); + encoder.writeLeftID(parentItem.id); + } + } else if (parent.constructor === String) { + encoder.writeParentInfo(true); + encoder.writeString(parent); + } else if (parent.constructor === ID) { + encoder.writeParentInfo(false); + encoder.writeLeftID(parent); + } else { + unexpectedCase(); + } + if (parentSub !== null) { + encoder.writeString(parentSub); + } + } + this.content.write(encoder, offset); + } + }; + var readItemContent = (decoder, info) => contentRefs[info & BITS5](decoder); + var contentRefs = [ + () => { + unexpectedCase(); + }, + // GC is not ItemContent + readContentDeleted, + // 1 + readContentJSON, + // 2 + readContentBinary, + // 3 + readContentString, + // 4 + readContentEmbed, + // 5 + readContentFormat, + // 6 + readContentType, + // 7 + readContentAny, + // 8 + readContentDoc, + // 9 + () => { + unexpectedCase(); + } + // 10 - Skip is not ItemContent + ]; + var structSkipRefNumber = 10; + var Skip = class extends AbstractStruct { + get deleted() { + return true; + } + delete() { + } + /** + * @param {Skip} right + * @return {boolean} + */ + mergeWith(right) { + if (this.constructor !== right.constructor) { + return false; + } + this.length += right.length; + return true; + } + /** + * @param {Transaction} transaction + * @param {number} offset + */ + integrate(transaction, offset) { + unexpectedCase(); + } + /** + * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder + * @param {number} offset + */ + write(encoder, offset) { + encoder.writeInfo(structSkipRefNumber); + writeVarUint(encoder.restEncoder, this.length - offset); + } + /** + * @param {Transaction} transaction + * @param {StructStore} store + * @return {null | number} + */ + getMissing(transaction, store) { + return null; + } + }; + var glo = ( + /** @type {any} */ + typeof globalThis !== "undefined" ? globalThis : typeof window !== "undefined" ? window : typeof global !== "undefined" ? global : {} + ); + var importIdentifier = "__ $YJS$ __"; + if (glo[importIdentifier] === true) { + console.error("Yjs was already imported. This breaks constructor checks and will lead to issues! - https://github.com/yjs/yjs/issues/438"); + } + glo[importIdentifier] = true; + + // node_modules/@hocuspocus/common/dist/hocuspocus-common.esm.js + var floor2 = Math.floor; + var min2 = (a, b) => a < b ? a : b; + var max2 = (a, b) => a > b ? a : b; + var BIT82 = 128; + var BITS72 = 127; + var MAX_SAFE_INTEGER2 = Number.MAX_SAFE_INTEGER; + var _encodeUtf8Polyfill2 = (str) => { + const encodedString = unescape(encodeURIComponent(str)); + const len = encodedString.length; + const buf = new Uint8Array(len); + for (let i = 0; i < len; i++) { + buf[i] = /** @type {number} */ + encodedString.codePointAt(i); + } + return buf; + }; + var utf8TextEncoder2 = ( + /** @type {TextEncoder} */ + typeof TextEncoder !== "undefined" ? new TextEncoder() : null + ); + var _encodeUtf8Native2 = (str) => utf8TextEncoder2.encode(str); + var encodeUtf82 = utf8TextEncoder2 ? _encodeUtf8Native2 : _encodeUtf8Polyfill2; + var utf8TextDecoder2 = typeof TextDecoder === "undefined" ? null : new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }); + if (utf8TextDecoder2 && utf8TextDecoder2.decode(new Uint8Array()).length === 1) { + utf8TextDecoder2 = null; + } + var write2 = (encoder, num) => { + const bufferLen = encoder.cbuf.length; + if (encoder.cpos === bufferLen) { + encoder.bufs.push(encoder.cbuf); + encoder.cbuf = new Uint8Array(bufferLen * 2); + encoder.cpos = 0; + } + encoder.cbuf[encoder.cpos++] = num; + }; + var writeVarUint2 = (encoder, num) => { + while (num > BITS72) { + write2(encoder, BIT82 | BITS72 & num); + num = floor2(num / 128); + } + write2(encoder, BITS72 & num); + }; + var _strBuffer2 = new Uint8Array(3e4); + var _maxStrBSize2 = _strBuffer2.length / 3; + var _writeVarStringNative2 = (encoder, str) => { + if (str.length < _maxStrBSize2) { + const written = utf8TextEncoder2.encodeInto(str, _strBuffer2).written || 0; + writeVarUint2(encoder, written); + for (let i = 0; i < written; i++) { + write2(encoder, _strBuffer2[i]); + } + } else { + writeVarUint8Array2(encoder, encodeUtf82(str)); + } + }; + var _writeVarStringPolyfill2 = (encoder, str) => { + const encodedString = unescape(encodeURIComponent(str)); + const len = encodedString.length; + writeVarUint2(encoder, len); + for (let i = 0; i < len; i++) { + write2( + encoder, + /** @type {number} */ + encodedString.codePointAt(i) + ); + } + }; + var writeVarString2 = utf8TextEncoder2 && /** @type {any} */ + utf8TextEncoder2.encodeInto ? _writeVarStringNative2 : _writeVarStringPolyfill2; + var writeUint8Array2 = (encoder, uint8Array) => { + const bufferLen = encoder.cbuf.length; + const cpos = encoder.cpos; + const leftCopyLen = min2(bufferLen - cpos, uint8Array.length); + const rightCopyLen = uint8Array.length - leftCopyLen; + encoder.cbuf.set(uint8Array.subarray(0, leftCopyLen), cpos); + encoder.cpos += leftCopyLen; + if (rightCopyLen > 0) { + encoder.bufs.push(encoder.cbuf); + encoder.cbuf = new Uint8Array(max2(bufferLen * 2, rightCopyLen)); + encoder.cbuf.set(uint8Array.subarray(leftCopyLen)); + encoder.cpos = rightCopyLen; + } + }; + var writeVarUint8Array2 = (encoder, uint8Array) => { + writeVarUint2(encoder, uint8Array.byteLength); + writeUint8Array2(encoder, uint8Array); + }; + var create7 = (s) => new Error(s); + var errorUnexpectedEndOfArray2 = create7("Unexpected end of array"); + var errorIntegerOutOfRange2 = create7("Integer out of Range"); + var readUint8Array2 = (decoder, len) => { + const view = new Uint8Array(decoder.arr.buffer, decoder.pos + decoder.arr.byteOffset, len); + decoder.pos += len; + return view; + }; + var readVarUint8Array2 = (decoder) => readUint8Array2(decoder, readVarUint2(decoder)); + var readUint82 = (decoder) => decoder.arr[decoder.pos++]; + var readVarUint2 = (decoder) => { + let num = 0; + let mult = 1; + const len = decoder.arr.length; + while (decoder.pos < len) { + const r = decoder.arr[decoder.pos++]; + num = num + (r & BITS72) * mult; + mult *= 128; + if (r < BIT82) { + return num; + } + if (num > MAX_SAFE_INTEGER2) { + throw errorIntegerOutOfRange2; + } + } + throw errorUnexpectedEndOfArray2; + }; + var _readVarStringPolyfill2 = (decoder) => { + let remainingLen = readVarUint2(decoder); + if (remainingLen === 0) { + return ""; + } else { + let encodedString = String.fromCodePoint(readUint82(decoder)); + if (--remainingLen < 100) { + while (remainingLen--) { + encodedString += String.fromCodePoint(readUint82(decoder)); + } + } else { + while (remainingLen > 0) { + const nextLen = remainingLen < 1e4 ? remainingLen : 1e4; + const bytes = decoder.arr.subarray(decoder.pos, decoder.pos + nextLen); + decoder.pos += nextLen; + encodedString += String.fromCodePoint.apply( + null, + /** @type {any} */ + bytes + ); + remainingLen -= nextLen; + } + } + return decodeURIComponent(escape(encodedString)); + } + }; + var _readVarStringNative2 = (decoder) => ( + /** @type any */ + utf8TextDecoder2.decode(readVarUint8Array2(decoder)) + ); + var readVarString2 = utf8TextDecoder2 ? _readVarStringNative2 : _readVarStringPolyfill2; + var AuthMessageType; + (function(AuthMessageType2) { + AuthMessageType2[AuthMessageType2["Token"] = 0] = "Token"; + AuthMessageType2[AuthMessageType2["PermissionDenied"] = 1] = "PermissionDenied"; + AuthMessageType2[AuthMessageType2["Authenticated"] = 2] = "Authenticated"; + })(AuthMessageType || (AuthMessageType = {})); + var writeAuthentication = (encoder, auth) => { + writeVarUint2(encoder, AuthMessageType.Token); + writeVarString2(encoder, auth); + }; + var readAuthMessage = (decoder, sendToken, permissionDeniedHandler, authenticatedHandler) => { + switch (readVarUint2(decoder)) { + case AuthMessageType.Token: { + sendToken(); + break; + } + case AuthMessageType.PermissionDenied: { + permissionDeniedHandler(readVarString2(decoder)); + break; + } + case AuthMessageType.Authenticated: { + authenticatedHandler(readVarString2(decoder)); + break; + } + } + }; + var awarenessStatesToArray = (states) => { + return Array.from(states.entries()).map(([key, value]) => { + return { + clientId: key, + ...value + }; + }); + }; + var WsReadyStates; + (function(WsReadyStates2) { + WsReadyStates2[WsReadyStates2["Connecting"] = 0] = "Connecting"; + WsReadyStates2[WsReadyStates2["Open"] = 1] = "Open"; + WsReadyStates2[WsReadyStates2["Closing"] = 2] = "Closing"; + WsReadyStates2[WsReadyStates2["Closed"] = 3] = "Closed"; + })(WsReadyStates || (WsReadyStates = {})); + + // node_modules/@lifeomic/attempt/dist/es6/src/index.js + function applyDefaults(options) { + if (!options) { + options = {}; + } + return { + delay: options.delay === void 0 ? 200 : options.delay, + initialDelay: options.initialDelay === void 0 ? 0 : options.initialDelay, + minDelay: options.minDelay === void 0 ? 0 : options.minDelay, + maxDelay: options.maxDelay === void 0 ? 0 : options.maxDelay, + factor: options.factor === void 0 ? 0 : options.factor, + maxAttempts: options.maxAttempts === void 0 ? 3 : options.maxAttempts, + timeout: options.timeout === void 0 ? 0 : options.timeout, + jitter: options.jitter === true, + initialJitter: options.initialJitter === true, + handleError: options.handleError === void 0 ? null : options.handleError, + handleTimeout: options.handleTimeout === void 0 ? null : options.handleTimeout, + beforeAttempt: options.beforeAttempt === void 0 ? null : options.beforeAttempt, + calculateDelay: options.calculateDelay === void 0 ? null : options.calculateDelay + }; + } + async function sleep(delay) { + return new Promise((resolve) => setTimeout(resolve, delay)); + } + function defaultCalculateDelay(context, options) { + let delay = options.delay; + if (delay === 0) { + return 0; + } + if (options.factor) { + delay *= Math.pow(options.factor, context.attemptNum - 1); + if (options.maxDelay !== 0) { + delay = Math.min(delay, options.maxDelay); + } + } + if (options.jitter) { + const min4 = Math.ceil(options.minDelay); + const max4 = Math.floor(delay); + delay = Math.floor(Math.random() * (max4 - min4 + 1)) + min4; + } + return Math.round(delay); + } + async function retry(attemptFunc, attemptOptions) { + const options = applyDefaults(attemptOptions); + for (const prop of [ + "delay", + "initialDelay", + "minDelay", + "maxDelay", + "maxAttempts", + "timeout" + ]) { + const value = options[prop]; + if (!Number.isInteger(value) || value < 0) { + throw new Error(`Value for ${prop} must be an integer greater than or equal to 0`); + } + } + if (options.factor.constructor !== Number || options.factor < 0) { + throw new Error(`Value for factor must be a number greater than or equal to 0`); + } + if (options.delay < options.minDelay) { + throw new Error(`delay cannot be less than minDelay (delay: ${options.delay}, minDelay: ${options.minDelay}`); + } + const context = { + attemptNum: 0, + attemptsRemaining: options.maxAttempts ? options.maxAttempts : -1, + aborted: false, + abort() { + context.aborted = true; + } + }; + const calculateDelay = options.calculateDelay || defaultCalculateDelay; + async function makeAttempt() { + if (options.beforeAttempt) { + options.beforeAttempt(context, options); + } + if (context.aborted) { + const err = new Error(`Attempt aborted`); + err.code = "ATTEMPT_ABORTED"; + throw err; + } + const onError = async (err) => { + if (options.handleError) { + await options.handleError(err, context, options); + } + if (context.aborted || context.attemptsRemaining === 0) { + throw err; + } + context.attemptNum++; + const delay = calculateDelay(context, options); + if (delay) { + await sleep(delay); + } + return makeAttempt(); + }; + if (context.attemptsRemaining > 0) { + context.attemptsRemaining--; + } + if (options.timeout) { + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + if (options.handleTimeout) { + try { + resolve(options.handleTimeout(context, options)); + } catch (e) { + reject(e); + } + } else { + const err = new Error(`Retry timeout (attemptNum: ${context.attemptNum}, timeout: ${options.timeout})`); + err.code = "ATTEMPT_TIMEOUT"; + reject(err); + } + }, options.timeout); + attemptFunc(context, options).then((result) => { + clearTimeout(timer); + resolve(result); + }).catch((err) => { + clearTimeout(timer); + onError(err).then(resolve).catch(reject); + }); + }); + } else { + return attemptFunc(context, options).catch(onError); + } + } + const initialDelay = options.calculateDelay ? options.calculateDelay(context, options) : options.initialDelay; + if (initialDelay) { + await sleep(initialDelay); + } + if (context.attemptNum < 1 && options.initialJitter) { + const delay = calculateDelay(context, options); + if (delay) { + await sleep(delay); + } + } + return makeAttempt(); + } + + // node_modules/@hocuspocus/provider/dist/hocuspocus-provider.esm.js + var floor3 = Math.floor; + var min3 = (a, b) => a < b ? a : b; + var max3 = (a, b) => a > b ? a : b; + var BIT72 = 64; + var BIT83 = 128; + var BITS62 = 63; + var BITS73 = 127; + var MAX_SAFE_INTEGER3 = Number.MAX_SAFE_INTEGER; + var create$2 = () => /* @__PURE__ */ new Set(); + var from2 = Array.from; + var _encodeUtf8Polyfill3 = (str) => { + const encodedString = unescape(encodeURIComponent(str)); + const len = encodedString.length; + const buf = new Uint8Array(len); + for (let i = 0; i < len; i++) { + buf[i] = /** @type {number} */ + encodedString.codePointAt(i); + } + return buf; + }; + var utf8TextEncoder3 = ( + /** @type {TextEncoder} */ + typeof TextEncoder !== "undefined" ? new TextEncoder() : null + ); + var _encodeUtf8Native3 = (str) => utf8TextEncoder3.encode(str); + var encodeUtf83 = utf8TextEncoder3 ? _encodeUtf8Native3 : _encodeUtf8Polyfill3; + var utf8TextDecoder3 = typeof TextDecoder === "undefined" ? null : new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }); + if (utf8TextDecoder3 && utf8TextDecoder3.decode(new Uint8Array()).length === 1) { + utf8TextDecoder3 = null; + } + var Encoder2 = class { + constructor() { + this.cpos = 0; + this.cbuf = new Uint8Array(100); + this.bufs = []; + } + }; + var createEncoder2 = () => new Encoder2(); + var length$1 = (encoder) => { + let len = encoder.cpos; + for (let i = 0; i < encoder.bufs.length; i++) { + len += encoder.bufs[i].length; + } + return len; + }; + var toUint8Array2 = (encoder) => { + const uint8arr = new Uint8Array(length$1(encoder)); + let curPos = 0; + for (let i = 0; i < encoder.bufs.length; i++) { + const d = encoder.bufs[i]; + uint8arr.set(d, curPos); + curPos += d.length; + } + uint8arr.set(new Uint8Array(encoder.cbuf.buffer, 0, encoder.cpos), curPos); + return uint8arr; + }; + var write3 = (encoder, num) => { + const bufferLen = encoder.cbuf.length; + if (encoder.cpos === bufferLen) { + encoder.bufs.push(encoder.cbuf); + encoder.cbuf = new Uint8Array(bufferLen * 2); + encoder.cpos = 0; + } + encoder.cbuf[encoder.cpos++] = num; + }; + var writeVarUint3 = (encoder, num) => { + while (num > BITS73) { + write3(encoder, BIT83 | BITS73 & num); + num = floor3(num / 128); + } + write3(encoder, BITS73 & num); + }; + var _strBuffer3 = new Uint8Array(3e4); + var _maxStrBSize3 = _strBuffer3.length / 3; + var _writeVarStringNative3 = (encoder, str) => { + if (str.length < _maxStrBSize3) { + const written = utf8TextEncoder3.encodeInto(str, _strBuffer3).written || 0; + writeVarUint3(encoder, written); + for (let i = 0; i < written; i++) { + write3(encoder, _strBuffer3[i]); + } + } else { + writeVarUint8Array3(encoder, encodeUtf83(str)); + } + }; + var _writeVarStringPolyfill3 = (encoder, str) => { + const encodedString = unescape(encodeURIComponent(str)); + const len = encodedString.length; + writeVarUint3(encoder, len); + for (let i = 0; i < len; i++) { + write3( + encoder, + /** @type {number} */ + encodedString.codePointAt(i) + ); + } + }; + var writeVarString3 = utf8TextEncoder3 && /** @type {any} */ + utf8TextEncoder3.encodeInto ? _writeVarStringNative3 : _writeVarStringPolyfill3; + var writeUint8Array3 = (encoder, uint8Array) => { + const bufferLen = encoder.cbuf.length; + const cpos = encoder.cpos; + const leftCopyLen = min3(bufferLen - cpos, uint8Array.length); + const rightCopyLen = uint8Array.length - leftCopyLen; + encoder.cbuf.set(uint8Array.subarray(0, leftCopyLen), cpos); + encoder.cpos += leftCopyLen; + if (rightCopyLen > 0) { + encoder.bufs.push(encoder.cbuf); + encoder.cbuf = new Uint8Array(max3(bufferLen * 2, rightCopyLen)); + encoder.cbuf.set(uint8Array.subarray(leftCopyLen)); + encoder.cpos = rightCopyLen; + } + }; + var writeVarUint8Array3 = (encoder, uint8Array) => { + writeVarUint3(encoder, uint8Array.byteLength); + writeUint8Array3(encoder, uint8Array); + }; + var create$1 = (s) => new Error(s); + var errorUnexpectedEndOfArray3 = create$1("Unexpected end of array"); + var errorIntegerOutOfRange3 = create$1("Integer out of Range"); + var Decoder2 = class { + /** + * @param {Uint8Array} uint8Array Binary data to decode + */ + constructor(uint8Array) { + this.arr = uint8Array; + this.pos = 0; + } + }; + var createDecoder2 = (uint8Array) => new Decoder2(uint8Array); + var readUint8Array3 = (decoder, len) => { + const view = new Uint8Array(decoder.arr.buffer, decoder.pos + decoder.arr.byteOffset, len); + decoder.pos += len; + return view; + }; + var readVarUint8Array3 = (decoder) => readUint8Array3(decoder, readVarUint3(decoder)); + var readUint83 = (decoder) => decoder.arr[decoder.pos++]; + var readVarUint3 = (decoder) => { + let num = 0; + let mult = 1; + const len = decoder.arr.length; + while (decoder.pos < len) { + const r = decoder.arr[decoder.pos++]; + num = num + (r & BITS73) * mult; + mult *= 128; + if (r < BIT83) { + return num; + } + if (num > MAX_SAFE_INTEGER3) { + throw errorIntegerOutOfRange3; + } + } + throw errorUnexpectedEndOfArray3; + }; + var readVarInt2 = (decoder) => { + let r = decoder.arr[decoder.pos++]; + let num = r & BITS62; + let mult = 64; + const sign = (r & BIT72) > 0 ? -1 : 1; + if ((r & BIT83) === 0) { + return sign * num; + } + const len = decoder.arr.length; + while (decoder.pos < len) { + r = decoder.arr[decoder.pos++]; + num = num + (r & BITS73) * mult; + mult *= 128; + if (r < BIT83) { + return sign * num; + } + if (num > MAX_SAFE_INTEGER3) { + throw errorIntegerOutOfRange3; + } + } + throw errorUnexpectedEndOfArray3; + }; + var _readVarStringPolyfill3 = (decoder) => { + let remainingLen = readVarUint3(decoder); + if (remainingLen === 0) { + return ""; + } else { + let encodedString = String.fromCodePoint(readUint83(decoder)); + if (--remainingLen < 100) { + while (remainingLen--) { + encodedString += String.fromCodePoint(readUint83(decoder)); + } + } else { + while (remainingLen > 0) { + const nextLen = remainingLen < 1e4 ? remainingLen : 1e4; + const bytes = decoder.arr.subarray(decoder.pos, decoder.pos + nextLen); + decoder.pos += nextLen; + encodedString += String.fromCodePoint.apply( + null, + /** @type {any} */ + bytes + ); + remainingLen -= nextLen; + } + } + return decodeURIComponent(escape(encodedString)); + } + }; + var _readVarStringNative3 = (decoder) => ( + /** @type any */ + utf8TextDecoder3.decode(readVarUint8Array3(decoder)) + ); + var readVarString3 = utf8TextDecoder3 ? _readVarStringNative3 : _readVarStringPolyfill3; + var peekVarString = (decoder) => { + const pos = decoder.pos; + const s = readVarString3(decoder); + decoder.pos = pos; + return s; + }; + var getUnixTime2 = Date.now; + var create8 = () => /* @__PURE__ */ new Map(); + var setIfUndefined2 = (map2, key, createT) => { + let set = map2.get(key); + if (set === void 0) { + map2.set(key, set = createT()); + } + return set; + }; + var Observable = class { + constructor() { + this._observers = create8(); + } + /** + * @param {N} name + * @param {function} f + */ + on(name, f) { + setIfUndefined2(this._observers, name, create$2).add(f); + } + /** + * @param {N} name + * @param {function} f + */ + once(name, f) { + const _f = (...args2) => { + this.off(name, _f); + f(...args2); + }; + this.on(name, _f); + } + /** + * @param {N} name + * @param {function} f + */ + off(name, f) { + const observers = this._observers.get(name); + if (observers !== void 0) { + observers.delete(f); + if (observers.size === 0) { + this._observers.delete(name); + } + } + } + /** + * Emit a named event. All registered event listeners that listen to the + * specified name will receive the event. + * + * @todo This should catch exceptions + * + * @param {N} name The event name. + * @param {Array} args The arguments that are applied to the event listener. + */ + emit(name, args2) { + return from2((this._observers.get(name) || create8()).values()).forEach((f) => f(...args2)); + } + destroy() { + this._observers = create8(); + } + }; + var keys2 = Object.keys; + var length2 = (obj) => keys2(obj).length; + var hasProperty2 = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key); + var equalityStrict = (a, b) => a === b; + var equalityDeep2 = (a, b) => { + if (a == null || b == null) { + return equalityStrict(a, b); + } + if (a.constructor !== b.constructor) { + return false; + } + if (a === b) { + return true; + } + switch (a.constructor) { + case ArrayBuffer: + a = new Uint8Array(a); + b = new Uint8Array(b); + // eslint-disable-next-line no-fallthrough + case Uint8Array: { + if (a.byteLength !== b.byteLength) { + return false; + } + for (let i = 0; i < a.length; i++) { + if (a[i] !== b[i]) { + return false; + } + } + break; + } + case Set: { + if (a.size !== b.size) { + return false; + } + for (const value of a) { + if (!b.has(value)) { + return false; + } + } + break; + } + case Map: { + if (a.size !== b.size) { + return false; + } + for (const key of a.keys()) { + if (!b.has(key) || !equalityDeep2(a.get(key), b.get(key))) { + return false; + } + } + break; + } + case Object: + if (length2(a) !== length2(b)) { + return false; + } + for (const key in a) { + if (!hasProperty2(a, key) || !equalityDeep2(a[key], b[key])) { + return false; + } + } + break; + case Array: + if (a.length !== b.length) { + return false; + } + for (let i = 0; i < a.length; i++) { + if (!equalityDeep2(a[i], b[i])) { + return false; + } + } + break; + default: + return false; + } + return true; + }; + var outdatedTimeout = 3e4; + var Awareness = class extends Observable { + /** + * @param {Y.Doc} doc + */ + constructor(doc2) { + super(); + this.doc = doc2; + this.clientID = doc2.clientID; + this.states = /* @__PURE__ */ new Map(); + this.meta = /* @__PURE__ */ new Map(); + this._checkInterval = /** @type {any} */ + setInterval(() => { + const now = getUnixTime2(); + if (this.getLocalState() !== null && outdatedTimeout / 2 <= now - /** @type {{lastUpdated:number}} */ + this.meta.get(this.clientID).lastUpdated) { + this.setLocalState(this.getLocalState()); + } + const remove = []; + this.meta.forEach((meta, clientid) => { + if (clientid !== this.clientID && outdatedTimeout <= now - meta.lastUpdated && this.states.has(clientid)) { + remove.push(clientid); + } + }); + if (remove.length > 0) { + removeAwarenessStates(this, remove, "timeout"); + } + }, floor3(outdatedTimeout / 10)); + doc2.on("destroy", () => { + this.destroy(); + }); + this.setLocalState({}); + } + destroy() { + this.emit("destroy", [this]); + this.setLocalState(null); + super.destroy(); + clearInterval(this._checkInterval); + } + /** + * @return {Object|null} + */ + getLocalState() { + return this.states.get(this.clientID) || null; + } + /** + * @param {Object|null} state + */ + setLocalState(state) { + const clientID = this.clientID; + const currLocalMeta = this.meta.get(clientID); + const clock = currLocalMeta === void 0 ? 0 : currLocalMeta.clock + 1; + const prevState = this.states.get(clientID); + if (state === null) { + this.states.delete(clientID); + } else { + this.states.set(clientID, state); + } + this.meta.set(clientID, { + clock, + lastUpdated: getUnixTime2() + }); + const added = []; + const updated = []; + const filteredUpdated = []; + const removed = []; + if (state === null) { + removed.push(clientID); + } else if (prevState == null) { + if (state != null) { + added.push(clientID); + } + } else { + updated.push(clientID); + if (!equalityDeep2(prevState, state)) { + filteredUpdated.push(clientID); + } + } + if (added.length > 0 || filteredUpdated.length > 0 || removed.length > 0) { + this.emit("change", [{ added, updated: filteredUpdated, removed }, "local"]); + } + this.emit("update", [{ added, updated, removed }, "local"]); + } + /** + * @param {string} field + * @param {any} value + */ + setLocalStateField(field, value) { + const state = this.getLocalState(); + if (state !== null) { + this.setLocalState({ + ...state, + [field]: value + }); + } + } + /** + * @return {Map>} + */ + getStates() { + return this.states; + } + }; + var removeAwarenessStates = (awareness, clients, origin) => { + const removed = []; + for (let i = 0; i < clients.length; i++) { + const clientID = clients[i]; + if (awareness.states.has(clientID)) { + awareness.states.delete(clientID); + if (clientID === awareness.clientID) { + const curMeta = ( + /** @type {MetaClientState} */ + awareness.meta.get(clientID) + ); + awareness.meta.set(clientID, { + clock: curMeta.clock + 1, + lastUpdated: getUnixTime2() + }); + } + removed.push(clientID); + } + } + if (removed.length > 0) { + awareness.emit("change", [{ added: [], updated: [], removed }, origin]); + awareness.emit("update", [{ added: [], updated: [], removed }, origin]); + } + }; + var encodeAwarenessUpdate = (awareness, clients, states = awareness.states) => { + const len = clients.length; + const encoder = createEncoder2(); + writeVarUint3(encoder, len); + for (let i = 0; i < len; i++) { + const clientID = clients[i]; + const state = states.get(clientID) || null; + const clock = ( + /** @type {MetaClientState} */ + awareness.meta.get(clientID).clock + ); + writeVarUint3(encoder, clientID); + writeVarUint3(encoder, clock); + writeVarString3(encoder, JSON.stringify(state)); + } + return toUint8Array2(encoder); + }; + var applyAwarenessUpdate = (awareness, update, origin) => { + const decoder = createDecoder2(update); + const timestamp = getUnixTime2(); + const added = []; + const updated = []; + const filteredUpdated = []; + const removed = []; + const len = readVarUint3(decoder); + for (let i = 0; i < len; i++) { + const clientID = readVarUint3(decoder); + let clock = readVarUint3(decoder); + const state = JSON.parse(readVarString3(decoder)); + const clientMeta = awareness.meta.get(clientID); + const prevState = awareness.states.get(clientID); + const currClock = clientMeta === void 0 ? 0 : clientMeta.clock; + if (currClock < clock || currClock === clock && state === null && awareness.states.has(clientID)) { + if (state === null) { + if (clientID === awareness.clientID && awareness.getLocalState() != null) { + clock++; + } else { + awareness.states.delete(clientID); + } + } else { + awareness.states.set(clientID, state); + } + awareness.meta.set(clientID, { + clock, + lastUpdated: timestamp + }); + if (clientMeta === void 0 && state !== null) { + added.push(clientID); + } else if (clientMeta !== void 0 && state === null) { + removed.push(clientID); + } else if (state !== null) { + if (!equalityDeep2(state, prevState)) { + filteredUpdated.push(clientID); + } + updated.push(clientID); + } + } + } + if (added.length > 0 || filteredUpdated.length > 0 || removed.length > 0) { + awareness.emit("change", [{ + added, + updated: filteredUpdated, + removed + }, origin]); + } + if (added.length > 0 || updated.length > 0 || removed.length > 0) { + awareness.emit("update", [{ + added, + updated, + removed + }, origin]); + } + }; + var EventEmitter = class { + constructor() { + this.callbacks = {}; + } + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type + on(event, fn) { + if (!this.callbacks[event]) { + this.callbacks[event] = []; + } + this.callbacks[event].push(fn); + return this; + } + emit(event, ...args2) { + const callbacks = this.callbacks[event]; + if (callbacks) { + callbacks.forEach((callback) => callback.apply(this, args2)); + } + return this; + } + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type + off(event, fn) { + const callbacks = this.callbacks[event]; + if (callbacks) { + if (fn) { + this.callbacks[event] = callbacks.filter((callback) => callback !== fn); + } else { + delete this.callbacks[event]; + } + } + return this; + } + removeAllListeners() { + this.callbacks = {}; + } + }; + var IncomingMessage = class { + constructor(data) { + this.data = data; + this.encoder = createEncoder2(); + this.decoder = createDecoder2(new Uint8Array(this.data)); + } + peekVarString() { + return peekVarString(this.decoder); + } + readVarUint() { + return readVarUint3(this.decoder); + } + readVarString() { + return readVarString3(this.decoder); + } + readVarUint8Array() { + return readVarUint8Array3(this.decoder); + } + writeVarUint(type) { + return writeVarUint3(this.encoder, type); + } + writeVarString(string) { + return writeVarString3(this.encoder, string); + } + writeVarUint8Array(data) { + return writeVarUint8Array3(this.encoder, data); + } + length() { + return length$1(this.encoder); + } + }; + var MessageType; + (function(MessageType2) { + MessageType2[MessageType2["Sync"] = 0] = "Sync"; + MessageType2[MessageType2["Awareness"] = 1] = "Awareness"; + MessageType2[MessageType2["Auth"] = 2] = "Auth"; + MessageType2[MessageType2["QueryAwareness"] = 3] = "QueryAwareness"; + MessageType2[MessageType2["Stateless"] = 5] = "Stateless"; + MessageType2[MessageType2["CLOSE"] = 7] = "CLOSE"; + MessageType2[MessageType2["SyncStatus"] = 8] = "SyncStatus"; + })(MessageType || (MessageType = {})); + var WebSocketStatus; + (function(WebSocketStatus2) { + WebSocketStatus2["Connecting"] = "connecting"; + WebSocketStatus2["Connected"] = "connected"; + WebSocketStatus2["Disconnected"] = "disconnected"; + })(WebSocketStatus || (WebSocketStatus = {})); + var OutgoingMessage = class { + constructor() { + this.encoder = createEncoder2(); + } + get(args2) { + return args2.encoder; + } + toUint8Array() { + return toUint8Array2(this.encoder); + } + }; + var CloseMessage = class extends OutgoingMessage { + constructor() { + super(...arguments); + this.type = MessageType.CLOSE; + this.description = "Ask the server to close the connection"; + } + get(args2) { + writeVarString3(this.encoder, args2.documentName); + writeVarUint3(this.encoder, this.type); + return this.encoder; + } + }; + var HocuspocusProviderWebsocket = class extends EventEmitter { + constructor(configuration) { + super(); + this.messageQueue = []; + this.configuration = { + url: "", + autoConnect: true, + preserveTrailingSlash: false, + // @ts-ignore + document: void 0, + WebSocketPolyfill: void 0, + // TODO: this should depend on awareness.outdatedTime + messageReconnectTimeout: 3e4, + // 1 second + delay: 1e3, + // instant + initialDelay: 0, + // double the delay each time + factor: 2, + // unlimited retries + maxAttempts: 0, + // wait at least 1 second + minDelay: 1e3, + // at least every 30 seconds + maxDelay: 3e4, + // randomize + jitter: true, + // retry forever + timeout: 0, + onOpen: () => null, + onConnect: () => null, + onMessage: () => null, + onOutgoingMessage: () => null, + onStatus: () => null, + onDisconnect: () => null, + onClose: () => null, + onDestroy: () => null, + onAwarenessUpdate: () => null, + onAwarenessChange: () => null, + handleTimeout: null, + providerMap: /* @__PURE__ */ new Map() + }; + this.webSocket = null; + this.webSocketHandlers = {}; + this.shouldConnect = true; + this.status = WebSocketStatus.Disconnected; + this.lastMessageReceived = 0; + this.identifier = 0; + this.intervals = { + connectionChecker: null + }; + this.connectionAttempt = null; + this.receivedOnOpenPayload = void 0; + this.closeTries = 0; + this.setConfiguration(configuration); + this.configuration.WebSocketPolyfill = configuration.WebSocketPolyfill ? configuration.WebSocketPolyfill : WebSocket; + this.on("open", this.configuration.onOpen); + this.on("open", this.onOpen.bind(this)); + this.on("connect", this.configuration.onConnect); + this.on("message", this.configuration.onMessage); + this.on("outgoingMessage", this.configuration.onOutgoingMessage); + this.on("status", this.configuration.onStatus); + this.on("disconnect", this.configuration.onDisconnect); + this.on("close", this.configuration.onClose); + this.on("destroy", this.configuration.onDestroy); + this.on("awarenessUpdate", this.configuration.onAwarenessUpdate); + this.on("awarenessChange", this.configuration.onAwarenessChange); + this.on("close", this.onClose.bind(this)); + this.on("message", this.onMessage.bind(this)); + this.intervals.connectionChecker = setInterval(this.checkConnection.bind(this), this.configuration.messageReconnectTimeout / 10); + if (this.shouldConnect) { + this.connect(); + } + } + async onOpen(event) { + this.status = WebSocketStatus.Connected; + this.emit("status", { status: WebSocketStatus.Connected }); + this.cancelWebsocketRetry = void 0; + this.receivedOnOpenPayload = event; + } + attach(provider) { + this.configuration.providerMap.set(provider.configuration.name, provider); + if (this.status === WebSocketStatus.Disconnected && this.shouldConnect) { + this.connect(); + } + if (this.receivedOnOpenPayload && this.status === WebSocketStatus.Connected) { + provider.onOpen(this.receivedOnOpenPayload); + } + } + detach(provider) { + if (this.configuration.providerMap.has(provider.configuration.name)) { + provider.send(CloseMessage, { + documentName: provider.configuration.name + }); + this.configuration.providerMap.delete(provider.configuration.name); + } + } + setConfiguration(configuration = {}) { + this.configuration = { ...this.configuration, ...configuration }; + if (!this.configuration.autoConnect) { + this.shouldConnect = false; + } + } + async connect() { + if (this.status === WebSocketStatus.Connected) { + return; + } + if (this.cancelWebsocketRetry) { + this.cancelWebsocketRetry(); + this.cancelWebsocketRetry = void 0; + } + this.receivedOnOpenPayload = void 0; + this.shouldConnect = true; + const abortableRetry = () => { + let cancelAttempt = false; + const retryPromise2 = retry(this.createWebSocketConnection.bind(this), { + delay: this.configuration.delay, + initialDelay: this.configuration.initialDelay, + factor: this.configuration.factor, + maxAttempts: this.configuration.maxAttempts, + minDelay: this.configuration.minDelay, + maxDelay: this.configuration.maxDelay, + jitter: this.configuration.jitter, + timeout: this.configuration.timeout, + handleTimeout: this.configuration.handleTimeout, + beforeAttempt: (context) => { + if (!this.shouldConnect || cancelAttempt) { + context.abort(); + } + } + }).catch((error) => { + if (error && error.code !== "ATTEMPT_ABORTED") { + throw error; + } + }); + return { + retryPromise: retryPromise2, + cancelFunc: () => { + cancelAttempt = true; + } + }; + }; + const { retryPromise, cancelFunc } = abortableRetry(); + this.cancelWebsocketRetry = cancelFunc; + return retryPromise; + } + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type + attachWebSocketListeners(ws, reject) { + const { identifier } = ws; + const onMessageHandler = (payload) => this.emit("message", payload); + const onCloseHandler = (payload) => this.emit("close", { event: payload }); + const onOpenHandler = (payload) => this.emit("open", payload); + const onErrorHandler = (err) => { + reject(err); + }; + this.webSocketHandlers[identifier] = { + message: onMessageHandler, + close: onCloseHandler, + open: onOpenHandler, + error: onErrorHandler + }; + const handlers = this.webSocketHandlers[ws.identifier]; + Object.keys(handlers).forEach((name) => { + ws.addEventListener(name, handlers[name]); + }); + } + cleanupWebSocket() { + if (!this.webSocket) { + return; + } + const { identifier } = this.webSocket; + const handlers = this.webSocketHandlers[identifier]; + Object.keys(handlers).forEach((name) => { + var _a; + (_a = this.webSocket) === null || _a === void 0 ? void 0 : _a.removeEventListener(name, handlers[name]); + delete this.webSocketHandlers[identifier]; + }); + this.webSocket.close(); + this.webSocket = null; + } + createWebSocketConnection() { + return new Promise((resolve, reject) => { + if (this.webSocket) { + this.messageQueue = []; + this.cleanupWebSocket(); + } + this.lastMessageReceived = 0; + this.identifier += 1; + const ws = new this.configuration.WebSocketPolyfill(this.url); + ws.binaryType = "arraybuffer"; + ws.identifier = this.identifier; + this.attachWebSocketListeners(ws, reject); + this.webSocket = ws; + this.status = WebSocketStatus.Connecting; + this.emit("status", { status: WebSocketStatus.Connecting }); + this.connectionAttempt = { + resolve, + reject + }; + }); + } + onMessage(event) { + var _a; + this.resolveConnectionAttempt(); + this.lastMessageReceived = getUnixTime2(); + const message = new IncomingMessage(event.data); + const documentName = message.peekVarString(); + (_a = this.configuration.providerMap.get(documentName)) === null || _a === void 0 ? void 0 : _a.onMessage(event); + } + resolveConnectionAttempt() { + if (this.connectionAttempt) { + this.connectionAttempt.resolve(); + this.connectionAttempt = null; + this.status = WebSocketStatus.Connected; + this.emit("status", { status: WebSocketStatus.Connected }); + this.emit("connect"); + this.messageQueue.forEach((message) => this.send(message)); + this.messageQueue = []; + } + } + stopConnectionAttempt() { + this.connectionAttempt = null; + } + rejectConnectionAttempt() { + var _a; + (_a = this.connectionAttempt) === null || _a === void 0 ? void 0 : _a.reject(); + this.connectionAttempt = null; + } + checkConnection() { + var _a; + if (this.status !== WebSocketStatus.Connected) { + return; + } + if (!this.lastMessageReceived) { + return; + } + if (this.configuration.messageReconnectTimeout >= getUnixTime2() - this.lastMessageReceived) { + return; + } + this.closeTries += 1; + if (this.closeTries > 2) { + this.onClose({ + event: { + code: 4408, + reason: "forced" + } + }); + this.closeTries = 0; + } else { + (_a = this.webSocket) === null || _a === void 0 ? void 0 : _a.close(); + this.messageQueue = []; + } + } + get serverUrl() { + if (this.configuration.preserveTrailingSlash) { + return this.configuration.url; + } + let url = this.configuration.url; + while (url[url.length - 1] === "/") { + url = url.slice(0, url.length - 1); + } + return url; + } + get url() { + return this.serverUrl; + } + disconnect() { + this.shouldConnect = false; + if (this.webSocket === null) { + return; + } + try { + this.webSocket.close(); + this.messageQueue = []; + } catch (e) { + console.error(e); + } + } + send(message) { + var _a; + if (((_a = this.webSocket) === null || _a === void 0 ? void 0 : _a.readyState) === WsReadyStates.Open) { + this.webSocket.send(message); + } else { + this.messageQueue.push(message); + } + } + onClose({ event }) { + this.closeTries = 0; + this.cleanupWebSocket(); + if (this.connectionAttempt) { + this.rejectConnectionAttempt(); + } + this.status = WebSocketStatus.Disconnected; + this.emit("status", { status: WebSocketStatus.Disconnected }); + this.emit("disconnect", { event }); + if (!this.cancelWebsocketRetry && this.shouldConnect) { + setTimeout(() => { + this.connect(); + }, this.configuration.delay); + } + } + destroy() { + this.emit("destroy"); + clearInterval(this.intervals.connectionChecker); + this.stopConnectionAttempt(); + this.disconnect(); + this.removeAllListeners(); + this.cleanupWebSocket(); + } + }; + var messageYjsSyncStep1 = 0; + var messageYjsSyncStep2 = 1; + var messageYjsUpdate = 2; + var writeSyncStep1 = (encoder, doc2) => { + writeVarUint3(encoder, messageYjsSyncStep1); + const sv = encodeStateVector(doc2); + writeVarUint8Array3(encoder, sv); + }; + var writeSyncStep2 = (encoder, doc2, encodedStateVector) => { + writeVarUint3(encoder, messageYjsSyncStep2); + writeVarUint8Array3(encoder, encodeStateAsUpdate(doc2, encodedStateVector)); + }; + var readSyncStep1 = (decoder, encoder, doc2) => writeSyncStep2(encoder, doc2, readVarUint8Array3(decoder)); + var readSyncStep2 = (decoder, doc2, transactionOrigin) => { + try { + applyUpdate(doc2, readVarUint8Array3(decoder), transactionOrigin); + } catch (error) { + console.error("Caught error while handling a Yjs update", error); + } + }; + var writeUpdate = (encoder, update) => { + writeVarUint3(encoder, messageYjsUpdate); + writeVarUint8Array3(encoder, update); + }; + var readUpdate = readSyncStep2; + var readSyncMessage = (decoder, encoder, doc2, transactionOrigin) => { + const messageType = readVarUint3(decoder); + switch (messageType) { + case messageYjsSyncStep1: + readSyncStep1(decoder, encoder, doc2); + break; + case messageYjsSyncStep2: + readSyncStep2(decoder, doc2, transactionOrigin); + break; + case messageYjsUpdate: + readUpdate(decoder, doc2, transactionOrigin); + break; + default: + throw new Error("Unknown message type"); + } + return messageType; + }; + var MessageReceiver = class { + constructor(message) { + this.message = message; + } + apply(provider, emitSynced) { + const { message } = this; + const type = message.readVarUint(); + const emptyMessageLength = message.length(); + switch (type) { + case MessageType.Sync: + this.applySyncMessage(provider, emitSynced); + break; + case MessageType.Awareness: + this.applyAwarenessMessage(provider); + break; + case MessageType.Auth: + this.applyAuthMessage(provider); + break; + case MessageType.QueryAwareness: + this.applyQueryAwarenessMessage(provider); + break; + case MessageType.Stateless: + provider.receiveStateless(readVarString3(message.decoder)); + break; + case MessageType.SyncStatus: + this.applySyncStatusMessage(provider, readVarInt2(message.decoder) === 1); + break; + case MessageType.CLOSE: + const event = { + code: 1e3, + reason: readVarString3(message.decoder), + // @ts-ignore + target: provider.configuration.websocketProvider.webSocket, + type: "close" + }; + provider.onClose(); + provider.configuration.onClose({ event }); + provider.forwardClose({ event }); + break; + default: + throw new Error(`Can\u2019t apply message of unknown type: ${type}`); + } + if (message.length() > emptyMessageLength + 1) { + provider.send(OutgoingMessage, { encoder: message.encoder }); + } + } + applySyncMessage(provider, emitSynced) { + const { message } = this; + message.writeVarUint(MessageType.Sync); + const syncMessageType = readSyncMessage(message.decoder, message.encoder, provider.document, provider); + if (emitSynced && syncMessageType === messageYjsSyncStep2) { + provider.synced = true; + } + } + applySyncStatusMessage(provider, applied) { + if (applied) { + provider.decrementUnsyncedChanges(); + } + } + applyAwarenessMessage(provider) { + if (!provider.awareness) + return; + const { message } = this; + applyAwarenessUpdate(provider.awareness, message.readVarUint8Array(), provider); + } + applyAuthMessage(provider) { + const { message } = this; + readAuthMessage(message.decoder, provider.sendToken.bind(provider), provider.permissionDeniedHandler.bind(provider), provider.authenticatedHandler.bind(provider)); + } + applyQueryAwarenessMessage(provider) { + if (!provider.awareness) + return; + const { message } = this; + message.writeVarUint(MessageType.Awareness); + message.writeVarUint8Array(encodeAwarenessUpdate(provider.awareness, Array.from(provider.awareness.getStates().keys()))); + } + }; + var MessageSender = class { + constructor(Message, args2 = {}) { + this.message = new Message(); + this.encoder = this.message.get(args2); + } + create() { + return toUint8Array2(this.encoder); + } + send(webSocket) { + webSocket === null || webSocket === void 0 ? void 0 : webSocket.send(this.create()); + } + }; + var AuthenticationMessage = class extends OutgoingMessage { + constructor() { + super(...arguments); + this.type = MessageType.Auth; + this.description = "Authentication"; + } + get(args2) { + if (typeof args2.token === "undefined") { + throw new Error("The authentication message requires `token` as an argument."); + } + writeVarString3(this.encoder, args2.documentName); + writeVarUint3(this.encoder, this.type); + writeAuthentication(this.encoder, args2.token); + return this.encoder; + } + }; + var AwarenessMessage = class extends OutgoingMessage { + constructor() { + super(...arguments); + this.type = MessageType.Awareness; + this.description = "Awareness states update"; + } + get(args2) { + if (typeof args2.awareness === "undefined") { + throw new Error("The awareness message requires awareness as an argument"); + } + if (typeof args2.clients === "undefined") { + throw new Error("The awareness message requires clients as an argument"); + } + writeVarString3(this.encoder, args2.documentName); + writeVarUint3(this.encoder, this.type); + let awarenessUpdate; + if (args2.states === void 0) { + awarenessUpdate = encodeAwarenessUpdate(args2.awareness, args2.clients); + } else { + awarenessUpdate = encodeAwarenessUpdate(args2.awareness, args2.clients, args2.states); + } + writeVarUint8Array3(this.encoder, awarenessUpdate); + return this.encoder; + } + }; + var StatelessMessage = class extends OutgoingMessage { + constructor() { + super(...arguments); + this.type = MessageType.Stateless; + this.description = "A stateless message"; + } + get(args2) { + var _a; + writeVarString3(this.encoder, args2.documentName); + writeVarUint3(this.encoder, this.type); + writeVarString3(this.encoder, (_a = args2.payload) !== null && _a !== void 0 ? _a : ""); + return this.encoder; + } + }; + var SyncStepOneMessage = class extends OutgoingMessage { + constructor() { + super(...arguments); + this.type = MessageType.Sync; + this.description = "First sync step"; + } + get(args2) { + if (typeof args2.document === "undefined") { + throw new Error("The sync step one message requires document as an argument"); + } + writeVarString3(this.encoder, args2.documentName); + writeVarUint3(this.encoder, this.type); + writeSyncStep1(this.encoder, args2.document); + return this.encoder; + } + }; + var UpdateMessage = class extends OutgoingMessage { + constructor() { + super(...arguments); + this.type = MessageType.Sync; + this.description = "A document update"; + } + get(args2) { + writeVarString3(this.encoder, args2.documentName); + writeVarUint3(this.encoder, this.type); + writeUpdate(this.encoder, args2.update); + return this.encoder; + } + }; + var AwarenessError = class extends Error { + constructor() { + super(...arguments); + this.code = 1001; + } + }; + var HocuspocusProvider = class extends EventEmitter { + constructor(configuration) { + var _a, _b, _c; + super(); + this.configuration = { + name: "", + // @ts-ignore + document: void 0, + // @ts-ignore + awareness: void 0, + token: null, + forceSyncInterval: false, + onAuthenticated: () => null, + onAuthenticationFailed: () => null, + onOpen: () => null, + onConnect: () => null, + onMessage: () => null, + onOutgoingMessage: () => null, + onSynced: () => null, + onStatus: () => null, + onDisconnect: () => null, + onClose: () => null, + onDestroy: () => null, + onAwarenessUpdate: () => null, + onAwarenessChange: () => null, + onStateless: () => null, + onUnsyncedChanges: () => null + }; + this.isSynced = false; + this.unsyncedChanges = 0; + this.isAuthenticated = false; + this.authorizedScope = void 0; + this.manageSocket = false; + this._isAttached = false; + this.intervals = { + forceSync: null + }; + this.boundDocumentUpdateHandler = this.documentUpdateHandler.bind(this); + this.boundAwarenessUpdateHandler = this.awarenessUpdateHandler.bind(this); + this.boundPageHide = this.pageHide.bind(this); + this.boundOnOpen = this.onOpen.bind(this); + this.boundOnClose = this.onClose.bind(this); + this.forwardConnect = () => this.emit("connect"); + this.forwardStatus = (e) => this.emit("status", e); + this.forwardClose = (e) => this.emit("close", e); + this.forwardDisconnect = (e) => this.emit("disconnect", e); + this.forwardDestroy = () => this.emit("destroy"); + this.setConfiguration(configuration); + this.configuration.document = configuration.document ? configuration.document : new Doc(); + this.configuration.awareness = configuration.awareness !== void 0 ? configuration.awareness : new Awareness(this.document); + this.on("open", this.configuration.onOpen); + this.on("message", this.configuration.onMessage); + this.on("outgoingMessage", this.configuration.onOutgoingMessage); + this.on("synced", this.configuration.onSynced); + this.on("destroy", this.configuration.onDestroy); + this.on("awarenessUpdate", this.configuration.onAwarenessUpdate); + this.on("awarenessChange", this.configuration.onAwarenessChange); + this.on("stateless", this.configuration.onStateless); + this.on("unsyncedChanges", this.configuration.onUnsyncedChanges); + this.on("authenticated", this.configuration.onAuthenticated); + this.on("authenticationFailed", this.configuration.onAuthenticationFailed); + (_a = this.awareness) === null || _a === void 0 ? void 0 : _a.on("update", () => { + this.emit("awarenessUpdate", { + states: awarenessStatesToArray(this.awareness.getStates()) + }); + }); + (_b = this.awareness) === null || _b === void 0 ? void 0 : _b.on("change", () => { + this.emit("awarenessChange", { + states: awarenessStatesToArray(this.awareness.getStates()) + }); + }); + this.document.on("update", this.boundDocumentUpdateHandler); + (_c = this.awareness) === null || _c === void 0 ? void 0 : _c.on("update", this.boundAwarenessUpdateHandler); + this.registerEventListeners(); + if (this.configuration.forceSyncInterval && typeof this.configuration.forceSyncInterval === "number") { + this.intervals.forceSync = setInterval(this.forceSync.bind(this), this.configuration.forceSyncInterval); + } + if (this.manageSocket) { + this.attach(); + } + } + setConfiguration(configuration = {}) { + if (!configuration.websocketProvider) { + this.manageSocket = true; + this.configuration.websocketProvider = new HocuspocusProviderWebsocket(configuration); + } + this.configuration = { ...this.configuration, ...configuration }; + } + get document() { + return this.configuration.document; + } + get isAttached() { + return this._isAttached; + } + get awareness() { + return this.configuration.awareness; + } + get hasUnsyncedChanges() { + return this.unsyncedChanges > 0; + } + resetUnsyncedChanges() { + this.unsyncedChanges = 1; + this.emit("unsyncedChanges", { number: this.unsyncedChanges }); + } + incrementUnsyncedChanges() { + this.unsyncedChanges += 1; + this.emit("unsyncedChanges", { number: this.unsyncedChanges }); + } + decrementUnsyncedChanges() { + if (this.unsyncedChanges > 0) { + this.unsyncedChanges -= 1; + } + if (this.unsyncedChanges === 0) { + this.synced = true; + } + this.emit("unsyncedChanges", { number: this.unsyncedChanges }); + } + forceSync() { + this.resetUnsyncedChanges(); + this.send(SyncStepOneMessage, { + document: this.document, + documentName: this.configuration.name + }); + } + pageHide() { + if (this.awareness) { + removeAwarenessStates(this.awareness, [this.document.clientID], "page hide"); + } + } + registerEventListeners() { + if (typeof window === "undefined" || !("addEventListener" in window)) { + return; + } + window.addEventListener("pagehide", this.boundPageHide); + } + sendStateless(payload) { + this.send(StatelessMessage, { + documentName: this.configuration.name, + payload + }); + } + async sendToken() { + let token; + try { + token = await this.getToken(); + } catch (error) { + this.permissionDeniedHandler(`Failed to get token during sendToken(): ${error}`); + return; + } + this.send(AuthenticationMessage, { + token: token !== null && token !== void 0 ? token : "", + documentName: this.configuration.name + }); + } + documentUpdateHandler(update, origin) { + if (origin === this) { + return; + } + this.incrementUnsyncedChanges(); + this.send(UpdateMessage, { update, documentName: this.configuration.name }); + } + awarenessUpdateHandler({ added, updated, removed }, origin) { + const changedClients = added.concat(updated).concat(removed); + this.send(AwarenessMessage, { + awareness: this.awareness, + clients: changedClients, + documentName: this.configuration.name + }); + } + /** + * Indicates whether a first handshake with the server has been established + * + * Note: this does not mean all updates from the client have been persisted to the backend. For this, + * use `hasUnsyncedChanges`. + */ + get synced() { + return this.isSynced; + } + set synced(state) { + if (this.isSynced === state) { + return; + } + this.isSynced = state; + if (state) { + this.emit("synced", { state }); + } + } + receiveStateless(payload) { + this.emit("stateless", { payload }); + } + // not needed, but provides backward compatibility with e.g. lexical/yjs + async connect() { + if (this.manageSocket) { + return this.configuration.websocketProvider.connect(); + } + console.warn("HocuspocusProvider::connect() is deprecated and does not do anything. Please connect/disconnect on the websocketProvider, or attach/deattach providers."); + } + disconnect() { + if (this.manageSocket) { + return this.configuration.websocketProvider.disconnect(); + } + console.warn("HocuspocusProvider::disconnect() is deprecated and does not do anything. Please connect/disconnect on the websocketProvider, or attach/deattach providers."); + } + async onOpen(event) { + this.isAuthenticated = false; + this.emit("open", { event }); + await this.sendToken(); + this.startSync(); + } + async getToken() { + if (typeof this.configuration.token === "function") { + const token = await this.configuration.token(); + return token; + } + return this.configuration.token; + } + startSync() { + this.resetUnsyncedChanges(); + this.send(SyncStepOneMessage, { + document: this.document, + documentName: this.configuration.name + }); + if (this.awareness && this.awareness.getLocalState() !== null) { + this.send(AwarenessMessage, { + awareness: this.awareness, + clients: [this.document.clientID], + documentName: this.configuration.name + }); + } + } + send(message, args2) { + if (!this._isAttached) + return; + const messageSender = new MessageSender(message, args2); + this.emit("outgoingMessage", { message: messageSender.message }); + messageSender.send(this.configuration.websocketProvider); + } + onMessage(event) { + const message = new IncomingMessage(event.data); + const documentName = message.readVarString(); + message.writeVarString(documentName); + this.emit("message", { event, message: new IncomingMessage(event.data) }); + new MessageReceiver(message).apply(this, true); + } + onClose() { + this.isAuthenticated = false; + this.synced = false; + if (this.awareness) { + removeAwarenessStates(this.awareness, Array.from(this.awareness.getStates().keys()).filter((client) => client !== this.document.clientID), this); + } + } + destroy() { + this.emit("destroy"); + if (this.intervals.forceSync) { + clearInterval(this.intervals.forceSync); + } + if (this.awareness) { + removeAwarenessStates(this.awareness, [this.document.clientID], "provider destroy"); + this.awareness.off("update", this.boundAwarenessUpdateHandler); + this.awareness.destroy(); + } + this.document.off("update", this.boundDocumentUpdateHandler); + this.removeAllListeners(); + this.detach(); + if (this.manageSocket) { + this.configuration.websocketProvider.destroy(); + } + if (typeof window === "undefined" || !("removeEventListener" in window)) { + return; + } + window.removeEventListener("pagehide", this.boundPageHide); + } + detach() { + this.configuration.websocketProvider.off("connect", this.configuration.onConnect); + this.configuration.websocketProvider.off("connect", this.forwardConnect); + this.configuration.websocketProvider.off("status", this.forwardStatus); + this.configuration.websocketProvider.off("status", this.configuration.onStatus); + this.configuration.websocketProvider.off("open", this.boundOnOpen); + this.configuration.websocketProvider.off("close", this.boundOnClose); + this.configuration.websocketProvider.off("close", this.configuration.onClose); + this.configuration.websocketProvider.off("close", this.forwardClose); + this.configuration.websocketProvider.off("disconnect", this.configuration.onDisconnect); + this.configuration.websocketProvider.off("disconnect", this.forwardDisconnect); + this.configuration.websocketProvider.off("destroy", this.configuration.onDestroy); + this.configuration.websocketProvider.off("destroy", this.forwardDestroy); + this.configuration.websocketProvider.detach(this); + this._isAttached = false; + } + attach() { + if (this._isAttached) + return; + this.configuration.websocketProvider.on("connect", this.configuration.onConnect); + this.configuration.websocketProvider.on("connect", this.forwardConnect); + this.configuration.websocketProvider.on("status", this.configuration.onStatus); + this.configuration.websocketProvider.on("status", this.forwardStatus); + this.configuration.websocketProvider.on("open", this.boundOnOpen); + this.configuration.websocketProvider.on("close", this.boundOnClose); + this.configuration.websocketProvider.on("close", this.configuration.onClose); + this.configuration.websocketProvider.on("close", this.forwardClose); + this.configuration.websocketProvider.on("disconnect", this.configuration.onDisconnect); + this.configuration.websocketProvider.on("disconnect", this.forwardDisconnect); + this.configuration.websocketProvider.on("destroy", this.configuration.onDestroy); + this.configuration.websocketProvider.on("destroy", this.forwardDestroy); + this.configuration.websocketProvider.attach(this); + this._isAttached = true; + } + permissionDeniedHandler(reason) { + this.emit("authenticationFailed", { reason }); + this.isAuthenticated = false; + } + authenticatedHandler(scope) { + this.isAuthenticated = true; + this.authorizedScope = scope; + this.emit("authenticated", { scope }); + } + setAwarenessField(key, value) { + if (!this.awareness) { + throw new AwarenessError(`Cannot set awareness field "${key}" to ${JSON.stringify(value)}. You have disabled Awareness for this provider by explicitly passing awareness: null in the provider configuration.`); + } + this.awareness.setLocalStateField(key, value); + } + }; + + // src/bridge/cursor-presence/userColor.ts + var CURSOR_PALETTE = [ + "#E53935", + "#1E88E5", + "#43A047", + "#FB8C00", + "#8E24AA", + "#00ACC1", + "#F4511E", + "#3949AB", + "#7CB342", + "#D81B60", + "#6D4C41", + "#546E7A" + ]; + function hashString(input) { + let hash = 2166136261; + for (let i = 0; i < input.length; i += 1) { + hash ^= input.charCodeAt(i); + hash = Math.imul(hash, 16777619); + } + return hash >>> 0; + } + function getUserColor(userId) { + const normalized = userId.trim() || "anonymous"; + return CURSOR_PALETTE[hashString(normalized) % CURSOR_PALETTE.length]; + } + + // src/bridge/cursor-presence/CursorPresenceProvider.ts + var DEFAULT_THROTTLE_MS = 33; + var CursorPresenceProvider = class { + constructor(provider, user, onRemoteChange, throttleMs = DEFAULT_THROTTLE_MS) { + this.provider = provider; + this.lastBroadcastAt = 0; + this.destroyed = false; + this.boundAwarenessChange = () => this.onRemoteChange(); + this.onRemoteChange = onRemoteChange; + this.throttleMs = throttleMs; + this.provider.setAwarenessField("user", user); + this.provider.setAwarenessField("cursor", null); + this.provider.awareness?.on("change", this.boundAwarenessChange); + } + /** + * Broadcast typing caret to peers via awareness. + * Does not create any local DOM — peers render it; the typist does not. + * Passing `null` clears immediately (no throttle) so inactive cursors vanish. + */ + setLocalCursor(cursor) { + if (this.destroyed) return; + if (cursor === null) { + window.clearTimeout(this.throttleTimer); + this.pendingCursor = null; + this.flushBroadcast(); + return; + } + this.pendingCursor = cursor; + const elapsed = performance.now() - this.lastBroadcastAt; + if (elapsed >= this.throttleMs) { + this.flushBroadcast(); + return; + } + window.clearTimeout(this.throttleTimer); + this.throttleTimer = window.setTimeout(() => this.flushBroadcast(), this.throttleMs - elapsed); + } + flushBroadcast() { + if (this.destroyed || this.pendingCursor === void 0) return; + this.lastBroadcastAt = performance.now(); + this.provider.setAwarenessField("cursor", this.pendingCursor); + this.pendingCursor = void 0; + } + syncOverlayFromAwareness(sync) { + const awareness = this.provider.awareness; + if (awareness) sync(awareness); + } + destroy() { + if (this.destroyed) return; + this.destroyed = true; + window.clearTimeout(this.throttleTimer); + this.provider.setAwarenessField("cursor", null); + this.provider.awareness?.off("change", this.boundAwarenessChange); + } + }; + + // src/bridge/cursor-presence/caretMetrics.ts + var MIRROR_PROPERTIES = [ + "direction", + "boxSizing", + "width", + "height", + "overflowX", + "overflowY", + "borderTopWidth", + "borderRightWidth", + "borderBottomWidth", + "borderLeftWidth", + "paddingTop", + "paddingRight", + "paddingBottom", + "paddingLeft", + "fontStyle", + "fontVariant", + "fontWeight", + "fontStretch", + "fontSize", + "fontSizeAdjust", + "lineHeight", + "fontFamily", + "textAlign", + "textTransform", + "textIndent", + "textDecoration", + "letterSpacing", + "wordSpacing", + "tabSize", + "whiteSpace", + "wordWrap", + "wordBreak" + ]; + var mirrorDiv = null; + function getMirrorDiv() { + if (!mirrorDiv) { + mirrorDiv = document.createElement("div"); + mirrorDiv.id = "lowcoder-cursor-mirror"; + mirrorDiv.setAttribute("aria-hidden", "true"); + mirrorDiv.style.cssText = "position:absolute;visibility:hidden;white-space:pre-wrap;word-wrap:break-word;top:0;left:-9999px;"; + document.body.appendChild(mirrorDiv); + } + return mirrorDiv; + } + function toKebabCase(prop) { + return prop.replace(/([A-Z])/g, "-$1").toLowerCase(); + } + function copyInputStyles(element2, div) { + const computed = window.getComputedStyle(element2); + for (const prop of MIRROR_PROPERTIES) { + const kebab = toKebabCase(prop); + div.style.setProperty(kebab, computed.getPropertyValue(kebab)); + } + div.style.width = `${element2.clientWidth}px`; + div.style.whiteSpace = element2 instanceof HTMLTextAreaElement ? "pre-wrap" : "nowrap"; + } + function fieldLineHeight(field) { + const style = window.getComputedStyle(field); + return parseFloat(style.lineHeight) || parseFloat(style.fontSize) * 1.2 || 20; + } + function getFieldFallbackCaret(field) { + const rect = field.getBoundingClientRect(); + const height = fieldLineHeight(field); + const style = window.getComputedStyle(field); + const padL = parseFloat(style.paddingLeft || "0"); + const padT = parseFloat(style.paddingTop || "0"); + return { + left: rect.left + padL + 4, + top: rect.top + padT + 2, + height + }; + } + function getContentEditableCaret(field) { + const sel = window.getSelection(); + if (!sel || sel.rangeCount === 0) return getFieldFallbackCaret(field); + const range = sel.getRangeAt(0); + if (!field.contains(range.startContainer)) return getFieldFallbackCaret(field); + const collapsed = range.cloneRange(); + collapsed.collapse(true); + const rects = collapsed.getClientRects(); + const rect = rects.length > 0 ? rects[0] : collapsed.getBoundingClientRect(); + if (rect.width === 0 && rect.height === 0) return getFieldFallbackCaret(field); + return { left: rect.left, top: rect.top, height: Math.max(rect.height, fieldLineHeight(field)) }; + } + function getCaretCoordinatesForField(field, position) { + if (field instanceof HTMLElement && field.isContentEditable && !(field instanceof HTMLInputElement) && !(field instanceof HTMLTextAreaElement)) { + return getContentEditableCaret(field); + } + if (position == null) return getFieldFallbackCaret(field); + const exact = getCaretCoordinates(field, position); + return exact ?? getFieldFallbackCaret(field); + } + function getCaretCoordinates(element2, position) { + if (!element2.isConnected) return null; + const div = getMirrorDiv(); + copyInputStyles(element2, div); + const value = element2.value; + const clamped = Math.max(0, Math.min(position, value.length)); + const before = value.slice(0, clamped); + const after = value.slice(clamped) || "."; + div.textContent = before; + const span = document.createElement("span"); + span.textContent = after; + div.appendChild(span); + const elementRect = element2.getBoundingClientRect(); + const spanRect = span.getBoundingClientRect(); + const divRect = div.getBoundingClientRect(); + const style = window.getComputedStyle(element2); + const lineHeight = parseFloat(style.lineHeight) || parseFloat(style.fontSize) * 1.2; + const left = elementRect.left - element2.scrollLeft + (spanRect.left - divRect.left) + parseFloat(style.borderLeftWidth || "0") + parseFloat(style.paddingLeft || "0"); + const top = elementRect.top - element2.scrollTop + (spanRect.top - divRect.top) + parseFloat(style.borderTopWidth || "0") + parseFloat(style.paddingTop || "0"); + div.textContent = ""; + const coords = { left, top, height: lineHeight }; + if (!Number.isFinite(coords.left) || !Number.isFinite(coords.top)) { + return null; + } + return coords; + } + function getSelectionRectsForField(field, anchor, head) { + if (field instanceof HTMLElement && field.isContentEditable && !(field instanceof HTMLInputElement) && !(field instanceof HTMLTextAreaElement)) { + const sel = window.getSelection(); + if (!sel || sel.rangeCount === 0 || anchor === head) return []; + const range = sel.getRangeAt(0); + if (!field.contains(range.startContainer)) return []; + const rects = []; + for (const r of Array.from(range.getClientRects())) { + rects.push({ left: r.left, top: r.top, width: r.width, height: r.height }); + } + return rects; + } + return getSelectionRects(field, anchor, head); + } + function getSelectionRects(element2, anchor, head) { + const start = Math.min(anchor, head); + const end = Math.max(anchor, head); + if (start === end) return []; + const startCoords = getCaretCoordinates(element2, start); + const endCoords = getCaretCoordinates(element2, end); + if (!startCoords || !endCoords) return []; + const height = startCoords.height; + if (Math.abs(startCoords.top - endCoords.top) < height * 0.5) { + return [{ + left: startCoords.left, + top: startCoords.top, + width: Math.max(2, endCoords.left - startCoords.left), + height + }]; + } + const value = element2.value; + const lineStart = value.lastIndexOf("\n", start) + 1; + const lineEnd = value.indexOf("\n", end); + const lineEndIndex = lineEnd === -1 ? value.length : lineEnd; + const lineEndCoords = getCaretCoordinates(element2, lineEndIndex); + const lineStartCoords = getCaretCoordinates(element2, lineStart); + const rects = []; + if (lineEndCoords) { + rects.push({ + left: startCoords.left, + top: startCoords.top, + width: Math.max(2, lineEndCoords.left - startCoords.left), + height + }); + } + if (lineStartCoords) { + rects.push({ + left: lineStartCoords.left, + top: endCoords.top, + width: Math.max(2, endCoords.left - lineStartCoords.left), + height + }); + } + return rects; + } + function destroyCaretMirror() { + mirrorDiv?.remove(); + mirrorDiv = null; + } + + // src/bridge/cursor-presence/textField.ts + var IGNORED_INPUT_TYPES = /* @__PURE__ */ new Set([ + "hidden", + "checkbox", + "radio", + "button", + "submit", + "file", + "password" + ]); + function isTextFieldElement(el) { + if (!el) return false; + if (el instanceof HTMLTextAreaElement) return true; + if (el instanceof HTMLInputElement) { + const type = (el.getAttribute("type") || el.type || "text").toLowerCase(); + return !IGNORED_INPUT_TYPES.has(type); + } + if (el instanceof HTMLElement && el.isContentEditable) return true; + return false; + } + function getFocusedTextField() { + const el = document.activeElement; + if (isTextFieldElement(el)) return el; + if (el instanceof HTMLElement) { + const inner = el.querySelector( + 'input:not([type="hidden"]):not([type="checkbox"]):not([type="radio"]), textarea, [contenteditable="true"]' + ); + if (isTextFieldElement(inner)) return inner; + } + return null; + } + function listEditableFields(container = document) { + const nodes = container.querySelectorAll( + [ + 'input[type="text"]', + 'input[type="email"]', + 'input[type="number"]', + 'input[type="tel"]', + 'input[type="url"]', + 'input[type="search"]', + 'input[type="short_text"]', + 'input[type="long_text"]', + 'input[type="phone_number"]', + "input[name]", + "input:not([type])", + "textarea", + '[contenteditable="true"]', + '[role="textbox"]' + ].join(", ") + ); + return Array.from(nodes).filter((field) => { + if (!isTextFieldElement(field)) return false; + const rect = field.getBoundingClientRect(); + return rect.width > 0 && rect.height > 0; + }); + } + function getFieldText(field) { + if (field instanceof HTMLInputElement || field instanceof HTMLTextAreaElement) { + return field.value; + } + return field.textContent ?? ""; + } + function getFieldSelection(field) { + if (field instanceof HTMLInputElement || field instanceof HTMLTextAreaElement) { + return { + anchor: field.selectionStart ?? 0, + head: field.selectionEnd ?? 0 + }; + } + const sel = window.getSelection(); + if (!sel || sel.rangeCount === 0) { + const len = getFieldText(field).length; + return { anchor: len, head: len }; + } + const range = sel.getRangeAt(0); + if (!field.contains(range.startContainer)) { + const len = getFieldText(field).length; + return { anchor: len, head: len }; + } + const pre = range.cloneRange(); + pre.selectNodeContents(field); + pre.setEnd(range.startContainer, range.startOffset); + const anchor = pre.toString().length; + pre.setEnd(range.endContainer, range.endOffset); + const head = pre.toString().length; + return { anchor, head }; + } + function extractQuestionUuid(value) { + const match2 = value.match( + /([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i + ); + return match2?.[1] ?? null; + } + function getCursorFieldKey(field, step, bridgeGetFieldKey) { + if (bridgeGetFieldKey && (field instanceof HTMLInputElement || field instanceof HTMLTextAreaElement)) { + return bridgeGetFieldKey(field); + } + const name = field.getAttribute("name")?.trim(); + if (name) return `name:${name}`; + const labelledBy = field.getAttribute("aria-labelledby") || ""; + const fromLabel = extractQuestionUuid(labelledBy); + if (fromLabel) return `qid:${fromLabel}`; + const id2 = field.getAttribute("id") || ""; + const fromId = extractQuestionUuid(id2); + if (fromId) return `qid:${fromId}`; + const qa = field.getAttribute("data-qa"); + if (qa) return `qa:${qa}:step:${step}`; + return `ce:step:${step}`; + } + function findFieldByCursorKey(key, step, bridgeFindFieldByKey) { + if (bridgeFindFieldByKey) { + const bridged = bridgeFindFieldByKey(key); + if (bridged) return bridged; + } + for (const field of listEditableFields()) { + if (getCursorFieldKey(field, step) === key) return field; + } + if (key.startsWith("name:")) { + const name = key.slice("name:".length); + const el = document.querySelector(`input[name="${CSS.escape(name)}"], textarea[name="${CSS.escape(name)}"]`); + if (isTextFieldElement(el)) return el; + } + if (key.startsWith("qid:")) { + const qid = key.slice("qid:".length); + const sel = `[aria-labelledby*="${CSS.escape(qid)}"], [id*="${CSS.escape(qid)}"]`; + for (const el of document.querySelectorAll(sel)) { + if (isTextFieldElement(el)) return el; + } + } + return null; + } + + // src/bridge/cursor-presence/RemoteCursor.ts + var CURSOR_CLASS = "lowcoder-remote-cursor"; + var LABEL_CLASS = "lowcoder-remote-cursor-label"; + var CARET_CLASS = "lowcoder-remote-cursor-caret"; + var SELECTION_CLASS = "lowcoder-remote-cursor-selection"; + var CARET_VERTICAL_OFFSET_PX = -6; + var RemoteCursor = class { + constructor(clientId) { + this.selectionHighlights = []; + this.selectionKey = ""; + this.visible = false; + this.clientId = clientId; + this.root = document.createElement("div"); + this.root.className = CURSOR_CLASS; + this.root.dataset.clientId = String(clientId); + this.root.style.cssText = "position:fixed;pointer-events:none;z-index:2147483646;transition:opacity 120ms ease;"; + this.label = document.createElement("div"); + this.label.className = LABEL_CLASS; + this.label.style.cssText = "position:absolute;transform:translate(-2px,calc(-100% - 4px));padding:1px 6px;border-radius:3px;font:500 11px/16px system-ui,sans-serif;color:#fff;white-space:nowrap;max-width:160px;overflow:hidden;text-overflow:ellipsis;"; + this.caret = document.createElement("div"); + this.caret.className = CARET_CLASS; + this.caret.style.cssText = "position:absolute;width:2px;border-radius:1px;transform:translateX(-1px);"; + this.root.append(this.caret, this.label); + this.hide(); + } + mount(container) { + if (!this.root.isConnected) container.appendChild(this.root); + } + update(state, overlayContainer) { + if (!state.online || !state.cursor?.typing) { + this.hide(); + return; + } + this.visible = true; + this.root.style.opacity = "1"; + this.root.style.display = "block"; + const { user, x, y, height, selectionRects } = state; + this.root.style.transform = `translate(${x}px, ${y}px)`; + this.label.textContent = user.name; + this.label.style.backgroundColor = user.color; + this.caret.style.backgroundColor = user.color; + this.caret.style.height = `${Math.max(6, height)}px`; + this.caret.style.top = `${CARET_VERTICAL_OFFSET_PX}px`; + this.renderSelectionHighlights(user.color, selectionRects, overlayContainer); + } + updatePosition(x, y) { + if (!this.visible) return; + this.root.style.transform = `translate(${x}px, ${y}px)`; + } + hide() { + this.visible = false; + this.root.style.opacity = "0"; + this.root.style.display = "none"; + this.selectionKey = ""; + this.clearSelectionHighlights(); + } + destroy() { + this.clearSelectionHighlights(); + this.root.remove(); + } + renderSelectionHighlights(color, selectionRects, overlayContainer) { + const key = selectionRects.map((r) => `${r.left},${r.top},${r.width},${r.height}`).join("|"); + if (key === this.selectionKey) return; + this.selectionKey = key; + this.clearSelectionHighlights(); + for (const rect of selectionRects) { + const highlight = document.createElement("div"); + highlight.className = SELECTION_CLASS; + highlight.style.cssText = `position:fixed;left:${rect.left}px;top:${rect.top}px;width:${rect.width}px;height:${rect.height}px;background:${color};opacity:0.28;border-radius:2px;pointer-events:none;z-index:2147483644;`; + overlayContainer.appendChild(highlight); + this.selectionHighlights.push(highlight); + } + } + clearSelectionHighlights() { + for (const el of this.selectionHighlights) el.remove(); + this.selectionHighlights = []; + } + }; + function ensureCursorStyles() { + if (document.getElementById("lowcoder-cursor-presence-styles")) return; + const style = document.createElement("style"); + style.id = "lowcoder-cursor-presence-styles"; + style.textContent = ` + .${CURSOR_CLASS} { contain: layout style; } + .${LABEL_CLASS} { box-shadow: 0 1px 3px rgba(0,0,0,0.25); } + .${CARET_CLASS} { animation: lowcoder-cursor-blink 1s step-end infinite; } + @keyframes lowcoder-cursor-blink { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } } + `; + document.head.appendChild(style); + } + + // src/bridge/cursor-presence/CursorOverlay.ts + var LERP_FACTOR = 0.35; + var CursorOverlay = class { + constructor(options) { + this.options = options; + this.cursors = /* @__PURE__ */ new Map(); + this.renderStates = /* @__PURE__ */ new Map(); + this.rafId = null; + this.destroyed = false; + ensureCursorStyles(); + this.container = document.createElement("div"); + this.container.id = "lowcoder-cursor-overlay"; + this.container.style.cssText = "position:fixed;inset:0;pointer-events:none;z-index:2147483647;overflow:visible;"; + document.documentElement.appendChild(this.container); + this.startAnimationLoop(); + } + /** + * Render cursors for other connected users only. + * The local typist never sees their own collaborative caret/label. + * Null / inactive remote cursors are hidden (no time-based timeout). + */ + syncFromAwareness(awareness) { + const localClientId = awareness.clientID; + const localUserId = this.options.localUserId; + const active = /* @__PURE__ */ new Set(); + awareness.getStates().forEach((rawState, clientId) => { + if (clientId === localClientId) return; + const state = rawState; + if (!state?.user) return; + if (state.user.id === localUserId) return; + if (!this.isActiveRemoteCursor(state.cursor)) { + this.removeRemote(clientId); + return; + } + active.add(clientId); + this.upsertRemoteState(clientId, state.user, state.cursor); + }); + for (const clientId of this.cursors.keys()) { + if (!active.has(clientId)) this.removeRemote(clientId); + } + this.renderAll(); + } + isActiveRemoteCursor(cursor) { + return cursor != null && cursor.typing === true; + } + upsertRemoteState(clientId, user, cursor) { + const existing = this.renderStates.get(clientId); + const metrics = this.resolveCursorMetrics(cursor); + const hasCursor = cursor?.typing === true && metrics != null; + this.renderStates.set(clientId, { + clientId, + user, + cursor, + x: existing?.x ?? metrics?.x ?? 0, + y: existing?.y ?? metrics?.y ?? 0, + targetX: metrics?.x ?? existing?.targetX ?? 0, + targetY: metrics?.y ?? existing?.targetY ?? 0, + height: metrics?.height ?? existing?.height ?? 16, + selectionRects: metrics?.selectionRects ?? [], + online: hasCursor + }); + if (!this.cursors.has(clientId)) { + const remoteCursor = new RemoteCursor(clientId); + remoteCursor.mount(this.container); + this.cursors.set(clientId, remoteCursor); + } + } + removeRemote(clientId) { + this.renderStates.delete(clientId); + this.cursors.get(clientId)?.destroy(); + this.cursors.delete(clientId); + } + resolveField(key) { + return findFieldByCursorKey( + key, + this.options.getCurrentStep(), + this.options.findFieldByKey + ); + } + resolveCursorMetrics(cursor) { + if (!this.isActiveRemoteCursor(cursor)) return null; + if (cursor.step !== this.options.getCurrentStep()) return null; + const field = this.resolveField(cursor.fieldKey); + if (!field?.isConnected) return null; + let caret = getCaretCoordinatesForField(field, cursor.selection.head) ?? getFieldFallbackCaret(field); + if (!caret || !Number.isFinite(caret.left)) { + caret = getFieldFallbackCaret(field); + } + return { + x: caret.left, + y: caret.top, + height: caret.height, + selectionRects: getSelectionRectsForField( + field, + cursor.selection.anchor, + cursor.selection.head + ) + }; + } + renderAll() { + for (const state of this.renderStates.values()) { + this.cursors.get(state.clientId)?.update(state, this.container); + } + } + startAnimationLoop() { + const tick = () => { + if (this.destroyed) return; + for (const state of this.renderStates.values()) { + const dx = state.targetX - state.x; + const dy = state.targetY - state.y; + if (Math.abs(dx) > 0.5 || Math.abs(dy) > 0.5) { + state.x += dx * LERP_FACTOR; + state.y += dy * LERP_FACTOR; + } else { + state.x = state.targetX; + state.y = state.targetY; + } + this.cursors.get(state.clientId)?.updatePosition(state.x, state.y); + } + this.rafId = window.requestAnimationFrame(tick); + }; + this.rafId = window.requestAnimationFrame(tick); + } + destroy() { + if (this.destroyed) return; + this.destroyed = true; + if (this.rafId != null) window.cancelAnimationFrame(this.rafId); + for (const cursor of this.cursors.values()) cursor.destroy(); + this.cursors.clear(); + this.renderStates.clear(); + this.container.remove(); + destroyCaretMirror(); + } + }; + + // src/bridge/cursor-presence/initTypeformCursorPresence.ts + var TYPING_IDLE_MS = 2500; + function readUserName(editorId) { + const params2 = new URLSearchParams(window.location.search); + return params2.get("username") || document.documentElement.getAttribute("data-lowcoder-username") || editorId; + } + function isRealUserActivity(event) { + return event.isTrusted === true; + } + function initTypeformCursorPresence(config) { + const userName = readUserName(config.editorId); + const user = { + id: config.editorId, + name: userName, + color: getUserColor(config.editorId), + role: config.role + }; + const canBroadcast = () => !config.isWelcomeScreen() && !(config.isSyncing?.() ?? false); + const overlay = new CursorOverlay({ + findFieldByKey: config.findFieldByKey, + getCurrentStep: config.getCurrentStep, + localUserId: config.editorId + }); + const presence = new CursorPresenceProvider( + config.provider, + user, + () => { + presence.syncOverlayFromAwareness((awareness) => overlay.syncFromAwareness(awareness)); + }, + 33 + ); + let isActive = false; + let idleTimer; + const clearCursor = () => { + isActive = false; + window.clearTimeout(idleTimer); + idleTimer = void 0; + presence.setLocalCursor(null); + }; + const syncOverlay = () => { + presence.syncOverlayFromAwareness((awareness) => overlay.syncFromAwareness(awareness)); + }; + const scheduleIdleClear = () => { + window.clearTimeout(idleTimer); + idleTimer = window.setTimeout(() => { + clearCursor(); + syncOverlay(); + }, TYPING_IDLE_MS); + }; + const publishCursor = () => { + if (!isActive) return; + if (!canBroadcast()) return; + const field = getFocusedTextField(); + if (!field) return; + const step = config.getCurrentStep(); + presence.setLocalCursor({ + fieldKey: getCursorFieldKey(field, step, config.getFieldKey), + step, + selection: getFieldSelection(field), + typing: true, + updatedAt: Date.now() + }); + }; + const activateCursor = (event) => { + if (!isRealUserActivity(event)) return; + if (!canBroadcast()) return; + const target = event.target; + if (target instanceof Element && !isTextFieldElement(target) && !getFocusedTextField()) { + return; + } + if (!getFocusedTextField()) return; + isActive = true; + publishCursor(); + scheduleIdleClear(); + }; + const listenerOpts = { capture: true, passive: true }; + const onInput = (event) => activateCursor(event); + const onCompositionUpdate = (event) => activateCursor(event); + const onKeyDown = (event) => { + if (!isRealUserActivity(event)) return; + if (!getFocusedTextField()) return; + activateCursor(event); + }; + const onSelectionChange = () => { + if (!isActive) return; + publishCursor(); + }; + const onFocusOut = () => { + window.setTimeout(() => { + if (!getFocusedTextField()) clearCursor(); + }, 0); + }; + const onScroll = () => { + if (isActive) publishCursor(); + syncOverlay(); + }; + const onResize = () => syncOverlay(); + document.addEventListener("input", onInput, listenerOpts); + document.addEventListener("compositionupdate", onCompositionUpdate, listenerOpts); + document.addEventListener("keydown", onKeyDown, listenerOpts); + document.addEventListener("selectionchange", onSelectionChange); + document.addEventListener("focusout", onFocusOut, listenerOpts); + document.addEventListener("scroll", onScroll, listenerOpts); + window.addEventListener("resize", onResize, { passive: true }); + let layoutTimer; + const domObserver = new MutationObserver(() => { + window.clearTimeout(layoutTimer); + layoutTimer = window.setTimeout(() => { + if (isActive) publishCursor(); + syncOverlay(); + }, 100); + }); + domObserver.observe(document.documentElement, { + childList: true, + subtree: true, + attributes: true + }); + const pollTimer = window.setInterval(() => { + if (isActive) publishCursor(); + syncOverlay(); + }, 100); + const onProviderStatus = () => syncOverlay(); + config.provider.on("synced", onProviderStatus); + presence.setLocalCursor(null); + syncOverlay(); + if (config.debug) { + console.log("[typeform-cursor-presence] started (idle-clear, no sync-clear)", { + userName, + editorId: config.editorId + }); + } + const destroy = () => { + window.clearInterval(pollTimer); + window.clearTimeout(layoutTimer); + window.clearTimeout(idleTimer); + config.provider.off("synced", onProviderStatus); + document.removeEventListener("input", onInput, listenerOpts); + document.removeEventListener("compositionupdate", onCompositionUpdate, listenerOpts); + document.removeEventListener("keydown", onKeyDown, listenerOpts); + document.removeEventListener("selectionchange", onSelectionChange); + document.removeEventListener("focusout", onFocusOut, listenerOpts); + document.removeEventListener("scroll", onScroll, listenerOpts); + window.removeEventListener("resize", onResize); + domObserver.disconnect(); + clearCursor(); + presence.destroy(); + overlay.destroy(); + }; + window.addEventListener("beforeunload", destroy, { once: true }); + return destroy; + } + + // src/bridge/typeform-bridge.ts + (() => { + const pageParams = new URLSearchParams(window.location.search); + const roomId = pageParams.get("roomId") || document.documentElement.getAttribute("data-lowcoder-room-id") || ""; + const role = pageParams.get("role") || document.documentElement.getAttribute("data-lowcoder-role") || "driver"; + const editorId = pageParams.get("editorId") || document.documentElement.getAttribute("data-lowcoder-editor-id") || "local"; + const peerId = `${editorId}|${role}|${Math.random().toString(36).slice(2, 10)}`; + const collabId = pageParams.get("collab") || document.documentElement.getAttribute("data-lowcoder-collab-id") || "default"; + const debug = pageParams.get("debug") === "1"; + const hocuspocusConfig = window.__LOWCODER_HOCUSPOCUS__ ?? {}; + const hocuspocusUrl = hocuspocusConfig.url || document.documentElement.getAttribute("data-lowcoder-hocuspocus-url") || "ws://localhost:3006"; + const hocuspocusToken = hocuspocusConfig.token || document.documentElement.getAttribute("data-lowcoder-hocuspocus-token") || ""; + const documentName = `typeform_${roomId}_${collabId}`; + let version = 0; + let lastAppliedVersion = 0; + let localStep = 0; + let isApplyingRemoteState = false; + let lastSentPayload = ""; + let sessionStarted = false; + let welcomeClickPending = false; + let providerReady = false; + let lastNavAt = 0; + let lastLocalInputAt = 0; + let isApplyingInputText = false; + let publishInputTimer; + let applyingGeneration = 0; + const outboundQueue = []; + let allAnswers = {}; + const doc2 = new Doc(); + const stateMap = doc2.getMap("state"); + const provider = new HocuspocusProvider({ + url: hocuspocusUrl, + name: documentName, + document: doc2, + token: hocuspocusToken || void 0, + onAuthenticationFailed: (data) => { + console.error("[typeform-bridge] Hocuspocus auth failed", data); + } + }); + function log(...args2) { + if (debug) console.log("[typeform-bridge]", role, ...args2); + } + function nextVersion() { + const remote = Number(stateMap.get("version") || 0); + version = Math.max(version, remote) + 1; + return version; + } + function publishPatch(patch) { + if (!providerReady) { + outboundQueue.push(patch); + return; + } + doc2.transact(() => { + if (patch.started) { + stateMap.set("started", true); + } + stateMap.set("version", patch.version); + stateMap.set("patchJson", JSON.stringify(patch)); + }); + log("published", { + version: patch.version, + step: patch.currentStep, + nav: patch.nav, + q: patch.questionKey + }); + } + function flushOutboundQueue() { + while (outboundQueue.length > 0) { + const patch = outboundQueue.shift(); + if (patch) publishPatch(patch); + } + } + function parseRemotePatch() { + const raw = stateMap.get("patchJson"); + if (typeof raw !== "string" || !raw) return null; + try { + return JSON.parse(raw); + } catch { + return null; + } + } + function shouldApplyPatch(patch) { + if (!patch) return false; + if (patch.lastEditor === peerId) return false; + if ((patch.version ?? 0) <= lastAppliedVersion && patch.currentStep === localStep) { + return false; + } + return true; + } + function syncFromRemoteState() { + const started = Boolean(stateMap.get("started")); + const patch = parseRemotePatch(); + if (started && !sessionStarted && role === "follower") { + onRemoteSessionStarted(); + } + if (patch && shouldApplyPatch(patch)) { + applyRemoteState(patch); + } + applyRemoteInputText(); + } + provider.on("status", ({ status }) => { + log("status", status, documentName); + if (status === WebSocketStatus.Connected) { + providerReady = true; + flushOutboundQueue(); + syncFromRemoteState(); + } + }); + provider.on("synced", () => { + providerReady = true; + flushOutboundQueue(); + syncFromRemoteState(); + }); + stateMap.observe((event) => { + if (event.keysChanged.has("patchJson") || event.keysChanged.has("version") || event.keysChanged.has("started") || event.keysChanged.has("inputTextJson") || event.keysChanged.has("inputTextsJson")) { + syncFromRemoteState(); + } + }); + function isVisible2(el) { + const node = el; + if (!node.getBoundingClientRect) return true; + const rect = node.getBoundingClientRect(); + return rect.width > 0 && rect.height > 0; + } + function listVisibleTextFields(container = document) { + return Array.from( + container.querySelectorAll( + [ + 'input[type="text"]', + 'input[type="email"]', + 'input[type="number"]', + 'input[type="tel"]', + 'input[type="url"]', + 'input[type="search"]', + 'input[type="short_text"]', + 'input[type="long_text"]', + 'input[type="phone_number"]', + "input[name]", + "input:not([type])", + "textarea" + ].join(", ") + ) + ).filter((field) => { + const type = (field.getAttribute("type") || field.type || "").toLowerCase(); + if (["hidden", "checkbox", "radio", "button", "submit", "file", "password"].includes(type)) { + return false; + } + return isVisible2(field); + }); + } + function getFocusedTextField2() { + const el = document.activeElement; + if (!el) return null; + if (el instanceof HTMLInputElement) { + const type = (el.getAttribute("type") || el.type || "text").toLowerCase(); + if (["hidden", "checkbox", "radio", "button", "submit", "file", "password"].includes(type)) { + return null; + } + return el; + } + if (el instanceof HTMLTextAreaElement) return el; + return null; + } + function extractQuestionUuid2(value) { + const match2 = value.match( + /([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i + ); + return match2?.[1] ?? null; + } + function getFieldKey(field) { + const name = field.getAttribute("name")?.trim(); + if (name) return `name:${name}`; + const labelledBy = field.getAttribute("aria-labelledby") || ""; + const fromLabel = extractQuestionUuid2(labelledBy); + if (fromLabel) return `qid:${fromLabel}`; + const id2 = field.getAttribute("id") || ""; + const fromId = extractQuestionUuid2(id2); + if (fromId) return `qid:${fromId}`; + const typeAttr = (field.getAttribute("type") || field.type || "text").toLowerCase(); + return `type:${typeAttr}:step:${localStep}`; + } + function findFieldByKey(key) { + const visible = listVisibleTextFields(document); + for (const field of visible) { + if (getFieldKey(field) === key) return field; + } + if (key.startsWith("name:")) { + const name = key.slice("name:".length); + const el = document.querySelector(`input[name="${CSS.escape(name)}"]`); + if (el && isVisible2(el)) return el; + } + if (key.startsWith("qid:")) { + const qid = key.slice("qid:".length); + const matches = Array.from( + document.querySelectorAll( + `input[aria-labelledby*="${CSS.escape(qid)}"], input[id*="${CSS.escape(qid)}"], textarea[aria-labelledby*="${CSS.escape(qid)}"]` + ) + ).filter(isVisible2); + if (matches[0]) return matches[0]; + } + return null; + } + function getActiveQuestionContainer() { + const candidates = [ + '[data-qa="question-container"]', + '[data-qa="question"]', + "fieldset", + '[role="group"]' + ]; + for (const selector of candidates) { + const nodes = Array.from(document.querySelectorAll(selector)).filter(isVisible2); + if (nodes.length > 0) { + return nodes[nodes.length - 1]; + } + } + return null; + } + function readInputTextsMap() { + const raw = stateMap.get("inputTextsJson"); + if (typeof raw !== "string" || !raw) return {}; + try { + const parsed = JSON.parse(raw); + return parsed && typeof parsed === "object" ? parsed : {}; + } catch { + return {}; + } + } + function syncAllAnswersForField(fieldKey, text2) { + const qKey = stepKey(localStep); + const container = getActiveQuestionContainer() || document; + listVisibleTextFields(container).forEach((field, index) => { + if (getFieldKey(field) === fieldKey) { + allAnswers[`${qKey}::field-${index}`] = text2; + } + }); + } + function publishInputText() { + if (!canPublish()) return; + if (isApplyingInputText || isApplyingRemoteState) return; + const field = getFocusedTextField2() || listVisibleTextFields(document)[0]; + if (!field) return; + const fieldKey = getFieldKey(field); + const text2 = field.value; + const remoteMap = readInputTextsMap(); + if ((remoteMap[fieldKey] ?? "") === text2) return; + if (!providerReady) return; + syncAllAnswersForField(fieldKey, text2); + const nextMap = { ...remoteMap, [fieldKey]: text2 }; + const payload = { + fieldKey, + step: localStep, + text: text2, + peerId, + version: nextVersion() + }; + doc2.transact(() => { + stateMap.set("inputTextJson", JSON.stringify(payload)); + stateMap.set("inputTextsJson", JSON.stringify(nextMap)); + stateMap.set("version", payload.version); + }); + log("published input text", payload); + } + function schedulePublishInputText() { + lastLocalInputAt = Date.now(); + window.clearTimeout(publishInputTimer); + publishInputTimer = window.setTimeout(() => { + publishInputText(); + }, 120); + } + function flushPublishInputText() { + window.clearTimeout(publishInputTimer); + publishInputText(); + } + function readLatestInputPayload() { + const raw = stateMap.get("inputTextJson"); + if (typeof raw !== "string" || !raw) return null; + try { + return JSON.parse(raw); + } catch { + return null; + } + } + function applyRemoteInputText() { + if (isOnWelcomeScreen()) return; + if (isApplyingInputText) return; + const map2 = readInputTextsMap(); + const visible = listVisibleTextFields(document); + const latestInput = readLatestInputPayload(); + const locallyTyping = Date.now() - lastLocalInputAt < 800; + isApplyingInputText = true; + try { + for (const field of visible) { + const key = getFieldKey(field); + if (!Object.prototype.hasOwnProperty.call(map2, key)) continue; + if (locallyTyping && document.activeElement === field) continue; + const next = map2[key] ?? ""; + if (latestInput?.peerId === peerId && latestInput.fieldKey === key && document.activeElement === field && field.value !== next) { + continue; + } + if (field.value === next) continue; + setNativeInputValue(field, next); + log("applied field text", key, next); + } + const payload = latestInput; + if (payload && payload.peerId !== peerId && typeof payload.text === "string" && payload.fieldKey) { + const target = findFieldByKey(payload.fieldKey); + if (target && !(locallyTyping && document.activeElement === target) && target.value !== payload.text) { + setNativeInputValue(target, payload.text); + log("applied field text (payload)", payload.fieldKey, payload.text); + } + } + } finally { + window.setTimeout(() => { + isApplyingInputText = false; + }, 50); + } + } + function stepKey(step) { + return `question-${Math.max(0, step)}`; + } + function collectVisibleAnswersForKey(qKey) { + const result = {}; + const container = getActiveQuestionContainer() || document; + listVisibleTextFields(container).forEach((field, index) => { + result[`${qKey}::field-${index}`] = field.value; + }); + container.querySelectorAll( + '[data-qa*="choice"], [role="radio"], [role="checkbox"], [role="option"], button[data-qa]' + ).forEach((el, index) => { + const selected = el.getAttribute("aria-checked") === "true" || el.getAttribute("aria-pressed") === "true" || el.getAttribute("aria-selected") === "true" || el.classList.contains("selected"); + if (!selected) return; + const value = el.getAttribute("data-qa") || el.textContent?.trim() || String(index); + result[`${qKey}::choice`] = value; + }); + return result; + } + function collectAnswers(forStep = localStep) { + const visible = collectVisibleAnswersForKey(stepKey(forStep)); + allAnswers = { ...allAnswers, ...visible }; + return allAnswers; + } + function getFormId() { + const target = pageParams.get("target"); + if (target) { + try { + const match3 = new URL(target).pathname.match(/\/to\/([^/?#]+)/); + if (match3?.[1]) return match3[1]; + } catch { + } + } + const match2 = window.location.pathname.match(/\/to\/([^/?#]+)/); + return match2?.[1] ?? ""; + } + function isOnWelcomeScreen() { + return !!(document.querySelector('[data-qa="start-button"]') || document.querySelector('[data-qa="welcome-screen"]') || document.querySelector('[data-qa="landing-wrapper"]') || document.querySelector('[data-qa="welcome-screen-paragraph"]')); + } + function buttonText(el) { + return (el.textContent ?? "").trim().toLowerCase(); + } + function isStartButton(el) { + if (!el) return false; + const button = el.closest('button, [role="button"], a'); + if (!button) return false; + const qa = button.getAttribute("data-qa") ?? ""; + if (/start/i.test(qa)) return true; + const text2 = buttonText(button); + return text2 === "start" || text2 === "begin" || text2 === "get started" || text2.includes("start"); + } + function isSubmitButton(el) { + if (!el) return false; + const button = el.closest('button, [role="button"], a, input'); + if (!button) return false; + const qa = (button.getAttribute("data-qa") ?? "").toLowerCase(); + const aria = (button.getAttribute("aria-label") ?? "").toLowerCase(); + const type = (button.getAttribute("type") ?? "").toLowerCase(); + if (/submit/i.test(qa) || /submit/i.test(aria) || type === "submit") return true; + const text2 = buttonText(button); + return text2 === "submit" || text2 === "done" || text2 === "send" || text2 === "finish"; + } + function isOkButton(el) { + if (!el) return false; + const button = el.closest('button, [role="button"]'); + if (!button) return false; + if (isBackButton(button)) return false; + const qa = button.getAttribute("data-qa") ?? ""; + if (/ok-button|submit-button|next/i.test(qa)) return true; + const text2 = buttonText(button); + return ["ok", "next", "continue", "submit", "done"].includes(text2); + } + function isBackButton(el) { + if (!el) return false; + const button = el.closest('button, [role="button"], a'); + if (!button) return false; + const qa = (button.getAttribute("data-qa") ?? "").toLowerCase(); + const aria = (button.getAttribute("aria-label") ?? "").toLowerCase(); + const title = (button.getAttribute("title") ?? "").toLowerCase(); + if (/prev|previous|back/.test(qa) || /prev|previous|back/.test(aria) || /prev|previous|back/.test(title)) { + return true; + } + const text2 = buttonText(button); + return text2 === "previous" || text2 === "prev" || text2 === "back" || text2 === "\u2190"; + } + function clickOkButton(allowSubmit = false) { + const selectors = [ + '[data-qa="ok-button-visible"]', + '[data-qa="ok-button"]', + '[data-qa*="next"]' + ]; + if (allowSubmit) { + selectors.splice(1, 0, '[data-qa="submit-button"]'); + } + for (const selector of selectors) { + const btn = document.querySelector(selector); + if (btn && isVisible2(btn) && !isBackButton(btn)) { + if (!allowSubmit && isSubmitButton(btn)) continue; + btn.click(); + return true; + } + } + const fallback = Array.from(document.querySelectorAll("button, [role='button']")).find( + (btn) => { + if (!isVisible2(btn) || isBackButton(btn)) return false; + if (!allowSubmit && isSubmitButton(btn)) return false; + const text2 = buttonText(btn); + return ["ok", "next", "continue"].includes(text2) || allowSubmit && ["submit", "done"].includes(text2); + } + ); + if (fallback) { + fallback.click(); + return true; + } + return false; + } + function clickBackButton() { + const selectors = [ + '[data-qa*="previous"]', + '[data-qa*="prev"]', + '[data-qa*="back"]', + '[aria-label*="Previous" i]', + '[aria-label*="Back" i]', + '[title*="Previous" i]', + '[title*="Back" i]' + ]; + for (const selector of selectors) { + try { + const btn = document.querySelector(selector); + if (btn && isVisible2(btn)) { + btn.click(); + return true; + } + } catch { + } + } + const fallback = Array.from(document.querySelectorAll("button, [role='button'], a")).find( + (btn) => isVisible2(btn) && isBackButton(btn) + ); + if (fallback) { + fallback.click(); + return true; + } + return false; + } + function advancePastWelcomeIfNeeded() { + if (!isOnWelcomeScreen()) return; + const startButton = document.querySelector( + '[data-qa="start-button"]' + ); + if (startButton) { + startButton.click(); + return; + } + const fallback = Array.from(document.querySelectorAll("button, [role='button']")).find( + (node) => isStartButton(node) + ); + fallback?.click(); + } + function buildPatch(opts = {}) { + const answeredStep = opts.answeredStep ?? localStep; + const current = opts.currentStep ?? localStep; + return { + formId: getFormId(), + answers: collectAnswers(answeredStep), + currentStep: current, + questionKey: stepKey(answeredStep), + version: nextVersion(), + lastEditor: peerId, + submitted: Boolean(opts.submitted), + started: true, + nav: opts.nav ?? "answer" + }; + } + function markSessionStarted() { + if (sessionStarted) return; + sessionStarted = true; + welcomeClickPending = false; + localStep = 0; + if (role === "driver") { + const startedPatch = buildPatch({ currentStep: 0, answeredStep: 0, nav: "start" }); + publishPatch(startedPatch); + log("session started (local)"); + } + } + function onRemoteSessionStarted() { + if (sessionStarted) return; + sessionStarted = true; + localStep = 0; + if (role === "follower") { + advancePastWelcomeIfNeeded(); + log("session started (remote)"); + } + } + function canPublish() { + return sessionStarted && !isApplyingRemoteState; + } + function sendPatch(submitted = false) { + if (!canPublish()) return; + const payload = buildPatch({ + submitted, + answeredStep: localStep, + currentStep: localStep, + nav: "answer" + }); + const serialized = JSON.stringify({ + answers: payload.answers, + currentStep: payload.currentStep, + submitted: payload.submitted, + nav: payload.nav + }); + if (serialized === lastSentPayload && !submitted) return; + lastSentPayload = serialized; + publishPatch(payload); + } + function publishNext(fromStep, submitted = false) { + if (!canPublish()) return; + const now = Date.now(); + if (!submitted && now - lastNavAt < 350) return; + lastNavAt = now; + const payload = buildPatch({ + submitted, + answeredStep: fromStep, + currentStep: fromStep + 1, + nav: "next" + }); + localStep = fromStep + 1; + lastSentPayload = ""; + lastAppliedVersion = Math.max(lastAppliedVersion, payload.version); + publishPatch(payload); + log("next", fromStep, "->", localStep); + window.setTimeout(() => applyRemoteInputText(), 300); + } + function publishPrev(fromStep) { + if (!canPublish()) return; + if (fromStep <= 0) return; + const now = Date.now(); + if (now - lastNavAt < 350) return; + lastNavAt = now; + const payload = buildPatch({ + answeredStep: fromStep, + currentStep: fromStep - 1, + nav: "prev" + }); + localStep = fromStep - 1; + lastSentPayload = ""; + lastAppliedVersion = Math.max(lastAppliedVersion, payload.version); + publishPatch(payload); + log("prev", fromStep, "->", localStep); + window.setTimeout(() => applyRemoteInputText(), 300); + } + function applyChoice(value) { + const container = getActiveQuestionContainer() || document; + const choices = container.querySelectorAll( + '[data-qa*="choice"], [role="radio"], [role="checkbox"], [role="option"], button[data-qa]' + ); + for (const el of choices) { + if (!isVisible2(el)) continue; + const label = el.textContent?.trim() || ""; + const qa = el.getAttribute("data-qa") || ""; + if (qa === value || label === value || qa.includes(value) || label.includes(value)) { + el.click(); + return true; + } + } + return false; + } + function setNativeInputValue(field, nextValue) { + if (field.value === nextValue) return; + const previous = field.value; + const proto = field instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype; + const setter = Object.getOwnPropertyDescriptor(proto, "value")?.set; + const tracker = field._valueTracker; + if (tracker) { + tracker.setValue(previous); + } + if (setter) { + setter.call(field, nextValue); + } else { + field.value = nextValue; + } + const inputType = nextValue.length < previous.length ? "deleteContentBackward" : nextValue.length > previous.length ? "insertText" : "insertReplacementText"; + field.dispatchEvent( + new InputEvent("input", { + bubbles: true, + cancelable: true, + data: inputType === "insertText" ? nextValue.slice(previous.length) : nextValue, + inputType + }) + ); + field.dispatchEvent(new Event("change", { bubbles: true })); + } + function applyAnswersForQuestion(answers, questionKey) { + const container = getActiveQuestionContainer() || document; + Object.entries(answers).forEach(([key, value]) => { + if (!key.startsWith(`${questionKey}::`)) return; + if (key.endsWith("::choice")) { + applyChoice(String(value ?? "")); + return; + } + if (key.includes("::field-")) { + const index = Number(key.split("::field-")[1] ?? 0); + const field = listVisibleTextFields(container)[index]; + if (!field) return; + const fieldKey = getFieldKey(field); + const inputTexts = readInputTextsMap(); + if (Object.prototype.hasOwnProperty.call(inputTexts, fieldKey)) return; + if (document.activeElement === field && Date.now() - lastLocalInputAt < 800) return; + setNativeInputValue(field, value == null ? "" : String(value)); + } + }); + } + function alignToRemoteStep(patch, generation) { + if (generation !== applyingGeneration) return; + const remoteStep = Math.max(0, patch.currentStep ?? 0); + if (localStep === remoteStep) { + applyAnswersForQuestion(patch.answers, stepKey(localStep)); + lastAppliedVersion = Math.max(lastAppliedVersion, patch.version ?? 0); + isApplyingRemoteState = false; + log("aligned on step", localStep); + window.setTimeout(() => applyRemoteInputText(), 250); + return; + } + if (localStep < remoteStep) { + applyAnswersForQuestion(patch.answers, stepKey(localStep)); + window.setTimeout(() => { + if (generation !== applyingGeneration) return; + const allowSubmit = Boolean(patch.submitted) && localStep + 1 >= remoteStep; + const advanced = clickOkButton(allowSubmit); + if (!advanced) { + lastAppliedVersion = Math.max(lastAppliedVersion, patch.version ?? 0); + isApplyingRemoteState = false; + log("catch-up stopped: no next control", localStep, "target", remoteStep); + return; + } + localStep += 1; + log("catch-up next ->", localStep, "target", remoteStep); + window.setTimeout(() => alignToRemoteStep(patch, generation), 450); + }, 180); + return; + } + window.setTimeout(() => { + if (generation !== applyingGeneration) return; + const moved = clickBackButton(); + if (moved) { + localStep = Math.max(0, localStep - 1); + log("catch-up prev ->", localStep, "target", remoteStep); + } else { + localStep = remoteStep; + applyAnswersForQuestion(patch.answers, stepKey(localStep)); + lastAppliedVersion = Math.max(lastAppliedVersion, patch.version ?? 0); + isApplyingRemoteState = false; + return; + } + window.setTimeout(() => alignToRemoteStep(patch, generation), 450); + }, 180); + } + function applyRemoteStateInner(patch) { + if (!patch?.answers && patch.nav === "answer") return; + applyingGeneration += 1; + const generation = applyingGeneration; + isApplyingRemoteState = true; + allAnswers = { ...allAnswers, ...patch.answers || {} }; + alignToRemoteStep(patch, generation); + } + function applyRemoteState(patch) { + if (!sessionStarted) { + if (role === "driver") return; + if (!patch.started && !Boolean(stateMap.get("started"))) return; + sessionStarted = true; + } + if (isOnWelcomeScreen()) { + if (role === "follower" && sessionStarted) { + advancePastWelcomeIfNeeded(); + window.setTimeout(() => applyRemoteStateInner(patch), 400); + } + return; + } + applyRemoteStateInner(patch); + } + function maybeMarkSessionStartedAfterWelcomeClick() { + if (role !== "driver" || sessionStarted || !welcomeClickPending) return; + if (!isOnWelcomeScreen()) { + markSessionStarted(); + } + } + const debounce = /* @__PURE__ */ (() => { + let timer; + return () => { + window.clearTimeout(timer); + timer = window.setTimeout(() => { + maybeMarkSessionStartedAfterWelcomeClick(); + sendPatch(false); + }, 150); + }; + })(); + document.addEventListener( + "click", + (event) => { + const target = event.target; + if (!sessionStarted) { + if (role === "driver" && isOnWelcomeScreen()) { + welcomeClickPending = true; + if (isStartButton(target)) { + window.setTimeout(() => markSessionStarted(), 0); + } + } + return; + } + if (isApplyingRemoteState) return; + if (isBackButton(target)) { + publishPrev(localStep); + return; + } + if (isOkButton(target)) { + publishNext(localStep, isSubmitButton(target)); + } + }, + true + ); + document.addEventListener( + "keydown", + (event) => { + if (!canPublish()) return; + if (isOnWelcomeScreen()) return; + if (event.key === "Enter") { + window.setTimeout(() => publishNext(localStep, false), 0); + return; + } + if (event.key === "ArrowUp") { + window.setTimeout(() => publishPrev(localStep), 0); + } + }, + true + ); + new MutationObserver(debounce).observe(document.documentElement, { + childList: true, + attributes: true, + subtree: true + }); + document.addEventListener( + "input", + () => { + if (isApplyingInputText || isApplyingRemoteState) return; + schedulePublishInputText(); + debounce(); + }, + true + ); + document.addEventListener( + "keyup", + (event) => { + if (isApplyingInputText || isApplyingRemoteState) return; + const key = event.key; + if (key === "Enter" || key === "ArrowUp" || key === "ArrowDown") return; + schedulePublishInputText(); + }, + true + ); + document.addEventListener( + "blur", + (event) => { + if (isApplyingInputText || isApplyingRemoteState) return; + const target = event.target; + if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement) { + flushPublishInputText(); + } + }, + true + ); + document.addEventListener("change", debounce, true); + document.addEventListener( + "submit", + (event) => { + if (isApplyingRemoteState) { + event.preventDefault(); + event.stopPropagation(); + log("blocked auto-submit during remote sync"); + return; + } + if (canPublish()) { + publishNext(localStep, true); + } + }, + true + ); + const originalFetch = window.fetch.bind(window); + window.fetch = async (...args2) => { + const response = await originalFetch(...args2); + debounce(); + return response; + }; + const originalOpen = XMLHttpRequest.prototype.open; + XMLHttpRequest.prototype.open = function patchedOpen(...args2) { + this.addEventListener("loadend", debounce); + return originalOpen.apply(this, args2); + }; + log("ready", { role, roomId, collabId, documentName, editorId, peerId }); + initTypeformCursorPresence({ + provider, + editorId, + role, + debug, + getFieldKey, + findFieldByKey, + getCurrentStep: () => localStep, + getSessionStarted: () => sessionStarted, + isWelcomeScreen: isOnWelcomeScreen, + isSyncing: () => isApplyingRemoteState || isApplyingInputText + }); + window.addEventListener("beforeunload", () => { + provider.destroy(); + doc2.destroy(); + }); + })(); +})(); diff --git a/server/proxy-service/build/bridge/website-bridge.js b/server/proxy-service/build/bridge/website-bridge.js new file mode 100644 index 0000000000..12bdf6fc72 --- /dev/null +++ b/server/proxy-service/build/bridge/website-bridge.js @@ -0,0 +1,11270 @@ +(() => { + var __defProp = Object.defineProperty; + var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; + var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); + + // node_modules/lib0/map.js + var create = () => /* @__PURE__ */ new Map(); + var copy = (m) => { + const r = create(); + m.forEach((v, k) => { + r.set(k, v); + }); + return r; + }; + var setIfUndefined = (map2, key, createT) => { + let set = map2.get(key); + if (set === void 0) { + map2.set(key, set = createT()); + } + return set; + }; + var map = (m, f) => { + const res = []; + for (const [key, value] of m) { + res.push(f(value, key)); + } + return res; + }; + var any = (m, f) => { + for (const [key, value] of m) { + if (f(value, key)) { + return true; + } + } + return false; + }; + + // node_modules/lib0/set.js + var create2 = () => /* @__PURE__ */ new Set(); + + // node_modules/lib0/array.js + var last = (arr) => arr[arr.length - 1]; + var appendTo = (dest, src) => { + for (let i = 0; i < src.length; i++) { + dest.push(src[i]); + } + }; + var from = Array.from; + var every = (arr, f) => { + for (let i = 0; i < arr.length; i++) { + if (!f(arr[i], i, arr)) { + return false; + } + } + return true; + }; + var some = (arr, f) => { + for (let i = 0; i < arr.length; i++) { + if (f(arr[i], i, arr)) { + return true; + } + } + return false; + }; + var unfold = (len, f) => { + const array = new Array(len); + for (let i = 0; i < len; i++) { + array[i] = f(i, array); + } + return array; + }; + var isArray = Array.isArray; + + // node_modules/lib0/observable.js + var ObservableV2 = class { + constructor() { + this._observers = create(); + } + /** + * @template {keyof EVENTS & string} NAME + * @param {NAME} name + * @param {EVENTS[NAME]} f + */ + on(name, f) { + setIfUndefined( + this._observers, + /** @type {string} */ + name, + create2 + ).add(f); + return f; + } + /** + * @template {keyof EVENTS & string} NAME + * @param {NAME} name + * @param {EVENTS[NAME]} f + */ + once(name, f) { + const _f = (...args2) => { + this.off( + name, + /** @type {any} */ + _f + ); + f(...args2); + }; + this.on( + name, + /** @type {any} */ + _f + ); + } + /** + * @template {keyof EVENTS & string} NAME + * @param {NAME} name + * @param {EVENTS[NAME]} f + */ + off(name, f) { + const observers = this._observers.get(name); + if (observers !== void 0) { + observers.delete(f); + if (observers.size === 0) { + this._observers.delete(name); + } + } + } + /** + * Emit a named event. All registered event listeners that listen to the + * specified name will receive the event. + * + * @todo This should catch exceptions + * + * @template {keyof EVENTS & string} NAME + * @param {NAME} name The event name. + * @param {Parameters} args The arguments that are applied to the event listener. + */ + emit(name, args2) { + return from((this._observers.get(name) || create()).values()).forEach((f) => f(...args2)); + } + destroy() { + this._observers = create(); + } + }; + + // node_modules/lib0/math.js + var floor = Math.floor; + var abs = Math.abs; + var min = (a, b) => a < b ? a : b; + var max = (a, b) => a > b ? a : b; + var isNaN = Number.isNaN; + var isNegativeZero = (n) => n !== 0 ? n < 0 : 1 / n < 0; + + // node_modules/lib0/binary.js + var BIT1 = 1; + var BIT2 = 2; + var BIT3 = 4; + var BIT4 = 8; + var BIT6 = 32; + var BIT7 = 64; + var BIT8 = 128; + var BIT18 = 1 << 17; + var BIT19 = 1 << 18; + var BIT20 = 1 << 19; + var BIT21 = 1 << 20; + var BIT22 = 1 << 21; + var BIT23 = 1 << 22; + var BIT24 = 1 << 23; + var BIT25 = 1 << 24; + var BIT26 = 1 << 25; + var BIT27 = 1 << 26; + var BIT28 = 1 << 27; + var BIT29 = 1 << 28; + var BIT30 = 1 << 29; + var BIT31 = 1 << 30; + var BIT32 = 1 << 31; + var BITS5 = 31; + var BITS6 = 63; + var BITS7 = 127; + var BITS17 = BIT18 - 1; + var BITS18 = BIT19 - 1; + var BITS19 = BIT20 - 1; + var BITS20 = BIT21 - 1; + var BITS21 = BIT22 - 1; + var BITS22 = BIT23 - 1; + var BITS23 = BIT24 - 1; + var BITS24 = BIT25 - 1; + var BITS25 = BIT26 - 1; + var BITS26 = BIT27 - 1; + var BITS27 = BIT28 - 1; + var BITS28 = BIT29 - 1; + var BITS29 = BIT30 - 1; + var BITS30 = BIT31 - 1; + var BITS31 = 2147483647; + + // node_modules/lib0/number.js + var MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER; + var MIN_SAFE_INTEGER = Number.MIN_SAFE_INTEGER; + var LOWEST_INT32 = 1 << 31; + var isInteger = Number.isInteger || ((num) => typeof num === "number" && isFinite(num) && floor(num) === num); + var isNaN2 = Number.isNaN; + var parseInt = Number.parseInt; + + // node_modules/lib0/string.js + var fromCharCode = String.fromCharCode; + var fromCodePoint = String.fromCodePoint; + var MAX_UTF16_CHARACTER = fromCharCode(65535); + var toLowerCase = (s) => s.toLowerCase(); + var trimLeftRegex = /^\s*/g; + var trimLeft = (s) => s.replace(trimLeftRegex, ""); + var fromCamelCaseRegex = /([A-Z])/g; + var fromCamelCase = (s, separator) => trimLeft(s.replace(fromCamelCaseRegex, (match2) => `${separator}${toLowerCase(match2)}`)); + var _encodeUtf8Polyfill = (str) => { + const encodedString = unescape(encodeURIComponent(str)); + const len = encodedString.length; + const buf = new Uint8Array(len); + for (let i = 0; i < len; i++) { + buf[i] = /** @type {number} */ + encodedString.codePointAt(i); + } + return buf; + }; + var utf8TextEncoder = ( + /** @type {TextEncoder} */ + typeof TextEncoder !== "undefined" ? new TextEncoder() : null + ); + var _encodeUtf8Native = (str) => utf8TextEncoder.encode(str); + var encodeUtf8 = utf8TextEncoder ? _encodeUtf8Native : _encodeUtf8Polyfill; + var utf8TextDecoder = typeof TextDecoder === "undefined" ? null : new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }); + if (utf8TextDecoder && utf8TextDecoder.decode(new Uint8Array()).length === 1) { + utf8TextDecoder = null; + } + var repeat = (source, n) => unfold(n, () => source).join(""); + + // node_modules/lib0/encoding.js + var Encoder = class { + constructor() { + this.cpos = 0; + this.cbuf = new Uint8Array(100); + this.bufs = []; + } + }; + var createEncoder = () => new Encoder(); + var length = (encoder) => { + let len = encoder.cpos; + for (let i = 0; i < encoder.bufs.length; i++) { + len += encoder.bufs[i].length; + } + return len; + }; + var toUint8Array = (encoder) => { + const uint8arr = new Uint8Array(length(encoder)); + let curPos = 0; + for (let i = 0; i < encoder.bufs.length; i++) { + const d = encoder.bufs[i]; + uint8arr.set(d, curPos); + curPos += d.length; + } + uint8arr.set(new Uint8Array(encoder.cbuf.buffer, 0, encoder.cpos), curPos); + return uint8arr; + }; + var verifyLen = (encoder, len) => { + const bufferLen = encoder.cbuf.length; + if (bufferLen - encoder.cpos < len) { + encoder.bufs.push(new Uint8Array(encoder.cbuf.buffer, 0, encoder.cpos)); + encoder.cbuf = new Uint8Array(max(bufferLen, len) * 2); + encoder.cpos = 0; + } + }; + var write = (encoder, num) => { + const bufferLen = encoder.cbuf.length; + if (encoder.cpos === bufferLen) { + encoder.bufs.push(encoder.cbuf); + encoder.cbuf = new Uint8Array(bufferLen * 2); + encoder.cpos = 0; + } + encoder.cbuf[encoder.cpos++] = num; + }; + var writeUint8 = write; + var writeVarUint = (encoder, num) => { + while (num > BITS7) { + write(encoder, BIT8 | BITS7 & num); + num = floor(num / 128); + } + write(encoder, BITS7 & num); + }; + var writeVarInt = (encoder, num) => { + const isNegative = isNegativeZero(num); + if (isNegative) { + num = -num; + } + write(encoder, (num > BITS6 ? BIT8 : 0) | (isNegative ? BIT7 : 0) | BITS6 & num); + num = floor(num / 64); + while (num > 0) { + write(encoder, (num > BITS7 ? BIT8 : 0) | BITS7 & num); + num = floor(num / 128); + } + }; + var _strBuffer = new Uint8Array(3e4); + var _maxStrBSize = _strBuffer.length / 3; + var _writeVarStringNative = (encoder, str) => { + if (str.length < _maxStrBSize) { + const written = utf8TextEncoder.encodeInto(str, _strBuffer).written || 0; + writeVarUint(encoder, written); + for (let i = 0; i < written; i++) { + write(encoder, _strBuffer[i]); + } + } else { + writeVarUint8Array(encoder, encodeUtf8(str)); + } + }; + var _writeVarStringPolyfill = (encoder, str) => { + const encodedString = unescape(encodeURIComponent(str)); + const len = encodedString.length; + writeVarUint(encoder, len); + for (let i = 0; i < len; i++) { + write( + encoder, + /** @type {number} */ + encodedString.codePointAt(i) + ); + } + }; + var writeVarString = utf8TextEncoder && /** @type {any} */ + utf8TextEncoder.encodeInto ? _writeVarStringNative : _writeVarStringPolyfill; + var writeUint8Array = (encoder, uint8Array) => { + const bufferLen = encoder.cbuf.length; + const cpos = encoder.cpos; + const leftCopyLen = min(bufferLen - cpos, uint8Array.length); + const rightCopyLen = uint8Array.length - leftCopyLen; + encoder.cbuf.set(uint8Array.subarray(0, leftCopyLen), cpos); + encoder.cpos += leftCopyLen; + if (rightCopyLen > 0) { + encoder.bufs.push(encoder.cbuf); + encoder.cbuf = new Uint8Array(max(bufferLen * 2, rightCopyLen)); + encoder.cbuf.set(uint8Array.subarray(leftCopyLen)); + encoder.cpos = rightCopyLen; + } + }; + var writeVarUint8Array = (encoder, uint8Array) => { + writeVarUint(encoder, uint8Array.byteLength); + writeUint8Array(encoder, uint8Array); + }; + var writeOnDataView = (encoder, len) => { + verifyLen(encoder, len); + const dview = new DataView(encoder.cbuf.buffer, encoder.cpos, len); + encoder.cpos += len; + return dview; + }; + var writeFloat32 = (encoder, num) => writeOnDataView(encoder, 4).setFloat32(0, num, false); + var writeFloat64 = (encoder, num) => writeOnDataView(encoder, 8).setFloat64(0, num, false); + var writeBigInt64 = (encoder, num) => ( + /** @type {any} */ + writeOnDataView(encoder, 8).setBigInt64(0, num, false) + ); + var floatTestBed = new DataView(new ArrayBuffer(4)); + var isFloat32 = (num) => { + floatTestBed.setFloat32(0, num); + return floatTestBed.getFloat32(0) === num; + }; + var writeAny = (encoder, data) => { + switch (typeof data) { + case "string": + write(encoder, 119); + writeVarString(encoder, data); + break; + case "number": + if (isInteger(data) && abs(data) <= BITS31) { + write(encoder, 125); + writeVarInt(encoder, data); + } else if (isFloat32(data)) { + write(encoder, 124); + writeFloat32(encoder, data); + } else { + write(encoder, 123); + writeFloat64(encoder, data); + } + break; + case "bigint": + write(encoder, 122); + writeBigInt64(encoder, data); + break; + case "object": + if (data === null) { + write(encoder, 126); + } else if (isArray(data)) { + write(encoder, 117); + writeVarUint(encoder, data.length); + for (let i = 0; i < data.length; i++) { + writeAny(encoder, data[i]); + } + } else if (data instanceof Uint8Array) { + write(encoder, 116); + writeVarUint8Array(encoder, data); + } else { + write(encoder, 118); + const keys3 = Object.keys(data); + writeVarUint(encoder, keys3.length); + for (let i = 0; i < keys3.length; i++) { + const key = keys3[i]; + writeVarString(encoder, key); + writeAny(encoder, data[key]); + } + } + break; + case "boolean": + write(encoder, data ? 120 : 121); + break; + default: + write(encoder, 127); + } + }; + var RleEncoder = class extends Encoder { + /** + * @param {function(Encoder, T):void} writer + */ + constructor(writer) { + super(); + this.w = writer; + this.s = null; + this.count = 0; + } + /** + * @param {T} v + */ + write(v) { + if (this.s === v) { + this.count++; + } else { + if (this.count > 0) { + writeVarUint(this, this.count - 1); + } + this.count = 1; + this.w(this, v); + this.s = v; + } + } + }; + var flushUintOptRleEncoder = (encoder) => { + if (encoder.count > 0) { + writeVarInt(encoder.encoder, encoder.count === 1 ? encoder.s : -encoder.s); + if (encoder.count > 1) { + writeVarUint(encoder.encoder, encoder.count - 2); + } + } + }; + var UintOptRleEncoder = class { + constructor() { + this.encoder = new Encoder(); + this.s = 0; + this.count = 0; + } + /** + * @param {number} v + */ + write(v) { + if (this.s === v) { + this.count++; + } else { + flushUintOptRleEncoder(this); + this.count = 1; + this.s = v; + } + } + /** + * Flush the encoded state and transform this to a Uint8Array. + * + * Note that this should only be called once. + */ + toUint8Array() { + flushUintOptRleEncoder(this); + return toUint8Array(this.encoder); + } + }; + var flushIntDiffOptRleEncoder = (encoder) => { + if (encoder.count > 0) { + const encodedDiff = encoder.diff * 2 + (encoder.count === 1 ? 0 : 1); + writeVarInt(encoder.encoder, encodedDiff); + if (encoder.count > 1) { + writeVarUint(encoder.encoder, encoder.count - 2); + } + } + }; + var IntDiffOptRleEncoder = class { + constructor() { + this.encoder = new Encoder(); + this.s = 0; + this.count = 0; + this.diff = 0; + } + /** + * @param {number} v + */ + write(v) { + if (this.diff === v - this.s) { + this.s = v; + this.count++; + } else { + flushIntDiffOptRleEncoder(this); + this.count = 1; + this.diff = v - this.s; + this.s = v; + } + } + /** + * Flush the encoded state and transform this to a Uint8Array. + * + * Note that this should only be called once. + */ + toUint8Array() { + flushIntDiffOptRleEncoder(this); + return toUint8Array(this.encoder); + } + }; + var StringEncoder = class { + constructor() { + this.sarr = []; + this.s = ""; + this.lensE = new UintOptRleEncoder(); + } + /** + * @param {string} string + */ + write(string) { + this.s += string; + if (this.s.length > 19) { + this.sarr.push(this.s); + this.s = ""; + } + this.lensE.write(string.length); + } + toUint8Array() { + const encoder = new Encoder(); + this.sarr.push(this.s); + this.s = ""; + writeVarString(encoder, this.sarr.join("")); + writeUint8Array(encoder, this.lensE.toUint8Array()); + return toUint8Array(encoder); + } + }; + + // node_modules/lib0/error.js + var create3 = (s) => new Error(s); + var methodUnimplemented = () => { + throw create3("Method unimplemented"); + }; + var unexpectedCase = () => { + throw create3("Unexpected case"); + }; + + // node_modules/lib0/decoding.js + var errorUnexpectedEndOfArray = create3("Unexpected end of array"); + var errorIntegerOutOfRange = create3("Integer out of Range"); + var Decoder = class { + /** + * @param {Uint8Array} uint8Array Binary data to decode + */ + constructor(uint8Array) { + this.arr = uint8Array; + this.pos = 0; + } + }; + var createDecoder = (uint8Array) => new Decoder(uint8Array); + var hasContent = (decoder) => decoder.pos !== decoder.arr.length; + var readUint8Array = (decoder, len) => { + const view = new Uint8Array(decoder.arr.buffer, decoder.pos + decoder.arr.byteOffset, len); + decoder.pos += len; + return view; + }; + var readVarUint8Array = (decoder) => readUint8Array(decoder, readVarUint(decoder)); + var readUint8 = (decoder) => decoder.arr[decoder.pos++]; + var readVarUint = (decoder) => { + let num = 0; + let mult = 1; + const len = decoder.arr.length; + while (decoder.pos < len) { + const r = decoder.arr[decoder.pos++]; + num = num + (r & BITS7) * mult; + mult *= 128; + if (r < BIT8) { + return num; + } + if (num > MAX_SAFE_INTEGER) { + throw errorIntegerOutOfRange; + } + } + throw errorUnexpectedEndOfArray; + }; + var readVarInt = (decoder) => { + let r = decoder.arr[decoder.pos++]; + let num = r & BITS6; + let mult = 64; + const sign = (r & BIT7) > 0 ? -1 : 1; + if ((r & BIT8) === 0) { + return sign * num; + } + const len = decoder.arr.length; + while (decoder.pos < len) { + r = decoder.arr[decoder.pos++]; + num = num + (r & BITS7) * mult; + mult *= 128; + if (r < BIT8) { + return sign * num; + } + if (num > MAX_SAFE_INTEGER) { + throw errorIntegerOutOfRange; + } + } + throw errorUnexpectedEndOfArray; + }; + var _readVarStringPolyfill = (decoder) => { + let remainingLen = readVarUint(decoder); + if (remainingLen === 0) { + return ""; + } else { + let encodedString = String.fromCodePoint(readUint8(decoder)); + if (--remainingLen < 100) { + while (remainingLen--) { + encodedString += String.fromCodePoint(readUint8(decoder)); + } + } else { + while (remainingLen > 0) { + const nextLen = remainingLen < 1e4 ? remainingLen : 1e4; + const bytes = decoder.arr.subarray(decoder.pos, decoder.pos + nextLen); + decoder.pos += nextLen; + encodedString += String.fromCodePoint.apply( + null, + /** @type {any} */ + bytes + ); + remainingLen -= nextLen; + } + } + return decodeURIComponent(escape(encodedString)); + } + }; + var _readVarStringNative = (decoder) => ( + /** @type any */ + utf8TextDecoder.decode(readVarUint8Array(decoder)) + ); + var readVarString = utf8TextDecoder ? _readVarStringNative : _readVarStringPolyfill; + var readFromDataView = (decoder, len) => { + const dv = new DataView(decoder.arr.buffer, decoder.arr.byteOffset + decoder.pos, len); + decoder.pos += len; + return dv; + }; + var readFloat32 = (decoder) => readFromDataView(decoder, 4).getFloat32(0, false); + var readFloat64 = (decoder) => readFromDataView(decoder, 8).getFloat64(0, false); + var readBigInt64 = (decoder) => ( + /** @type {any} */ + readFromDataView(decoder, 8).getBigInt64(0, false) + ); + var readAnyLookupTable = [ + (decoder) => void 0, + // CASE 127: undefined + (decoder) => null, + // CASE 126: null + readVarInt, + // CASE 125: integer + readFloat32, + // CASE 124: float32 + readFloat64, + // CASE 123: float64 + readBigInt64, + // CASE 122: bigint + (decoder) => false, + // CASE 121: boolean (false) + (decoder) => true, + // CASE 120: boolean (true) + readVarString, + // CASE 119: string + (decoder) => { + const len = readVarUint(decoder); + const obj = {}; + for (let i = 0; i < len; i++) { + const key = readVarString(decoder); + obj[key] = readAny(decoder); + } + return obj; + }, + (decoder) => { + const len = readVarUint(decoder); + const arr = []; + for (let i = 0; i < len; i++) { + arr.push(readAny(decoder)); + } + return arr; + }, + readVarUint8Array + // CASE 116: Uint8Array + ]; + var readAny = (decoder) => readAnyLookupTable[127 - readUint8(decoder)](decoder); + var RleDecoder = class extends Decoder { + /** + * @param {Uint8Array} uint8Array + * @param {function(Decoder):T} reader + */ + constructor(uint8Array, reader) { + super(uint8Array); + this.reader = reader; + this.s = null; + this.count = 0; + } + read() { + if (this.count === 0) { + this.s = this.reader(this); + if (hasContent(this)) { + this.count = readVarUint(this) + 1; + } else { + this.count = -1; + } + } + this.count--; + return ( + /** @type {T} */ + this.s + ); + } + }; + var UintOptRleDecoder = class extends Decoder { + /** + * @param {Uint8Array} uint8Array + */ + constructor(uint8Array) { + super(uint8Array); + this.s = 0; + this.count = 0; + } + read() { + if (this.count === 0) { + this.s = readVarInt(this); + const isNegative = isNegativeZero(this.s); + this.count = 1; + if (isNegative) { + this.s = -this.s; + this.count = readVarUint(this) + 2; + } + } + this.count--; + return ( + /** @type {number} */ + this.s + ); + } + }; + var IntDiffOptRleDecoder = class extends Decoder { + /** + * @param {Uint8Array} uint8Array + */ + constructor(uint8Array) { + super(uint8Array); + this.s = 0; + this.count = 0; + this.diff = 0; + } + /** + * @return {number} + */ + read() { + if (this.count === 0) { + const diff = readVarInt(this); + const hasCount = diff & 1; + this.diff = floor(diff / 2); + this.count = 1; + if (hasCount) { + this.count = readVarUint(this) + 2; + } + } + this.s += this.diff; + this.count--; + return this.s; + } + }; + var StringDecoder = class { + /** + * @param {Uint8Array} uint8Array + */ + constructor(uint8Array) { + this.decoder = new UintOptRleDecoder(uint8Array); + this.str = readVarString(this.decoder); + this.spos = 0; + } + /** + * @return {string} + */ + read() { + const end = this.spos + this.decoder.read(); + const res = this.str.slice(this.spos, end); + this.spos = end; + return res; + } + }; + + // node_modules/lib0/webcrypto.js + var subtle = crypto.subtle; + var getRandomValues = crypto.getRandomValues.bind(crypto); + + // node_modules/lib0/random.js + var uint32 = () => getRandomValues(new Uint32Array(1))[0]; + var uuidv4Template = "10000000-1000-4000-8000" + -1e11; + var uuidv4 = () => uuidv4Template.replace( + /[018]/g, + /** @param {number} c */ + (c) => (c ^ uint32() & 15 >> c / 4).toString(16) + ); + + // node_modules/lib0/time.js + var getUnixTime = Date.now; + + // node_modules/lib0/promise.js + var create4 = (f) => ( + /** @type {Promise} */ + new Promise(f) + ); + var all = Promise.all.bind(Promise); + + // node_modules/lib0/conditions.js + var undefinedToNull = (v) => v === void 0 ? null : v; + + // node_modules/lib0/storage.js + var VarStoragePolyfill = class { + constructor() { + this.map = /* @__PURE__ */ new Map(); + } + /** + * @param {string} key + * @param {any} newValue + */ + setItem(key, newValue) { + this.map.set(key, newValue); + } + /** + * @param {string} key + */ + getItem(key) { + return this.map.get(key); + } + }; + var _localStorage = new VarStoragePolyfill(); + var usePolyfill = true; + try { + if (typeof localStorage !== "undefined" && localStorage) { + _localStorage = localStorage; + usePolyfill = false; + } + } catch (e) { + } + var varStorage = _localStorage; + + // node_modules/lib0/trait/equality.js + var EqualityTraitSymbol = Symbol("Equality"); + var equals = (a, b) => a === b || !!a?.[EqualityTraitSymbol]?.(b) || false; + + // node_modules/lib0/object.js + var isObject = (o) => typeof o === "object"; + var assign = Object.assign; + var keys = Object.keys; + var forEach = (obj, f) => { + for (const key in obj) { + f(obj[key], key); + } + }; + var size = (obj) => keys(obj).length; + var isEmpty = (obj) => { + for (const _k in obj) { + return false; + } + return true; + }; + var every2 = (obj, f) => { + for (const key in obj) { + if (!f(obj[key], key)) { + return false; + } + } + return true; + }; + var hasProperty = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key); + var equalFlat = (a, b) => a === b || size(a) === size(b) && every2(a, (val, key) => (val !== void 0 || hasProperty(b, key)) && equals(b[key], val)); + var freeze = Object.freeze; + var deepFreeze = (o) => { + for (const key in o) { + const c = o[key]; + if (typeof c === "object" || typeof c === "function") { + deepFreeze(o[key]); + } + } + return freeze(o); + }; + + // node_modules/lib0/function.js + var callAll = (fs, args2, i = 0) => { + try { + for (; i < fs.length; i++) { + fs[i](...args2); + } + } finally { + if (i < fs.length) { + callAll(fs, args2, i + 1); + } + } + }; + var id = (a) => a; + var equalityDeep = (a, b) => { + if (a === b) { + return true; + } + if (a == null || b == null || a.constructor !== b.constructor && (a.constructor || Object) !== (b.constructor || Object)) { + return false; + } + if (a[EqualityTraitSymbol] != null) { + return a[EqualityTraitSymbol](b); + } + switch (a.constructor) { + case ArrayBuffer: + a = new Uint8Array(a); + b = new Uint8Array(b); + // eslint-disable-next-line no-fallthrough + case Uint8Array: { + if (a.byteLength !== b.byteLength) { + return false; + } + for (let i = 0; i < a.length; i++) { + if (a[i] !== b[i]) { + return false; + } + } + break; + } + case Set: { + if (a.size !== b.size) { + return false; + } + for (const value of a) { + if (!b.has(value)) { + return false; + } + } + break; + } + case Map: { + if (a.size !== b.size) { + return false; + } + for (const key of a.keys()) { + if (!b.has(key) || !equalityDeep(a.get(key), b.get(key))) { + return false; + } + } + break; + } + case void 0: + case Object: + if (size(a) !== size(b)) { + return false; + } + for (const key in a) { + if (!hasProperty(a, key) || !equalityDeep(a[key], b[key])) { + return false; + } + } + break; + case Array: + if (a.length !== b.length) { + return false; + } + for (let i = 0; i < a.length; i++) { + if (!equalityDeep(a[i], b[i])) { + return false; + } + } + break; + default: + return false; + } + return true; + }; + var isOneOf = (value, options) => options.includes(value); + + // node_modules/lib0/environment.js + var isNode = typeof process !== "undefined" && process.release && /node|io\.js/.test(process.release.name) && Object.prototype.toString.call(typeof process !== "undefined" ? process : 0) === "[object process]"; + var isMac = typeof navigator !== "undefined" ? /Mac/.test(navigator.platform) : false; + var params; + var args = []; + var computeParams = () => { + if (params === void 0) { + if (isNode) { + params = create(); + const pargs = process.argv; + let currParamName = null; + for (let i = 0; i < pargs.length; i++) { + const parg = pargs[i]; + if (parg[0] === "-") { + if (currParamName !== null) { + params.set(currParamName, ""); + } + currParamName = parg; + } else { + if (currParamName !== null) { + params.set(currParamName, parg); + currParamName = null; + } else { + args.push(parg); + } + } + } + if (currParamName !== null) { + params.set(currParamName, ""); + } + } else if (typeof location === "object") { + params = create(); + (location.search || "?").slice(1).split("&").forEach((kv) => { + if (kv.length !== 0) { + const [key, value] = kv.split("="); + params.set(`--${fromCamelCase(key, "-")}`, value); + params.set(`-${fromCamelCase(key, "-")}`, value); + } + }); + } else { + params = create(); + } + } + return params; + }; + var hasParam = (name) => computeParams().has(name); + var getVariable = (name) => isNode ? undefinedToNull(process.env[name.toUpperCase().replaceAll("-", "_")]) : undefinedToNull(varStorage.getItem(name)); + var hasConf = (name) => hasParam("--" + name) || getVariable(name) !== null; + var production = hasConf("production"); + var forceColor = isNode && isOneOf(process.env.FORCE_COLOR, ["true", "1", "2"]); + var supportsColor = forceColor || !hasParam("--no-colors") && // @todo deprecate --no-colors + !hasConf("no-color") && (!isNode || process.stdout.isTTY) && (!isNode || hasParam("--color") || getVariable("COLORTERM") !== null || (getVariable("TERM") || "").includes("color")); + + // node_modules/lib0/buffer.js + var createUint8ArrayFromLen = (len) => new Uint8Array(len); + var copyUint8Array = (uint8Array) => { + const newBuf = createUint8ArrayFromLen(uint8Array.byteLength); + newBuf.set(uint8Array); + return newBuf; + }; + + // node_modules/lib0/pair.js + var Pair = class { + /** + * @param {L} left + * @param {R} right + */ + constructor(left, right) { + this.left = left; + this.right = right; + } + }; + var create5 = (left, right) => new Pair(left, right); + + // node_modules/lib0/prng.js + var bool = (gen) => gen.next() >= 0.5; + var int53 = (gen, min4, max4) => floor(gen.next() * (max4 + 1 - min4) + min4); + var int32 = (gen, min4, max4) => floor(gen.next() * (max4 + 1 - min4) + min4); + var int31 = (gen, min4, max4) => int32(gen, min4, max4); + var letter = (gen) => fromCharCode(int31(gen, 97, 122)); + var word = (gen, minLen = 0, maxLen = 20) => { + const len = int31(gen, minLen, maxLen); + let str = ""; + for (let i = 0; i < len; i++) { + str += letter(gen); + } + return str; + }; + var oneOf = (gen, array) => array[int31(gen, 0, array.length - 1)]; + + // node_modules/lib0/schema.js + var schemaSymbol = Symbol("0schema"); + var ValidationError = class { + constructor() { + this._rerrs = []; + } + /** + * @param {string?} path + * @param {string} expected + * @param {string} has + * @param {string?} message + */ + extend(path, expected, has, message = null) { + this._rerrs.push({ path, expected, has, message }); + } + toString() { + const s = []; + for (let i = this._rerrs.length - 1; i > 0; i--) { + const r = this._rerrs[i]; + s.push(repeat(" ", (this._rerrs.length - i) * 2) + `${r.path != null ? `[${r.path}] ` : ""}${r.has} doesn't match ${r.expected}. ${r.message}`); + } + return s.join("\n"); + } + }; + var shapeExtends = (a, b) => { + if (a === b) return true; + if (a == null || b == null || a.constructor !== b.constructor) return false; + if (a[EqualityTraitSymbol]) return equals(a, b); + if (isArray(a)) { + return every( + a, + (aitem) => some(b, (bitem) => shapeExtends(aitem, bitem)) + ); + } else if (isObject(a)) { + return every2( + a, + (aitem, akey) => shapeExtends(aitem, b[akey]) + ); + } + return false; + }; + var Schema = class { + /** + * @param {Schema} other + */ + extends(other) { + let [a, b] = [ + /** @type {any} */ + this.shape, + /** @type {any} */ + other.shape + ]; + if ( + /** @type {typeof Schema} */ + this.constructor._dilutes + ) [b, a] = [a, b]; + return shapeExtends(a, b); + } + /** + * Overwrite this when necessary. By default, we only check the `shape` property which every shape + * should have. + * @param {Schema} other + */ + equals(other) { + return this.constructor === other.constructor && equalityDeep(this.shape, other.shape); + } + [schemaSymbol]() { + return true; + } + /** + * @param {object} other + */ + [EqualityTraitSymbol](other) { + return this.equals( + /** @type {any} */ + other + ); + } + /** + * Use `schema.validate(obj)` with a typed parameter that is already of typed to be an instance of + * Schema. Validate will check the structure of the parameter and return true iff the instance + * really is an instance of Schema. + * + * @param {T} o + * @return {boolean} + */ + validate(o) { + return this.check(o); + } + /* c8 ignore start */ + /** + * Similar to validate, but this method accepts untyped parameters. + * + * @param {any} _o + * @param {ValidationError} [_err] + * @return {_o is T} + */ + check(_o, _err) { + methodUnimplemented(); + } + /* c8 ignore stop */ + /** + * @type {Schema} + */ + get nullable() { + return $union(this, $null); + } + /** + * @type {$Optional>} + */ + get optional() { + return new $Optional( + /** @type {Schema} */ + this + ); + } + /** + * Cast a variable to a specific type. Returns the casted value, or throws an exception otherwise. + * Use this if you know that the type is of a specific type and you just want to convince the type + * system. + * + * **Do not rely on these error messages!** + * Performs an assertion check only if not in a production environment. + * + * @template OO + * @param {OO} o + * @return {Extract extends never ? T : (OO extends Array ? T : Extract)} + */ + cast(o) { + assert(o, this); + return ( + /** @type {any} */ + o + ); + } + /** + * EXPECTO PATRONUM!! 🪄 + * This function protects against type errors. Though it may not work in the real world. + * + * "After all this time?" + * "Always." - Snape, talking about type safety + * + * Ensures that a variable is a a specific type. Returns the value, or throws an exception if the assertion check failed. + * Use this if you know that the type is of a specific type and you just want to convince the type + * system. + * + * Can be useful when defining lambdas: `s.lambda(s.$number, s.$void).expect((n) => n + 1)` + * + * **Do not rely on these error messages!** + * Performs an assertion check if not in a production environment. + * + * @param {T} o + * @return {o extends T ? T : never} + */ + expect(o) { + assert(o, this); + return o; + } + }; + // this.shape must not be defined on Schema. Otherwise typecheck on metatypes (e.g. $$object) won't work as expected anymore + /** + * If true, the more things are added to the shape the more objects this schema will accept (e.g. + * union). By default, the more objects are added, the the fewer objects this schema will accept. + * @protected + */ + __publicField(Schema, "_dilutes", false); + var $ConstructedBy = class extends Schema { + /** + * @param {C} c + * @param {((o:Instance)=>boolean)|null} check + */ + constructor(c, check) { + super(); + this.shape = c; + this._c = check; + } + /** + * @param {any} o + * @param {ValidationError} [err] + * @return {o is C extends ((...args:any[]) => infer T) ? T : (C extends (new (...args:any[]) => any) ? InstanceType : never)} o + */ + check(o, err = void 0) { + const c = o?.constructor === this.shape && (this._c == null || this._c(o)); + !c && err?.extend(null, this.shape.name, o?.constructor.name, o?.constructor !== this.shape ? "Constructor match failed" : "Check failed"); + return c; + } + }; + var $constructedBy = (c, check = null) => new $ConstructedBy(c, check); + var $$constructedBy = $constructedBy($ConstructedBy); + var $Custom = class extends Schema { + /** + * @param {(o:any) => boolean} check + */ + constructor(check) { + super(); + this.shape = check; + } + /** + * @param {any} o + * @param {ValidationError} err + * @return {o is any} + */ + check(o, err) { + const c = this.shape(o); + !c && err?.extend(null, "custom prop", o?.constructor.name, "failed to check custom prop"); + return c; + } + }; + var $custom = (check) => new $Custom(check); + var $$custom = $constructedBy($Custom); + var $Literal = class extends Schema { + /** + * @param {Array} literals + */ + constructor(literals) { + super(); + this.shape = literals; + } + /** + * + * @param {any} o + * @param {ValidationError} [err] + * @return {o is T} + */ + check(o, err) { + const c = this.shape.some((a) => a === o); + !c && err?.extend(null, this.shape.join(" | "), o.toString()); + return c; + } + }; + var $literal = (...literals) => new $Literal(literals); + var $$literal = $constructedBy($Literal); + var _regexEscape = ( + /** @type {any} */ + RegExp.escape || /** @type {(str:string) => string} */ + ((str) => str.replace(/[().|&,$^[\]]/g, (s) => "\\" + s)) + ); + var _schemaStringTemplateToRegex = (s) => { + if ($string.check(s)) { + return [_regexEscape(s)]; + } + if ($$literal.check(s)) { + return ( + /** @type {Array} */ + s.shape.map((v) => v + "") + ); + } + if ($$number.check(s)) { + return ["[+-]?\\d+.?\\d*"]; + } + if ($$string.check(s)) { + return [".*"]; + } + if ($$union.check(s)) { + return s.shape.map(_schemaStringTemplateToRegex).flat(1); + } + unexpectedCase(); + }; + var $StringTemplate = class extends Schema { + /** + * @param {T} shape + */ + constructor(shape) { + super(); + this.shape = shape; + this._r = new RegExp("^" + shape.map(_schemaStringTemplateToRegex).map((opts) => `(${opts.join("|")})`).join("") + "$"); + } + /** + * @param {any} o + * @param {ValidationError} [err] + * @return {o is CastStringTemplateArgsToTemplate} + */ + check(o, err) { + const c = this._r.exec(o) != null; + !c && err?.extend(null, this._r.toString(), o.toString(), "String doesn't match string template."); + return c; + } + }; + var $$stringTemplate = $constructedBy($StringTemplate); + var isOptionalSymbol = Symbol("optional"); + var $Optional = class extends Schema { + /** + * @param {S} shape + */ + constructor(shape) { + super(); + this.shape = shape; + } + /** + * @param {any} o + * @param {ValidationError} [err] + * @return {o is (Unwrap|undefined)} + */ + check(o, err) { + const c = o === void 0 || this.shape.check(o); + !c && err?.extend(null, "undefined (optional)", "()"); + return c; + } + get [isOptionalSymbol]() { + return true; + } + }; + var $$optional = $constructedBy($Optional); + var $Never = class extends Schema { + /** + * @param {any} _o + * @param {ValidationError} [err] + * @return {_o is never} + */ + check(_o, err) { + err?.extend(null, "never", typeof _o); + return false; + } + }; + var $never = new $Never(); + var $$never = $constructedBy($Never); + var _$Object = class _$Object extends Schema { + /** + * @param {S} shape + * @param {boolean} partial + */ + constructor(shape, partial = false) { + super(); + this.shape = shape; + this._isPartial = partial; + } + /** + * @type {Schema>>} + */ + get partial() { + return new _$Object(this.shape, true); + } + /** + * @param {any} o + * @param {ValidationError} err + * @return {o is $ObjectToType} + */ + check(o, err) { + if (o == null) { + err?.extend(null, "object", "null"); + return false; + } + return every2(this.shape, (vv, vk) => { + const c = this._isPartial && !hasProperty(o, vk) || vv.check(o[vk], err); + !c && err?.extend(vk.toString(), vv.toString(), typeof o[vk], "Object property does not match"); + return c; + }); + } + }; + __publicField(_$Object, "_dilutes", true); + var $Object = _$Object; + var $object = (def) => ( + /** @type {any} */ + new $Object(def) + ); + var $$object = $constructedBy($Object); + var $objectAny = $custom((o) => o != null && (o.constructor === Object || o.constructor == null)); + var $Record = class extends Schema { + /** + * @param {Keys} keys + * @param {Values} values + */ + constructor(keys3, values) { + super(); + this.shape = { + keys: keys3, + values + }; + } + /** + * @param {any} o + * @param {ValidationError} err + * @return {o is { [key in Unwrap]: Unwrap }} + */ + check(o, err) { + return o != null && every2(o, (vv, vk) => { + const ck = this.shape.keys.check(vk, err); + !ck && err?.extend(vk + "", "Record", typeof o, ck ? "Key doesn't match schema" : "Value doesn't match value"); + return ck && this.shape.values.check(vv, err); + }); + } + }; + var $record = (keys3, values) => new $Record(keys3, values); + var $$record = $constructedBy($Record); + var $Tuple = class extends Schema { + /** + * @param {S} shape + */ + constructor(shape) { + super(); + this.shape = shape; + } + /** + * @param {any} o + * @param {ValidationError} err + * @return {o is { [K in keyof S]: S[K] extends Schema ? Type : never }} + */ + check(o, err) { + return o != null && every2(this.shape, (vv, vk) => { + const c = ( + /** @type {Schema} */ + vv.check(o[vk], err) + ); + !c && err?.extend(vk.toString(), "Tuple", typeof vv); + return c; + }); + } + }; + var $tuple = (...def) => new $Tuple(def); + var $$tuple = $constructedBy($Tuple); + var $Array = class extends Schema { + /** + * @param {Array} v + */ + constructor(v) { + super(); + this.shape = v.length === 1 ? v[0] : new $Union(v); + } + /** + * @param {any} o + * @param {ValidationError} [err] + * @return {o is Array ? T : never>} o + */ + check(o, err) { + const c = isArray(o) && every(o, (oi) => this.shape.check(oi)); + !c && err?.extend(null, "Array", ""); + return c; + } + }; + var $array = (...def) => new $Array(def); + var $$array = $constructedBy($Array); + var $arrayAny = $custom((o) => isArray(o)); + var $InstanceOf = class extends Schema { + /** + * @param {new (...args:any) => T} constructor + * @param {((o:T) => boolean)|null} check + */ + constructor(constructor, check) { + super(); + this.shape = constructor; + this._c = check; + } + /** + * @param {any} o + * @param {ValidationError} err + * @return {o is T} + */ + check(o, err) { + const c = o instanceof this.shape && (this._c == null || this._c(o)); + !c && err?.extend(null, this.shape.name, o?.constructor.name); + return c; + } + }; + var $instanceOf = (c, check = null) => new $InstanceOf(c, check); + var $$instanceOf = $constructedBy($InstanceOf); + var $$schema = $instanceOf(Schema); + var $Lambda = class extends Schema { + /** + * @param {Args} args + */ + constructor(args2) { + super(); + this.len = args2.length - 1; + this.args = $tuple(...args2.slice(-1)); + this.res = args2[this.len]; + } + /** + * @param {any} f + * @param {ValidationError} err + * @return {f is _LArgsToLambdaDef} + */ + check(f, err) { + const c = f.constructor === Function && f.length <= this.len; + !c && err?.extend(null, "function", typeof f); + return c; + } + }; + var $$lambda = $constructedBy($Lambda); + var $function = $custom((o) => typeof o === "function"); + var $Intersection = class extends Schema { + /** + * @param {T} v + */ + constructor(v) { + super(); + this.shape = v; + } + /** + * @param {any} o + * @param {ValidationError} [err] + * @return {o is Intersect>} + */ + check(o, err) { + const c = every(this.shape, (check) => check.check(o, err)); + !c && err?.extend(null, "Intersectinon", typeof o); + return c; + } + }; + var $$intersect = $constructedBy($Intersection, (o) => o.shape.length > 0); + var $Union = class extends Schema { + /** + * @param {Array>} v + */ + constructor(v) { + super(); + this.shape = v; + } + /** + * @param {any} o + * @param {ValidationError} [err] + * @return {o is S} + */ + check(o, err) { + const c = some(this.shape, (vv) => vv.check(o, err)); + err?.extend(null, "Union", typeof o); + return c; + } + }; + __publicField($Union, "_dilutes", true); + var $union = (...schemas) => schemas.findIndex(($s) => $$union.check($s)) >= 0 ? $union(...schemas.map(($s) => $($s)).map(($s) => $$union.check($s) ? $s.shape : [$s]).flat(1)) : schemas.length === 1 ? schemas[0] : new $Union(schemas); + var $$union = ( + /** @type {Schema<$Union>} */ + $constructedBy($Union) + ); + var _t = () => true; + var $any = $custom(_t); + var $$any = ( + /** @type {Schema>} */ + $constructedBy($Custom, (o) => o.shape === _t) + ); + var $bigint = $custom((o) => typeof o === "bigint"); + var $$bigint = ( + /** @type {Schema>} */ + $custom((o) => o === $bigint) + ); + var $symbol = $custom((o) => typeof o === "symbol"); + var $$symbol = ( + /** @type {Schema>} */ + $custom((o) => o === $symbol) + ); + var $number = $custom((o) => typeof o === "number"); + var $$number = ( + /** @type {Schema>} */ + $custom((o) => o === $number) + ); + var $string = $custom((o) => typeof o === "string"); + var $$string = ( + /** @type {Schema>} */ + $custom((o) => o === $string) + ); + var $boolean = $custom((o) => typeof o === "boolean"); + var $$boolean = ( + /** @type {Schema>} */ + $custom((o) => o === $boolean) + ); + var $undefined = $literal(void 0); + var $$undefined = ( + /** @type {Schema>} */ + $constructedBy($Literal, (o) => o.shape.length === 1 && o.shape[0] === void 0) + ); + var $void = $literal(void 0); + var $null = $literal(null); + var $$null = ( + /** @type {Schema>} */ + $constructedBy($Literal, (o) => o.shape.length === 1 && o.shape[0] === null) + ); + var $uint8Array = $constructedBy(Uint8Array); + var $$uint8Array = ( + /** @type {Schema>} */ + $constructedBy($ConstructedBy, (o) => o.shape === Uint8Array) + ); + var $primitive = $union($number, $string, $null, $undefined, $bigint, $boolean, $symbol); + var $json = (() => { + const $jsonArr = ( + /** @type {$Array<$any>} */ + $array($any) + ); + const $jsonRecord = ( + /** @type {$Record<$string,$any>} */ + $record($string, $any) + ); + const $json2 = $union($number, $string, $null, $boolean, $jsonArr, $jsonRecord); + $jsonArr.shape = $json2; + $jsonRecord.shape.values = $json2; + return $json2; + })(); + var $ = (o) => { + if ($$schema.check(o)) { + return ( + /** @type {any} */ + o + ); + } else if ($objectAny.check(o)) { + const o2 = {}; + for (const k in o) { + o2[k] = $(o[k]); + } + return ( + /** @type {any} */ + $object(o2) + ); + } else if ($arrayAny.check(o)) { + return ( + /** @type {any} */ + $union(...o.map($)) + ); + } else if ($primitive.check(o)) { + return ( + /** @type {any} */ + $literal(o) + ); + } else if ($function.check(o)) { + return ( + /** @type {any} */ + $constructedBy( + /** @type {any} */ + o + ) + ); + } + unexpectedCase(); + }; + var assert = production ? () => { + } : (o, schema) => { + const err = new ValidationError(); + if (!schema.check(o, err)) { + throw create3(`Expected value to be of type ${schema.constructor.name}. +${err.toString()}`); + } + }; + var PatternMatcher = class { + /** + * @param {Schema} [$state] + */ + constructor($state) { + this.patterns = []; + this.$state = $state; + } + /** + * @template P + * @template R + * @param {P} pattern + * @param {(o:NoInfer>>,s:State)=>R} handler + * @return {PatternMatcher>,R>>} + */ + if(pattern, handler) { + this.patterns.push({ if: $(pattern), h: handler }); + return this; + } + /** + * @template R + * @param {(o:any,s:State)=>R} h + */ + else(h) { + return this.if($any, h); + } + /** + * @return {State extends undefined + * ? >(o:In,state?:undefined)=>PatternMatchResult + * : >(o:In,state:State)=>PatternMatchResult} + */ + done() { + return ( + /** @type {any} */ + (o, s) => { + for (let i = 0; i < this.patterns.length; i++) { + const p = this.patterns[i]; + if (p.if.check(o)) { + return p.h(o, s); + } + } + throw create3("Unhandled pattern"); + } + ); + } + }; + var match = (state) => new PatternMatcher( + /** @type {any} */ + state + ); + var _random = ( + /** @type {any} */ + match( + /** @type {Schema} */ + $any + ).if($$number, (_o, gen) => int53(gen, MIN_SAFE_INTEGER, MAX_SAFE_INTEGER)).if($$string, (_o, gen) => word(gen)).if($$boolean, (_o, gen) => bool(gen)).if($$bigint, (_o, gen) => BigInt(int53(gen, MIN_SAFE_INTEGER, MAX_SAFE_INTEGER))).if($$union, (o, gen) => random(gen, oneOf(gen, o.shape))).if($$object, (o, gen) => { + const res = {}; + for (const k in o.shape) { + let prop = o.shape[k]; + if ($$optional.check(prop)) { + if (bool(gen)) { + continue; + } + prop = prop.shape; + } + res[k] = _random(prop, gen); + } + return res; + }).if($$array, (o, gen) => { + const arr = []; + const n = int32(gen, 0, 42); + for (let i = 0; i < n; i++) { + arr.push(random(gen, o.shape)); + } + return arr; + }).if($$literal, (o, gen) => { + return oneOf(gen, o.shape); + }).if($$null, (o, gen) => { + return null; + }).if($$lambda, (o, gen) => { + const res = random(gen, o.res); + return () => res; + }).if($$any, (o, gen) => random(gen, oneOf(gen, [ + $number, + $string, + $null, + $undefined, + $bigint, + $boolean, + $array($number), + $record($union("a", "b", "c"), $number) + ]))).if($$record, (o, gen) => { + const res = {}; + const keysN = int53(gen, 0, 3); + for (let i = 0; i < keysN; i++) { + const key = random(gen, o.shape.keys); + const val = random(gen, o.shape.values); + res[key] = val; + } + return res; + }).done() + ); + var random = (gen, schema) => ( + /** @type {any} */ + _random($(schema), gen) + ); + + // node_modules/lib0/dom.js + var doc = ( + /** @type {Document} */ + typeof document !== "undefined" ? document : {} + ); + var $fragment = $custom((el) => el.nodeType === DOCUMENT_FRAGMENT_NODE); + var domParser = ( + /** @type {DOMParser} */ + typeof DOMParser !== "undefined" ? new DOMParser() : null + ); + var $element = $custom((el) => el.nodeType === ELEMENT_NODE); + var $text = $custom((el) => el.nodeType === TEXT_NODE); + var mapToStyleString = (m) => map(m, (value, key) => `${key}:${value};`).join(""); + var ELEMENT_NODE = doc.ELEMENT_NODE; + var TEXT_NODE = doc.TEXT_NODE; + var CDATA_SECTION_NODE = doc.CDATA_SECTION_NODE; + var COMMENT_NODE = doc.COMMENT_NODE; + var DOCUMENT_NODE = doc.DOCUMENT_NODE; + var DOCUMENT_TYPE_NODE = doc.DOCUMENT_TYPE_NODE; + var DOCUMENT_FRAGMENT_NODE = doc.DOCUMENT_FRAGMENT_NODE; + var $node = $custom((el) => el.nodeType === DOCUMENT_NODE); + + // node_modules/lib0/symbol.js + var create6 = Symbol; + + // node_modules/lib0/logging.common.js + var BOLD = create6(); + var UNBOLD = create6(); + var BLUE = create6(); + var GREY = create6(); + var GREEN = create6(); + var RED = create6(); + var PURPLE = create6(); + var ORANGE = create6(); + var UNCOLOR = create6(); + var computeNoColorLoggingArgs = (args2) => { + if (args2.length === 1 && args2[0]?.constructor === Function) { + args2 = /** @type {Array} */ + /** @type {[function]} */ + args2[0](); + } + const strBuilder = []; + const logArgs = []; + let i = 0; + for (; i < args2.length; i++) { + const arg = args2[i]; + if (arg === void 0) { + break; + } else if (arg.constructor === String || arg.constructor === Number) { + strBuilder.push(arg); + } else if (arg.constructor === Object) { + break; + } + } + if (i > 0) { + logArgs.push(strBuilder.join("")); + } + for (; i < args2.length; i++) { + const arg = args2[i]; + if (!(arg instanceof Symbol)) { + logArgs.push(arg); + } + } + return logArgs; + }; + var lastLoggingTime = getUnixTime(); + + // node_modules/lib0/logging.js + var _browserStyleMap = { + [BOLD]: create5("font-weight", "bold"), + [UNBOLD]: create5("font-weight", "normal"), + [BLUE]: create5("color", "blue"), + [GREEN]: create5("color", "green"), + [GREY]: create5("color", "grey"), + [RED]: create5("color", "red"), + [PURPLE]: create5("color", "purple"), + [ORANGE]: create5("color", "orange"), + // not well supported in chrome when debugging node with inspector - TODO: deprecate + [UNCOLOR]: create5("color", "black") + }; + var computeBrowserLoggingArgs = (args2) => { + if (args2.length === 1 && args2[0]?.constructor === Function) { + args2 = /** @type {Array} */ + /** @type {[function]} */ + args2[0](); + } + const strBuilder = []; + const styles = []; + const currentStyle = create(); + let logArgs = []; + let i = 0; + for (; i < args2.length; i++) { + const arg = args2[i]; + const style = _browserStyleMap[arg]; + if (style !== void 0) { + currentStyle.set(style.left, style.right); + } else { + if (arg === void 0) { + break; + } + if (arg.constructor === String || arg.constructor === Number) { + const style2 = mapToStyleString(currentStyle); + if (i > 0 || style2.length > 0) { + strBuilder.push("%c" + arg); + styles.push(style2); + } else { + strBuilder.push(arg); + } + } else { + break; + } + } + } + if (i > 0) { + logArgs = styles; + logArgs.unshift(strBuilder.join("")); + } + for (; i < args2.length; i++) { + const arg = args2[i]; + if (!(arg instanceof Symbol)) { + logArgs.push(arg); + } + } + return logArgs; + }; + var computeLoggingArgs = supportsColor ? computeBrowserLoggingArgs : computeNoColorLoggingArgs; + var print = (...args2) => { + console.log(...computeLoggingArgs(args2)); + vconsoles.forEach((vc) => vc.print(args2)); + }; + var warn = (...args2) => { + console.warn(...computeLoggingArgs(args2)); + args2.unshift(ORANGE); + vconsoles.forEach((vc) => vc.print(args2)); + }; + var vconsoles = create2(); + + // node_modules/lib0/iterator.js + var createIterator = (next) => ({ + /** + * @return {IterableIterator} + */ + [Symbol.iterator]() { + return this; + }, + // @ts-ignore + next + }); + var iteratorFilter = (iterator, filter) => createIterator(() => { + let res; + do { + res = iterator.next(); + } while (!res.done && !filter(res.value)); + return res; + }); + var iteratorMap = (iterator, fmap) => createIterator(() => { + const { done, value } = iterator.next(); + return { done, value: done ? void 0 : fmap(value) }; + }); + + // node_modules/yjs/dist/yjs.mjs + var DeleteItem = class { + /** + * @param {number} clock + * @param {number} len + */ + constructor(clock, len) { + this.clock = clock; + this.len = len; + } + }; + var DeleteSet = class { + constructor() { + this.clients = /* @__PURE__ */ new Map(); + } + }; + var iterateDeletedStructs = (transaction, ds, f) => ds.clients.forEach((deletes, clientid) => { + const structs = ( + /** @type {Array} */ + transaction.doc.store.clients.get(clientid) + ); + if (structs != null) { + const lastStruct = structs[structs.length - 1]; + const clockState = lastStruct.id.clock + lastStruct.length; + for (let i = 0, del = deletes[i]; i < deletes.length && del.clock < clockState; del = deletes[++i]) { + iterateStructs(transaction, structs, del.clock, del.len, f); + } + } + }); + var findIndexDS = (dis, clock) => { + let left = 0; + let right = dis.length - 1; + while (left <= right) { + const midindex = floor((left + right) / 2); + const mid = dis[midindex]; + const midclock = mid.clock; + if (midclock <= clock) { + if (clock < midclock + mid.len) { + return midindex; + } + left = midindex + 1; + } else { + right = midindex - 1; + } + } + return null; + }; + var isDeleted = (ds, id2) => { + const dis = ds.clients.get(id2.client); + return dis !== void 0 && findIndexDS(dis, id2.clock) !== null; + }; + var sortAndMergeDeleteSet = (ds) => { + ds.clients.forEach((dels) => { + dels.sort((a, b) => a.clock - b.clock); + let i, j; + for (i = 1, j = 1; i < dels.length; i++) { + const left = dels[j - 1]; + const right = dels[i]; + if (left.clock + left.len >= right.clock) { + dels[j - 1] = new DeleteItem(left.clock, max(left.len, right.clock + right.len - left.clock)); + } else { + if (j < i) { + dels[j] = right; + } + j++; + } + } + dels.length = j; + }); + }; + var mergeDeleteSets = (dss) => { + const merged = new DeleteSet(); + for (let dssI = 0; dssI < dss.length; dssI++) { + dss[dssI].clients.forEach((delsLeft, client) => { + if (!merged.clients.has(client)) { + const dels = delsLeft.slice(); + for (let i = dssI + 1; i < dss.length; i++) { + appendTo(dels, dss[i].clients.get(client) || []); + } + merged.clients.set(client, dels); + } + }); + } + sortAndMergeDeleteSet(merged); + return merged; + }; + var addToDeleteSet = (ds, client, clock, length3) => { + setIfUndefined(ds.clients, client, () => ( + /** @type {Array} */ + [] + )).push(new DeleteItem(clock, length3)); + }; + var createDeleteSet = () => new DeleteSet(); + var createDeleteSetFromStructStore = (ss) => { + const ds = createDeleteSet(); + ss.clients.forEach((structs, client) => { + const dsitems = []; + for (let i = 0; i < structs.length; i++) { + const struct = structs[i]; + if (struct.deleted) { + const clock = struct.id.clock; + let len = struct.length; + if (i + 1 < structs.length) { + for (let next = structs[i + 1]; i + 1 < structs.length && next.deleted; next = structs[++i + 1]) { + len += next.length; + } + } + dsitems.push(new DeleteItem(clock, len)); + } + } + if (dsitems.length > 0) { + ds.clients.set(client, dsitems); + } + }); + return ds; + }; + var writeDeleteSet = (encoder, ds) => { + writeVarUint(encoder.restEncoder, ds.clients.size); + from(ds.clients.entries()).sort((a, b) => b[0] - a[0]).forEach(([client, dsitems]) => { + encoder.resetDsCurVal(); + writeVarUint(encoder.restEncoder, client); + const len = dsitems.length; + writeVarUint(encoder.restEncoder, len); + for (let i = 0; i < len; i++) { + const item = dsitems[i]; + encoder.writeDsClock(item.clock); + encoder.writeDsLen(item.len); + } + }); + }; + var readDeleteSet = (decoder) => { + const ds = new DeleteSet(); + const numClients = readVarUint(decoder.restDecoder); + for (let i = 0; i < numClients; i++) { + decoder.resetDsCurVal(); + const client = readVarUint(decoder.restDecoder); + const numberOfDeletes = readVarUint(decoder.restDecoder); + if (numberOfDeletes > 0) { + const dsField = setIfUndefined(ds.clients, client, () => ( + /** @type {Array} */ + [] + )); + for (let i2 = 0; i2 < numberOfDeletes; i2++) { + dsField.push(new DeleteItem(decoder.readDsClock(), decoder.readDsLen())); + } + } + } + return ds; + }; + var readAndApplyDeleteSet = (decoder, transaction, store) => { + const unappliedDS = new DeleteSet(); + const numClients = readVarUint(decoder.restDecoder); + for (let i = 0; i < numClients; i++) { + decoder.resetDsCurVal(); + const client = readVarUint(decoder.restDecoder); + const numberOfDeletes = readVarUint(decoder.restDecoder); + const structs = store.clients.get(client) || []; + const state = getState(store, client); + for (let i2 = 0; i2 < numberOfDeletes; i2++) { + const clock = decoder.readDsClock(); + const clockEnd = clock + decoder.readDsLen(); + if (clock < state) { + if (state < clockEnd) { + addToDeleteSet(unappliedDS, client, state, clockEnd - state); + } + let index = findIndexSS(structs, clock); + let struct = structs[index]; + if (!struct.deleted && struct.id.clock < clock) { + structs.splice(index + 1, 0, splitItem(transaction, struct, clock - struct.id.clock)); + index++; + } + while (index < structs.length) { + struct = structs[index++]; + if (struct.id.clock < clockEnd) { + if (!struct.deleted) { + if (clockEnd < struct.id.clock + struct.length) { + structs.splice(index, 0, splitItem(transaction, struct, clockEnd - struct.id.clock)); + } + struct.delete(transaction); + } + } else { + break; + } + } + } else { + addToDeleteSet(unappliedDS, client, clock, clockEnd - clock); + } + } + } + if (unappliedDS.clients.size > 0) { + const ds = new UpdateEncoderV2(); + writeVarUint(ds.restEncoder, 0); + writeDeleteSet(ds, unappliedDS); + return ds.toUint8Array(); + } + return null; + }; + var generateNewClientId = uint32; + var Doc = class _Doc extends ObservableV2 { + /** + * @param {DocOpts} opts configuration + */ + constructor({ guid = uuidv4(), collectionid = null, gc = true, gcFilter = () => true, meta = null, autoLoad = false, shouldLoad = true } = {}) { + super(); + this.gc = gc; + this.gcFilter = gcFilter; + this.clientID = generateNewClientId(); + this.guid = guid; + this.collectionid = collectionid; + this.share = /* @__PURE__ */ new Map(); + this.store = new StructStore(); + this._transaction = null; + this._transactionCleanups = []; + this.subdocs = /* @__PURE__ */ new Set(); + this._item = null; + this.shouldLoad = shouldLoad; + this.autoLoad = autoLoad; + this.meta = meta; + this.isLoaded = false; + this.isSynced = false; + this.isDestroyed = false; + this.whenLoaded = create4((resolve) => { + this.on("load", () => { + this.isLoaded = true; + resolve(this); + }); + }); + const provideSyncedPromise = () => create4((resolve) => { + const eventHandler = (isSynced) => { + if (isSynced === void 0 || isSynced === true) { + this.off("sync", eventHandler); + resolve(); + } + }; + this.on("sync", eventHandler); + }); + this.on("sync", (isSynced) => { + if (isSynced === false && this.isSynced) { + this.whenSynced = provideSyncedPromise(); + } + this.isSynced = isSynced === void 0 || isSynced === true; + if (this.isSynced && !this.isLoaded) { + this.emit("load", [this]); + } + }); + this.whenSynced = provideSyncedPromise(); + } + /** + * Notify the parent document that you request to load data into this subdocument (if it is a subdocument). + * + * `load()` might be used in the future to request any provider to load the most current data. + * + * It is safe to call `load()` multiple times. + */ + load() { + const item = this._item; + if (item !== null && !this.shouldLoad) { + transact( + /** @type {any} */ + item.parent.doc, + (transaction) => { + transaction.subdocsLoaded.add(this); + }, + null, + true + ); + } + this.shouldLoad = true; + } + getSubdocs() { + return this.subdocs; + } + getSubdocGuids() { + return new Set(from(this.subdocs).map((doc2) => doc2.guid)); + } + /** + * Changes that happen inside of a transaction are bundled. This means that + * the observer fires _after_ the transaction is finished and that all changes + * that happened inside of the transaction are sent as one message to the + * other peers. + * + * @template T + * @param {function(Transaction):T} f The function that should be executed as a transaction + * @param {any} [origin] Origin of who started the transaction. Will be stored on transaction.origin + * @return T + * + * @public + */ + transact(f, origin = null) { + return transact(this, f, origin); + } + /** + * Define a shared data type. + * + * Multiple calls of `ydoc.get(name, TypeConstructor)` yield the same result + * and do not overwrite each other. I.e. + * `ydoc.get(name, Y.Array) === ydoc.get(name, Y.Array)` + * + * After this method is called, the type is also available on `ydoc.share.get(name)`. + * + * *Best Practices:* + * Define all types right after the Y.Doc instance is created and store them in a separate object. + * Also use the typed methods `getText(name)`, `getArray(name)`, .. + * + * @template {typeof AbstractType} Type + * @example + * const ydoc = new Y.Doc(..) + * const appState = { + * document: ydoc.getText('document') + * comments: ydoc.getArray('comments') + * } + * + * @param {string} name + * @param {Type} TypeConstructor The constructor of the type definition. E.g. Y.Text, Y.Array, Y.Map, ... + * @return {InstanceType} The created type. Constructed with TypeConstructor + * + * @public + */ + get(name, TypeConstructor = ( + /** @type {any} */ + AbstractType + )) { + const type = setIfUndefined(this.share, name, () => { + const t = new TypeConstructor(); + t._integrate(this, null); + return t; + }); + const Constr = type.constructor; + if (TypeConstructor !== AbstractType && Constr !== TypeConstructor) { + if (Constr === AbstractType) { + const t = new TypeConstructor(); + t._map = type._map; + type._map.forEach( + /** @param {Item?} n */ + (n) => { + for (; n !== null; n = n.left) { + n.parent = t; + } + } + ); + t._start = type._start; + for (let n = t._start; n !== null; n = n.right) { + n.parent = t; + } + t._length = type._length; + this.share.set(name, t); + t._integrate(this, null); + return ( + /** @type {InstanceType} */ + t + ); + } else { + throw new Error(`Type with the name ${name} has already been defined with a different constructor`); + } + } + return ( + /** @type {InstanceType} */ + type + ); + } + /** + * @template T + * @param {string} [name] + * @return {YArray} + * + * @public + */ + getArray(name = "") { + return ( + /** @type {YArray} */ + this.get(name, YArray) + ); + } + /** + * @param {string} [name] + * @return {YText} + * + * @public + */ + getText(name = "") { + return this.get(name, YText); + } + /** + * @template T + * @param {string} [name] + * @return {YMap} + * + * @public + */ + getMap(name = "") { + return ( + /** @type {YMap} */ + this.get(name, YMap) + ); + } + /** + * @param {string} [name] + * @return {YXmlElement} + * + * @public + */ + getXmlElement(name = "") { + return ( + /** @type {YXmlElement<{[key:string]:string}>} */ + this.get(name, YXmlElement) + ); + } + /** + * @param {string} [name] + * @return {YXmlFragment} + * + * @public + */ + getXmlFragment(name = "") { + return this.get(name, YXmlFragment); + } + /** + * Converts the entire document into a js object, recursively traversing each yjs type + * Doesn't log types that have not been defined (using ydoc.getType(..)). + * + * @deprecated Do not use this method and rather call toJSON directly on the shared types. + * + * @return {Object} + */ + toJSON() { + const doc2 = {}; + this.share.forEach((value, key) => { + doc2[key] = value.toJSON(); + }); + return doc2; + } + /** + * Emit `destroy` event and unregister all event handlers. + */ + destroy() { + this.isDestroyed = true; + from(this.subdocs).forEach((subdoc) => subdoc.destroy()); + const item = this._item; + if (item !== null) { + this._item = null; + const content = ( + /** @type {ContentDoc} */ + item.content + ); + content.doc = new _Doc({ guid: this.guid, ...content.opts, shouldLoad: false }); + content.doc._item = item; + transact( + /** @type {any} */ + item.parent.doc, + (transaction) => { + const doc2 = content.doc; + if (!item.deleted) { + transaction.subdocsAdded.add(doc2); + } + transaction.subdocsRemoved.add(this); + }, + null, + true + ); + } + this.emit("destroyed", [true]); + this.emit("destroy", [this]); + super.destroy(); + } + }; + var DSDecoderV1 = class { + /** + * @param {decoding.Decoder} decoder + */ + constructor(decoder) { + this.restDecoder = decoder; + } + resetDsCurVal() { + } + /** + * @return {number} + */ + readDsClock() { + return readVarUint(this.restDecoder); + } + /** + * @return {number} + */ + readDsLen() { + return readVarUint(this.restDecoder); + } + }; + var UpdateDecoderV1 = class extends DSDecoderV1 { + /** + * @return {ID} + */ + readLeftID() { + return createID(readVarUint(this.restDecoder), readVarUint(this.restDecoder)); + } + /** + * @return {ID} + */ + readRightID() { + return createID(readVarUint(this.restDecoder), readVarUint(this.restDecoder)); + } + /** + * Read the next client id. + * Use this in favor of readID whenever possible to reduce the number of objects created. + */ + readClient() { + return readVarUint(this.restDecoder); + } + /** + * @return {number} info An unsigned 8-bit integer + */ + readInfo() { + return readUint8(this.restDecoder); + } + /** + * @return {string} + */ + readString() { + return readVarString(this.restDecoder); + } + /** + * @return {boolean} isKey + */ + readParentInfo() { + return readVarUint(this.restDecoder) === 1; + } + /** + * @return {number} info An unsigned 8-bit integer + */ + readTypeRef() { + return readVarUint(this.restDecoder); + } + /** + * Write len of a struct - well suited for Opt RLE encoder. + * + * @return {number} len + */ + readLen() { + return readVarUint(this.restDecoder); + } + /** + * @return {any} + */ + readAny() { + return readAny(this.restDecoder); + } + /** + * @return {Uint8Array} + */ + readBuf() { + return copyUint8Array(readVarUint8Array(this.restDecoder)); + } + /** + * Legacy implementation uses JSON parse. We use any-decoding in v2. + * + * @return {any} + */ + readJSON() { + return JSON.parse(readVarString(this.restDecoder)); + } + /** + * @return {string} + */ + readKey() { + return readVarString(this.restDecoder); + } + }; + var DSDecoderV2 = class { + /** + * @param {decoding.Decoder} decoder + */ + constructor(decoder) { + this.dsCurrVal = 0; + this.restDecoder = decoder; + } + resetDsCurVal() { + this.dsCurrVal = 0; + } + /** + * @return {number} + */ + readDsClock() { + this.dsCurrVal += readVarUint(this.restDecoder); + return this.dsCurrVal; + } + /** + * @return {number} + */ + readDsLen() { + const diff = readVarUint(this.restDecoder) + 1; + this.dsCurrVal += diff; + return diff; + } + }; + var UpdateDecoderV2 = class extends DSDecoderV2 { + /** + * @param {decoding.Decoder} decoder + */ + constructor(decoder) { + super(decoder); + this.keys = []; + readVarUint(decoder); + this.keyClockDecoder = new IntDiffOptRleDecoder(readVarUint8Array(decoder)); + this.clientDecoder = new UintOptRleDecoder(readVarUint8Array(decoder)); + this.leftClockDecoder = new IntDiffOptRleDecoder(readVarUint8Array(decoder)); + this.rightClockDecoder = new IntDiffOptRleDecoder(readVarUint8Array(decoder)); + this.infoDecoder = new RleDecoder(readVarUint8Array(decoder), readUint8); + this.stringDecoder = new StringDecoder(readVarUint8Array(decoder)); + this.parentInfoDecoder = new RleDecoder(readVarUint8Array(decoder), readUint8); + this.typeRefDecoder = new UintOptRleDecoder(readVarUint8Array(decoder)); + this.lenDecoder = new UintOptRleDecoder(readVarUint8Array(decoder)); + } + /** + * @return {ID} + */ + readLeftID() { + return new ID(this.clientDecoder.read(), this.leftClockDecoder.read()); + } + /** + * @return {ID} + */ + readRightID() { + return new ID(this.clientDecoder.read(), this.rightClockDecoder.read()); + } + /** + * Read the next client id. + * Use this in favor of readID whenever possible to reduce the number of objects created. + */ + readClient() { + return this.clientDecoder.read(); + } + /** + * @return {number} info An unsigned 8-bit integer + */ + readInfo() { + return ( + /** @type {number} */ + this.infoDecoder.read() + ); + } + /** + * @return {string} + */ + readString() { + return this.stringDecoder.read(); + } + /** + * @return {boolean} + */ + readParentInfo() { + return this.parentInfoDecoder.read() === 1; + } + /** + * @return {number} An unsigned 8-bit integer + */ + readTypeRef() { + return this.typeRefDecoder.read(); + } + /** + * Write len of a struct - well suited for Opt RLE encoder. + * + * @return {number} + */ + readLen() { + return this.lenDecoder.read(); + } + /** + * @return {any} + */ + readAny() { + return readAny(this.restDecoder); + } + /** + * @return {Uint8Array} + */ + readBuf() { + return readVarUint8Array(this.restDecoder); + } + /** + * This is mainly here for legacy purposes. + * + * Initial we incoded objects using JSON. Now we use the much faster lib0/any-encoder. This method mainly exists for legacy purposes for the v1 encoder. + * + * @return {any} + */ + readJSON() { + return readAny(this.restDecoder); + } + /** + * @return {string} + */ + readKey() { + const keyClock = this.keyClockDecoder.read(); + if (keyClock < this.keys.length) { + return this.keys[keyClock]; + } else { + const key = this.stringDecoder.read(); + this.keys.push(key); + return key; + } + } + }; + var DSEncoderV1 = class { + constructor() { + this.restEncoder = createEncoder(); + } + toUint8Array() { + return toUint8Array(this.restEncoder); + } + resetDsCurVal() { + } + /** + * @param {number} clock + */ + writeDsClock(clock) { + writeVarUint(this.restEncoder, clock); + } + /** + * @param {number} len + */ + writeDsLen(len) { + writeVarUint(this.restEncoder, len); + } + }; + var UpdateEncoderV1 = class extends DSEncoderV1 { + /** + * @param {ID} id + */ + writeLeftID(id2) { + writeVarUint(this.restEncoder, id2.client); + writeVarUint(this.restEncoder, id2.clock); + } + /** + * @param {ID} id + */ + writeRightID(id2) { + writeVarUint(this.restEncoder, id2.client); + writeVarUint(this.restEncoder, id2.clock); + } + /** + * Use writeClient and writeClock instead of writeID if possible. + * @param {number} client + */ + writeClient(client) { + writeVarUint(this.restEncoder, client); + } + /** + * @param {number} info An unsigned 8-bit integer + */ + writeInfo(info) { + writeUint8(this.restEncoder, info); + } + /** + * @param {string} s + */ + writeString(s) { + writeVarString(this.restEncoder, s); + } + /** + * @param {boolean} isYKey + */ + writeParentInfo(isYKey) { + writeVarUint(this.restEncoder, isYKey ? 1 : 0); + } + /** + * @param {number} info An unsigned 8-bit integer + */ + writeTypeRef(info) { + writeVarUint(this.restEncoder, info); + } + /** + * Write len of a struct - well suited for Opt RLE encoder. + * + * @param {number} len + */ + writeLen(len) { + writeVarUint(this.restEncoder, len); + } + /** + * @param {any} any + */ + writeAny(any2) { + writeAny(this.restEncoder, any2); + } + /** + * @param {Uint8Array} buf + */ + writeBuf(buf) { + writeVarUint8Array(this.restEncoder, buf); + } + /** + * @param {any} embed + */ + writeJSON(embed) { + writeVarString(this.restEncoder, JSON.stringify(embed)); + } + /** + * @param {string} key + */ + writeKey(key) { + writeVarString(this.restEncoder, key); + } + }; + var DSEncoderV2 = class { + constructor() { + this.restEncoder = createEncoder(); + this.dsCurrVal = 0; + } + toUint8Array() { + return toUint8Array(this.restEncoder); + } + resetDsCurVal() { + this.dsCurrVal = 0; + } + /** + * @param {number} clock + */ + writeDsClock(clock) { + const diff = clock - this.dsCurrVal; + this.dsCurrVal = clock; + writeVarUint(this.restEncoder, diff); + } + /** + * @param {number} len + */ + writeDsLen(len) { + if (len === 0) { + unexpectedCase(); + } + writeVarUint(this.restEncoder, len - 1); + this.dsCurrVal += len; + } + }; + var UpdateEncoderV2 = class extends DSEncoderV2 { + constructor() { + super(); + this.keyMap = /* @__PURE__ */ new Map(); + this.keyClock = 0; + this.keyClockEncoder = new IntDiffOptRleEncoder(); + this.clientEncoder = new UintOptRleEncoder(); + this.leftClockEncoder = new IntDiffOptRleEncoder(); + this.rightClockEncoder = new IntDiffOptRleEncoder(); + this.infoEncoder = new RleEncoder(writeUint8); + this.stringEncoder = new StringEncoder(); + this.parentInfoEncoder = new RleEncoder(writeUint8); + this.typeRefEncoder = new UintOptRleEncoder(); + this.lenEncoder = new UintOptRleEncoder(); + } + toUint8Array() { + const encoder = createEncoder(); + writeVarUint(encoder, 0); + writeVarUint8Array(encoder, this.keyClockEncoder.toUint8Array()); + writeVarUint8Array(encoder, this.clientEncoder.toUint8Array()); + writeVarUint8Array(encoder, this.leftClockEncoder.toUint8Array()); + writeVarUint8Array(encoder, this.rightClockEncoder.toUint8Array()); + writeVarUint8Array(encoder, toUint8Array(this.infoEncoder)); + writeVarUint8Array(encoder, this.stringEncoder.toUint8Array()); + writeVarUint8Array(encoder, toUint8Array(this.parentInfoEncoder)); + writeVarUint8Array(encoder, this.typeRefEncoder.toUint8Array()); + writeVarUint8Array(encoder, this.lenEncoder.toUint8Array()); + writeUint8Array(encoder, toUint8Array(this.restEncoder)); + return toUint8Array(encoder); + } + /** + * @param {ID} id + */ + writeLeftID(id2) { + this.clientEncoder.write(id2.client); + this.leftClockEncoder.write(id2.clock); + } + /** + * @param {ID} id + */ + writeRightID(id2) { + this.clientEncoder.write(id2.client); + this.rightClockEncoder.write(id2.clock); + } + /** + * @param {number} client + */ + writeClient(client) { + this.clientEncoder.write(client); + } + /** + * @param {number} info An unsigned 8-bit integer + */ + writeInfo(info) { + this.infoEncoder.write(info); + } + /** + * @param {string} s + */ + writeString(s) { + this.stringEncoder.write(s); + } + /** + * @param {boolean} isYKey + */ + writeParentInfo(isYKey) { + this.parentInfoEncoder.write(isYKey ? 1 : 0); + } + /** + * @param {number} info An unsigned 8-bit integer + */ + writeTypeRef(info) { + this.typeRefEncoder.write(info); + } + /** + * Write len of a struct - well suited for Opt RLE encoder. + * + * @param {number} len + */ + writeLen(len) { + this.lenEncoder.write(len); + } + /** + * @param {any} any + */ + writeAny(any2) { + writeAny(this.restEncoder, any2); + } + /** + * @param {Uint8Array} buf + */ + writeBuf(buf) { + writeVarUint8Array(this.restEncoder, buf); + } + /** + * This is mainly here for legacy purposes. + * + * Initial we incoded objects using JSON. Now we use the much faster lib0/any-encoder. This method mainly exists for legacy purposes for the v1 encoder. + * + * @param {any} embed + */ + writeJSON(embed) { + writeAny(this.restEncoder, embed); + } + /** + * Property keys are often reused. For example, in y-prosemirror the key `bold` might + * occur very often. For a 3d application, the key `position` might occur very often. + * + * We cache these keys in a Map and refer to them via a unique number. + * + * @param {string} key + */ + writeKey(key) { + const clock = this.keyMap.get(key); + if (clock === void 0) { + this.keyClockEncoder.write(this.keyClock++); + this.stringEncoder.write(key); + } else { + this.keyClockEncoder.write(clock); + } + } + }; + var writeStructs = (encoder, structs, client, clock) => { + clock = max(clock, structs[0].id.clock); + const startNewStructs = findIndexSS(structs, clock); + writeVarUint(encoder.restEncoder, structs.length - startNewStructs); + encoder.writeClient(client); + writeVarUint(encoder.restEncoder, clock); + const firstStruct = structs[startNewStructs]; + firstStruct.write(encoder, clock - firstStruct.id.clock); + for (let i = startNewStructs + 1; i < structs.length; i++) { + structs[i].write(encoder, 0); + } + }; + var writeClientsStructs = (encoder, store, _sm) => { + const sm = /* @__PURE__ */ new Map(); + _sm.forEach((clock, client) => { + if (getState(store, client) > clock) { + sm.set(client, clock); + } + }); + getStateVector(store).forEach((_clock, client) => { + if (!_sm.has(client)) { + sm.set(client, 0); + } + }); + writeVarUint(encoder.restEncoder, sm.size); + from(sm.entries()).sort((a, b) => b[0] - a[0]).forEach(([client, clock]) => { + writeStructs( + encoder, + /** @type {Array} */ + store.clients.get(client), + client, + clock + ); + }); + }; + var readClientsStructRefs = (decoder, doc2) => { + const clientRefs = create(); + const numOfStateUpdates = readVarUint(decoder.restDecoder); + for (let i = 0; i < numOfStateUpdates; i++) { + const numberOfStructs = readVarUint(decoder.restDecoder); + const refs = new Array(numberOfStructs); + const client = decoder.readClient(); + let clock = readVarUint(decoder.restDecoder); + clientRefs.set(client, { i: 0, refs }); + for (let i2 = 0; i2 < numberOfStructs; i2++) { + const info = decoder.readInfo(); + switch (BITS5 & info) { + case 0: { + const len = decoder.readLen(); + refs[i2] = new GC(createID(client, clock), len); + clock += len; + break; + } + case 10: { + const len = readVarUint(decoder.restDecoder); + refs[i2] = new Skip(createID(client, clock), len); + clock += len; + break; + } + default: { + const cantCopyParentInfo = (info & (BIT7 | BIT8)) === 0; + const struct = new Item( + createID(client, clock), + null, + // left + (info & BIT8) === BIT8 ? decoder.readLeftID() : null, + // origin + null, + // right + (info & BIT7) === BIT7 ? decoder.readRightID() : null, + // right origin + cantCopyParentInfo ? decoder.readParentInfo() ? doc2.get(decoder.readString()) : decoder.readLeftID() : null, + // parent + cantCopyParentInfo && (info & BIT6) === BIT6 ? decoder.readString() : null, + // parentSub + readItemContent(decoder, info) + // item content + ); + refs[i2] = struct; + clock += struct.length; + } + } + } + } + return clientRefs; + }; + var integrateStructs = (transaction, store, clientsStructRefs) => { + const stack = []; + let clientsStructRefsIds = from(clientsStructRefs.keys()).sort((a, b) => a - b); + if (clientsStructRefsIds.length === 0) { + return null; + } + const getNextStructTarget = () => { + if (clientsStructRefsIds.length === 0) { + return null; + } + let nextStructsTarget = ( + /** @type {{i:number,refs:Array}} */ + clientsStructRefs.get(clientsStructRefsIds[clientsStructRefsIds.length - 1]) + ); + while (nextStructsTarget.refs.length === nextStructsTarget.i) { + clientsStructRefsIds.pop(); + if (clientsStructRefsIds.length > 0) { + nextStructsTarget = /** @type {{i:number,refs:Array}} */ + clientsStructRefs.get(clientsStructRefsIds[clientsStructRefsIds.length - 1]); + } else { + return null; + } + } + return nextStructsTarget; + }; + let curStructsTarget = getNextStructTarget(); + if (curStructsTarget === null) { + return null; + } + const restStructs = new StructStore(); + const missingSV = /* @__PURE__ */ new Map(); + const updateMissingSv = (client, clock) => { + const mclock = missingSV.get(client); + if (mclock == null || mclock > clock) { + missingSV.set(client, clock); + } + }; + let stackHead = ( + /** @type {any} */ + curStructsTarget.refs[ + /** @type {any} */ + curStructsTarget.i++ + ] + ); + const state = /* @__PURE__ */ new Map(); + const addStackToRestSS = () => { + for (const item of stack) { + const client = item.id.client; + const inapplicableItems = clientsStructRefs.get(client); + if (inapplicableItems) { + inapplicableItems.i--; + restStructs.clients.set(client, inapplicableItems.refs.slice(inapplicableItems.i)); + clientsStructRefs.delete(client); + inapplicableItems.i = 0; + inapplicableItems.refs = []; + } else { + restStructs.clients.set(client, [item]); + } + clientsStructRefsIds = clientsStructRefsIds.filter((c) => c !== client); + } + stack.length = 0; + }; + while (true) { + if (stackHead.constructor !== Skip) { + const localClock = setIfUndefined(state, stackHead.id.client, () => getState(store, stackHead.id.client)); + const offset = localClock - stackHead.id.clock; + if (offset < 0) { + stack.push(stackHead); + updateMissingSv(stackHead.id.client, stackHead.id.clock - 1); + addStackToRestSS(); + } else { + const missing = stackHead.getMissing(transaction, store); + if (missing !== null) { + stack.push(stackHead); + const structRefs = clientsStructRefs.get( + /** @type {number} */ + missing + ) || { refs: [], i: 0 }; + if (structRefs.refs.length === structRefs.i) { + updateMissingSv( + /** @type {number} */ + missing, + getState(store, missing) + ); + addStackToRestSS(); + } else { + stackHead = structRefs.refs[structRefs.i++]; + continue; + } + } else if (offset === 0 || offset < stackHead.length) { + stackHead.integrate(transaction, offset); + state.set(stackHead.id.client, stackHead.id.clock + stackHead.length); + } + } + } + if (stack.length > 0) { + stackHead = /** @type {GC|Item} */ + stack.pop(); + } else if (curStructsTarget !== null && curStructsTarget.i < curStructsTarget.refs.length) { + stackHead = /** @type {GC|Item} */ + curStructsTarget.refs[curStructsTarget.i++]; + } else { + curStructsTarget = getNextStructTarget(); + if (curStructsTarget === null) { + break; + } else { + stackHead = /** @type {GC|Item} */ + curStructsTarget.refs[curStructsTarget.i++]; + } + } + } + if (restStructs.clients.size > 0) { + const encoder = new UpdateEncoderV2(); + writeClientsStructs(encoder, restStructs, /* @__PURE__ */ new Map()); + writeVarUint(encoder.restEncoder, 0); + return { missing: missingSV, update: encoder.toUint8Array() }; + } + return null; + }; + var writeStructsFromTransaction = (encoder, transaction) => writeClientsStructs(encoder, transaction.doc.store, transaction.beforeState); + var readUpdateV2 = (decoder, ydoc, transactionOrigin, structDecoder = new UpdateDecoderV2(decoder)) => transact(ydoc, (transaction) => { + transaction.local = false; + let retry2 = false; + const doc2 = transaction.doc; + const store = doc2.store; + const ss = readClientsStructRefs(structDecoder, doc2); + const restStructs = integrateStructs(transaction, store, ss); + const pending = store.pendingStructs; + if (pending) { + for (const [client, clock] of pending.missing) { + if (clock < getState(store, client)) { + retry2 = true; + break; + } + } + if (restStructs) { + for (const [client, clock] of restStructs.missing) { + const mclock = pending.missing.get(client); + if (mclock == null || mclock > clock) { + pending.missing.set(client, clock); + } + } + pending.update = mergeUpdatesV2([pending.update, restStructs.update]); + } + } else { + store.pendingStructs = restStructs; + } + const dsRest = readAndApplyDeleteSet(structDecoder, transaction, store); + if (store.pendingDs) { + const pendingDSUpdate = new UpdateDecoderV2(createDecoder(store.pendingDs)); + readVarUint(pendingDSUpdate.restDecoder); + const dsRest2 = readAndApplyDeleteSet(pendingDSUpdate, transaction, store); + if (dsRest && dsRest2) { + store.pendingDs = mergeUpdatesV2([dsRest, dsRest2]); + } else { + store.pendingDs = dsRest || dsRest2; + } + } else { + store.pendingDs = dsRest; + } + if (retry2) { + const update = ( + /** @type {{update: Uint8Array}} */ + store.pendingStructs.update + ); + store.pendingStructs = null; + applyUpdateV2(transaction.doc, update); + } + }, transactionOrigin, false); + var applyUpdateV2 = (ydoc, update, transactionOrigin, YDecoder = UpdateDecoderV2) => { + const decoder = createDecoder(update); + readUpdateV2(decoder, ydoc, transactionOrigin, new YDecoder(decoder)); + }; + var applyUpdate = (ydoc, update, transactionOrigin) => applyUpdateV2(ydoc, update, transactionOrigin, UpdateDecoderV1); + var writeStateAsUpdate = (encoder, doc2, targetStateVector = /* @__PURE__ */ new Map()) => { + writeClientsStructs(encoder, doc2.store, targetStateVector); + writeDeleteSet(encoder, createDeleteSetFromStructStore(doc2.store)); + }; + var encodeStateAsUpdateV2 = (doc2, encodedTargetStateVector = new Uint8Array([0]), encoder = new UpdateEncoderV2()) => { + const targetStateVector = decodeStateVector(encodedTargetStateVector); + writeStateAsUpdate(encoder, doc2, targetStateVector); + const updates = [encoder.toUint8Array()]; + if (doc2.store.pendingDs) { + updates.push(doc2.store.pendingDs); + } + if (doc2.store.pendingStructs) { + updates.push(diffUpdateV2(doc2.store.pendingStructs.update, encodedTargetStateVector)); + } + if (updates.length > 1) { + if (encoder.constructor === UpdateEncoderV1) { + return mergeUpdates(updates.map((update, i) => i === 0 ? update : convertUpdateFormatV2ToV1(update))); + } else if (encoder.constructor === UpdateEncoderV2) { + return mergeUpdatesV2(updates); + } + } + return updates[0]; + }; + var encodeStateAsUpdate = (doc2, encodedTargetStateVector) => encodeStateAsUpdateV2(doc2, encodedTargetStateVector, new UpdateEncoderV1()); + var readStateVector = (decoder) => { + const ss = /* @__PURE__ */ new Map(); + const ssLength = readVarUint(decoder.restDecoder); + for (let i = 0; i < ssLength; i++) { + const client = readVarUint(decoder.restDecoder); + const clock = readVarUint(decoder.restDecoder); + ss.set(client, clock); + } + return ss; + }; + var decodeStateVector = (decodedState) => readStateVector(new DSDecoderV1(createDecoder(decodedState))); + var writeStateVector = (encoder, sv) => { + writeVarUint(encoder.restEncoder, sv.size); + from(sv.entries()).sort((a, b) => b[0] - a[0]).forEach(([client, clock]) => { + writeVarUint(encoder.restEncoder, client); + writeVarUint(encoder.restEncoder, clock); + }); + return encoder; + }; + var writeDocumentStateVector = (encoder, doc2) => writeStateVector(encoder, getStateVector(doc2.store)); + var encodeStateVectorV2 = (doc2, encoder = new DSEncoderV2()) => { + if (doc2 instanceof Map) { + writeStateVector(encoder, doc2); + } else { + writeDocumentStateVector(encoder, doc2); + } + return encoder.toUint8Array(); + }; + var encodeStateVector = (doc2) => encodeStateVectorV2(doc2, new DSEncoderV1()); + var EventHandler = class { + constructor() { + this.l = []; + } + }; + var createEventHandler = () => new EventHandler(); + var addEventHandlerListener = (eventHandler, f) => eventHandler.l.push(f); + var removeEventHandlerListener = (eventHandler, f) => { + const l = eventHandler.l; + const len = l.length; + eventHandler.l = l.filter((g) => f !== g); + if (len === eventHandler.l.length) { + console.error("[yjs] Tried to remove event handler that doesn't exist."); + } + }; + var callEventHandlerListeners = (eventHandler, arg0, arg1) => callAll(eventHandler.l, [arg0, arg1]); + var ID = class { + /** + * @param {number} client client id + * @param {number} clock unique per client id, continuous number + */ + constructor(client, clock) { + this.client = client; + this.clock = clock; + } + }; + var compareIDs = (a, b) => a === b || a !== null && b !== null && a.client === b.client && a.clock === b.clock; + var createID = (client, clock) => new ID(client, clock); + var findRootTypeKey = (type) => { + for (const [key, value] of type.doc.share.entries()) { + if (value === type) { + return key; + } + } + throw unexpectedCase(); + }; + var Snapshot = class { + /** + * @param {DeleteSet} ds + * @param {Map} sv state map + */ + constructor(ds, sv) { + this.ds = ds; + this.sv = sv; + } + }; + var createSnapshot = (ds, sm) => new Snapshot(ds, sm); + var emptySnapshot = createSnapshot(createDeleteSet(), /* @__PURE__ */ new Map()); + var isVisible = (item, snapshot) => snapshot === void 0 ? !item.deleted : snapshot.sv.has(item.id.client) && (snapshot.sv.get(item.id.client) || 0) > item.id.clock && !isDeleted(snapshot.ds, item.id); + var splitSnapshotAffectedStructs = (transaction, snapshot) => { + const meta = setIfUndefined(transaction.meta, splitSnapshotAffectedStructs, create2); + const store = transaction.doc.store; + if (!meta.has(snapshot)) { + snapshot.sv.forEach((clock, client) => { + if (clock < getState(store, client)) { + getItemCleanStart(transaction, createID(client, clock)); + } + }); + iterateDeletedStructs(transaction, snapshot.ds, (_item) => { + }); + meta.add(snapshot); + } + }; + var StructStore = class { + constructor() { + this.clients = /* @__PURE__ */ new Map(); + this.pendingStructs = null; + this.pendingDs = null; + } + }; + var getStateVector = (store) => { + const sm = /* @__PURE__ */ new Map(); + store.clients.forEach((structs, client) => { + const struct = structs[structs.length - 1]; + sm.set(client, struct.id.clock + struct.length); + }); + return sm; + }; + var getState = (store, client) => { + const structs = store.clients.get(client); + if (structs === void 0) { + return 0; + } + const lastStruct = structs[structs.length - 1]; + return lastStruct.id.clock + lastStruct.length; + }; + var addStruct = (store, struct) => { + let structs = store.clients.get(struct.id.client); + if (structs === void 0) { + structs = []; + store.clients.set(struct.id.client, structs); + } else { + const lastStruct = structs[structs.length - 1]; + if (lastStruct.id.clock + lastStruct.length !== struct.id.clock) { + throw unexpectedCase(); + } + } + structs.push(struct); + }; + var findIndexSS = (structs, clock) => { + let left = 0; + let right = structs.length - 1; + let mid = structs[right]; + let midclock = mid.id.clock; + if (midclock === clock) { + return right; + } + let midindex = floor(clock / (midclock + mid.length - 1) * right); + while (left <= right) { + mid = structs[midindex]; + midclock = mid.id.clock; + if (midclock <= clock) { + if (clock < midclock + mid.length) { + return midindex; + } + left = midindex + 1; + } else { + right = midindex - 1; + } + midindex = floor((left + right) / 2); + } + throw unexpectedCase(); + }; + var find = (store, id2) => { + const structs = store.clients.get(id2.client); + return structs[findIndexSS(structs, id2.clock)]; + }; + var getItem = ( + /** @type {function(StructStore,ID):Item} */ + find + ); + var findIndexCleanStart = (transaction, structs, clock) => { + const index = findIndexSS(structs, clock); + const struct = structs[index]; + if (struct.id.clock < clock && struct instanceof Item) { + structs.splice(index + 1, 0, splitItem(transaction, struct, clock - struct.id.clock)); + return index + 1; + } + return index; + }; + var getItemCleanStart = (transaction, id2) => { + const structs = ( + /** @type {Array} */ + transaction.doc.store.clients.get(id2.client) + ); + return structs[findIndexCleanStart(transaction, structs, id2.clock)]; + }; + var getItemCleanEnd = (transaction, store, id2) => { + const structs = store.clients.get(id2.client); + const index = findIndexSS(structs, id2.clock); + const struct = structs[index]; + if (id2.clock !== struct.id.clock + struct.length - 1 && struct.constructor !== GC) { + structs.splice(index + 1, 0, splitItem(transaction, struct, id2.clock - struct.id.clock + 1)); + } + return struct; + }; + var replaceStruct = (store, struct, newStruct) => { + const structs = ( + /** @type {Array} */ + store.clients.get(struct.id.client) + ); + structs[findIndexSS(structs, struct.id.clock)] = newStruct; + }; + var iterateStructs = (transaction, structs, clockStart, len, f) => { + if (len === 0) { + return; + } + const clockEnd = clockStart + len; + let index = findIndexCleanStart(transaction, structs, clockStart); + let struct; + do { + struct = structs[index++]; + if (clockEnd < struct.id.clock + struct.length) { + findIndexCleanStart(transaction, structs, clockEnd); + } + f(struct); + } while (index < structs.length && structs[index].id.clock < clockEnd); + }; + var Transaction = class { + /** + * @param {Doc} doc + * @param {any} origin + * @param {boolean} local + */ + constructor(doc2, origin, local) { + this.doc = doc2; + this.deleteSet = new DeleteSet(); + this.beforeState = getStateVector(doc2.store); + this.afterState = /* @__PURE__ */ new Map(); + this.changed = /* @__PURE__ */ new Map(); + this.changedParentTypes = /* @__PURE__ */ new Map(); + this._mergeStructs = []; + this.origin = origin; + this.meta = /* @__PURE__ */ new Map(); + this.local = local; + this.subdocsAdded = /* @__PURE__ */ new Set(); + this.subdocsRemoved = /* @__PURE__ */ new Set(); + this.subdocsLoaded = /* @__PURE__ */ new Set(); + this._needFormattingCleanup = false; + } + }; + var writeUpdateMessageFromTransaction = (encoder, transaction) => { + if (transaction.deleteSet.clients.size === 0 && !any(transaction.afterState, (clock, client) => transaction.beforeState.get(client) !== clock)) { + return false; + } + sortAndMergeDeleteSet(transaction.deleteSet); + writeStructsFromTransaction(encoder, transaction); + writeDeleteSet(encoder, transaction.deleteSet); + return true; + }; + var addChangedTypeToTransaction = (transaction, type, parentSub) => { + const item = type._item; + if (item === null || item.id.clock < (transaction.beforeState.get(item.id.client) || 0) && !item.deleted) { + setIfUndefined(transaction.changed, type, create2).add(parentSub); + } + }; + var tryToMergeWithLefts = (structs, pos) => { + let right = structs[pos]; + let left = structs[pos - 1]; + let i = pos; + for (; i > 0; right = left, left = structs[--i - 1]) { + if (left.deleted === right.deleted && left.constructor === right.constructor) { + if (left.mergeWith(right)) { + if (right instanceof Item && right.parentSub !== null && /** @type {AbstractType} */ + right.parent._map.get(right.parentSub) === right) { + right.parent._map.set( + right.parentSub, + /** @type {Item} */ + left + ); + } + continue; + } + } + break; + } + const merged = pos - i; + if (merged) { + structs.splice(pos + 1 - merged, merged); + } + return merged; + }; + var tryGcDeleteSet = (ds, store, gcFilter) => { + for (const [client, deleteItems] of ds.clients.entries()) { + const structs = ( + /** @type {Array} */ + store.clients.get(client) + ); + for (let di = deleteItems.length - 1; di >= 0; di--) { + const deleteItem = deleteItems[di]; + const endDeleteItemClock = deleteItem.clock + deleteItem.len; + for (let si = findIndexSS(structs, deleteItem.clock), struct = structs[si]; si < structs.length && struct.id.clock < endDeleteItemClock; struct = structs[++si]) { + const struct2 = structs[si]; + if (deleteItem.clock + deleteItem.len <= struct2.id.clock) { + break; + } + if (struct2 instanceof Item && struct2.deleted && !struct2.keep && gcFilter(struct2)) { + struct2.gc(store, false); + } + } + } + } + }; + var tryMergeDeleteSet = (ds, store) => { + ds.clients.forEach((deleteItems, client) => { + const structs = ( + /** @type {Array} */ + store.clients.get(client) + ); + for (let di = deleteItems.length - 1; di >= 0; di--) { + const deleteItem = deleteItems[di]; + const mostRightIndexToCheck = min(structs.length - 1, 1 + findIndexSS(structs, deleteItem.clock + deleteItem.len - 1)); + for (let si = mostRightIndexToCheck, struct = structs[si]; si > 0 && struct.id.clock >= deleteItem.clock; struct = structs[si]) { + si -= 1 + tryToMergeWithLefts(structs, si); + } + } + }); + }; + var cleanupTransactions = (transactionCleanups, i) => { + if (i < transactionCleanups.length) { + const transaction = transactionCleanups[i]; + const doc2 = transaction.doc; + const store = doc2.store; + const ds = transaction.deleteSet; + const mergeStructs = transaction._mergeStructs; + try { + sortAndMergeDeleteSet(ds); + transaction.afterState = getStateVector(transaction.doc.store); + doc2.emit("beforeObserverCalls", [transaction, doc2]); + const fs = []; + transaction.changed.forEach( + (subs, itemtype) => fs.push(() => { + if (itemtype._item === null || !itemtype._item.deleted) { + itemtype._callObserver(transaction, subs); + } + }) + ); + fs.push(() => { + transaction.changedParentTypes.forEach((events, type) => { + if (type._dEH.l.length > 0 && (type._item === null || !type._item.deleted)) { + events = events.filter( + (event) => event.target._item === null || !event.target._item.deleted + ); + events.forEach((event) => { + event.currentTarget = type; + event._path = null; + }); + events.sort((event1, event2) => event1.path.length - event2.path.length); + fs.push(() => { + callEventHandlerListeners(type._dEH, events, transaction); + }); + } + }); + fs.push(() => doc2.emit("afterTransaction", [transaction, doc2])); + fs.push(() => { + if (transaction._needFormattingCleanup) { + cleanupYTextAfterTransaction(transaction); + } + }); + }); + callAll(fs, []); + } finally { + if (doc2.gc) { + tryGcDeleteSet(ds, store, doc2.gcFilter); + } + tryMergeDeleteSet(ds, store); + transaction.afterState.forEach((clock, client) => { + const beforeClock = transaction.beforeState.get(client) || 0; + if (beforeClock !== clock) { + const structs = ( + /** @type {Array} */ + store.clients.get(client) + ); + const firstChangePos = max(findIndexSS(structs, beforeClock), 1); + for (let i2 = structs.length - 1; i2 >= firstChangePos; ) { + i2 -= 1 + tryToMergeWithLefts(structs, i2); + } + } + }); + for (let i2 = mergeStructs.length - 1; i2 >= 0; i2--) { + const { client, clock } = mergeStructs[i2].id; + const structs = ( + /** @type {Array} */ + store.clients.get(client) + ); + const replacedStructPos = findIndexSS(structs, clock); + if (replacedStructPos + 1 < structs.length) { + if (tryToMergeWithLefts(structs, replacedStructPos + 1) > 1) { + continue; + } + } + if (replacedStructPos > 0) { + tryToMergeWithLefts(structs, replacedStructPos); + } + } + if (!transaction.local && transaction.afterState.get(doc2.clientID) !== transaction.beforeState.get(doc2.clientID)) { + print(ORANGE, BOLD, "[yjs] ", UNBOLD, RED, "Changed the client-id because another client seems to be using it."); + doc2.clientID = generateNewClientId(); + } + doc2.emit("afterTransactionCleanup", [transaction, doc2]); + if (doc2._observers.has("update")) { + const encoder = new UpdateEncoderV1(); + const hasContent2 = writeUpdateMessageFromTransaction(encoder, transaction); + if (hasContent2) { + doc2.emit("update", [encoder.toUint8Array(), transaction.origin, doc2, transaction]); + } + } + if (doc2._observers.has("updateV2")) { + const encoder = new UpdateEncoderV2(); + const hasContent2 = writeUpdateMessageFromTransaction(encoder, transaction); + if (hasContent2) { + doc2.emit("updateV2", [encoder.toUint8Array(), transaction.origin, doc2, transaction]); + } + } + const { subdocsAdded, subdocsLoaded, subdocsRemoved } = transaction; + if (subdocsAdded.size > 0 || subdocsRemoved.size > 0 || subdocsLoaded.size > 0) { + subdocsAdded.forEach((subdoc) => { + subdoc.clientID = doc2.clientID; + if (subdoc.collectionid == null) { + subdoc.collectionid = doc2.collectionid; + } + doc2.subdocs.add(subdoc); + }); + subdocsRemoved.forEach((subdoc) => doc2.subdocs.delete(subdoc)); + doc2.emit("subdocs", [{ loaded: subdocsLoaded, added: subdocsAdded, removed: subdocsRemoved }, doc2, transaction]); + subdocsRemoved.forEach((subdoc) => subdoc.destroy()); + } + if (transactionCleanups.length <= i + 1) { + doc2._transactionCleanups = []; + doc2.emit("afterAllTransactions", [doc2, transactionCleanups]); + } else { + cleanupTransactions(transactionCleanups, i + 1); + } + } + } + }; + var transact = (doc2, f, origin = null, local = true) => { + const transactionCleanups = doc2._transactionCleanups; + let initialCall = false; + let result = null; + if (doc2._transaction === null) { + initialCall = true; + doc2._transaction = new Transaction(doc2, origin, local); + transactionCleanups.push(doc2._transaction); + if (transactionCleanups.length === 1) { + doc2.emit("beforeAllTransactions", [doc2]); + } + doc2.emit("beforeTransaction", [doc2._transaction, doc2]); + } + try { + result = f(doc2._transaction); + } finally { + if (initialCall) { + const finishCleanup = doc2._transaction === transactionCleanups[0]; + doc2._transaction = null; + if (finishCleanup) { + cleanupTransactions(transactionCleanups, 0); + } + } + } + return result; + }; + function* lazyStructReaderGenerator(decoder) { + const numOfStateUpdates = readVarUint(decoder.restDecoder); + for (let i = 0; i < numOfStateUpdates; i++) { + const numberOfStructs = readVarUint(decoder.restDecoder); + const client = decoder.readClient(); + let clock = readVarUint(decoder.restDecoder); + for (let i2 = 0; i2 < numberOfStructs; i2++) { + const info = decoder.readInfo(); + if (info === 10) { + const len = readVarUint(decoder.restDecoder); + yield new Skip(createID(client, clock), len); + clock += len; + } else if ((BITS5 & info) !== 0) { + const cantCopyParentInfo = (info & (BIT7 | BIT8)) === 0; + const struct = new Item( + createID(client, clock), + null, + // left + (info & BIT8) === BIT8 ? decoder.readLeftID() : null, + // origin + null, + // right + (info & BIT7) === BIT7 ? decoder.readRightID() : null, + // right origin + // @ts-ignore Force writing a string here. + cantCopyParentInfo ? decoder.readParentInfo() ? decoder.readString() : decoder.readLeftID() : null, + // parent + cantCopyParentInfo && (info & BIT6) === BIT6 ? decoder.readString() : null, + // parentSub + readItemContent(decoder, info) + // item content + ); + yield struct; + clock += struct.length; + } else { + const len = decoder.readLen(); + yield new GC(createID(client, clock), len); + clock += len; + } + } + } + } + var LazyStructReader = class { + /** + * @param {UpdateDecoderV1 | UpdateDecoderV2} decoder + * @param {boolean} filterSkips + */ + constructor(decoder, filterSkips) { + this.gen = lazyStructReaderGenerator(decoder); + this.curr = null; + this.done = false; + this.filterSkips = filterSkips; + this.next(); + } + /** + * @return {Item | GC | Skip |null} + */ + next() { + do { + this.curr = this.gen.next().value || null; + } while (this.filterSkips && this.curr !== null && this.curr.constructor === Skip); + return this.curr; + } + }; + var LazyStructWriter = class { + /** + * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder + */ + constructor(encoder) { + this.currClient = 0; + this.startClock = 0; + this.written = 0; + this.encoder = encoder; + this.clientStructs = []; + } + }; + var mergeUpdates = (updates) => mergeUpdatesV2(updates, UpdateDecoderV1, UpdateEncoderV1); + var sliceStruct = (left, diff) => { + if (left.constructor === GC) { + const { client, clock } = left.id; + return new GC(createID(client, clock + diff), left.length - diff); + } else if (left.constructor === Skip) { + const { client, clock } = left.id; + return new Skip(createID(client, clock + diff), left.length - diff); + } else { + const leftItem = ( + /** @type {Item} */ + left + ); + const { client, clock } = leftItem.id; + return new Item( + createID(client, clock + diff), + null, + createID(client, clock + diff - 1), + null, + leftItem.rightOrigin, + leftItem.parent, + leftItem.parentSub, + leftItem.content.splice(diff) + ); + } + }; + var mergeUpdatesV2 = (updates, YDecoder = UpdateDecoderV2, YEncoder = UpdateEncoderV2) => { + if (updates.length === 1) { + return updates[0]; + } + const updateDecoders = updates.map((update) => new YDecoder(createDecoder(update))); + let lazyStructDecoders = updateDecoders.map((decoder) => new LazyStructReader(decoder, true)); + let currWrite = null; + const updateEncoder = new YEncoder(); + const lazyStructEncoder = new LazyStructWriter(updateEncoder); + while (true) { + lazyStructDecoders = lazyStructDecoders.filter((dec) => dec.curr !== null); + lazyStructDecoders.sort( + /** @type {function(any,any):number} */ + (dec1, dec2) => { + if (dec1.curr.id.client === dec2.curr.id.client) { + const clockDiff = dec1.curr.id.clock - dec2.curr.id.clock; + if (clockDiff === 0) { + return dec1.curr.constructor === dec2.curr.constructor ? 0 : dec1.curr.constructor === Skip ? 1 : -1; + } else { + return clockDiff; + } + } else { + return dec2.curr.id.client - dec1.curr.id.client; + } + } + ); + if (lazyStructDecoders.length === 0) { + break; + } + const currDecoder = lazyStructDecoders[0]; + const firstClient = ( + /** @type {Item | GC} */ + currDecoder.curr.id.client + ); + if (currWrite !== null) { + let curr = ( + /** @type {Item | GC | null} */ + currDecoder.curr + ); + let iterated = false; + while (curr !== null && curr.id.clock + curr.length <= currWrite.struct.id.clock + currWrite.struct.length && curr.id.client >= currWrite.struct.id.client) { + curr = currDecoder.next(); + iterated = true; + } + if (curr === null || // current decoder is empty + curr.id.client !== firstClient || // check whether there is another decoder that has has updates from `firstClient` + iterated && curr.id.clock > currWrite.struct.id.clock + currWrite.struct.length) { + continue; + } + if (firstClient !== currWrite.struct.id.client) { + writeStructToLazyStructWriter(lazyStructEncoder, currWrite.struct, currWrite.offset); + currWrite = { struct: curr, offset: 0 }; + currDecoder.next(); + } else { + if (currWrite.struct.id.clock + currWrite.struct.length < curr.id.clock) { + if (currWrite.struct.constructor === Skip) { + currWrite.struct.length = curr.id.clock + curr.length - currWrite.struct.id.clock; + } else { + writeStructToLazyStructWriter(lazyStructEncoder, currWrite.struct, currWrite.offset); + const diff = curr.id.clock - currWrite.struct.id.clock - currWrite.struct.length; + const struct = new Skip(createID(firstClient, currWrite.struct.id.clock + currWrite.struct.length), diff); + currWrite = { struct, offset: 0 }; + } + } else { + const diff = currWrite.struct.id.clock + currWrite.struct.length - curr.id.clock; + if (diff > 0) { + if (currWrite.struct.constructor === Skip) { + currWrite.struct.length -= diff; + } else { + curr = sliceStruct(curr, diff); + } + } + if (!currWrite.struct.mergeWith( + /** @type {any} */ + curr + )) { + writeStructToLazyStructWriter(lazyStructEncoder, currWrite.struct, currWrite.offset); + currWrite = { struct: curr, offset: 0 }; + currDecoder.next(); + } + } + } + } else { + currWrite = { struct: ( + /** @type {Item | GC} */ + currDecoder.curr + ), offset: 0 }; + currDecoder.next(); + } + for (let next = currDecoder.curr; next !== null && next.id.client === firstClient && next.id.clock === currWrite.struct.id.clock + currWrite.struct.length && next.constructor !== Skip; next = currDecoder.next()) { + writeStructToLazyStructWriter(lazyStructEncoder, currWrite.struct, currWrite.offset); + currWrite = { struct: next, offset: 0 }; + } + } + if (currWrite !== null) { + writeStructToLazyStructWriter(lazyStructEncoder, currWrite.struct, currWrite.offset); + currWrite = null; + } + finishLazyStructWriting(lazyStructEncoder); + const dss = updateDecoders.map((decoder) => readDeleteSet(decoder)); + const ds = mergeDeleteSets(dss); + writeDeleteSet(updateEncoder, ds); + return updateEncoder.toUint8Array(); + }; + var diffUpdateV2 = (update, sv, YDecoder = UpdateDecoderV2, YEncoder = UpdateEncoderV2) => { + const state = decodeStateVector(sv); + const encoder = new YEncoder(); + const lazyStructWriter = new LazyStructWriter(encoder); + const decoder = new YDecoder(createDecoder(update)); + const reader = new LazyStructReader(decoder, false); + while (reader.curr) { + const curr = reader.curr; + const currClient = curr.id.client; + const svClock = state.get(currClient) || 0; + if (reader.curr.constructor === Skip) { + reader.next(); + continue; + } + if (curr.id.clock + curr.length > svClock) { + writeStructToLazyStructWriter(lazyStructWriter, curr, max(svClock - curr.id.clock, 0)); + reader.next(); + while (reader.curr && reader.curr.id.client === currClient) { + writeStructToLazyStructWriter(lazyStructWriter, reader.curr, 0); + reader.next(); + } + } else { + while (reader.curr && reader.curr.id.client === currClient && reader.curr.id.clock + reader.curr.length <= svClock) { + reader.next(); + } + } + } + finishLazyStructWriting(lazyStructWriter); + const ds = readDeleteSet(decoder); + writeDeleteSet(encoder, ds); + return encoder.toUint8Array(); + }; + var flushLazyStructWriter = (lazyWriter) => { + if (lazyWriter.written > 0) { + lazyWriter.clientStructs.push({ written: lazyWriter.written, restEncoder: toUint8Array(lazyWriter.encoder.restEncoder) }); + lazyWriter.encoder.restEncoder = createEncoder(); + lazyWriter.written = 0; + } + }; + var writeStructToLazyStructWriter = (lazyWriter, struct, offset) => { + if (lazyWriter.written > 0 && lazyWriter.currClient !== struct.id.client) { + flushLazyStructWriter(lazyWriter); + } + if (lazyWriter.written === 0) { + lazyWriter.currClient = struct.id.client; + lazyWriter.encoder.writeClient(struct.id.client); + writeVarUint(lazyWriter.encoder.restEncoder, struct.id.clock + offset); + } + struct.write(lazyWriter.encoder, offset); + lazyWriter.written++; + }; + var finishLazyStructWriting = (lazyWriter) => { + flushLazyStructWriter(lazyWriter); + const restEncoder = lazyWriter.encoder.restEncoder; + writeVarUint(restEncoder, lazyWriter.clientStructs.length); + for (let i = 0; i < lazyWriter.clientStructs.length; i++) { + const partStructs = lazyWriter.clientStructs[i]; + writeVarUint(restEncoder, partStructs.written); + writeUint8Array(restEncoder, partStructs.restEncoder); + } + }; + var convertUpdateFormat = (update, blockTransformer, YDecoder, YEncoder) => { + const updateDecoder = new YDecoder(createDecoder(update)); + const lazyDecoder = new LazyStructReader(updateDecoder, false); + const updateEncoder = new YEncoder(); + const lazyWriter = new LazyStructWriter(updateEncoder); + for (let curr = lazyDecoder.curr; curr !== null; curr = lazyDecoder.next()) { + writeStructToLazyStructWriter(lazyWriter, blockTransformer(curr), 0); + } + finishLazyStructWriting(lazyWriter); + const ds = readDeleteSet(updateDecoder); + writeDeleteSet(updateEncoder, ds); + return updateEncoder.toUint8Array(); + }; + var convertUpdateFormatV2ToV1 = (update) => convertUpdateFormat(update, id, UpdateDecoderV2, UpdateEncoderV1); + var errorComputeChanges = "You must not compute changes after the event-handler fired."; + var YEvent = class { + /** + * @param {T} target The changed type. + * @param {Transaction} transaction + */ + constructor(target, transaction) { + this.target = target; + this.currentTarget = target; + this.transaction = transaction; + this._changes = null; + this._keys = null; + this._delta = null; + this._path = null; + } + /** + * Computes the path from `y` to the changed type. + * + * @todo v14 should standardize on path: Array<{parent, index}> because that is easier to work with. + * + * The following property holds: + * @example + * let type = y + * event.path.forEach(dir => { + * type = type.get(dir) + * }) + * type === event.target // => true + */ + get path() { + return this._path || (this._path = getPathTo(this.currentTarget, this.target)); + } + /** + * Check if a struct is deleted by this event. + * + * In contrast to change.deleted, this method also returns true if the struct was added and then deleted. + * + * @param {AbstractStruct} struct + * @return {boolean} + */ + deletes(struct) { + return isDeleted(this.transaction.deleteSet, struct.id); + } + /** + * @type {Map} + */ + get keys() { + if (this._keys === null) { + if (this.transaction.doc._transactionCleanups.length === 0) { + throw create3(errorComputeChanges); + } + const keys3 = /* @__PURE__ */ new Map(); + const target = this.target; + const changed = ( + /** @type Set */ + this.transaction.changed.get(target) + ); + changed.forEach((key) => { + if (key !== null) { + const item = ( + /** @type {Item} */ + target._map.get(key) + ); + let action; + let oldValue; + if (this.adds(item)) { + let prev = item.left; + while (prev !== null && this.adds(prev)) { + prev = prev.left; + } + if (this.deletes(item)) { + if (prev !== null && this.deletes(prev)) { + action = "delete"; + oldValue = last(prev.content.getContent()); + } else { + return; + } + } else { + if (prev !== null && this.deletes(prev)) { + action = "update"; + oldValue = last(prev.content.getContent()); + } else { + action = "add"; + oldValue = void 0; + } + } + } else { + if (this.deletes(item)) { + action = "delete"; + oldValue = last( + /** @type {Item} */ + item.content.getContent() + ); + } else { + return; + } + } + keys3.set(key, { action, oldValue }); + } + }); + this._keys = keys3; + } + return this._keys; + } + /** + * This is a computed property. Note that this can only be safely computed during the + * event call. Computing this property after other changes happened might result in + * unexpected behavior (incorrect computation of deltas). A safe way to collect changes + * is to store the `changes` or the `delta` object. Avoid storing the `transaction` object. + * + * @type {Array<{insert?: string | Array | object | AbstractType, retain?: number, delete?: number, attributes?: Object}>} + */ + get delta() { + return this.changes.delta; + } + /** + * Check if a struct is added by this event. + * + * In contrast to change.deleted, this method also returns true if the struct was added and then deleted. + * + * @param {AbstractStruct} struct + * @return {boolean} + */ + adds(struct) { + return struct.id.clock >= (this.transaction.beforeState.get(struct.id.client) || 0); + } + /** + * This is a computed property. Note that this can only be safely computed during the + * event call. Computing this property after other changes happened might result in + * unexpected behavior (incorrect computation of deltas). A safe way to collect changes + * is to store the `changes` or the `delta` object. Avoid storing the `transaction` object. + * + * @type {{added:Set,deleted:Set,keys:Map,delta:Array<{insert?:Array|string, delete?:number, retain?:number}>}} + */ + get changes() { + let changes = this._changes; + if (changes === null) { + if (this.transaction.doc._transactionCleanups.length === 0) { + throw create3(errorComputeChanges); + } + const target = this.target; + const added = create2(); + const deleted = create2(); + const delta = []; + changes = { + added, + deleted, + delta, + keys: this.keys + }; + const changed = ( + /** @type Set */ + this.transaction.changed.get(target) + ); + if (changed.has(null)) { + let lastOp = null; + const packOp = () => { + if (lastOp) { + delta.push(lastOp); + } + }; + for (let item = target._start; item !== null; item = item.right) { + if (item.deleted) { + if (this.deletes(item) && !this.adds(item)) { + if (lastOp === null || lastOp.delete === void 0) { + packOp(); + lastOp = { delete: 0 }; + } + lastOp.delete += item.length; + deleted.add(item); + } + } else { + if (this.adds(item)) { + if (lastOp === null || lastOp.insert === void 0) { + packOp(); + lastOp = { insert: [] }; + } + lastOp.insert = lastOp.insert.concat(item.content.getContent()); + added.add(item); + } else { + if (lastOp === null || lastOp.retain === void 0) { + packOp(); + lastOp = { retain: 0 }; + } + lastOp.retain += item.length; + } + } + } + if (lastOp !== null && lastOp.retain === void 0) { + packOp(); + } + } + this._changes = changes; + } + return ( + /** @type {any} */ + changes + ); + } + }; + var getPathTo = (parent, child) => { + const path = []; + while (child._item !== null && child !== parent) { + if (child._item.parentSub !== null) { + path.unshift(child._item.parentSub); + } else { + let i = 0; + let c = ( + /** @type {AbstractType} */ + child._item.parent._start + ); + while (c !== child._item && c !== null) { + if (!c.deleted && c.countable) { + i += c.length; + } + c = c.right; + } + path.unshift(i); + } + child = /** @type {AbstractType} */ + child._item.parent; + } + return path; + }; + var warnPrematureAccess = () => { + warn("Invalid access: Add Yjs type to a document before reading data."); + }; + var maxSearchMarker = 80; + var globalSearchMarkerTimestamp = 0; + var ArraySearchMarker = class { + /** + * @param {Item} p + * @param {number} index + */ + constructor(p, index) { + p.marker = true; + this.p = p; + this.index = index; + this.timestamp = globalSearchMarkerTimestamp++; + } + }; + var refreshMarkerTimestamp = (marker) => { + marker.timestamp = globalSearchMarkerTimestamp++; + }; + var overwriteMarker = (marker, p, index) => { + marker.p.marker = false; + marker.p = p; + p.marker = true; + marker.index = index; + marker.timestamp = globalSearchMarkerTimestamp++; + }; + var markPosition = (searchMarker, p, index) => { + if (searchMarker.length >= maxSearchMarker) { + const marker = searchMarker.reduce((a, b) => a.timestamp < b.timestamp ? a : b); + overwriteMarker(marker, p, index); + return marker; + } else { + const pm = new ArraySearchMarker(p, index); + searchMarker.push(pm); + return pm; + } + }; + var findMarker = (yarray, index) => { + if (yarray._start === null || index === 0 || yarray._searchMarker === null) { + return null; + } + const marker = yarray._searchMarker.length === 0 ? null : yarray._searchMarker.reduce((a, b) => abs(index - a.index) < abs(index - b.index) ? a : b); + let p = yarray._start; + let pindex = 0; + if (marker !== null) { + p = marker.p; + pindex = marker.index; + refreshMarkerTimestamp(marker); + } + while (p.right !== null && pindex < index) { + if (!p.deleted && p.countable) { + if (index < pindex + p.length) { + break; + } + pindex += p.length; + } + p = p.right; + } + while (p.left !== null && pindex > index) { + p = p.left; + if (!p.deleted && p.countable) { + pindex -= p.length; + } + } + while (p.left !== null && p.left.id.client === p.id.client && p.left.id.clock + p.left.length === p.id.clock) { + p = p.left; + if (!p.deleted && p.countable) { + pindex -= p.length; + } + } + if (marker !== null && abs(marker.index - pindex) < /** @type {YText|YArray} */ + p.parent.length / maxSearchMarker) { + overwriteMarker(marker, p, pindex); + return marker; + } else { + return markPosition(yarray._searchMarker, p, pindex); + } + }; + var updateMarkerChanges = (searchMarker, index, len) => { + for (let i = searchMarker.length - 1; i >= 0; i--) { + const m = searchMarker[i]; + if (len > 0) { + let p = m.p; + p.marker = false; + while (p && (p.deleted || !p.countable)) { + p = p.left; + if (p && !p.deleted && p.countable) { + m.index -= p.length; + } + } + if (p === null || p.marker === true) { + searchMarker.splice(i, 1); + continue; + } + m.p = p; + p.marker = true; + } + if (index < m.index || len > 0 && index === m.index) { + m.index = max(index, m.index + len); + } + } + }; + var callTypeObservers = (type, transaction, event) => { + const changedType = type; + const changedParentTypes = transaction.changedParentTypes; + while (true) { + setIfUndefined(changedParentTypes, type, () => []).push(event); + if (type._item === null) { + break; + } + type = /** @type {AbstractType} */ + type._item.parent; + } + callEventHandlerListeners(changedType._eH, event, transaction); + }; + var AbstractType = class { + constructor() { + this._item = null; + this._map = /* @__PURE__ */ new Map(); + this._start = null; + this.doc = null; + this._length = 0; + this._eH = createEventHandler(); + this._dEH = createEventHandler(); + this._searchMarker = null; + } + /** + * @return {AbstractType|null} + */ + get parent() { + return this._item ? ( + /** @type {AbstractType} */ + this._item.parent + ) : null; + } + /** + * Integrate this type into the Yjs instance. + * + * * Save this struct in the os + * * This type is sent to other client + * * Observer functions are fired + * + * @param {Doc} y The Yjs instance + * @param {Item|null} item + */ + _integrate(y, item) { + this.doc = y; + this._item = item; + } + /** + * @return {AbstractType} + */ + _copy() { + throw methodUnimplemented(); + } + /** + * Makes a copy of this data type that can be included somewhere else. + * + * Note that the content is only readable _after_ it has been included somewhere in the Ydoc. + * + * @return {AbstractType} + */ + clone() { + throw methodUnimplemented(); + } + /** + * @param {UpdateEncoderV1 | UpdateEncoderV2} _encoder + */ + _write(_encoder) { + } + /** + * The first non-deleted item + */ + get _first() { + let n = this._start; + while (n !== null && n.deleted) { + n = n.right; + } + return n; + } + /** + * Creates YEvent and calls all type observers. + * Must be implemented by each type. + * + * @param {Transaction} transaction + * @param {Set} _parentSubs Keys changed on this type. `null` if list was modified. + */ + _callObserver(transaction, _parentSubs) { + if (!transaction.local && this._searchMarker) { + this._searchMarker.length = 0; + } + } + /** + * Observe all events that are created on this type. + * + * @param {function(EventType, Transaction):void} f Observer function + */ + observe(f) { + addEventHandlerListener(this._eH, f); + } + /** + * Observe all events that are created by this type and its children. + * + * @param {function(Array>,Transaction):void} f Observer function + */ + observeDeep(f) { + addEventHandlerListener(this._dEH, f); + } + /** + * Unregister an observer function. + * + * @param {function(EventType,Transaction):void} f Observer function + */ + unobserve(f) { + removeEventHandlerListener(this._eH, f); + } + /** + * Unregister an observer function. + * + * @param {function(Array>,Transaction):void} f Observer function + */ + unobserveDeep(f) { + removeEventHandlerListener(this._dEH, f); + } + /** + * @abstract + * @return {any} + */ + toJSON() { + } + }; + var typeListSlice = (type, start, end) => { + type.doc ?? warnPrematureAccess(); + if (start < 0) { + start = type._length + start; + } + if (end < 0) { + end = type._length + end; + } + let len = end - start; + const cs = []; + let n = type._start; + while (n !== null && len > 0) { + if (n.countable && !n.deleted) { + const c = n.content.getContent(); + if (c.length <= start) { + start -= c.length; + } else { + for (let i = start; i < c.length && len > 0; i++) { + cs.push(c[i]); + len--; + } + start = 0; + } + } + n = n.right; + } + return cs; + }; + var typeListToArray = (type) => { + type.doc ?? warnPrematureAccess(); + const cs = []; + let n = type._start; + while (n !== null) { + if (n.countable && !n.deleted) { + const c = n.content.getContent(); + for (let i = 0; i < c.length; i++) { + cs.push(c[i]); + } + } + n = n.right; + } + return cs; + }; + var typeListForEach = (type, f) => { + let index = 0; + let n = type._start; + type.doc ?? warnPrematureAccess(); + while (n !== null) { + if (n.countable && !n.deleted) { + const c = n.content.getContent(); + for (let i = 0; i < c.length; i++) { + f(c[i], index++, type); + } + } + n = n.right; + } + }; + var typeListMap = (type, f) => { + const result = []; + typeListForEach(type, (c, i) => { + result.push(f(c, i, type)); + }); + return result; + }; + var typeListCreateIterator = (type) => { + let n = type._start; + let currentContent = null; + let currentContentIndex = 0; + return { + [Symbol.iterator]() { + return this; + }, + next: () => { + if (currentContent === null) { + while (n !== null && n.deleted) { + n = n.right; + } + if (n === null) { + return { + done: true, + value: void 0 + }; + } + currentContent = n.content.getContent(); + currentContentIndex = 0; + n = n.right; + } + const value = currentContent[currentContentIndex++]; + if (currentContent.length <= currentContentIndex) { + currentContent = null; + } + return { + done: false, + value + }; + } + }; + }; + var typeListGet = (type, index) => { + type.doc ?? warnPrematureAccess(); + const marker = findMarker(type, index); + let n = type._start; + if (marker !== null) { + n = marker.p; + index -= marker.index; + } + for (; n !== null; n = n.right) { + if (!n.deleted && n.countable) { + if (index < n.length) { + return n.content.getContent()[index]; + } + index -= n.length; + } + } + }; + var typeListInsertGenericsAfter = (transaction, parent, referenceItem, content) => { + let left = referenceItem; + const doc2 = transaction.doc; + const ownClientId = doc2.clientID; + const store = doc2.store; + const right = referenceItem === null ? parent._start : referenceItem.right; + let jsonContent = []; + const packJsonContent = () => { + if (jsonContent.length > 0) { + left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentAny(jsonContent)); + left.integrate(transaction, 0); + jsonContent = []; + } + }; + content.forEach((c) => { + if (c === null) { + jsonContent.push(c); + } else { + switch (c.constructor) { + case Number: + case Object: + case Boolean: + case Array: + case String: + jsonContent.push(c); + break; + default: + packJsonContent(); + switch (c.constructor) { + case Uint8Array: + case ArrayBuffer: + left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentBinary(new Uint8Array( + /** @type {Uint8Array} */ + c + ))); + left.integrate(transaction, 0); + break; + case Doc: + left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentDoc( + /** @type {Doc} */ + c + )); + left.integrate(transaction, 0); + break; + default: + if (c instanceof AbstractType) { + left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentType(c)); + left.integrate(transaction, 0); + } else { + throw new Error("Unexpected content type in insert operation"); + } + } + } + } + }); + packJsonContent(); + }; + var lengthExceeded = () => create3("Length exceeded!"); + var typeListInsertGenerics = (transaction, parent, index, content) => { + if (index > parent._length) { + throw lengthExceeded(); + } + if (index === 0) { + if (parent._searchMarker) { + updateMarkerChanges(parent._searchMarker, index, content.length); + } + return typeListInsertGenericsAfter(transaction, parent, null, content); + } + const startIndex = index; + const marker = findMarker(parent, index); + let n = parent._start; + if (marker !== null) { + n = marker.p; + index -= marker.index; + if (index === 0) { + n = n.prev; + index += n && n.countable && !n.deleted ? n.length : 0; + } + } + for (; n !== null; n = n.right) { + if (!n.deleted && n.countable) { + if (index <= n.length) { + if (index < n.length) { + getItemCleanStart(transaction, createID(n.id.client, n.id.clock + index)); + } + break; + } + index -= n.length; + } + } + if (parent._searchMarker) { + updateMarkerChanges(parent._searchMarker, startIndex, content.length); + } + return typeListInsertGenericsAfter(transaction, parent, n, content); + }; + var typeListPushGenerics = (transaction, parent, content) => { + const marker = (parent._searchMarker || []).reduce((maxMarker, currMarker) => currMarker.index > maxMarker.index ? currMarker : maxMarker, { index: 0, p: parent._start }); + let n = marker.p; + if (n) { + while (n.right) { + n = n.right; + } + } + return typeListInsertGenericsAfter(transaction, parent, n, content); + }; + var typeListDelete = (transaction, parent, index, length3) => { + if (length3 === 0) { + return; + } + const startIndex = index; + const startLength = length3; + const marker = findMarker(parent, index); + let n = parent._start; + if (marker !== null) { + n = marker.p; + index -= marker.index; + } + for (; n !== null && index > 0; n = n.right) { + if (!n.deleted && n.countable) { + if (index < n.length) { + getItemCleanStart(transaction, createID(n.id.client, n.id.clock + index)); + } + index -= n.length; + } + } + while (length3 > 0 && n !== null) { + if (!n.deleted) { + if (length3 < n.length) { + getItemCleanStart(transaction, createID(n.id.client, n.id.clock + length3)); + } + n.delete(transaction); + length3 -= n.length; + } + n = n.right; + } + if (length3 > 0) { + throw lengthExceeded(); + } + if (parent._searchMarker) { + updateMarkerChanges( + parent._searchMarker, + startIndex, + -startLength + length3 + /* in case we remove the above exception */ + ); + } + }; + var typeMapDelete = (transaction, parent, key) => { + const c = parent._map.get(key); + if (c !== void 0) { + c.delete(transaction); + } + }; + var typeMapSet = (transaction, parent, key, value) => { + const left = parent._map.get(key) || null; + const doc2 = transaction.doc; + const ownClientId = doc2.clientID; + let content; + if (value == null) { + content = new ContentAny([value]); + } else { + switch (value.constructor) { + case Number: + case Object: + case Boolean: + case Array: + case String: + case Date: + case BigInt: + content = new ContentAny([value]); + break; + case Uint8Array: + content = new ContentBinary( + /** @type {Uint8Array} */ + value + ); + break; + case Doc: + content = new ContentDoc( + /** @type {Doc} */ + value + ); + break; + default: + if (value instanceof AbstractType) { + content = new ContentType(value); + } else { + throw new Error("Unexpected content type"); + } + } + } + new Item(createID(ownClientId, getState(doc2.store, ownClientId)), left, left && left.lastId, null, null, parent, key, content).integrate(transaction, 0); + }; + var typeMapGet = (parent, key) => { + parent.doc ?? warnPrematureAccess(); + const val = parent._map.get(key); + return val !== void 0 && !val.deleted ? val.content.getContent()[val.length - 1] : void 0; + }; + var typeMapGetAll = (parent) => { + const res = {}; + parent.doc ?? warnPrematureAccess(); + parent._map.forEach((value, key) => { + if (!value.deleted) { + res[key] = value.content.getContent()[value.length - 1]; + } + }); + return res; + }; + var typeMapHas = (parent, key) => { + parent.doc ?? warnPrematureAccess(); + const val = parent._map.get(key); + return val !== void 0 && !val.deleted; + }; + var typeMapGetAllSnapshot = (parent, snapshot) => { + const res = {}; + parent._map.forEach((value, key) => { + let v = value; + while (v !== null && (!snapshot.sv.has(v.id.client) || v.id.clock >= (snapshot.sv.get(v.id.client) || 0))) { + v = v.left; + } + if (v !== null && isVisible(v, snapshot)) { + res[key] = v.content.getContent()[v.length - 1]; + } + }); + return res; + }; + var createMapIterator = (type) => { + type.doc ?? warnPrematureAccess(); + return iteratorFilter( + type._map.entries(), + /** @param {any} entry */ + (entry) => !entry[1].deleted + ); + }; + var YArrayEvent = class extends YEvent { + }; + var YArray = class _YArray extends AbstractType { + constructor() { + super(); + this._prelimContent = []; + this._searchMarker = []; + } + /** + * Construct a new YArray containing the specified items. + * @template {Object|Array|number|null|string|Uint8Array} T + * @param {Array} items + * @return {YArray} + */ + static from(items) { + const a = new _YArray(); + a.push(items); + return a; + } + /** + * Integrate this type into the Yjs instance. + * + * * Save this struct in the os + * * This type is sent to other client + * * Observer functions are fired + * + * @param {Doc} y The Yjs instance + * @param {Item} item + */ + _integrate(y, item) { + super._integrate(y, item); + this.insert( + 0, + /** @type {Array} */ + this._prelimContent + ); + this._prelimContent = null; + } + /** + * @return {YArray} + */ + _copy() { + return new _YArray(); + } + /** + * Makes a copy of this data type that can be included somewhere else. + * + * Note that the content is only readable _after_ it has been included somewhere in the Ydoc. + * + * @return {YArray} + */ + clone() { + const arr = new _YArray(); + arr.insert(0, this.toArray().map( + (el) => el instanceof AbstractType ? ( + /** @type {typeof el} */ + el.clone() + ) : el + )); + return arr; + } + get length() { + this.doc ?? warnPrematureAccess(); + return this._length; + } + /** + * Creates YArrayEvent and calls observers. + * + * @param {Transaction} transaction + * @param {Set} parentSubs Keys changed on this type. `null` if list was modified. + */ + _callObserver(transaction, parentSubs) { + super._callObserver(transaction, parentSubs); + callTypeObservers(this, transaction, new YArrayEvent(this, transaction)); + } + /** + * Inserts new content at an index. + * + * Important: This function expects an array of content. Not just a content + * object. The reason for this "weirdness" is that inserting several elements + * is very efficient when it is done as a single operation. + * + * @example + * // Insert character 'a' at position 0 + * yarray.insert(0, ['a']) + * // Insert numbers 1, 2 at position 1 + * yarray.insert(1, [1, 2]) + * + * @param {number} index The index to insert content at. + * @param {Array} content The array of content + */ + insert(index, content) { + if (this.doc !== null) { + transact(this.doc, (transaction) => { + typeListInsertGenerics( + transaction, + this, + index, + /** @type {any} */ + content + ); + }); + } else { + this._prelimContent.splice(index, 0, ...content); + } + } + /** + * Appends content to this YArray. + * + * @param {Array} content Array of content to append. + * + * @todo Use the following implementation in all types. + */ + push(content) { + if (this.doc !== null) { + transact(this.doc, (transaction) => { + typeListPushGenerics( + transaction, + this, + /** @type {any} */ + content + ); + }); + } else { + this._prelimContent.push(...content); + } + } + /** + * Prepends content to this YArray. + * + * @param {Array} content Array of content to prepend. + */ + unshift(content) { + this.insert(0, content); + } + /** + * Deletes elements starting from an index. + * + * @param {number} index Index at which to start deleting elements + * @param {number} length The number of elements to remove. Defaults to 1. + */ + delete(index, length3 = 1) { + if (this.doc !== null) { + transact(this.doc, (transaction) => { + typeListDelete(transaction, this, index, length3); + }); + } else { + this._prelimContent.splice(index, length3); + } + } + /** + * Returns the i-th element from a YArray. + * + * @param {number} index The index of the element to return from the YArray + * @return {T} + */ + get(index) { + return typeListGet(this, index); + } + /** + * Transforms this YArray to a JavaScript Array. + * + * @return {Array} + */ + toArray() { + return typeListToArray(this); + } + /** + * Returns a portion of this YArray into a JavaScript Array selected + * from start to end (end not included). + * + * @param {number} [start] + * @param {number} [end] + * @return {Array} + */ + slice(start = 0, end = this.length) { + return typeListSlice(this, start, end); + } + /** + * Transforms this Shared Type to a JSON object. + * + * @return {Array} + */ + toJSON() { + return this.map((c) => c instanceof AbstractType ? c.toJSON() : c); + } + /** + * Returns an Array with the result of calling a provided function on every + * element of this YArray. + * + * @template M + * @param {function(T,number,YArray):M} f Function that produces an element of the new Array + * @return {Array} A new array with each element being the result of the + * callback function + */ + map(f) { + return typeListMap( + this, + /** @type {any} */ + f + ); + } + /** + * Executes a provided function once on every element of this YArray. + * + * @param {function(T,number,YArray):void} f A function to execute on every element of this YArray. + */ + forEach(f) { + typeListForEach(this, f); + } + /** + * @return {IterableIterator} + */ + [Symbol.iterator]() { + return typeListCreateIterator(this); + } + /** + * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder + */ + _write(encoder) { + encoder.writeTypeRef(YArrayRefID); + } + }; + var readYArray = (_decoder) => new YArray(); + var YMapEvent = class extends YEvent { + /** + * @param {YMap} ymap The YArray that changed. + * @param {Transaction} transaction + * @param {Set} subs The keys that changed. + */ + constructor(ymap, transaction, subs) { + super(ymap, transaction); + this.keysChanged = subs; + } + }; + var YMap = class _YMap extends AbstractType { + /** + * + * @param {Iterable=} entries - an optional iterable to initialize the YMap + */ + constructor(entries) { + super(); + this._prelimContent = null; + if (entries === void 0) { + this._prelimContent = /* @__PURE__ */ new Map(); + } else { + this._prelimContent = new Map(entries); + } + } + /** + * Integrate this type into the Yjs instance. + * + * * Save this struct in the os + * * This type is sent to other client + * * Observer functions are fired + * + * @param {Doc} y The Yjs instance + * @param {Item} item + */ + _integrate(y, item) { + super._integrate(y, item); + this._prelimContent.forEach((value, key) => { + this.set(key, value); + }); + this._prelimContent = null; + } + /** + * @return {YMap} + */ + _copy() { + return new _YMap(); + } + /** + * Makes a copy of this data type that can be included somewhere else. + * + * Note that the content is only readable _after_ it has been included somewhere in the Ydoc. + * + * @return {YMap} + */ + clone() { + const map2 = new _YMap(); + this.forEach((value, key) => { + map2.set(key, value instanceof AbstractType ? ( + /** @type {typeof value} */ + value.clone() + ) : value); + }); + return map2; + } + /** + * Creates YMapEvent and calls observers. + * + * @param {Transaction} transaction + * @param {Set} parentSubs Keys changed on this type. `null` if list was modified. + */ + _callObserver(transaction, parentSubs) { + callTypeObservers(this, transaction, new YMapEvent(this, transaction, parentSubs)); + } + /** + * Transforms this Shared Type to a JSON object. + * + * @return {Object} + */ + toJSON() { + this.doc ?? warnPrematureAccess(); + const map2 = {}; + this._map.forEach((item, key) => { + if (!item.deleted) { + const v = item.content.getContent()[item.length - 1]; + map2[key] = v instanceof AbstractType ? v.toJSON() : v; + } + }); + return map2; + } + /** + * Returns the size of the YMap (count of key/value pairs) + * + * @return {number} + */ + get size() { + return [...createMapIterator(this)].length; + } + /** + * Returns the keys for each element in the YMap Type. + * + * @return {IterableIterator} + */ + keys() { + return iteratorMap( + createMapIterator(this), + /** @param {any} v */ + (v) => v[0] + ); + } + /** + * Returns the values for each element in the YMap Type. + * + * @return {IterableIterator} + */ + values() { + return iteratorMap( + createMapIterator(this), + /** @param {any} v */ + (v) => v[1].content.getContent()[v[1].length - 1] + ); + } + /** + * Returns an Iterator of [key, value] pairs + * + * @return {IterableIterator<[string, MapType]>} + */ + entries() { + return iteratorMap( + createMapIterator(this), + /** @param {any} v */ + (v) => ( + /** @type {any} */ + [v[0], v[1].content.getContent()[v[1].length - 1]] + ) + ); + } + /** + * Executes a provided function on once on every key-value pair. + * + * @param {function(MapType,string,YMap):void} f A function to execute on every element of this YArray. + */ + forEach(f) { + this.doc ?? warnPrematureAccess(); + this._map.forEach((item, key) => { + if (!item.deleted) { + f(item.content.getContent()[item.length - 1], key, this); + } + }); + } + /** + * Returns an Iterator of [key, value] pairs + * + * @return {IterableIterator<[string, MapType]>} + */ + [Symbol.iterator]() { + return this.entries(); + } + /** + * Remove a specified element from this YMap. + * + * @param {string} key The key of the element to remove. + */ + delete(key) { + if (this.doc !== null) { + transact(this.doc, (transaction) => { + typeMapDelete(transaction, this, key); + }); + } else { + this._prelimContent.delete(key); + } + } + /** + * Adds or updates an element with a specified key and value. + * @template {MapType} VAL + * + * @param {string} key The key of the element to add to this YMap + * @param {VAL} value The value of the element to add + * @return {VAL} + */ + set(key, value) { + if (this.doc !== null) { + transact(this.doc, (transaction) => { + typeMapSet( + transaction, + this, + key, + /** @type {any} */ + value + ); + }); + } else { + this._prelimContent.set(key, value); + } + return value; + } + /** + * Returns a specified element from this YMap. + * + * @param {string} key + * @return {MapType|undefined} + */ + get(key) { + return ( + /** @type {any} */ + typeMapGet(this, key) + ); + } + /** + * Returns a boolean indicating whether the specified key exists or not. + * + * @param {string} key The key to test. + * @return {boolean} + */ + has(key) { + return typeMapHas(this, key); + } + /** + * Removes all elements from this YMap. + */ + clear() { + if (this.doc !== null) { + transact(this.doc, (transaction) => { + this.forEach(function(_value, key, map2) { + typeMapDelete(transaction, map2, key); + }); + }); + } else { + this._prelimContent.clear(); + } + } + /** + * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder + */ + _write(encoder) { + encoder.writeTypeRef(YMapRefID); + } + }; + var readYMap = (_decoder) => new YMap(); + var equalAttrs = (a, b) => a === b || typeof a === "object" && typeof b === "object" && a && b && equalFlat(a, b); + var ItemTextListPosition = class { + /** + * @param {Item|null} left + * @param {Item|null} right + * @param {number} index + * @param {Map} currentAttributes + */ + constructor(left, right, index, currentAttributes) { + this.left = left; + this.right = right; + this.index = index; + this.currentAttributes = currentAttributes; + } + /** + * Only call this if you know that this.right is defined + */ + forward() { + if (this.right === null) { + unexpectedCase(); + } + switch (this.right.content.constructor) { + case ContentFormat: + if (!this.right.deleted) { + updateCurrentAttributes( + this.currentAttributes, + /** @type {ContentFormat} */ + this.right.content + ); + } + break; + default: + if (!this.right.deleted) { + this.index += this.right.length; + } + break; + } + this.left = this.right; + this.right = this.right.right; + } + }; + var findNextPosition = (transaction, pos, count) => { + while (pos.right !== null && count > 0) { + switch (pos.right.content.constructor) { + case ContentFormat: + if (!pos.right.deleted) { + updateCurrentAttributes( + pos.currentAttributes, + /** @type {ContentFormat} */ + pos.right.content + ); + } + break; + default: + if (!pos.right.deleted) { + if (count < pos.right.length) { + getItemCleanStart(transaction, createID(pos.right.id.client, pos.right.id.clock + count)); + } + pos.index += pos.right.length; + count -= pos.right.length; + } + break; + } + pos.left = pos.right; + pos.right = pos.right.right; + } + return pos; + }; + var findPosition = (transaction, parent, index, useSearchMarker) => { + const currentAttributes = /* @__PURE__ */ new Map(); + const marker = useSearchMarker ? findMarker(parent, index) : null; + if (marker) { + const pos = new ItemTextListPosition(marker.p.left, marker.p, marker.index, currentAttributes); + return findNextPosition(transaction, pos, index - marker.index); + } else { + const pos = new ItemTextListPosition(null, parent._start, 0, currentAttributes); + return findNextPosition(transaction, pos, index); + } + }; + var insertNegatedAttributes = (transaction, parent, currPos, negatedAttributes) => { + while (currPos.right !== null && (currPos.right.deleted === true || currPos.right.content.constructor === ContentFormat && equalAttrs( + negatedAttributes.get( + /** @type {ContentFormat} */ + currPos.right.content.key + ), + /** @type {ContentFormat} */ + currPos.right.content.value + ))) { + if (!currPos.right.deleted) { + negatedAttributes.delete( + /** @type {ContentFormat} */ + currPos.right.content.key + ); + } + currPos.forward(); + } + const doc2 = transaction.doc; + const ownClientId = doc2.clientID; + negatedAttributes.forEach((val, key) => { + const left = currPos.left; + const right = currPos.right; + const nextFormat = new Item(createID(ownClientId, getState(doc2.store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentFormat(key, val)); + nextFormat.integrate(transaction, 0); + currPos.right = nextFormat; + currPos.forward(); + }); + }; + var updateCurrentAttributes = (currentAttributes, format) => { + const { key, value } = format; + if (value === null) { + currentAttributes.delete(key); + } else { + currentAttributes.set(key, value); + } + }; + var minimizeAttributeChanges = (currPos, attributes) => { + while (true) { + if (currPos.right === null) { + break; + } else if (currPos.right.deleted || currPos.right.content.constructor === ContentFormat && equalAttrs( + attributes[ + /** @type {ContentFormat} */ + currPos.right.content.key + ] ?? null, + /** @type {ContentFormat} */ + currPos.right.content.value + )) ; + else { + break; + } + currPos.forward(); + } + }; + var insertAttributes = (transaction, parent, currPos, attributes) => { + const doc2 = transaction.doc; + const ownClientId = doc2.clientID; + const negatedAttributes = /* @__PURE__ */ new Map(); + for (const key in attributes) { + const val = attributes[key]; + const currentVal = currPos.currentAttributes.get(key) ?? null; + if (!equalAttrs(currentVal, val)) { + negatedAttributes.set(key, currentVal); + const { left, right } = currPos; + currPos.right = new Item(createID(ownClientId, getState(doc2.store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentFormat(key, val)); + currPos.right.integrate(transaction, 0); + currPos.forward(); + } + } + return negatedAttributes; + }; + var insertText = (transaction, parent, currPos, text2, attributes) => { + currPos.currentAttributes.forEach((_val, key) => { + if (attributes[key] === void 0) { + attributes[key] = null; + } + }); + const doc2 = transaction.doc; + const ownClientId = doc2.clientID; + minimizeAttributeChanges(currPos, attributes); + const negatedAttributes = insertAttributes(transaction, parent, currPos, attributes); + const content = text2.constructor === String ? new ContentString( + /** @type {string} */ + text2 + ) : text2 instanceof AbstractType ? new ContentType(text2) : new ContentEmbed(text2); + let { left, right, index } = currPos; + if (parent._searchMarker) { + updateMarkerChanges(parent._searchMarker, currPos.index, content.getLength()); + } + right = new Item(createID(ownClientId, getState(doc2.store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, content); + right.integrate(transaction, 0); + currPos.right = right; + currPos.index = index; + currPos.forward(); + insertNegatedAttributes(transaction, parent, currPos, negatedAttributes); + }; + var formatText = (transaction, parent, currPos, length3, attributes) => { + const doc2 = transaction.doc; + const ownClientId = doc2.clientID; + minimizeAttributeChanges(currPos, attributes); + const negatedAttributes = insertAttributes(transaction, parent, currPos, attributes); + iterationLoop: while (currPos.right !== null && (length3 > 0 || negatedAttributes.size > 0 && (currPos.right.deleted || currPos.right.content.constructor === ContentFormat))) { + if (!currPos.right.deleted) { + switch (currPos.right.content.constructor) { + case ContentFormat: { + const { key, value } = ( + /** @type {ContentFormat} */ + currPos.right.content + ); + const attr = attributes[key]; + if (attr !== void 0) { + if (equalAttrs(attr, value)) { + negatedAttributes.delete(key); + } else { + if (length3 === 0) { + break iterationLoop; + } + negatedAttributes.set(key, value); + } + currPos.right.delete(transaction); + } else { + currPos.currentAttributes.set(key, value); + } + break; + } + default: + if (length3 < currPos.right.length) { + getItemCleanStart(transaction, createID(currPos.right.id.client, currPos.right.id.clock + length3)); + } + length3 -= currPos.right.length; + break; + } + } + currPos.forward(); + } + if (length3 > 0) { + let newlines = ""; + for (; length3 > 0; length3--) { + newlines += "\n"; + } + currPos.right = new Item(createID(ownClientId, getState(doc2.store, ownClientId)), currPos.left, currPos.left && currPos.left.lastId, currPos.right, currPos.right && currPos.right.id, parent, null, new ContentString(newlines)); + currPos.right.integrate(transaction, 0); + currPos.forward(); + } + insertNegatedAttributes(transaction, parent, currPos, negatedAttributes); + }; + var cleanupFormattingGap = (transaction, start, curr, startAttributes, currAttributes) => { + let end = start; + const endFormats = create(); + while (end && (!end.countable || end.deleted)) { + if (!end.deleted && end.content.constructor === ContentFormat) { + const cf = ( + /** @type {ContentFormat} */ + end.content + ); + endFormats.set(cf.key, cf); + } + end = end.right; + } + let cleanups = 0; + let reachedCurr = false; + while (start !== end) { + if (curr === start) { + reachedCurr = true; + } + if (!start.deleted) { + const content = start.content; + switch (content.constructor) { + case ContentFormat: { + const { key, value } = ( + /** @type {ContentFormat} */ + content + ); + const startAttrValue = startAttributes.get(key) ?? null; + if (endFormats.get(key) !== content || startAttrValue === value) { + start.delete(transaction); + cleanups++; + if (!reachedCurr && (currAttributes.get(key) ?? null) === value && startAttrValue !== value) { + if (startAttrValue === null) { + currAttributes.delete(key); + } else { + currAttributes.set(key, startAttrValue); + } + } + } + if (!reachedCurr && !start.deleted) { + updateCurrentAttributes( + currAttributes, + /** @type {ContentFormat} */ + content + ); + } + break; + } + } + } + start = /** @type {Item} */ + start.right; + } + return cleanups; + }; + var cleanupContextlessFormattingGap = (transaction, item) => { + while (item && item.right && (item.right.deleted || !item.right.countable)) { + item = item.right; + } + const attrs = /* @__PURE__ */ new Set(); + while (item && (item.deleted || !item.countable)) { + if (!item.deleted && item.content.constructor === ContentFormat) { + const key = ( + /** @type {ContentFormat} */ + item.content.key + ); + if (attrs.has(key)) { + item.delete(transaction); + } else { + attrs.add(key); + } + } + item = item.left; + } + }; + var cleanupYTextFormatting = (type) => { + let res = 0; + transact( + /** @type {Doc} */ + type.doc, + (transaction) => { + let start = ( + /** @type {Item} */ + type._start + ); + let end = type._start; + let startAttributes = create(); + const currentAttributes = copy(startAttributes); + while (end) { + if (end.deleted === false) { + switch (end.content.constructor) { + case ContentFormat: + updateCurrentAttributes( + currentAttributes, + /** @type {ContentFormat} */ + end.content + ); + break; + default: + res += cleanupFormattingGap(transaction, start, end, startAttributes, currentAttributes); + startAttributes = copy(currentAttributes); + start = end; + break; + } + } + end = end.right; + } + } + ); + return res; + }; + var cleanupYTextAfterTransaction = (transaction) => { + const needFullCleanup = /* @__PURE__ */ new Set(); + const doc2 = transaction.doc; + for (const [client, afterClock] of transaction.afterState.entries()) { + const clock = transaction.beforeState.get(client) || 0; + if (afterClock === clock) { + continue; + } + iterateStructs( + transaction, + /** @type {Array} */ + doc2.store.clients.get(client), + clock, + afterClock, + (item) => { + if (!item.deleted && /** @type {Item} */ + item.content.constructor === ContentFormat && item.constructor !== GC) { + needFullCleanup.add( + /** @type {any} */ + item.parent + ); + } + } + ); + } + transact(doc2, (t) => { + iterateDeletedStructs(transaction, transaction.deleteSet, (item) => { + if (item instanceof GC || !/** @type {YText} */ + item.parent._hasFormatting || needFullCleanup.has( + /** @type {YText} */ + item.parent + )) { + return; + } + const parent = ( + /** @type {YText} */ + item.parent + ); + if (item.content.constructor === ContentFormat) { + needFullCleanup.add(parent); + } else { + cleanupContextlessFormattingGap(t, item); + } + }); + for (const yText of needFullCleanup) { + cleanupYTextFormatting(yText); + } + }); + }; + var deleteText = (transaction, currPos, length3) => { + const startLength = length3; + const startAttrs = copy(currPos.currentAttributes); + const start = currPos.right; + while (length3 > 0 && currPos.right !== null) { + if (currPos.right.deleted === false) { + switch (currPos.right.content.constructor) { + case ContentType: + case ContentEmbed: + case ContentString: + if (length3 < currPos.right.length) { + getItemCleanStart(transaction, createID(currPos.right.id.client, currPos.right.id.clock + length3)); + } + length3 -= currPos.right.length; + currPos.right.delete(transaction); + break; + } + } + currPos.forward(); + } + if (start) { + cleanupFormattingGap(transaction, start, currPos.right, startAttrs, currPos.currentAttributes); + } + const parent = ( + /** @type {AbstractType} */ + /** @type {Item} */ + (currPos.left || currPos.right).parent + ); + if (parent._searchMarker) { + updateMarkerChanges(parent._searchMarker, currPos.index, -startLength + length3); + } + return currPos; + }; + var YTextEvent = class extends YEvent { + /** + * @param {YText} ytext + * @param {Transaction} transaction + * @param {Set} subs The keys that changed + */ + constructor(ytext, transaction, subs) { + super(ytext, transaction); + this.childListChanged = false; + this.keysChanged = /* @__PURE__ */ new Set(); + subs.forEach((sub) => { + if (sub === null) { + this.childListChanged = true; + } else { + this.keysChanged.add(sub); + } + }); + } + /** + * @type {{added:Set,deleted:Set,keys:Map,delta:Array<{insert?:Array|string, delete?:number, retain?:number}>}} + */ + get changes() { + if (this._changes === null) { + const changes = { + keys: this.keys, + delta: this.delta, + added: /* @__PURE__ */ new Set(), + deleted: /* @__PURE__ */ new Set() + }; + this._changes = changes; + } + return ( + /** @type {any} */ + this._changes + ); + } + /** + * Compute the changes in the delta format. + * A {@link https://quilljs.com/docs/delta/|Quill Delta}) that represents the changes on the document. + * + * @type {Array<{insert?:string|object|AbstractType, delete?:number, retain?:number, attributes?: Object}>} + * + * @public + */ + get delta() { + if (this._delta === null) { + const y = ( + /** @type {Doc} */ + this.target.doc + ); + const delta = []; + transact(y, (transaction) => { + const currentAttributes = /* @__PURE__ */ new Map(); + const oldAttributes = /* @__PURE__ */ new Map(); + let item = this.target._start; + let action = null; + const attributes = {}; + let insert = ""; + let retain = 0; + let deleteLen = 0; + const addOp = () => { + if (action !== null) { + let op = null; + switch (action) { + case "delete": + if (deleteLen > 0) { + op = { delete: deleteLen }; + } + deleteLen = 0; + break; + case "insert": + if (typeof insert === "object" || insert.length > 0) { + op = { insert }; + if (currentAttributes.size > 0) { + op.attributes = {}; + currentAttributes.forEach((value, key) => { + if (value !== null) { + op.attributes[key] = value; + } + }); + } + } + insert = ""; + break; + case "retain": + if (retain > 0) { + op = { retain }; + if (!isEmpty(attributes)) { + op.attributes = assign({}, attributes); + } + } + retain = 0; + break; + } + if (op) delta.push(op); + action = null; + } + }; + while (item !== null) { + switch (item.content.constructor) { + case ContentType: + case ContentEmbed: + if (this.adds(item)) { + if (!this.deletes(item)) { + addOp(); + action = "insert"; + insert = item.content.getContent()[0]; + addOp(); + } + } else if (this.deletes(item)) { + if (action !== "delete") { + addOp(); + action = "delete"; + } + deleteLen += 1; + } else if (!item.deleted) { + if (action !== "retain") { + addOp(); + action = "retain"; + } + retain += 1; + } + break; + case ContentString: + if (this.adds(item)) { + if (!this.deletes(item)) { + if (action !== "insert") { + addOp(); + action = "insert"; + } + insert += /** @type {ContentString} */ + item.content.str; + } + } else if (this.deletes(item)) { + if (action !== "delete") { + addOp(); + action = "delete"; + } + deleteLen += item.length; + } else if (!item.deleted) { + if (action !== "retain") { + addOp(); + action = "retain"; + } + retain += item.length; + } + break; + case ContentFormat: { + const { key, value } = ( + /** @type {ContentFormat} */ + item.content + ); + if (this.adds(item)) { + if (!this.deletes(item)) { + const curVal = currentAttributes.get(key) ?? null; + if (!equalAttrs(curVal, value)) { + if (action === "retain") { + addOp(); + } + if (equalAttrs(value, oldAttributes.get(key) ?? null)) { + delete attributes[key]; + } else { + attributes[key] = value; + } + } else if (value !== null) { + item.delete(transaction); + } + } + } else if (this.deletes(item)) { + oldAttributes.set(key, value); + const curVal = currentAttributes.get(key) ?? null; + if (!equalAttrs(curVal, value)) { + if (action === "retain") { + addOp(); + } + attributes[key] = curVal; + } + } else if (!item.deleted) { + oldAttributes.set(key, value); + const attr = attributes[key]; + if (attr !== void 0) { + if (!equalAttrs(attr, value)) { + if (action === "retain") { + addOp(); + } + if (value === null) { + delete attributes[key]; + } else { + attributes[key] = value; + } + } else if (attr !== null) { + item.delete(transaction); + } + } + } + if (!item.deleted) { + if (action === "insert") { + addOp(); + } + updateCurrentAttributes( + currentAttributes, + /** @type {ContentFormat} */ + item.content + ); + } + break; + } + } + item = item.right; + } + addOp(); + while (delta.length > 0) { + const lastOp = delta[delta.length - 1]; + if (lastOp.retain !== void 0 && lastOp.attributes === void 0) { + delta.pop(); + } else { + break; + } + } + }); + this._delta = delta; + } + return ( + /** @type {any} */ + this._delta + ); + } + }; + var YText = class _YText extends AbstractType { + /** + * @param {String} [string] The initial value of the YText. + */ + constructor(string) { + super(); + this._pending = string !== void 0 ? [() => this.insert(0, string)] : []; + this._searchMarker = []; + this._hasFormatting = false; + } + /** + * Number of characters of this text type. + * + * @type {number} + */ + get length() { + this.doc ?? warnPrematureAccess(); + return this._length; + } + /** + * @param {Doc} y + * @param {Item} item + */ + _integrate(y, item) { + super._integrate(y, item); + try { + this._pending.forEach((f) => f()); + } catch (e) { + console.error(e); + } + this._pending = null; + } + _copy() { + return new _YText(); + } + /** + * Makes a copy of this data type that can be included somewhere else. + * + * Note that the content is only readable _after_ it has been included somewhere in the Ydoc. + * + * @return {YText} + */ + clone() { + const text2 = new _YText(); + text2.applyDelta(this.toDelta()); + return text2; + } + /** + * Creates YTextEvent and calls observers. + * + * @param {Transaction} transaction + * @param {Set} parentSubs Keys changed on this type. `null` if list was modified. + */ + _callObserver(transaction, parentSubs) { + super._callObserver(transaction, parentSubs); + const event = new YTextEvent(this, transaction, parentSubs); + callTypeObservers(this, transaction, event); + if (!transaction.local && this._hasFormatting) { + transaction._needFormattingCleanup = true; + } + } + /** + * Returns the unformatted string representation of this YText type. + * + * @public + */ + toString() { + this.doc ?? warnPrematureAccess(); + let str = ""; + let n = this._start; + while (n !== null) { + if (!n.deleted && n.countable && n.content.constructor === ContentString) { + str += /** @type {ContentString} */ + n.content.str; + } + n = n.right; + } + return str; + } + /** + * Returns the unformatted string representation of this YText type. + * + * @return {string} + * @public + */ + toJSON() { + return this.toString(); + } + /** + * Apply a {@link Delta} on this shared YText type. + * + * @param {Array} delta The changes to apply on this element. + * @param {object} opts + * @param {boolean} [opts.sanitize] Sanitize input delta. Removes ending newlines if set to true. + * + * + * @public + */ + applyDelta(delta, { sanitize = true } = {}) { + if (this.doc !== null) { + transact(this.doc, (transaction) => { + const currPos = new ItemTextListPosition(null, this._start, 0, /* @__PURE__ */ new Map()); + for (let i = 0; i < delta.length; i++) { + const op = delta[i]; + if (op.insert !== void 0) { + const ins = !sanitize && typeof op.insert === "string" && i === delta.length - 1 && currPos.right === null && op.insert.slice(-1) === "\n" ? op.insert.slice(0, -1) : op.insert; + if (typeof ins !== "string" || ins.length > 0) { + insertText(transaction, this, currPos, ins, op.attributes || {}); + } + } else if (op.retain !== void 0) { + formatText(transaction, this, currPos, op.retain, op.attributes || {}); + } else if (op.delete !== void 0) { + deleteText(transaction, currPos, op.delete); + } + } + }); + } else { + this._pending.push(() => this.applyDelta(delta)); + } + } + /** + * Returns the Delta representation of this YText type. + * + * @param {Snapshot} [snapshot] + * @param {Snapshot} [prevSnapshot] + * @param {function('removed' | 'added', ID):any} [computeYChange] + * @return {any} The Delta representation of this type. + * + * @public + */ + toDelta(snapshot, prevSnapshot, computeYChange) { + this.doc ?? warnPrematureAccess(); + const ops = []; + const currentAttributes = /* @__PURE__ */ new Map(); + const doc2 = ( + /** @type {Doc} */ + this.doc + ); + let str = ""; + let n = this._start; + function packStr() { + if (str.length > 0) { + const attributes = {}; + let addAttributes = false; + currentAttributes.forEach((value, key) => { + addAttributes = true; + attributes[key] = value; + }); + const op = { insert: str }; + if (addAttributes) { + op.attributes = attributes; + } + ops.push(op); + str = ""; + } + } + const computeDelta = () => { + while (n !== null) { + if (isVisible(n, snapshot) || prevSnapshot !== void 0 && isVisible(n, prevSnapshot)) { + switch (n.content.constructor) { + case ContentString: { + const cur = currentAttributes.get("ychange"); + if (snapshot !== void 0 && !isVisible(n, snapshot)) { + if (cur === void 0 || cur.user !== n.id.client || cur.type !== "removed") { + packStr(); + currentAttributes.set("ychange", computeYChange ? computeYChange("removed", n.id) : { type: "removed" }); + } + } else if (prevSnapshot !== void 0 && !isVisible(n, prevSnapshot)) { + if (cur === void 0 || cur.user !== n.id.client || cur.type !== "added") { + packStr(); + currentAttributes.set("ychange", computeYChange ? computeYChange("added", n.id) : { type: "added" }); + } + } else if (cur !== void 0) { + packStr(); + currentAttributes.delete("ychange"); + } + str += /** @type {ContentString} */ + n.content.str; + break; + } + case ContentType: + case ContentEmbed: { + packStr(); + const op = { + insert: n.content.getContent()[0] + }; + if (currentAttributes.size > 0) { + const attrs = ( + /** @type {Object} */ + {} + ); + op.attributes = attrs; + currentAttributes.forEach((value, key) => { + attrs[key] = value; + }); + } + ops.push(op); + break; + } + case ContentFormat: + if (isVisible(n, snapshot)) { + packStr(); + updateCurrentAttributes( + currentAttributes, + /** @type {ContentFormat} */ + n.content + ); + } + break; + } + } + n = n.right; + } + packStr(); + }; + if (snapshot || prevSnapshot) { + transact(doc2, (transaction) => { + if (snapshot) { + splitSnapshotAffectedStructs(transaction, snapshot); + } + if (prevSnapshot) { + splitSnapshotAffectedStructs(transaction, prevSnapshot); + } + computeDelta(); + }, "cleanup"); + } else { + computeDelta(); + } + return ops; + } + /** + * Insert text at a given index. + * + * @param {number} index The index at which to start inserting. + * @param {String} text The text to insert at the specified position. + * @param {TextAttributes} [attributes] Optionally define some formatting + * information to apply on the inserted + * Text. + * @public + */ + insert(index, text2, attributes) { + if (text2.length <= 0) { + return; + } + const y = this.doc; + if (y !== null) { + transact(y, (transaction) => { + const pos = findPosition(transaction, this, index, !attributes); + if (!attributes) { + attributes = {}; + pos.currentAttributes.forEach((v, k) => { + attributes[k] = v; + }); + } + insertText(transaction, this, pos, text2, attributes); + }); + } else { + this._pending.push(() => this.insert(index, text2, attributes)); + } + } + /** + * Inserts an embed at a index. + * + * @param {number} index The index to insert the embed at. + * @param {Object | AbstractType} embed The Object that represents the embed. + * @param {TextAttributes} [attributes] Attribute information to apply on the + * embed + * + * @public + */ + insertEmbed(index, embed, attributes) { + const y = this.doc; + if (y !== null) { + transact(y, (transaction) => { + const pos = findPosition(transaction, this, index, !attributes); + insertText(transaction, this, pos, embed, attributes || {}); + }); + } else { + this._pending.push(() => this.insertEmbed(index, embed, attributes || {})); + } + } + /** + * Deletes text starting from an index. + * + * @param {number} index Index at which to start deleting. + * @param {number} length The number of characters to remove. Defaults to 1. + * + * @public + */ + delete(index, length3) { + if (length3 === 0) { + return; + } + const y = this.doc; + if (y !== null) { + transact(y, (transaction) => { + deleteText(transaction, findPosition(transaction, this, index, true), length3); + }); + } else { + this._pending.push(() => this.delete(index, length3)); + } + } + /** + * Assigns properties to a range of text. + * + * @param {number} index The position where to start formatting. + * @param {number} length The amount of characters to assign properties to. + * @param {TextAttributes} attributes Attribute information to apply on the + * text. + * + * @public + */ + format(index, length3, attributes) { + if (length3 === 0) { + return; + } + const y = this.doc; + if (y !== null) { + transact(y, (transaction) => { + const pos = findPosition(transaction, this, index, false); + if (pos.right === null) { + return; + } + formatText(transaction, this, pos, length3, attributes); + }); + } else { + this._pending.push(() => this.format(index, length3, attributes)); + } + } + /** + * Removes an attribute. + * + * @note Xml-Text nodes don't have attributes. You can use this feature to assign properties to complete text-blocks. + * + * @param {String} attributeName The attribute name that is to be removed. + * + * @public + */ + removeAttribute(attributeName) { + if (this.doc !== null) { + transact(this.doc, (transaction) => { + typeMapDelete(transaction, this, attributeName); + }); + } else { + this._pending.push(() => this.removeAttribute(attributeName)); + } + } + /** + * Sets or updates an attribute. + * + * @note Xml-Text nodes don't have attributes. You can use this feature to assign properties to complete text-blocks. + * + * @param {String} attributeName The attribute name that is to be set. + * @param {any} attributeValue The attribute value that is to be set. + * + * @public + */ + setAttribute(attributeName, attributeValue) { + if (this.doc !== null) { + transact(this.doc, (transaction) => { + typeMapSet(transaction, this, attributeName, attributeValue); + }); + } else { + this._pending.push(() => this.setAttribute(attributeName, attributeValue)); + } + } + /** + * Returns an attribute value that belongs to the attribute name. + * + * @note Xml-Text nodes don't have attributes. You can use this feature to assign properties to complete text-blocks. + * + * @param {String} attributeName The attribute name that identifies the + * queried value. + * @return {any} The queried attribute value. + * + * @public + */ + getAttribute(attributeName) { + return ( + /** @type {any} */ + typeMapGet(this, attributeName) + ); + } + /** + * Returns all attribute name/value pairs in a JSON Object. + * + * @note Xml-Text nodes don't have attributes. You can use this feature to assign properties to complete text-blocks. + * + * @return {Object} A JSON Object that describes the attributes. + * + * @public + */ + getAttributes() { + return typeMapGetAll(this); + } + /** + * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder + */ + _write(encoder) { + encoder.writeTypeRef(YTextRefID); + } + }; + var readYText = (_decoder) => new YText(); + var YXmlTreeWalker = class { + /** + * @param {YXmlFragment | YXmlElement} root + * @param {function(AbstractType):boolean} [f] + */ + constructor(root, f = () => true) { + this._filter = f; + this._root = root; + this._currentNode = /** @type {Item} */ + root._start; + this._firstCall = true; + root.doc ?? warnPrematureAccess(); + } + [Symbol.iterator]() { + return this; + } + /** + * Get the next node. + * + * @return {IteratorResult} The next node. + * + * @public + */ + next() { + let n = this._currentNode; + let type = n && n.content && /** @type {any} */ + n.content.type; + if (n !== null && (!this._firstCall || n.deleted || !this._filter(type))) { + do { + type = /** @type {any} */ + n.content.type; + if (!n.deleted && (type.constructor === YXmlElement || type.constructor === YXmlFragment) && type._start !== null) { + n = type._start; + } else { + while (n !== null) { + const nxt = n.next; + if (nxt !== null) { + n = nxt; + break; + } else if (n.parent === this._root) { + n = null; + } else { + n = /** @type {AbstractType} */ + n.parent._item; + } + } + } + } while (n !== null && (n.deleted || !this._filter( + /** @type {ContentType} */ + n.content.type + ))); + } + this._firstCall = false; + if (n === null) { + return { value: void 0, done: true }; + } + this._currentNode = n; + return { value: ( + /** @type {any} */ + n.content.type + ), done: false }; + } + }; + var YXmlFragment = class _YXmlFragment extends AbstractType { + constructor() { + super(); + this._prelimContent = []; + } + /** + * @type {YXmlElement|YXmlText|null} + */ + get firstChild() { + const first = this._first; + return first ? first.content.getContent()[0] : null; + } + /** + * Integrate this type into the Yjs instance. + * + * * Save this struct in the os + * * This type is sent to other client + * * Observer functions are fired + * + * @param {Doc} y The Yjs instance + * @param {Item} item + */ + _integrate(y, item) { + super._integrate(y, item); + this.insert( + 0, + /** @type {Array} */ + this._prelimContent + ); + this._prelimContent = null; + } + _copy() { + return new _YXmlFragment(); + } + /** + * Makes a copy of this data type that can be included somewhere else. + * + * Note that the content is only readable _after_ it has been included somewhere in the Ydoc. + * + * @return {YXmlFragment} + */ + clone() { + const el = new _YXmlFragment(); + el.insert(0, this.toArray().map((item) => item instanceof AbstractType ? item.clone() : item)); + return el; + } + get length() { + this.doc ?? warnPrematureAccess(); + return this._prelimContent === null ? this._length : this._prelimContent.length; + } + /** + * Create a subtree of childNodes. + * + * @example + * const walker = elem.createTreeWalker(dom => dom.nodeName === 'div') + * for (let node in walker) { + * // `node` is a div node + * nop(node) + * } + * + * @param {function(AbstractType):boolean} filter Function that is called on each child element and + * returns a Boolean indicating whether the child + * is to be included in the subtree. + * @return {YXmlTreeWalker} A subtree and a position within it. + * + * @public + */ + createTreeWalker(filter) { + return new YXmlTreeWalker(this, filter); + } + /** + * Returns the first YXmlElement that matches the query. + * Similar to DOM's {@link querySelector}. + * + * Query support: + * - tagname + * TODO: + * - id + * - attribute + * + * @param {CSS_Selector} query The query on the children. + * @return {YXmlElement|YXmlText|YXmlHook|null} The first element that matches the query or null. + * + * @public + */ + querySelector(query) { + query = query.toUpperCase(); + const iterator = new YXmlTreeWalker(this, (element2) => element2.nodeName && element2.nodeName.toUpperCase() === query); + const next = iterator.next(); + if (next.done) { + return null; + } else { + return next.value; + } + } + /** + * Returns all YXmlElements that match the query. + * Similar to Dom's {@link querySelectorAll}. + * + * @todo Does not yet support all queries. Currently only query by tagName. + * + * @param {CSS_Selector} query The query on the children + * @return {Array} The elements that match this query. + * + * @public + */ + querySelectorAll(query) { + query = query.toUpperCase(); + return from(new YXmlTreeWalker(this, (element2) => element2.nodeName && element2.nodeName.toUpperCase() === query)); + } + /** + * Creates YXmlEvent and calls observers. + * + * @param {Transaction} transaction + * @param {Set} parentSubs Keys changed on this type. `null` if list was modified. + */ + _callObserver(transaction, parentSubs) { + callTypeObservers(this, transaction, new YXmlEvent(this, parentSubs, transaction)); + } + /** + * Get the string representation of all the children of this YXmlFragment. + * + * @return {string} The string representation of all children. + */ + toString() { + return typeListMap(this, (xml) => xml.toString()).join(""); + } + /** + * @return {string} + */ + toJSON() { + return this.toString(); + } + /** + * Creates a Dom Element that mirrors this YXmlElement. + * + * @param {Document} [_document=document] The document object (you must define + * this when calling this method in + * nodejs) + * @param {Object} [hooks={}] Optional property to customize how hooks + * are presented in the DOM + * @param {any} [binding] You should not set this property. This is + * used if DomBinding wants to create a + * association to the created DOM type. + * @return {Node} The {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element} + * + * @public + */ + toDOM(_document = document, hooks = {}, binding) { + const fragment = _document.createDocumentFragment(); + if (binding !== void 0) { + binding._createAssociation(fragment, this); + } + typeListForEach(this, (xmlType) => { + fragment.insertBefore(xmlType.toDOM(_document, hooks, binding), null); + }); + return fragment; + } + /** + * Inserts new content at an index. + * + * @example + * // Insert character 'a' at position 0 + * xml.insert(0, [new Y.XmlText('text')]) + * + * @param {number} index The index to insert content at + * @param {Array} content The array of content + */ + insert(index, content) { + if (this.doc !== null) { + transact(this.doc, (transaction) => { + typeListInsertGenerics(transaction, this, index, content); + }); + } else { + this._prelimContent.splice(index, 0, ...content); + } + } + /** + * Inserts new content at an index. + * + * @example + * // Insert character 'a' at position 0 + * xml.insert(0, [new Y.XmlText('text')]) + * + * @param {null|Item|YXmlElement|YXmlText} ref The index to insert content at + * @param {Array} content The array of content + */ + insertAfter(ref, content) { + if (this.doc !== null) { + transact(this.doc, (transaction) => { + const refItem = ref && ref instanceof AbstractType ? ref._item : ref; + typeListInsertGenericsAfter(transaction, this, refItem, content); + }); + } else { + const pc = ( + /** @type {Array} */ + this._prelimContent + ); + const index = ref === null ? 0 : pc.findIndex((el) => el === ref) + 1; + if (index === 0 && ref !== null) { + throw create3("Reference item not found"); + } + pc.splice(index, 0, ...content); + } + } + /** + * Deletes elements starting from an index. + * + * @param {number} index Index at which to start deleting elements + * @param {number} [length=1] The number of elements to remove. Defaults to 1. + */ + delete(index, length3 = 1) { + if (this.doc !== null) { + transact(this.doc, (transaction) => { + typeListDelete(transaction, this, index, length3); + }); + } else { + this._prelimContent.splice(index, length3); + } + } + /** + * Transforms this YArray to a JavaScript Array. + * + * @return {Array} + */ + toArray() { + return typeListToArray(this); + } + /** + * Appends content to this YArray. + * + * @param {Array} content Array of content to append. + */ + push(content) { + this.insert(this.length, content); + } + /** + * Prepends content to this YArray. + * + * @param {Array} content Array of content to prepend. + */ + unshift(content) { + this.insert(0, content); + } + /** + * Returns the i-th element from a YArray. + * + * @param {number} index The index of the element to return from the YArray + * @return {YXmlElement|YXmlText} + */ + get(index) { + return typeListGet(this, index); + } + /** + * Returns a portion of this YXmlFragment into a JavaScript Array selected + * from start to end (end not included). + * + * @param {number} [start] + * @param {number} [end] + * @return {Array} + */ + slice(start = 0, end = this.length) { + return typeListSlice(this, start, end); + } + /** + * Executes a provided function on once on every child element. + * + * @param {function(YXmlElement|YXmlText,number, typeof self):void} f A function to execute on every element of this YArray. + */ + forEach(f) { + typeListForEach(this, f); + } + /** + * Transform the properties of this type to binary and write it to an + * BinaryEncoder. + * + * This is called when this Item is sent to a remote peer. + * + * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder The encoder to write data to. + */ + _write(encoder) { + encoder.writeTypeRef(YXmlFragmentRefID); + } + }; + var readYXmlFragment = (_decoder) => new YXmlFragment(); + var YXmlElement = class _YXmlElement extends YXmlFragment { + constructor(nodeName = "UNDEFINED") { + super(); + this.nodeName = nodeName; + this._prelimAttrs = /* @__PURE__ */ new Map(); + } + /** + * @type {YXmlElement|YXmlText|null} + */ + get nextSibling() { + const n = this._item ? this._item.next : null; + return n ? ( + /** @type {YXmlElement|YXmlText} */ + /** @type {ContentType} */ + n.content.type + ) : null; + } + /** + * @type {YXmlElement|YXmlText|null} + */ + get prevSibling() { + const n = this._item ? this._item.prev : null; + return n ? ( + /** @type {YXmlElement|YXmlText} */ + /** @type {ContentType} */ + n.content.type + ) : null; + } + /** + * Integrate this type into the Yjs instance. + * + * * Save this struct in the os + * * This type is sent to other client + * * Observer functions are fired + * + * @param {Doc} y The Yjs instance + * @param {Item} item + */ + _integrate(y, item) { + super._integrate(y, item); + /** @type {Map} */ + this._prelimAttrs.forEach((value, key) => { + this.setAttribute(key, value); + }); + this._prelimAttrs = null; + } + /** + * Creates an Item with the same effect as this Item (without position effect) + * + * @return {YXmlElement} + */ + _copy() { + return new _YXmlElement(this.nodeName); + } + /** + * Makes a copy of this data type that can be included somewhere else. + * + * Note that the content is only readable _after_ it has been included somewhere in the Ydoc. + * + * @return {YXmlElement} + */ + clone() { + const el = new _YXmlElement(this.nodeName); + const attrs = this.getAttributes(); + forEach(attrs, (value, key) => { + el.setAttribute( + key, + /** @type {any} */ + value + ); + }); + el.insert(0, this.toArray().map((v) => v instanceof AbstractType ? v.clone() : v)); + return el; + } + /** + * Returns the XML serialization of this YXmlElement. + * The attributes are ordered by attribute-name, so you can easily use this + * method to compare YXmlElements + * + * @return {string} The string representation of this type. + * + * @public + */ + toString() { + const attrs = this.getAttributes(); + const stringBuilder = []; + const keys3 = []; + for (const key in attrs) { + keys3.push(key); + } + keys3.sort(); + const keysLen = keys3.length; + for (let i = 0; i < keysLen; i++) { + const key = keys3[i]; + stringBuilder.push(key + '="' + attrs[key] + '"'); + } + const nodeName = this.nodeName.toLocaleLowerCase(); + const attrsString = stringBuilder.length > 0 ? " " + stringBuilder.join(" ") : ""; + return `<${nodeName}${attrsString}>${super.toString()}`; + } + /** + * Removes an attribute from this YXmlElement. + * + * @param {string} attributeName The attribute name that is to be removed. + * + * @public + */ + removeAttribute(attributeName) { + if (this.doc !== null) { + transact(this.doc, (transaction) => { + typeMapDelete(transaction, this, attributeName); + }); + } else { + this._prelimAttrs.delete(attributeName); + } + } + /** + * Sets or updates an attribute. + * + * @template {keyof KV & string} KEY + * + * @param {KEY} attributeName The attribute name that is to be set. + * @param {KV[KEY]} attributeValue The attribute value that is to be set. + * + * @public + */ + setAttribute(attributeName, attributeValue) { + if (this.doc !== null) { + transact(this.doc, (transaction) => { + typeMapSet(transaction, this, attributeName, attributeValue); + }); + } else { + this._prelimAttrs.set(attributeName, attributeValue); + } + } + /** + * Returns an attribute value that belongs to the attribute name. + * + * @template {keyof KV & string} KEY + * + * @param {KEY} attributeName The attribute name that identifies the + * queried value. + * @return {KV[KEY]|undefined} The queried attribute value. + * + * @public + */ + getAttribute(attributeName) { + return ( + /** @type {any} */ + typeMapGet(this, attributeName) + ); + } + /** + * Returns whether an attribute exists + * + * @param {string} attributeName The attribute name to check for existence. + * @return {boolean} whether the attribute exists. + * + * @public + */ + hasAttribute(attributeName) { + return ( + /** @type {any} */ + typeMapHas(this, attributeName) + ); + } + /** + * Returns all attribute name/value pairs in a JSON Object. + * + * @param {Snapshot} [snapshot] + * @return {{ [Key in Extract]?: KV[Key]}} A JSON Object that describes the attributes. + * + * @public + */ + getAttributes(snapshot) { + return ( + /** @type {any} */ + snapshot ? typeMapGetAllSnapshot(this, snapshot) : typeMapGetAll(this) + ); + } + /** + * Creates a Dom Element that mirrors this YXmlElement. + * + * @param {Document} [_document=document] The document object (you must define + * this when calling this method in + * nodejs) + * @param {Object} [hooks={}] Optional property to customize how hooks + * are presented in the DOM + * @param {any} [binding] You should not set this property. This is + * used if DomBinding wants to create a + * association to the created DOM type. + * @return {Node} The {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element} + * + * @public + */ + toDOM(_document = document, hooks = {}, binding) { + const dom = _document.createElement(this.nodeName); + const attrs = this.getAttributes(); + for (const key in attrs) { + const value = attrs[key]; + if (typeof value === "string") { + dom.setAttribute(key, value); + } + } + typeListForEach(this, (yxml) => { + dom.appendChild(yxml.toDOM(_document, hooks, binding)); + }); + if (binding !== void 0) { + binding._createAssociation(dom, this); + } + return dom; + } + /** + * Transform the properties of this type to binary and write it to an + * BinaryEncoder. + * + * This is called when this Item is sent to a remote peer. + * + * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder The encoder to write data to. + */ + _write(encoder) { + encoder.writeTypeRef(YXmlElementRefID); + encoder.writeKey(this.nodeName); + } + }; + var readYXmlElement = (decoder) => new YXmlElement(decoder.readKey()); + var YXmlEvent = class extends YEvent { + /** + * @param {YXmlElement|YXmlText|YXmlFragment} target The target on which the event is created. + * @param {Set} subs The set of changed attributes. `null` is included if the + * child list changed. + * @param {Transaction} transaction The transaction instance with which the + * change was created. + */ + constructor(target, subs, transaction) { + super(target, transaction); + this.childListChanged = false; + this.attributesChanged = /* @__PURE__ */ new Set(); + subs.forEach((sub) => { + if (sub === null) { + this.childListChanged = true; + } else { + this.attributesChanged.add(sub); + } + }); + } + }; + var YXmlHook = class _YXmlHook extends YMap { + /** + * @param {string} hookName nodeName of the Dom Node. + */ + constructor(hookName) { + super(); + this.hookName = hookName; + } + /** + * Creates an Item with the same effect as this Item (without position effect) + */ + _copy() { + return new _YXmlHook(this.hookName); + } + /** + * Makes a copy of this data type that can be included somewhere else. + * + * Note that the content is only readable _after_ it has been included somewhere in the Ydoc. + * + * @return {YXmlHook} + */ + clone() { + const el = new _YXmlHook(this.hookName); + this.forEach((value, key) => { + el.set(key, value); + }); + return el; + } + /** + * Creates a Dom Element that mirrors this YXmlElement. + * + * @param {Document} [_document=document] The document object (you must define + * this when calling this method in + * nodejs) + * @param {Object.} [hooks] Optional property to customize how hooks + * are presented in the DOM + * @param {any} [binding] You should not set this property. This is + * used if DomBinding wants to create a + * association to the created DOM type + * @return {Element} The {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element} + * + * @public + */ + toDOM(_document = document, hooks = {}, binding) { + const hook = hooks[this.hookName]; + let dom; + if (hook !== void 0) { + dom = hook.createDom(this); + } else { + dom = document.createElement(this.hookName); + } + dom.setAttribute("data-yjs-hook", this.hookName); + if (binding !== void 0) { + binding._createAssociation(dom, this); + } + return dom; + } + /** + * Transform the properties of this type to binary and write it to an + * BinaryEncoder. + * + * This is called when this Item is sent to a remote peer. + * + * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder The encoder to write data to. + */ + _write(encoder) { + encoder.writeTypeRef(YXmlHookRefID); + encoder.writeKey(this.hookName); + } + }; + var readYXmlHook = (decoder) => new YXmlHook(decoder.readKey()); + var YXmlText = class _YXmlText extends YText { + /** + * @type {YXmlElement|YXmlText|null} + */ + get nextSibling() { + const n = this._item ? this._item.next : null; + return n ? ( + /** @type {YXmlElement|YXmlText} */ + /** @type {ContentType} */ + n.content.type + ) : null; + } + /** + * @type {YXmlElement|YXmlText|null} + */ + get prevSibling() { + const n = this._item ? this._item.prev : null; + return n ? ( + /** @type {YXmlElement|YXmlText} */ + /** @type {ContentType} */ + n.content.type + ) : null; + } + _copy() { + return new _YXmlText(); + } + /** + * Makes a copy of this data type that can be included somewhere else. + * + * Note that the content is only readable _after_ it has been included somewhere in the Ydoc. + * + * @return {YXmlText} + */ + clone() { + const text2 = new _YXmlText(); + text2.applyDelta(this.toDelta()); + return text2; + } + /** + * Creates a Dom Element that mirrors this YXmlText. + * + * @param {Document} [_document=document] The document object (you must define + * this when calling this method in + * nodejs) + * @param {Object} [hooks] Optional property to customize how hooks + * are presented in the DOM + * @param {any} [binding] You should not set this property. This is + * used if DomBinding wants to create a + * association to the created DOM type. + * @return {Text} The {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element} + * + * @public + */ + toDOM(_document = document, hooks, binding) { + const dom = _document.createTextNode(this.toString()); + if (binding !== void 0) { + binding._createAssociation(dom, this); + } + return dom; + } + toString() { + return this.toDelta().map((delta) => { + const nestedNodes = []; + for (const nodeName in delta.attributes) { + const attrs = []; + for (const key in delta.attributes[nodeName]) { + attrs.push({ key, value: delta.attributes[nodeName][key] }); + } + attrs.sort((a, b) => a.key < b.key ? -1 : 1); + nestedNodes.push({ nodeName, attrs }); + } + nestedNodes.sort((a, b) => a.nodeName < b.nodeName ? -1 : 1); + let str = ""; + for (let i = 0; i < nestedNodes.length; i++) { + const node = nestedNodes[i]; + str += `<${node.nodeName}`; + for (let j = 0; j < node.attrs.length; j++) { + const attr = node.attrs[j]; + str += ` ${attr.key}="${attr.value}"`; + } + str += ">"; + } + str += delta.insert; + for (let i = nestedNodes.length - 1; i >= 0; i--) { + str += ``; + } + return str; + }).join(""); + } + /** + * @return {string} + */ + toJSON() { + return this.toString(); + } + /** + * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder + */ + _write(encoder) { + encoder.writeTypeRef(YXmlTextRefID); + } + }; + var readYXmlText = (decoder) => new YXmlText(); + var AbstractStruct = class { + /** + * @param {ID} id + * @param {number} length + */ + constructor(id2, length3) { + this.id = id2; + this.length = length3; + } + /** + * @type {boolean} + */ + get deleted() { + throw methodUnimplemented(); + } + /** + * Merge this struct with the item to the right. + * This method is already assuming that `this.id.clock + this.length === this.id.clock`. + * Also this method does *not* remove right from StructStore! + * @param {AbstractStruct} right + * @return {boolean} whether this merged with right + */ + mergeWith(right) { + return false; + } + /** + * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder The encoder to write data to. + * @param {number} offset + * @param {number} encodingRef + */ + write(encoder, offset, encodingRef) { + throw methodUnimplemented(); + } + /** + * @param {Transaction} transaction + * @param {number} offset + */ + integrate(transaction, offset) { + throw methodUnimplemented(); + } + }; + var structGCRefNumber = 0; + var GC = class extends AbstractStruct { + get deleted() { + return true; + } + delete() { + } + /** + * @param {GC} right + * @return {boolean} + */ + mergeWith(right) { + if (this.constructor !== right.constructor) { + return false; + } + this.length += right.length; + return true; + } + /** + * @param {Transaction} transaction + * @param {number} offset + */ + integrate(transaction, offset) { + if (offset > 0) { + this.id.clock += offset; + this.length -= offset; + } + addStruct(transaction.doc.store, this); + } + /** + * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder + * @param {number} offset + */ + write(encoder, offset) { + encoder.writeInfo(structGCRefNumber); + encoder.writeLen(this.length - offset); + } + /** + * @param {Transaction} transaction + * @param {StructStore} store + * @return {null | number} + */ + getMissing(transaction, store) { + return null; + } + }; + var ContentBinary = class _ContentBinary { + /** + * @param {Uint8Array} content + */ + constructor(content) { + this.content = content; + } + /** + * @return {number} + */ + getLength() { + return 1; + } + /** + * @return {Array} + */ + getContent() { + return [this.content]; + } + /** + * @return {boolean} + */ + isCountable() { + return true; + } + /** + * @return {ContentBinary} + */ + copy() { + return new _ContentBinary(this.content); + } + /** + * @param {number} offset + * @return {ContentBinary} + */ + splice(offset) { + throw methodUnimplemented(); + } + /** + * @param {ContentBinary} right + * @return {boolean} + */ + mergeWith(right) { + return false; + } + /** + * @param {Transaction} transaction + * @param {Item} item + */ + integrate(transaction, item) { + } + /** + * @param {Transaction} transaction + */ + delete(transaction) { + } + /** + * @param {StructStore} store + */ + gc(store) { + } + /** + * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder + * @param {number} offset + */ + write(encoder, offset) { + encoder.writeBuf(this.content); + } + /** + * @return {number} + */ + getRef() { + return 3; + } + }; + var readContentBinary = (decoder) => new ContentBinary(decoder.readBuf()); + var ContentDeleted = class _ContentDeleted { + /** + * @param {number} len + */ + constructor(len) { + this.len = len; + } + /** + * @return {number} + */ + getLength() { + return this.len; + } + /** + * @return {Array} + */ + getContent() { + return []; + } + /** + * @return {boolean} + */ + isCountable() { + return false; + } + /** + * @return {ContentDeleted} + */ + copy() { + return new _ContentDeleted(this.len); + } + /** + * @param {number} offset + * @return {ContentDeleted} + */ + splice(offset) { + const right = new _ContentDeleted(this.len - offset); + this.len = offset; + return right; + } + /** + * @param {ContentDeleted} right + * @return {boolean} + */ + mergeWith(right) { + this.len += right.len; + return true; + } + /** + * @param {Transaction} transaction + * @param {Item} item + */ + integrate(transaction, item) { + addToDeleteSet(transaction.deleteSet, item.id.client, item.id.clock, this.len); + item.markDeleted(); + } + /** + * @param {Transaction} transaction + */ + delete(transaction) { + } + /** + * @param {StructStore} store + */ + gc(store) { + } + /** + * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder + * @param {number} offset + */ + write(encoder, offset) { + encoder.writeLen(this.len - offset); + } + /** + * @return {number} + */ + getRef() { + return 1; + } + }; + var readContentDeleted = (decoder) => new ContentDeleted(decoder.readLen()); + var createDocFromOpts = (guid, opts) => new Doc({ guid, ...opts, shouldLoad: opts.shouldLoad || opts.autoLoad || false }); + var ContentDoc = class _ContentDoc { + /** + * @param {Doc} doc + */ + constructor(doc2) { + if (doc2._item) { + console.error("This document was already integrated as a sub-document. You should create a second instance instead with the same guid."); + } + this.doc = doc2; + const opts = {}; + this.opts = opts; + if (!doc2.gc) { + opts.gc = false; + } + if (doc2.autoLoad) { + opts.autoLoad = true; + } + if (doc2.meta !== null) { + opts.meta = doc2.meta; + } + } + /** + * @return {number} + */ + getLength() { + return 1; + } + /** + * @return {Array} + */ + getContent() { + return [this.doc]; + } + /** + * @return {boolean} + */ + isCountable() { + return true; + } + /** + * @return {ContentDoc} + */ + copy() { + return new _ContentDoc(createDocFromOpts(this.doc.guid, this.opts)); + } + /** + * @param {number} offset + * @return {ContentDoc} + */ + splice(offset) { + throw methodUnimplemented(); + } + /** + * @param {ContentDoc} right + * @return {boolean} + */ + mergeWith(right) { + return false; + } + /** + * @param {Transaction} transaction + * @param {Item} item + */ + integrate(transaction, item) { + this.doc._item = item; + transaction.subdocsAdded.add(this.doc); + if (this.doc.shouldLoad) { + transaction.subdocsLoaded.add(this.doc); + } + } + /** + * @param {Transaction} transaction + */ + delete(transaction) { + if (transaction.subdocsAdded.has(this.doc)) { + transaction.subdocsAdded.delete(this.doc); + } else { + transaction.subdocsRemoved.add(this.doc); + } + } + /** + * @param {StructStore} store + */ + gc(store) { + } + /** + * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder + * @param {number} offset + */ + write(encoder, offset) { + encoder.writeString(this.doc.guid); + encoder.writeAny(this.opts); + } + /** + * @return {number} + */ + getRef() { + return 9; + } + }; + var readContentDoc = (decoder) => new ContentDoc(createDocFromOpts(decoder.readString(), decoder.readAny())); + var ContentEmbed = class _ContentEmbed { + /** + * @param {Object} embed + */ + constructor(embed) { + this.embed = embed; + } + /** + * @return {number} + */ + getLength() { + return 1; + } + /** + * @return {Array} + */ + getContent() { + return [this.embed]; + } + /** + * @return {boolean} + */ + isCountable() { + return true; + } + /** + * @return {ContentEmbed} + */ + copy() { + return new _ContentEmbed(this.embed); + } + /** + * @param {number} offset + * @return {ContentEmbed} + */ + splice(offset) { + throw methodUnimplemented(); + } + /** + * @param {ContentEmbed} right + * @return {boolean} + */ + mergeWith(right) { + return false; + } + /** + * @param {Transaction} transaction + * @param {Item} item + */ + integrate(transaction, item) { + } + /** + * @param {Transaction} transaction + */ + delete(transaction) { + } + /** + * @param {StructStore} store + */ + gc(store) { + } + /** + * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder + * @param {number} offset + */ + write(encoder, offset) { + encoder.writeJSON(this.embed); + } + /** + * @return {number} + */ + getRef() { + return 5; + } + }; + var readContentEmbed = (decoder) => new ContentEmbed(decoder.readJSON()); + var ContentFormat = class _ContentFormat { + /** + * @param {string} key + * @param {Object} value + */ + constructor(key, value) { + this.key = key; + this.value = value; + } + /** + * @return {number} + */ + getLength() { + return 1; + } + /** + * @return {Array} + */ + getContent() { + return []; + } + /** + * @return {boolean} + */ + isCountable() { + return false; + } + /** + * @return {ContentFormat} + */ + copy() { + return new _ContentFormat(this.key, this.value); + } + /** + * @param {number} _offset + * @return {ContentFormat} + */ + splice(_offset) { + throw methodUnimplemented(); + } + /** + * @param {ContentFormat} _right + * @return {boolean} + */ + mergeWith(_right) { + return false; + } + /** + * @param {Transaction} _transaction + * @param {Item} item + */ + integrate(_transaction, item) { + const p = ( + /** @type {YText} */ + item.parent + ); + p._searchMarker = null; + p._hasFormatting = true; + } + /** + * @param {Transaction} transaction + */ + delete(transaction) { + } + /** + * @param {StructStore} store + */ + gc(store) { + } + /** + * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder + * @param {number} offset + */ + write(encoder, offset) { + encoder.writeKey(this.key); + encoder.writeJSON(this.value); + } + /** + * @return {number} + */ + getRef() { + return 6; + } + }; + var readContentFormat = (decoder) => new ContentFormat(decoder.readKey(), decoder.readJSON()); + var ContentJSON = class _ContentJSON { + /** + * @param {Array} arr + */ + constructor(arr) { + this.arr = arr; + } + /** + * @return {number} + */ + getLength() { + return this.arr.length; + } + /** + * @return {Array} + */ + getContent() { + return this.arr; + } + /** + * @return {boolean} + */ + isCountable() { + return true; + } + /** + * @return {ContentJSON} + */ + copy() { + return new _ContentJSON(this.arr); + } + /** + * @param {number} offset + * @return {ContentJSON} + */ + splice(offset) { + const right = new _ContentJSON(this.arr.slice(offset)); + this.arr = this.arr.slice(0, offset); + return right; + } + /** + * @param {ContentJSON} right + * @return {boolean} + */ + mergeWith(right) { + this.arr = this.arr.concat(right.arr); + return true; + } + /** + * @param {Transaction} transaction + * @param {Item} item + */ + integrate(transaction, item) { + } + /** + * @param {Transaction} transaction + */ + delete(transaction) { + } + /** + * @param {StructStore} store + */ + gc(store) { + } + /** + * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder + * @param {number} offset + */ + write(encoder, offset) { + const len = this.arr.length; + encoder.writeLen(len - offset); + for (let i = offset; i < len; i++) { + const c = this.arr[i]; + encoder.writeString(c === void 0 ? "undefined" : JSON.stringify(c)); + } + } + /** + * @return {number} + */ + getRef() { + return 2; + } + }; + var readContentJSON = (decoder) => { + const len = decoder.readLen(); + const cs = []; + for (let i = 0; i < len; i++) { + const c = decoder.readString(); + if (c === "undefined") { + cs.push(void 0); + } else { + cs.push(JSON.parse(c)); + } + } + return new ContentJSON(cs); + }; + var isDevMode = getVariable("node_env") === "development"; + var ContentAny = class _ContentAny { + /** + * @param {Array} arr + */ + constructor(arr) { + this.arr = arr; + isDevMode && deepFreeze(arr); + } + /** + * @return {number} + */ + getLength() { + return this.arr.length; + } + /** + * @return {Array} + */ + getContent() { + return this.arr; + } + /** + * @return {boolean} + */ + isCountable() { + return true; + } + /** + * @return {ContentAny} + */ + copy() { + return new _ContentAny(this.arr); + } + /** + * @param {number} offset + * @return {ContentAny} + */ + splice(offset) { + const right = new _ContentAny(this.arr.slice(offset)); + this.arr = this.arr.slice(0, offset); + return right; + } + /** + * @param {ContentAny} right + * @return {boolean} + */ + mergeWith(right) { + this.arr = this.arr.concat(right.arr); + return true; + } + /** + * @param {Transaction} transaction + * @param {Item} item + */ + integrate(transaction, item) { + } + /** + * @param {Transaction} transaction + */ + delete(transaction) { + } + /** + * @param {StructStore} store + */ + gc(store) { + } + /** + * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder + * @param {number} offset + */ + write(encoder, offset) { + const len = this.arr.length; + encoder.writeLen(len - offset); + for (let i = offset; i < len; i++) { + const c = this.arr[i]; + encoder.writeAny(c); + } + } + /** + * @return {number} + */ + getRef() { + return 8; + } + }; + var readContentAny = (decoder) => { + const len = decoder.readLen(); + const cs = []; + for (let i = 0; i < len; i++) { + cs.push(decoder.readAny()); + } + return new ContentAny(cs); + }; + var ContentString = class _ContentString { + /** + * @param {string} str + */ + constructor(str) { + this.str = str; + } + /** + * @return {number} + */ + getLength() { + return this.str.length; + } + /** + * @return {Array} + */ + getContent() { + return this.str.split(""); + } + /** + * @return {boolean} + */ + isCountable() { + return true; + } + /** + * @return {ContentString} + */ + copy() { + return new _ContentString(this.str); + } + /** + * @param {number} offset + * @return {ContentString} + */ + splice(offset) { + const right = new _ContentString(this.str.slice(offset)); + this.str = this.str.slice(0, offset); + const firstCharCode = this.str.charCodeAt(offset - 1); + if (firstCharCode >= 55296 && firstCharCode <= 56319) { + this.str = this.str.slice(0, offset - 1) + "\uFFFD"; + right.str = "\uFFFD" + right.str.slice(1); + } + return right; + } + /** + * @param {ContentString} right + * @return {boolean} + */ + mergeWith(right) { + this.str += right.str; + return true; + } + /** + * @param {Transaction} transaction + * @param {Item} item + */ + integrate(transaction, item) { + } + /** + * @param {Transaction} transaction + */ + delete(transaction) { + } + /** + * @param {StructStore} store + */ + gc(store) { + } + /** + * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder + * @param {number} offset + */ + write(encoder, offset) { + encoder.writeString(offset === 0 ? this.str : this.str.slice(offset)); + } + /** + * @return {number} + */ + getRef() { + return 4; + } + }; + var readContentString = (decoder) => new ContentString(decoder.readString()); + var typeRefs = [ + readYArray, + readYMap, + readYText, + readYXmlElement, + readYXmlFragment, + readYXmlHook, + readYXmlText + ]; + var YArrayRefID = 0; + var YMapRefID = 1; + var YTextRefID = 2; + var YXmlElementRefID = 3; + var YXmlFragmentRefID = 4; + var YXmlHookRefID = 5; + var YXmlTextRefID = 6; + var ContentType = class _ContentType { + /** + * @param {AbstractType} type + */ + constructor(type) { + this.type = type; + } + /** + * @return {number} + */ + getLength() { + return 1; + } + /** + * @return {Array} + */ + getContent() { + return [this.type]; + } + /** + * @return {boolean} + */ + isCountable() { + return true; + } + /** + * @return {ContentType} + */ + copy() { + return new _ContentType(this.type._copy()); + } + /** + * @param {number} offset + * @return {ContentType} + */ + splice(offset) { + throw methodUnimplemented(); + } + /** + * @param {ContentType} right + * @return {boolean} + */ + mergeWith(right) { + return false; + } + /** + * @param {Transaction} transaction + * @param {Item} item + */ + integrate(transaction, item) { + this.type._integrate(transaction.doc, item); + } + /** + * @param {Transaction} transaction + */ + delete(transaction) { + let item = this.type._start; + while (item !== null) { + if (!item.deleted) { + item.delete(transaction); + } else if (item.id.clock < (transaction.beforeState.get(item.id.client) || 0)) { + transaction._mergeStructs.push(item); + } + item = item.right; + } + this.type._map.forEach((item2) => { + if (!item2.deleted) { + item2.delete(transaction); + } else if (item2.id.clock < (transaction.beforeState.get(item2.id.client) || 0)) { + transaction._mergeStructs.push(item2); + } + }); + transaction.changed.delete(this.type); + } + /** + * @param {StructStore} store + */ + gc(store) { + let item = this.type._start; + while (item !== null) { + item.gc(store, true); + item = item.right; + } + this.type._start = null; + this.type._map.forEach( + /** @param {Item | null} item */ + (item2) => { + while (item2 !== null) { + item2.gc(store, true); + item2 = item2.left; + } + } + ); + this.type._map = /* @__PURE__ */ new Map(); + } + /** + * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder + * @param {number} offset + */ + write(encoder, offset) { + this.type._write(encoder); + } + /** + * @return {number} + */ + getRef() { + return 7; + } + }; + var readContentType = (decoder) => new ContentType(typeRefs[decoder.readTypeRef()](decoder)); + var splitItem = (transaction, leftItem, diff) => { + const { client, clock } = leftItem.id; + const rightItem = new Item( + createID(client, clock + diff), + leftItem, + createID(client, clock + diff - 1), + leftItem.right, + leftItem.rightOrigin, + leftItem.parent, + leftItem.parentSub, + leftItem.content.splice(diff) + ); + if (leftItem.deleted) { + rightItem.markDeleted(); + } + if (leftItem.keep) { + rightItem.keep = true; + } + if (leftItem.redone !== null) { + rightItem.redone = createID(leftItem.redone.client, leftItem.redone.clock + diff); + } + leftItem.right = rightItem; + if (rightItem.right !== null) { + rightItem.right.left = rightItem; + } + transaction._mergeStructs.push(rightItem); + if (rightItem.parentSub !== null && rightItem.right === null) { + rightItem.parent._map.set(rightItem.parentSub, rightItem); + } + leftItem.length = diff; + return rightItem; + }; + var Item = class _Item extends AbstractStruct { + /** + * @param {ID} id + * @param {Item | null} left + * @param {ID | null} origin + * @param {Item | null} right + * @param {ID | null} rightOrigin + * @param {AbstractType|ID|null} parent Is a type if integrated, is null if it is possible to copy parent from left or right, is ID before integration to search for it. + * @param {string | null} parentSub + * @param {AbstractContent} content + */ + constructor(id2, left, origin, right, rightOrigin, parent, parentSub, content) { + super(id2, content.getLength()); + this.origin = origin; + this.left = left; + this.right = right; + this.rightOrigin = rightOrigin; + this.parent = parent; + this.parentSub = parentSub; + this.redone = null; + this.content = content; + this.info = this.content.isCountable() ? BIT2 : 0; + } + /** + * This is used to mark the item as an indexed fast-search marker + * + * @type {boolean} + */ + set marker(isMarked) { + if ((this.info & BIT4) > 0 !== isMarked) { + this.info ^= BIT4; + } + } + get marker() { + return (this.info & BIT4) > 0; + } + /** + * If true, do not garbage collect this Item. + */ + get keep() { + return (this.info & BIT1) > 0; + } + set keep(doKeep) { + if (this.keep !== doKeep) { + this.info ^= BIT1; + } + } + get countable() { + return (this.info & BIT2) > 0; + } + /** + * Whether this item was deleted or not. + * @type {Boolean} + */ + get deleted() { + return (this.info & BIT3) > 0; + } + set deleted(doDelete) { + if (this.deleted !== doDelete) { + this.info ^= BIT3; + } + } + markDeleted() { + this.info |= BIT3; + } + /** + * Return the creator clientID of the missing op or define missing items and return null. + * + * @param {Transaction} transaction + * @param {StructStore} store + * @return {null | number} + */ + getMissing(transaction, store) { + if (this.origin && this.origin.client !== this.id.client && this.origin.clock >= getState(store, this.origin.client)) { + return this.origin.client; + } + if (this.rightOrigin && this.rightOrigin.client !== this.id.client && this.rightOrigin.clock >= getState(store, this.rightOrigin.client)) { + return this.rightOrigin.client; + } + if (this.parent && this.parent.constructor === ID && this.id.client !== this.parent.client && this.parent.clock >= getState(store, this.parent.client)) { + return this.parent.client; + } + if (this.origin) { + this.left = getItemCleanEnd(transaction, store, this.origin); + this.origin = this.left.lastId; + } + if (this.rightOrigin) { + this.right = getItemCleanStart(transaction, this.rightOrigin); + this.rightOrigin = this.right.id; + } + if (this.left && this.left.constructor === GC || this.right && this.right.constructor === GC) { + this.parent = null; + } else if (!this.parent) { + if (this.left && this.left.constructor === _Item) { + this.parent = this.left.parent; + this.parentSub = this.left.parentSub; + } else if (this.right && this.right.constructor === _Item) { + this.parent = this.right.parent; + this.parentSub = this.right.parentSub; + } + } else if (this.parent.constructor === ID) { + const parentItem = getItem(store, this.parent); + if (parentItem.constructor === GC) { + this.parent = null; + } else { + this.parent = /** @type {ContentType} */ + parentItem.content.type; + } + } + return null; + } + /** + * @param {Transaction} transaction + * @param {number} offset + */ + integrate(transaction, offset) { + if (offset > 0) { + this.id.clock += offset; + this.left = getItemCleanEnd(transaction, transaction.doc.store, createID(this.id.client, this.id.clock - 1)); + this.origin = this.left.lastId; + this.content = this.content.splice(offset); + this.length -= offset; + } + if (this.parent) { + if (!this.left && (!this.right || this.right.left !== null) || this.left && this.left.right !== this.right) { + let left = this.left; + let o; + if (left !== null) { + o = left.right; + } else if (this.parentSub !== null) { + o = /** @type {AbstractType} */ + this.parent._map.get(this.parentSub) || null; + while (o !== null && o.left !== null) { + o = o.left; + } + } else { + o = /** @type {AbstractType} */ + this.parent._start; + } + const conflictingItems = /* @__PURE__ */ new Set(); + const itemsBeforeOrigin = /* @__PURE__ */ new Set(); + while (o !== null && o !== this.right) { + itemsBeforeOrigin.add(o); + conflictingItems.add(o); + if (compareIDs(this.origin, o.origin)) { + if (o.id.client < this.id.client) { + left = o; + conflictingItems.clear(); + } else if (compareIDs(this.rightOrigin, o.rightOrigin)) { + break; + } + } else if (o.origin !== null && itemsBeforeOrigin.has(getItem(transaction.doc.store, o.origin))) { + if (!conflictingItems.has(getItem(transaction.doc.store, o.origin))) { + left = o; + conflictingItems.clear(); + } + } else { + break; + } + o = o.right; + } + this.left = left; + } + if (this.left !== null) { + const right = this.left.right; + this.right = right; + this.left.right = this; + } else { + let r; + if (this.parentSub !== null) { + r = /** @type {AbstractType} */ + this.parent._map.get(this.parentSub) || null; + while (r !== null && r.left !== null) { + r = r.left; + } + } else { + r = /** @type {AbstractType} */ + this.parent._start; + this.parent._start = this; + } + this.right = r; + } + if (this.right !== null) { + this.right.left = this; + } else if (this.parentSub !== null) { + this.parent._map.set(this.parentSub, this); + if (this.left !== null) { + this.left.delete(transaction); + } + } + if (this.parentSub === null && this.countable && !this.deleted) { + this.parent._length += this.length; + } + addStruct(transaction.doc.store, this); + this.content.integrate(transaction, this); + addChangedTypeToTransaction( + transaction, + /** @type {AbstractType} */ + this.parent, + this.parentSub + ); + if ( + /** @type {AbstractType} */ + this.parent._item !== null && /** @type {AbstractType} */ + this.parent._item.deleted || this.parentSub !== null && this.right !== null + ) { + this.delete(transaction); + } + } else { + new GC(this.id, this.length).integrate(transaction, 0); + } + } + /** + * Returns the next non-deleted item + */ + get next() { + let n = this.right; + while (n !== null && n.deleted) { + n = n.right; + } + return n; + } + /** + * Returns the previous non-deleted item + */ + get prev() { + let n = this.left; + while (n !== null && n.deleted) { + n = n.left; + } + return n; + } + /** + * Computes the last content address of this Item. + */ + get lastId() { + return this.length === 1 ? this.id : createID(this.id.client, this.id.clock + this.length - 1); + } + /** + * Try to merge two items + * + * @param {Item} right + * @return {boolean} + */ + mergeWith(right) { + if (this.constructor === right.constructor && compareIDs(right.origin, this.lastId) && this.right === right && compareIDs(this.rightOrigin, right.rightOrigin) && this.id.client === right.id.client && this.id.clock + this.length === right.id.clock && this.deleted === right.deleted && this.redone === null && right.redone === null && this.content.constructor === right.content.constructor && this.content.mergeWith(right.content)) { + const searchMarker = ( + /** @type {AbstractType} */ + this.parent._searchMarker + ); + if (searchMarker) { + searchMarker.forEach((marker) => { + if (marker.p === right) { + marker.p = this; + if (!this.deleted && this.countable) { + marker.index -= this.length; + } + } + }); + } + if (right.keep) { + this.keep = true; + } + this.right = right.right; + if (this.right !== null) { + this.right.left = this; + } + this.length += right.length; + return true; + } + return false; + } + /** + * Mark this Item as deleted. + * + * @param {Transaction} transaction + */ + delete(transaction) { + if (!this.deleted) { + const parent = ( + /** @type {AbstractType} */ + this.parent + ); + if (this.countable && this.parentSub === null) { + parent._length -= this.length; + } + this.markDeleted(); + addToDeleteSet(transaction.deleteSet, this.id.client, this.id.clock, this.length); + addChangedTypeToTransaction(transaction, parent, this.parentSub); + this.content.delete(transaction); + } + } + /** + * @param {StructStore} store + * @param {boolean} parentGCd + */ + gc(store, parentGCd) { + if (!this.deleted) { + throw unexpectedCase(); + } + this.content.gc(store); + if (parentGCd) { + replaceStruct(store, this, new GC(this.id, this.length)); + } else { + this.content = new ContentDeleted(this.length); + } + } + /** + * Transform the properties of this type to binary and write it to an + * BinaryEncoder. + * + * This is called when this Item is sent to a remote peer. + * + * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder The encoder to write data to. + * @param {number} offset + */ + write(encoder, offset) { + const origin = offset > 0 ? createID(this.id.client, this.id.clock + offset - 1) : this.origin; + const rightOrigin = this.rightOrigin; + const parentSub = this.parentSub; + const info = this.content.getRef() & BITS5 | (origin === null ? 0 : BIT8) | // origin is defined + (rightOrigin === null ? 0 : BIT7) | // right origin is defined + (parentSub === null ? 0 : BIT6); + encoder.writeInfo(info); + if (origin !== null) { + encoder.writeLeftID(origin); + } + if (rightOrigin !== null) { + encoder.writeRightID(rightOrigin); + } + if (origin === null && rightOrigin === null) { + const parent = ( + /** @type {AbstractType} */ + this.parent + ); + if (parent._item !== void 0) { + const parentItem = parent._item; + if (parentItem === null) { + const ykey = findRootTypeKey(parent); + encoder.writeParentInfo(true); + encoder.writeString(ykey); + } else { + encoder.writeParentInfo(false); + encoder.writeLeftID(parentItem.id); + } + } else if (parent.constructor === String) { + encoder.writeParentInfo(true); + encoder.writeString(parent); + } else if (parent.constructor === ID) { + encoder.writeParentInfo(false); + encoder.writeLeftID(parent); + } else { + unexpectedCase(); + } + if (parentSub !== null) { + encoder.writeString(parentSub); + } + } + this.content.write(encoder, offset); + } + }; + var readItemContent = (decoder, info) => contentRefs[info & BITS5](decoder); + var contentRefs = [ + () => { + unexpectedCase(); + }, + // GC is not ItemContent + readContentDeleted, + // 1 + readContentJSON, + // 2 + readContentBinary, + // 3 + readContentString, + // 4 + readContentEmbed, + // 5 + readContentFormat, + // 6 + readContentType, + // 7 + readContentAny, + // 8 + readContentDoc, + // 9 + () => { + unexpectedCase(); + } + // 10 - Skip is not ItemContent + ]; + var structSkipRefNumber = 10; + var Skip = class extends AbstractStruct { + get deleted() { + return true; + } + delete() { + } + /** + * @param {Skip} right + * @return {boolean} + */ + mergeWith(right) { + if (this.constructor !== right.constructor) { + return false; + } + this.length += right.length; + return true; + } + /** + * @param {Transaction} transaction + * @param {number} offset + */ + integrate(transaction, offset) { + unexpectedCase(); + } + /** + * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder + * @param {number} offset + */ + write(encoder, offset) { + encoder.writeInfo(structSkipRefNumber); + writeVarUint(encoder.restEncoder, this.length - offset); + } + /** + * @param {Transaction} transaction + * @param {StructStore} store + * @return {null | number} + */ + getMissing(transaction, store) { + return null; + } + }; + var glo = ( + /** @type {any} */ + typeof globalThis !== "undefined" ? globalThis : typeof window !== "undefined" ? window : typeof global !== "undefined" ? global : {} + ); + var importIdentifier = "__ $YJS$ __"; + if (glo[importIdentifier] === true) { + console.error("Yjs was already imported. This breaks constructor checks and will lead to issues! - https://github.com/yjs/yjs/issues/438"); + } + glo[importIdentifier] = true; + + // node_modules/@hocuspocus/common/dist/hocuspocus-common.esm.js + var floor2 = Math.floor; + var min2 = (a, b) => a < b ? a : b; + var max2 = (a, b) => a > b ? a : b; + var BIT82 = 128; + var BITS72 = 127; + var MAX_SAFE_INTEGER2 = Number.MAX_SAFE_INTEGER; + var _encodeUtf8Polyfill2 = (str) => { + const encodedString = unescape(encodeURIComponent(str)); + const len = encodedString.length; + const buf = new Uint8Array(len); + for (let i = 0; i < len; i++) { + buf[i] = /** @type {number} */ + encodedString.codePointAt(i); + } + return buf; + }; + var utf8TextEncoder2 = ( + /** @type {TextEncoder} */ + typeof TextEncoder !== "undefined" ? new TextEncoder() : null + ); + var _encodeUtf8Native2 = (str) => utf8TextEncoder2.encode(str); + var encodeUtf82 = utf8TextEncoder2 ? _encodeUtf8Native2 : _encodeUtf8Polyfill2; + var utf8TextDecoder2 = typeof TextDecoder === "undefined" ? null : new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }); + if (utf8TextDecoder2 && utf8TextDecoder2.decode(new Uint8Array()).length === 1) { + utf8TextDecoder2 = null; + } + var write2 = (encoder, num) => { + const bufferLen = encoder.cbuf.length; + if (encoder.cpos === bufferLen) { + encoder.bufs.push(encoder.cbuf); + encoder.cbuf = new Uint8Array(bufferLen * 2); + encoder.cpos = 0; + } + encoder.cbuf[encoder.cpos++] = num; + }; + var writeVarUint2 = (encoder, num) => { + while (num > BITS72) { + write2(encoder, BIT82 | BITS72 & num); + num = floor2(num / 128); + } + write2(encoder, BITS72 & num); + }; + var _strBuffer2 = new Uint8Array(3e4); + var _maxStrBSize2 = _strBuffer2.length / 3; + var _writeVarStringNative2 = (encoder, str) => { + if (str.length < _maxStrBSize2) { + const written = utf8TextEncoder2.encodeInto(str, _strBuffer2).written || 0; + writeVarUint2(encoder, written); + for (let i = 0; i < written; i++) { + write2(encoder, _strBuffer2[i]); + } + } else { + writeVarUint8Array2(encoder, encodeUtf82(str)); + } + }; + var _writeVarStringPolyfill2 = (encoder, str) => { + const encodedString = unescape(encodeURIComponent(str)); + const len = encodedString.length; + writeVarUint2(encoder, len); + for (let i = 0; i < len; i++) { + write2( + encoder, + /** @type {number} */ + encodedString.codePointAt(i) + ); + } + }; + var writeVarString2 = utf8TextEncoder2 && /** @type {any} */ + utf8TextEncoder2.encodeInto ? _writeVarStringNative2 : _writeVarStringPolyfill2; + var writeUint8Array2 = (encoder, uint8Array) => { + const bufferLen = encoder.cbuf.length; + const cpos = encoder.cpos; + const leftCopyLen = min2(bufferLen - cpos, uint8Array.length); + const rightCopyLen = uint8Array.length - leftCopyLen; + encoder.cbuf.set(uint8Array.subarray(0, leftCopyLen), cpos); + encoder.cpos += leftCopyLen; + if (rightCopyLen > 0) { + encoder.bufs.push(encoder.cbuf); + encoder.cbuf = new Uint8Array(max2(bufferLen * 2, rightCopyLen)); + encoder.cbuf.set(uint8Array.subarray(leftCopyLen)); + encoder.cpos = rightCopyLen; + } + }; + var writeVarUint8Array2 = (encoder, uint8Array) => { + writeVarUint2(encoder, uint8Array.byteLength); + writeUint8Array2(encoder, uint8Array); + }; + var create7 = (s) => new Error(s); + var errorUnexpectedEndOfArray2 = create7("Unexpected end of array"); + var errorIntegerOutOfRange2 = create7("Integer out of Range"); + var readUint8Array2 = (decoder, len) => { + const view = new Uint8Array(decoder.arr.buffer, decoder.pos + decoder.arr.byteOffset, len); + decoder.pos += len; + return view; + }; + var readVarUint8Array2 = (decoder) => readUint8Array2(decoder, readVarUint2(decoder)); + var readUint82 = (decoder) => decoder.arr[decoder.pos++]; + var readVarUint2 = (decoder) => { + let num = 0; + let mult = 1; + const len = decoder.arr.length; + while (decoder.pos < len) { + const r = decoder.arr[decoder.pos++]; + num = num + (r & BITS72) * mult; + mult *= 128; + if (r < BIT82) { + return num; + } + if (num > MAX_SAFE_INTEGER2) { + throw errorIntegerOutOfRange2; + } + } + throw errorUnexpectedEndOfArray2; + }; + var _readVarStringPolyfill2 = (decoder) => { + let remainingLen = readVarUint2(decoder); + if (remainingLen === 0) { + return ""; + } else { + let encodedString = String.fromCodePoint(readUint82(decoder)); + if (--remainingLen < 100) { + while (remainingLen--) { + encodedString += String.fromCodePoint(readUint82(decoder)); + } + } else { + while (remainingLen > 0) { + const nextLen = remainingLen < 1e4 ? remainingLen : 1e4; + const bytes = decoder.arr.subarray(decoder.pos, decoder.pos + nextLen); + decoder.pos += nextLen; + encodedString += String.fromCodePoint.apply( + null, + /** @type {any} */ + bytes + ); + remainingLen -= nextLen; + } + } + return decodeURIComponent(escape(encodedString)); + } + }; + var _readVarStringNative2 = (decoder) => ( + /** @type any */ + utf8TextDecoder2.decode(readVarUint8Array2(decoder)) + ); + var readVarString2 = utf8TextDecoder2 ? _readVarStringNative2 : _readVarStringPolyfill2; + var AuthMessageType; + (function(AuthMessageType2) { + AuthMessageType2[AuthMessageType2["Token"] = 0] = "Token"; + AuthMessageType2[AuthMessageType2["PermissionDenied"] = 1] = "PermissionDenied"; + AuthMessageType2[AuthMessageType2["Authenticated"] = 2] = "Authenticated"; + })(AuthMessageType || (AuthMessageType = {})); + var writeAuthentication = (encoder, auth) => { + writeVarUint2(encoder, AuthMessageType.Token); + writeVarString2(encoder, auth); + }; + var readAuthMessage = (decoder, sendToken, permissionDeniedHandler, authenticatedHandler) => { + switch (readVarUint2(decoder)) { + case AuthMessageType.Token: { + sendToken(); + break; + } + case AuthMessageType.PermissionDenied: { + permissionDeniedHandler(readVarString2(decoder)); + break; + } + case AuthMessageType.Authenticated: { + authenticatedHandler(readVarString2(decoder)); + break; + } + } + }; + var awarenessStatesToArray = (states) => { + return Array.from(states.entries()).map(([key, value]) => { + return { + clientId: key, + ...value + }; + }); + }; + var WsReadyStates; + (function(WsReadyStates2) { + WsReadyStates2[WsReadyStates2["Connecting"] = 0] = "Connecting"; + WsReadyStates2[WsReadyStates2["Open"] = 1] = "Open"; + WsReadyStates2[WsReadyStates2["Closing"] = 2] = "Closing"; + WsReadyStates2[WsReadyStates2["Closed"] = 3] = "Closed"; + })(WsReadyStates || (WsReadyStates = {})); + + // node_modules/@lifeomic/attempt/dist/es6/src/index.js + function applyDefaults(options) { + if (!options) { + options = {}; + } + return { + delay: options.delay === void 0 ? 200 : options.delay, + initialDelay: options.initialDelay === void 0 ? 0 : options.initialDelay, + minDelay: options.minDelay === void 0 ? 0 : options.minDelay, + maxDelay: options.maxDelay === void 0 ? 0 : options.maxDelay, + factor: options.factor === void 0 ? 0 : options.factor, + maxAttempts: options.maxAttempts === void 0 ? 3 : options.maxAttempts, + timeout: options.timeout === void 0 ? 0 : options.timeout, + jitter: options.jitter === true, + initialJitter: options.initialJitter === true, + handleError: options.handleError === void 0 ? null : options.handleError, + handleTimeout: options.handleTimeout === void 0 ? null : options.handleTimeout, + beforeAttempt: options.beforeAttempt === void 0 ? null : options.beforeAttempt, + calculateDelay: options.calculateDelay === void 0 ? null : options.calculateDelay + }; + } + async function sleep(delay) { + return new Promise((resolve) => setTimeout(resolve, delay)); + } + function defaultCalculateDelay(context, options) { + let delay = options.delay; + if (delay === 0) { + return 0; + } + if (options.factor) { + delay *= Math.pow(options.factor, context.attemptNum - 1); + if (options.maxDelay !== 0) { + delay = Math.min(delay, options.maxDelay); + } + } + if (options.jitter) { + const min4 = Math.ceil(options.minDelay); + const max4 = Math.floor(delay); + delay = Math.floor(Math.random() * (max4 - min4 + 1)) + min4; + } + return Math.round(delay); + } + async function retry(attemptFunc, attemptOptions) { + const options = applyDefaults(attemptOptions); + for (const prop of [ + "delay", + "initialDelay", + "minDelay", + "maxDelay", + "maxAttempts", + "timeout" + ]) { + const value = options[prop]; + if (!Number.isInteger(value) || value < 0) { + throw new Error(`Value for ${prop} must be an integer greater than or equal to 0`); + } + } + if (options.factor.constructor !== Number || options.factor < 0) { + throw new Error(`Value for factor must be a number greater than or equal to 0`); + } + if (options.delay < options.minDelay) { + throw new Error(`delay cannot be less than minDelay (delay: ${options.delay}, minDelay: ${options.minDelay}`); + } + const context = { + attemptNum: 0, + attemptsRemaining: options.maxAttempts ? options.maxAttempts : -1, + aborted: false, + abort() { + context.aborted = true; + } + }; + const calculateDelay = options.calculateDelay || defaultCalculateDelay; + async function makeAttempt() { + if (options.beforeAttempt) { + options.beforeAttempt(context, options); + } + if (context.aborted) { + const err = new Error(`Attempt aborted`); + err.code = "ATTEMPT_ABORTED"; + throw err; + } + const onError = async (err) => { + if (options.handleError) { + await options.handleError(err, context, options); + } + if (context.aborted || context.attemptsRemaining === 0) { + throw err; + } + context.attemptNum++; + const delay = calculateDelay(context, options); + if (delay) { + await sleep(delay); + } + return makeAttempt(); + }; + if (context.attemptsRemaining > 0) { + context.attemptsRemaining--; + } + if (options.timeout) { + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + if (options.handleTimeout) { + try { + resolve(options.handleTimeout(context, options)); + } catch (e) { + reject(e); + } + } else { + const err = new Error(`Retry timeout (attemptNum: ${context.attemptNum}, timeout: ${options.timeout})`); + err.code = "ATTEMPT_TIMEOUT"; + reject(err); + } + }, options.timeout); + attemptFunc(context, options).then((result) => { + clearTimeout(timer); + resolve(result); + }).catch((err) => { + clearTimeout(timer); + onError(err).then(resolve).catch(reject); + }); + }); + } else { + return attemptFunc(context, options).catch(onError); + } + } + const initialDelay = options.calculateDelay ? options.calculateDelay(context, options) : options.initialDelay; + if (initialDelay) { + await sleep(initialDelay); + } + if (context.attemptNum < 1 && options.initialJitter) { + const delay = calculateDelay(context, options); + if (delay) { + await sleep(delay); + } + } + return makeAttempt(); + } + + // node_modules/@hocuspocus/provider/dist/hocuspocus-provider.esm.js + var floor3 = Math.floor; + var min3 = (a, b) => a < b ? a : b; + var max3 = (a, b) => a > b ? a : b; + var BIT72 = 64; + var BIT83 = 128; + var BITS62 = 63; + var BITS73 = 127; + var MAX_SAFE_INTEGER3 = Number.MAX_SAFE_INTEGER; + var create$2 = () => /* @__PURE__ */ new Set(); + var from2 = Array.from; + var _encodeUtf8Polyfill3 = (str) => { + const encodedString = unescape(encodeURIComponent(str)); + const len = encodedString.length; + const buf = new Uint8Array(len); + for (let i = 0; i < len; i++) { + buf[i] = /** @type {number} */ + encodedString.codePointAt(i); + } + return buf; + }; + var utf8TextEncoder3 = ( + /** @type {TextEncoder} */ + typeof TextEncoder !== "undefined" ? new TextEncoder() : null + ); + var _encodeUtf8Native3 = (str) => utf8TextEncoder3.encode(str); + var encodeUtf83 = utf8TextEncoder3 ? _encodeUtf8Native3 : _encodeUtf8Polyfill3; + var utf8TextDecoder3 = typeof TextDecoder === "undefined" ? null : new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }); + if (utf8TextDecoder3 && utf8TextDecoder3.decode(new Uint8Array()).length === 1) { + utf8TextDecoder3 = null; + } + var Encoder2 = class { + constructor() { + this.cpos = 0; + this.cbuf = new Uint8Array(100); + this.bufs = []; + } + }; + var createEncoder2 = () => new Encoder2(); + var length$1 = (encoder) => { + let len = encoder.cpos; + for (let i = 0; i < encoder.bufs.length; i++) { + len += encoder.bufs[i].length; + } + return len; + }; + var toUint8Array2 = (encoder) => { + const uint8arr = new Uint8Array(length$1(encoder)); + let curPos = 0; + for (let i = 0; i < encoder.bufs.length; i++) { + const d = encoder.bufs[i]; + uint8arr.set(d, curPos); + curPos += d.length; + } + uint8arr.set(new Uint8Array(encoder.cbuf.buffer, 0, encoder.cpos), curPos); + return uint8arr; + }; + var write3 = (encoder, num) => { + const bufferLen = encoder.cbuf.length; + if (encoder.cpos === bufferLen) { + encoder.bufs.push(encoder.cbuf); + encoder.cbuf = new Uint8Array(bufferLen * 2); + encoder.cpos = 0; + } + encoder.cbuf[encoder.cpos++] = num; + }; + var writeVarUint3 = (encoder, num) => { + while (num > BITS73) { + write3(encoder, BIT83 | BITS73 & num); + num = floor3(num / 128); + } + write3(encoder, BITS73 & num); + }; + var _strBuffer3 = new Uint8Array(3e4); + var _maxStrBSize3 = _strBuffer3.length / 3; + var _writeVarStringNative3 = (encoder, str) => { + if (str.length < _maxStrBSize3) { + const written = utf8TextEncoder3.encodeInto(str, _strBuffer3).written || 0; + writeVarUint3(encoder, written); + for (let i = 0; i < written; i++) { + write3(encoder, _strBuffer3[i]); + } + } else { + writeVarUint8Array3(encoder, encodeUtf83(str)); + } + }; + var _writeVarStringPolyfill3 = (encoder, str) => { + const encodedString = unescape(encodeURIComponent(str)); + const len = encodedString.length; + writeVarUint3(encoder, len); + for (let i = 0; i < len; i++) { + write3( + encoder, + /** @type {number} */ + encodedString.codePointAt(i) + ); + } + }; + var writeVarString3 = utf8TextEncoder3 && /** @type {any} */ + utf8TextEncoder3.encodeInto ? _writeVarStringNative3 : _writeVarStringPolyfill3; + var writeUint8Array3 = (encoder, uint8Array) => { + const bufferLen = encoder.cbuf.length; + const cpos = encoder.cpos; + const leftCopyLen = min3(bufferLen - cpos, uint8Array.length); + const rightCopyLen = uint8Array.length - leftCopyLen; + encoder.cbuf.set(uint8Array.subarray(0, leftCopyLen), cpos); + encoder.cpos += leftCopyLen; + if (rightCopyLen > 0) { + encoder.bufs.push(encoder.cbuf); + encoder.cbuf = new Uint8Array(max3(bufferLen * 2, rightCopyLen)); + encoder.cbuf.set(uint8Array.subarray(leftCopyLen)); + encoder.cpos = rightCopyLen; + } + }; + var writeVarUint8Array3 = (encoder, uint8Array) => { + writeVarUint3(encoder, uint8Array.byteLength); + writeUint8Array3(encoder, uint8Array); + }; + var create$1 = (s) => new Error(s); + var errorUnexpectedEndOfArray3 = create$1("Unexpected end of array"); + var errorIntegerOutOfRange3 = create$1("Integer out of Range"); + var Decoder2 = class { + /** + * @param {Uint8Array} uint8Array Binary data to decode + */ + constructor(uint8Array) { + this.arr = uint8Array; + this.pos = 0; + } + }; + var createDecoder2 = (uint8Array) => new Decoder2(uint8Array); + var readUint8Array3 = (decoder, len) => { + const view = new Uint8Array(decoder.arr.buffer, decoder.pos + decoder.arr.byteOffset, len); + decoder.pos += len; + return view; + }; + var readVarUint8Array3 = (decoder) => readUint8Array3(decoder, readVarUint3(decoder)); + var readUint83 = (decoder) => decoder.arr[decoder.pos++]; + var readVarUint3 = (decoder) => { + let num = 0; + let mult = 1; + const len = decoder.arr.length; + while (decoder.pos < len) { + const r = decoder.arr[decoder.pos++]; + num = num + (r & BITS73) * mult; + mult *= 128; + if (r < BIT83) { + return num; + } + if (num > MAX_SAFE_INTEGER3) { + throw errorIntegerOutOfRange3; + } + } + throw errorUnexpectedEndOfArray3; + }; + var readVarInt2 = (decoder) => { + let r = decoder.arr[decoder.pos++]; + let num = r & BITS62; + let mult = 64; + const sign = (r & BIT72) > 0 ? -1 : 1; + if ((r & BIT83) === 0) { + return sign * num; + } + const len = decoder.arr.length; + while (decoder.pos < len) { + r = decoder.arr[decoder.pos++]; + num = num + (r & BITS73) * mult; + mult *= 128; + if (r < BIT83) { + return sign * num; + } + if (num > MAX_SAFE_INTEGER3) { + throw errorIntegerOutOfRange3; + } + } + throw errorUnexpectedEndOfArray3; + }; + var _readVarStringPolyfill3 = (decoder) => { + let remainingLen = readVarUint3(decoder); + if (remainingLen === 0) { + return ""; + } else { + let encodedString = String.fromCodePoint(readUint83(decoder)); + if (--remainingLen < 100) { + while (remainingLen--) { + encodedString += String.fromCodePoint(readUint83(decoder)); + } + } else { + while (remainingLen > 0) { + const nextLen = remainingLen < 1e4 ? remainingLen : 1e4; + const bytes = decoder.arr.subarray(decoder.pos, decoder.pos + nextLen); + decoder.pos += nextLen; + encodedString += String.fromCodePoint.apply( + null, + /** @type {any} */ + bytes + ); + remainingLen -= nextLen; + } + } + return decodeURIComponent(escape(encodedString)); + } + }; + var _readVarStringNative3 = (decoder) => ( + /** @type any */ + utf8TextDecoder3.decode(readVarUint8Array3(decoder)) + ); + var readVarString3 = utf8TextDecoder3 ? _readVarStringNative3 : _readVarStringPolyfill3; + var peekVarString = (decoder) => { + const pos = decoder.pos; + const s = readVarString3(decoder); + decoder.pos = pos; + return s; + }; + var getUnixTime2 = Date.now; + var create8 = () => /* @__PURE__ */ new Map(); + var setIfUndefined2 = (map2, key, createT) => { + let set = map2.get(key); + if (set === void 0) { + map2.set(key, set = createT()); + } + return set; + }; + var Observable = class { + constructor() { + this._observers = create8(); + } + /** + * @param {N} name + * @param {function} f + */ + on(name, f) { + setIfUndefined2(this._observers, name, create$2).add(f); + } + /** + * @param {N} name + * @param {function} f + */ + once(name, f) { + const _f = (...args2) => { + this.off(name, _f); + f(...args2); + }; + this.on(name, _f); + } + /** + * @param {N} name + * @param {function} f + */ + off(name, f) { + const observers = this._observers.get(name); + if (observers !== void 0) { + observers.delete(f); + if (observers.size === 0) { + this._observers.delete(name); + } + } + } + /** + * Emit a named event. All registered event listeners that listen to the + * specified name will receive the event. + * + * @todo This should catch exceptions + * + * @param {N} name The event name. + * @param {Array} args The arguments that are applied to the event listener. + */ + emit(name, args2) { + return from2((this._observers.get(name) || create8()).values()).forEach((f) => f(...args2)); + } + destroy() { + this._observers = create8(); + } + }; + var keys2 = Object.keys; + var length2 = (obj) => keys2(obj).length; + var hasProperty2 = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key); + var equalityStrict = (a, b) => a === b; + var equalityDeep2 = (a, b) => { + if (a == null || b == null) { + return equalityStrict(a, b); + } + if (a.constructor !== b.constructor) { + return false; + } + if (a === b) { + return true; + } + switch (a.constructor) { + case ArrayBuffer: + a = new Uint8Array(a); + b = new Uint8Array(b); + // eslint-disable-next-line no-fallthrough + case Uint8Array: { + if (a.byteLength !== b.byteLength) { + return false; + } + for (let i = 0; i < a.length; i++) { + if (a[i] !== b[i]) { + return false; + } + } + break; + } + case Set: { + if (a.size !== b.size) { + return false; + } + for (const value of a) { + if (!b.has(value)) { + return false; + } + } + break; + } + case Map: { + if (a.size !== b.size) { + return false; + } + for (const key of a.keys()) { + if (!b.has(key) || !equalityDeep2(a.get(key), b.get(key))) { + return false; + } + } + break; + } + case Object: + if (length2(a) !== length2(b)) { + return false; + } + for (const key in a) { + if (!hasProperty2(a, key) || !equalityDeep2(a[key], b[key])) { + return false; + } + } + break; + case Array: + if (a.length !== b.length) { + return false; + } + for (let i = 0; i < a.length; i++) { + if (!equalityDeep2(a[i], b[i])) { + return false; + } + } + break; + default: + return false; + } + return true; + }; + var outdatedTimeout = 3e4; + var Awareness = class extends Observable { + /** + * @param {Y.Doc} doc + */ + constructor(doc2) { + super(); + this.doc = doc2; + this.clientID = doc2.clientID; + this.states = /* @__PURE__ */ new Map(); + this.meta = /* @__PURE__ */ new Map(); + this._checkInterval = /** @type {any} */ + setInterval(() => { + const now = getUnixTime2(); + if (this.getLocalState() !== null && outdatedTimeout / 2 <= now - /** @type {{lastUpdated:number}} */ + this.meta.get(this.clientID).lastUpdated) { + this.setLocalState(this.getLocalState()); + } + const remove = []; + this.meta.forEach((meta, clientid) => { + if (clientid !== this.clientID && outdatedTimeout <= now - meta.lastUpdated && this.states.has(clientid)) { + remove.push(clientid); + } + }); + if (remove.length > 0) { + removeAwarenessStates(this, remove, "timeout"); + } + }, floor3(outdatedTimeout / 10)); + doc2.on("destroy", () => { + this.destroy(); + }); + this.setLocalState({}); + } + destroy() { + this.emit("destroy", [this]); + this.setLocalState(null); + super.destroy(); + clearInterval(this._checkInterval); + } + /** + * @return {Object|null} + */ + getLocalState() { + return this.states.get(this.clientID) || null; + } + /** + * @param {Object|null} state + */ + setLocalState(state) { + const clientID = this.clientID; + const currLocalMeta = this.meta.get(clientID); + const clock = currLocalMeta === void 0 ? 0 : currLocalMeta.clock + 1; + const prevState = this.states.get(clientID); + if (state === null) { + this.states.delete(clientID); + } else { + this.states.set(clientID, state); + } + this.meta.set(clientID, { + clock, + lastUpdated: getUnixTime2() + }); + const added = []; + const updated = []; + const filteredUpdated = []; + const removed = []; + if (state === null) { + removed.push(clientID); + } else if (prevState == null) { + if (state != null) { + added.push(clientID); + } + } else { + updated.push(clientID); + if (!equalityDeep2(prevState, state)) { + filteredUpdated.push(clientID); + } + } + if (added.length > 0 || filteredUpdated.length > 0 || removed.length > 0) { + this.emit("change", [{ added, updated: filteredUpdated, removed }, "local"]); + } + this.emit("update", [{ added, updated, removed }, "local"]); + } + /** + * @param {string} field + * @param {any} value + */ + setLocalStateField(field, value) { + const state = this.getLocalState(); + if (state !== null) { + this.setLocalState({ + ...state, + [field]: value + }); + } + } + /** + * @return {Map>} + */ + getStates() { + return this.states; + } + }; + var removeAwarenessStates = (awareness, clients, origin) => { + const removed = []; + for (let i = 0; i < clients.length; i++) { + const clientID = clients[i]; + if (awareness.states.has(clientID)) { + awareness.states.delete(clientID); + if (clientID === awareness.clientID) { + const curMeta = ( + /** @type {MetaClientState} */ + awareness.meta.get(clientID) + ); + awareness.meta.set(clientID, { + clock: curMeta.clock + 1, + lastUpdated: getUnixTime2() + }); + } + removed.push(clientID); + } + } + if (removed.length > 0) { + awareness.emit("change", [{ added: [], updated: [], removed }, origin]); + awareness.emit("update", [{ added: [], updated: [], removed }, origin]); + } + }; + var encodeAwarenessUpdate = (awareness, clients, states = awareness.states) => { + const len = clients.length; + const encoder = createEncoder2(); + writeVarUint3(encoder, len); + for (let i = 0; i < len; i++) { + const clientID = clients[i]; + const state = states.get(clientID) || null; + const clock = ( + /** @type {MetaClientState} */ + awareness.meta.get(clientID).clock + ); + writeVarUint3(encoder, clientID); + writeVarUint3(encoder, clock); + writeVarString3(encoder, JSON.stringify(state)); + } + return toUint8Array2(encoder); + }; + var applyAwarenessUpdate = (awareness, update, origin) => { + const decoder = createDecoder2(update); + const timestamp = getUnixTime2(); + const added = []; + const updated = []; + const filteredUpdated = []; + const removed = []; + const len = readVarUint3(decoder); + for (let i = 0; i < len; i++) { + const clientID = readVarUint3(decoder); + let clock = readVarUint3(decoder); + const state = JSON.parse(readVarString3(decoder)); + const clientMeta = awareness.meta.get(clientID); + const prevState = awareness.states.get(clientID); + const currClock = clientMeta === void 0 ? 0 : clientMeta.clock; + if (currClock < clock || currClock === clock && state === null && awareness.states.has(clientID)) { + if (state === null) { + if (clientID === awareness.clientID && awareness.getLocalState() != null) { + clock++; + } else { + awareness.states.delete(clientID); + } + } else { + awareness.states.set(clientID, state); + } + awareness.meta.set(clientID, { + clock, + lastUpdated: timestamp + }); + if (clientMeta === void 0 && state !== null) { + added.push(clientID); + } else if (clientMeta !== void 0 && state === null) { + removed.push(clientID); + } else if (state !== null) { + if (!equalityDeep2(state, prevState)) { + filteredUpdated.push(clientID); + } + updated.push(clientID); + } + } + } + if (added.length > 0 || filteredUpdated.length > 0 || removed.length > 0) { + awareness.emit("change", [{ + added, + updated: filteredUpdated, + removed + }, origin]); + } + if (added.length > 0 || updated.length > 0 || removed.length > 0) { + awareness.emit("update", [{ + added, + updated, + removed + }, origin]); + } + }; + var EventEmitter = class { + constructor() { + this.callbacks = {}; + } + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type + on(event, fn) { + if (!this.callbacks[event]) { + this.callbacks[event] = []; + } + this.callbacks[event].push(fn); + return this; + } + emit(event, ...args2) { + const callbacks = this.callbacks[event]; + if (callbacks) { + callbacks.forEach((callback) => callback.apply(this, args2)); + } + return this; + } + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type + off(event, fn) { + const callbacks = this.callbacks[event]; + if (callbacks) { + if (fn) { + this.callbacks[event] = callbacks.filter((callback) => callback !== fn); + } else { + delete this.callbacks[event]; + } + } + return this; + } + removeAllListeners() { + this.callbacks = {}; + } + }; + var IncomingMessage = class { + constructor(data) { + this.data = data; + this.encoder = createEncoder2(); + this.decoder = createDecoder2(new Uint8Array(this.data)); + } + peekVarString() { + return peekVarString(this.decoder); + } + readVarUint() { + return readVarUint3(this.decoder); + } + readVarString() { + return readVarString3(this.decoder); + } + readVarUint8Array() { + return readVarUint8Array3(this.decoder); + } + writeVarUint(type) { + return writeVarUint3(this.encoder, type); + } + writeVarString(string) { + return writeVarString3(this.encoder, string); + } + writeVarUint8Array(data) { + return writeVarUint8Array3(this.encoder, data); + } + length() { + return length$1(this.encoder); + } + }; + var MessageType; + (function(MessageType2) { + MessageType2[MessageType2["Sync"] = 0] = "Sync"; + MessageType2[MessageType2["Awareness"] = 1] = "Awareness"; + MessageType2[MessageType2["Auth"] = 2] = "Auth"; + MessageType2[MessageType2["QueryAwareness"] = 3] = "QueryAwareness"; + MessageType2[MessageType2["Stateless"] = 5] = "Stateless"; + MessageType2[MessageType2["CLOSE"] = 7] = "CLOSE"; + MessageType2[MessageType2["SyncStatus"] = 8] = "SyncStatus"; + })(MessageType || (MessageType = {})); + var WebSocketStatus; + (function(WebSocketStatus2) { + WebSocketStatus2["Connecting"] = "connecting"; + WebSocketStatus2["Connected"] = "connected"; + WebSocketStatus2["Disconnected"] = "disconnected"; + })(WebSocketStatus || (WebSocketStatus = {})); + var OutgoingMessage = class { + constructor() { + this.encoder = createEncoder2(); + } + get(args2) { + return args2.encoder; + } + toUint8Array() { + return toUint8Array2(this.encoder); + } + }; + var CloseMessage = class extends OutgoingMessage { + constructor() { + super(...arguments); + this.type = MessageType.CLOSE; + this.description = "Ask the server to close the connection"; + } + get(args2) { + writeVarString3(this.encoder, args2.documentName); + writeVarUint3(this.encoder, this.type); + return this.encoder; + } + }; + var HocuspocusProviderWebsocket = class extends EventEmitter { + constructor(configuration) { + super(); + this.messageQueue = []; + this.configuration = { + url: "", + autoConnect: true, + preserveTrailingSlash: false, + // @ts-ignore + document: void 0, + WebSocketPolyfill: void 0, + // TODO: this should depend on awareness.outdatedTime + messageReconnectTimeout: 3e4, + // 1 second + delay: 1e3, + // instant + initialDelay: 0, + // double the delay each time + factor: 2, + // unlimited retries + maxAttempts: 0, + // wait at least 1 second + minDelay: 1e3, + // at least every 30 seconds + maxDelay: 3e4, + // randomize + jitter: true, + // retry forever + timeout: 0, + onOpen: () => null, + onConnect: () => null, + onMessage: () => null, + onOutgoingMessage: () => null, + onStatus: () => null, + onDisconnect: () => null, + onClose: () => null, + onDestroy: () => null, + onAwarenessUpdate: () => null, + onAwarenessChange: () => null, + handleTimeout: null, + providerMap: /* @__PURE__ */ new Map() + }; + this.webSocket = null; + this.webSocketHandlers = {}; + this.shouldConnect = true; + this.status = WebSocketStatus.Disconnected; + this.lastMessageReceived = 0; + this.identifier = 0; + this.intervals = { + connectionChecker: null + }; + this.connectionAttempt = null; + this.receivedOnOpenPayload = void 0; + this.closeTries = 0; + this.setConfiguration(configuration); + this.configuration.WebSocketPolyfill = configuration.WebSocketPolyfill ? configuration.WebSocketPolyfill : WebSocket; + this.on("open", this.configuration.onOpen); + this.on("open", this.onOpen.bind(this)); + this.on("connect", this.configuration.onConnect); + this.on("message", this.configuration.onMessage); + this.on("outgoingMessage", this.configuration.onOutgoingMessage); + this.on("status", this.configuration.onStatus); + this.on("disconnect", this.configuration.onDisconnect); + this.on("close", this.configuration.onClose); + this.on("destroy", this.configuration.onDestroy); + this.on("awarenessUpdate", this.configuration.onAwarenessUpdate); + this.on("awarenessChange", this.configuration.onAwarenessChange); + this.on("close", this.onClose.bind(this)); + this.on("message", this.onMessage.bind(this)); + this.intervals.connectionChecker = setInterval(this.checkConnection.bind(this), this.configuration.messageReconnectTimeout / 10); + if (this.shouldConnect) { + this.connect(); + } + } + async onOpen(event) { + this.status = WebSocketStatus.Connected; + this.emit("status", { status: WebSocketStatus.Connected }); + this.cancelWebsocketRetry = void 0; + this.receivedOnOpenPayload = event; + } + attach(provider) { + this.configuration.providerMap.set(provider.configuration.name, provider); + if (this.status === WebSocketStatus.Disconnected && this.shouldConnect) { + this.connect(); + } + if (this.receivedOnOpenPayload && this.status === WebSocketStatus.Connected) { + provider.onOpen(this.receivedOnOpenPayload); + } + } + detach(provider) { + if (this.configuration.providerMap.has(provider.configuration.name)) { + provider.send(CloseMessage, { + documentName: provider.configuration.name + }); + this.configuration.providerMap.delete(provider.configuration.name); + } + } + setConfiguration(configuration = {}) { + this.configuration = { ...this.configuration, ...configuration }; + if (!this.configuration.autoConnect) { + this.shouldConnect = false; + } + } + async connect() { + if (this.status === WebSocketStatus.Connected) { + return; + } + if (this.cancelWebsocketRetry) { + this.cancelWebsocketRetry(); + this.cancelWebsocketRetry = void 0; + } + this.receivedOnOpenPayload = void 0; + this.shouldConnect = true; + const abortableRetry = () => { + let cancelAttempt = false; + const retryPromise2 = retry(this.createWebSocketConnection.bind(this), { + delay: this.configuration.delay, + initialDelay: this.configuration.initialDelay, + factor: this.configuration.factor, + maxAttempts: this.configuration.maxAttempts, + minDelay: this.configuration.minDelay, + maxDelay: this.configuration.maxDelay, + jitter: this.configuration.jitter, + timeout: this.configuration.timeout, + handleTimeout: this.configuration.handleTimeout, + beforeAttempt: (context) => { + if (!this.shouldConnect || cancelAttempt) { + context.abort(); + } + } + }).catch((error) => { + if (error && error.code !== "ATTEMPT_ABORTED") { + throw error; + } + }); + return { + retryPromise: retryPromise2, + cancelFunc: () => { + cancelAttempt = true; + } + }; + }; + const { retryPromise, cancelFunc } = abortableRetry(); + this.cancelWebsocketRetry = cancelFunc; + return retryPromise; + } + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type + attachWebSocketListeners(ws, reject) { + const { identifier } = ws; + const onMessageHandler = (payload) => this.emit("message", payload); + const onCloseHandler = (payload) => this.emit("close", { event: payload }); + const onOpenHandler = (payload) => this.emit("open", payload); + const onErrorHandler = (err) => { + reject(err); + }; + this.webSocketHandlers[identifier] = { + message: onMessageHandler, + close: onCloseHandler, + open: onOpenHandler, + error: onErrorHandler + }; + const handlers = this.webSocketHandlers[ws.identifier]; + Object.keys(handlers).forEach((name) => { + ws.addEventListener(name, handlers[name]); + }); + } + cleanupWebSocket() { + if (!this.webSocket) { + return; + } + const { identifier } = this.webSocket; + const handlers = this.webSocketHandlers[identifier]; + Object.keys(handlers).forEach((name) => { + var _a; + (_a = this.webSocket) === null || _a === void 0 ? void 0 : _a.removeEventListener(name, handlers[name]); + delete this.webSocketHandlers[identifier]; + }); + this.webSocket.close(); + this.webSocket = null; + } + createWebSocketConnection() { + return new Promise((resolve, reject) => { + if (this.webSocket) { + this.messageQueue = []; + this.cleanupWebSocket(); + } + this.lastMessageReceived = 0; + this.identifier += 1; + const ws = new this.configuration.WebSocketPolyfill(this.url); + ws.binaryType = "arraybuffer"; + ws.identifier = this.identifier; + this.attachWebSocketListeners(ws, reject); + this.webSocket = ws; + this.status = WebSocketStatus.Connecting; + this.emit("status", { status: WebSocketStatus.Connecting }); + this.connectionAttempt = { + resolve, + reject + }; + }); + } + onMessage(event) { + var _a; + this.resolveConnectionAttempt(); + this.lastMessageReceived = getUnixTime2(); + const message = new IncomingMessage(event.data); + const documentName = message.peekVarString(); + (_a = this.configuration.providerMap.get(documentName)) === null || _a === void 0 ? void 0 : _a.onMessage(event); + } + resolveConnectionAttempt() { + if (this.connectionAttempt) { + this.connectionAttempt.resolve(); + this.connectionAttempt = null; + this.status = WebSocketStatus.Connected; + this.emit("status", { status: WebSocketStatus.Connected }); + this.emit("connect"); + this.messageQueue.forEach((message) => this.send(message)); + this.messageQueue = []; + } + } + stopConnectionAttempt() { + this.connectionAttempt = null; + } + rejectConnectionAttempt() { + var _a; + (_a = this.connectionAttempt) === null || _a === void 0 ? void 0 : _a.reject(); + this.connectionAttempt = null; + } + checkConnection() { + var _a; + if (this.status !== WebSocketStatus.Connected) { + return; + } + if (!this.lastMessageReceived) { + return; + } + if (this.configuration.messageReconnectTimeout >= getUnixTime2() - this.lastMessageReceived) { + return; + } + this.closeTries += 1; + if (this.closeTries > 2) { + this.onClose({ + event: { + code: 4408, + reason: "forced" + } + }); + this.closeTries = 0; + } else { + (_a = this.webSocket) === null || _a === void 0 ? void 0 : _a.close(); + this.messageQueue = []; + } + } + get serverUrl() { + if (this.configuration.preserveTrailingSlash) { + return this.configuration.url; + } + let url = this.configuration.url; + while (url[url.length - 1] === "/") { + url = url.slice(0, url.length - 1); + } + return url; + } + get url() { + return this.serverUrl; + } + disconnect() { + this.shouldConnect = false; + if (this.webSocket === null) { + return; + } + try { + this.webSocket.close(); + this.messageQueue = []; + } catch (e) { + console.error(e); + } + } + send(message) { + var _a; + if (((_a = this.webSocket) === null || _a === void 0 ? void 0 : _a.readyState) === WsReadyStates.Open) { + this.webSocket.send(message); + } else { + this.messageQueue.push(message); + } + } + onClose({ event }) { + this.closeTries = 0; + this.cleanupWebSocket(); + if (this.connectionAttempt) { + this.rejectConnectionAttempt(); + } + this.status = WebSocketStatus.Disconnected; + this.emit("status", { status: WebSocketStatus.Disconnected }); + this.emit("disconnect", { event }); + if (!this.cancelWebsocketRetry && this.shouldConnect) { + setTimeout(() => { + this.connect(); + }, this.configuration.delay); + } + } + destroy() { + this.emit("destroy"); + clearInterval(this.intervals.connectionChecker); + this.stopConnectionAttempt(); + this.disconnect(); + this.removeAllListeners(); + this.cleanupWebSocket(); + } + }; + var messageYjsSyncStep1 = 0; + var messageYjsSyncStep2 = 1; + var messageYjsUpdate = 2; + var writeSyncStep1 = (encoder, doc2) => { + writeVarUint3(encoder, messageYjsSyncStep1); + const sv = encodeStateVector(doc2); + writeVarUint8Array3(encoder, sv); + }; + var writeSyncStep2 = (encoder, doc2, encodedStateVector) => { + writeVarUint3(encoder, messageYjsSyncStep2); + writeVarUint8Array3(encoder, encodeStateAsUpdate(doc2, encodedStateVector)); + }; + var readSyncStep1 = (decoder, encoder, doc2) => writeSyncStep2(encoder, doc2, readVarUint8Array3(decoder)); + var readSyncStep2 = (decoder, doc2, transactionOrigin) => { + try { + applyUpdate(doc2, readVarUint8Array3(decoder), transactionOrigin); + } catch (error) { + console.error("Caught error while handling a Yjs update", error); + } + }; + var writeUpdate = (encoder, update) => { + writeVarUint3(encoder, messageYjsUpdate); + writeVarUint8Array3(encoder, update); + }; + var readUpdate = readSyncStep2; + var readSyncMessage = (decoder, encoder, doc2, transactionOrigin) => { + const messageType = readVarUint3(decoder); + switch (messageType) { + case messageYjsSyncStep1: + readSyncStep1(decoder, encoder, doc2); + break; + case messageYjsSyncStep2: + readSyncStep2(decoder, doc2, transactionOrigin); + break; + case messageYjsUpdate: + readUpdate(decoder, doc2, transactionOrigin); + break; + default: + throw new Error("Unknown message type"); + } + return messageType; + }; + var MessageReceiver = class { + constructor(message) { + this.message = message; + } + apply(provider, emitSynced) { + const { message } = this; + const type = message.readVarUint(); + const emptyMessageLength = message.length(); + switch (type) { + case MessageType.Sync: + this.applySyncMessage(provider, emitSynced); + break; + case MessageType.Awareness: + this.applyAwarenessMessage(provider); + break; + case MessageType.Auth: + this.applyAuthMessage(provider); + break; + case MessageType.QueryAwareness: + this.applyQueryAwarenessMessage(provider); + break; + case MessageType.Stateless: + provider.receiveStateless(readVarString3(message.decoder)); + break; + case MessageType.SyncStatus: + this.applySyncStatusMessage(provider, readVarInt2(message.decoder) === 1); + break; + case MessageType.CLOSE: + const event = { + code: 1e3, + reason: readVarString3(message.decoder), + // @ts-ignore + target: provider.configuration.websocketProvider.webSocket, + type: "close" + }; + provider.onClose(); + provider.configuration.onClose({ event }); + provider.forwardClose({ event }); + break; + default: + throw new Error(`Can\u2019t apply message of unknown type: ${type}`); + } + if (message.length() > emptyMessageLength + 1) { + provider.send(OutgoingMessage, { encoder: message.encoder }); + } + } + applySyncMessage(provider, emitSynced) { + const { message } = this; + message.writeVarUint(MessageType.Sync); + const syncMessageType = readSyncMessage(message.decoder, message.encoder, provider.document, provider); + if (emitSynced && syncMessageType === messageYjsSyncStep2) { + provider.synced = true; + } + } + applySyncStatusMessage(provider, applied) { + if (applied) { + provider.decrementUnsyncedChanges(); + } + } + applyAwarenessMessage(provider) { + if (!provider.awareness) + return; + const { message } = this; + applyAwarenessUpdate(provider.awareness, message.readVarUint8Array(), provider); + } + applyAuthMessage(provider) { + const { message } = this; + readAuthMessage(message.decoder, provider.sendToken.bind(provider), provider.permissionDeniedHandler.bind(provider), provider.authenticatedHandler.bind(provider)); + } + applyQueryAwarenessMessage(provider) { + if (!provider.awareness) + return; + const { message } = this; + message.writeVarUint(MessageType.Awareness); + message.writeVarUint8Array(encodeAwarenessUpdate(provider.awareness, Array.from(provider.awareness.getStates().keys()))); + } + }; + var MessageSender = class { + constructor(Message, args2 = {}) { + this.message = new Message(); + this.encoder = this.message.get(args2); + } + create() { + return toUint8Array2(this.encoder); + } + send(webSocket) { + webSocket === null || webSocket === void 0 ? void 0 : webSocket.send(this.create()); + } + }; + var AuthenticationMessage = class extends OutgoingMessage { + constructor() { + super(...arguments); + this.type = MessageType.Auth; + this.description = "Authentication"; + } + get(args2) { + if (typeof args2.token === "undefined") { + throw new Error("The authentication message requires `token` as an argument."); + } + writeVarString3(this.encoder, args2.documentName); + writeVarUint3(this.encoder, this.type); + writeAuthentication(this.encoder, args2.token); + return this.encoder; + } + }; + var AwarenessMessage = class extends OutgoingMessage { + constructor() { + super(...arguments); + this.type = MessageType.Awareness; + this.description = "Awareness states update"; + } + get(args2) { + if (typeof args2.awareness === "undefined") { + throw new Error("The awareness message requires awareness as an argument"); + } + if (typeof args2.clients === "undefined") { + throw new Error("The awareness message requires clients as an argument"); + } + writeVarString3(this.encoder, args2.documentName); + writeVarUint3(this.encoder, this.type); + let awarenessUpdate; + if (args2.states === void 0) { + awarenessUpdate = encodeAwarenessUpdate(args2.awareness, args2.clients); + } else { + awarenessUpdate = encodeAwarenessUpdate(args2.awareness, args2.clients, args2.states); + } + writeVarUint8Array3(this.encoder, awarenessUpdate); + return this.encoder; + } + }; + var StatelessMessage = class extends OutgoingMessage { + constructor() { + super(...arguments); + this.type = MessageType.Stateless; + this.description = "A stateless message"; + } + get(args2) { + var _a; + writeVarString3(this.encoder, args2.documentName); + writeVarUint3(this.encoder, this.type); + writeVarString3(this.encoder, (_a = args2.payload) !== null && _a !== void 0 ? _a : ""); + return this.encoder; + } + }; + var SyncStepOneMessage = class extends OutgoingMessage { + constructor() { + super(...arguments); + this.type = MessageType.Sync; + this.description = "First sync step"; + } + get(args2) { + if (typeof args2.document === "undefined") { + throw new Error("The sync step one message requires document as an argument"); + } + writeVarString3(this.encoder, args2.documentName); + writeVarUint3(this.encoder, this.type); + writeSyncStep1(this.encoder, args2.document); + return this.encoder; + } + }; + var UpdateMessage = class extends OutgoingMessage { + constructor() { + super(...arguments); + this.type = MessageType.Sync; + this.description = "A document update"; + } + get(args2) { + writeVarString3(this.encoder, args2.documentName); + writeVarUint3(this.encoder, this.type); + writeUpdate(this.encoder, args2.update); + return this.encoder; + } + }; + var AwarenessError = class extends Error { + constructor() { + super(...arguments); + this.code = 1001; + } + }; + var HocuspocusProvider = class extends EventEmitter { + constructor(configuration) { + var _a, _b, _c; + super(); + this.configuration = { + name: "", + // @ts-ignore + document: void 0, + // @ts-ignore + awareness: void 0, + token: null, + forceSyncInterval: false, + onAuthenticated: () => null, + onAuthenticationFailed: () => null, + onOpen: () => null, + onConnect: () => null, + onMessage: () => null, + onOutgoingMessage: () => null, + onSynced: () => null, + onStatus: () => null, + onDisconnect: () => null, + onClose: () => null, + onDestroy: () => null, + onAwarenessUpdate: () => null, + onAwarenessChange: () => null, + onStateless: () => null, + onUnsyncedChanges: () => null + }; + this.isSynced = false; + this.unsyncedChanges = 0; + this.isAuthenticated = false; + this.authorizedScope = void 0; + this.manageSocket = false; + this._isAttached = false; + this.intervals = { + forceSync: null + }; + this.boundDocumentUpdateHandler = this.documentUpdateHandler.bind(this); + this.boundAwarenessUpdateHandler = this.awarenessUpdateHandler.bind(this); + this.boundPageHide = this.pageHide.bind(this); + this.boundOnOpen = this.onOpen.bind(this); + this.boundOnClose = this.onClose.bind(this); + this.forwardConnect = () => this.emit("connect"); + this.forwardStatus = (e) => this.emit("status", e); + this.forwardClose = (e) => this.emit("close", e); + this.forwardDisconnect = (e) => this.emit("disconnect", e); + this.forwardDestroy = () => this.emit("destroy"); + this.setConfiguration(configuration); + this.configuration.document = configuration.document ? configuration.document : new Doc(); + this.configuration.awareness = configuration.awareness !== void 0 ? configuration.awareness : new Awareness(this.document); + this.on("open", this.configuration.onOpen); + this.on("message", this.configuration.onMessage); + this.on("outgoingMessage", this.configuration.onOutgoingMessage); + this.on("synced", this.configuration.onSynced); + this.on("destroy", this.configuration.onDestroy); + this.on("awarenessUpdate", this.configuration.onAwarenessUpdate); + this.on("awarenessChange", this.configuration.onAwarenessChange); + this.on("stateless", this.configuration.onStateless); + this.on("unsyncedChanges", this.configuration.onUnsyncedChanges); + this.on("authenticated", this.configuration.onAuthenticated); + this.on("authenticationFailed", this.configuration.onAuthenticationFailed); + (_a = this.awareness) === null || _a === void 0 ? void 0 : _a.on("update", () => { + this.emit("awarenessUpdate", { + states: awarenessStatesToArray(this.awareness.getStates()) + }); + }); + (_b = this.awareness) === null || _b === void 0 ? void 0 : _b.on("change", () => { + this.emit("awarenessChange", { + states: awarenessStatesToArray(this.awareness.getStates()) + }); + }); + this.document.on("update", this.boundDocumentUpdateHandler); + (_c = this.awareness) === null || _c === void 0 ? void 0 : _c.on("update", this.boundAwarenessUpdateHandler); + this.registerEventListeners(); + if (this.configuration.forceSyncInterval && typeof this.configuration.forceSyncInterval === "number") { + this.intervals.forceSync = setInterval(this.forceSync.bind(this), this.configuration.forceSyncInterval); + } + if (this.manageSocket) { + this.attach(); + } + } + setConfiguration(configuration = {}) { + if (!configuration.websocketProvider) { + this.manageSocket = true; + this.configuration.websocketProvider = new HocuspocusProviderWebsocket(configuration); + } + this.configuration = { ...this.configuration, ...configuration }; + } + get document() { + return this.configuration.document; + } + get isAttached() { + return this._isAttached; + } + get awareness() { + return this.configuration.awareness; + } + get hasUnsyncedChanges() { + return this.unsyncedChanges > 0; + } + resetUnsyncedChanges() { + this.unsyncedChanges = 1; + this.emit("unsyncedChanges", { number: this.unsyncedChanges }); + } + incrementUnsyncedChanges() { + this.unsyncedChanges += 1; + this.emit("unsyncedChanges", { number: this.unsyncedChanges }); + } + decrementUnsyncedChanges() { + if (this.unsyncedChanges > 0) { + this.unsyncedChanges -= 1; + } + if (this.unsyncedChanges === 0) { + this.synced = true; + } + this.emit("unsyncedChanges", { number: this.unsyncedChanges }); + } + forceSync() { + this.resetUnsyncedChanges(); + this.send(SyncStepOneMessage, { + document: this.document, + documentName: this.configuration.name + }); + } + pageHide() { + if (this.awareness) { + removeAwarenessStates(this.awareness, [this.document.clientID], "page hide"); + } + } + registerEventListeners() { + if (typeof window === "undefined" || !("addEventListener" in window)) { + return; + } + window.addEventListener("pagehide", this.boundPageHide); + } + sendStateless(payload) { + this.send(StatelessMessage, { + documentName: this.configuration.name, + payload + }); + } + async sendToken() { + let token; + try { + token = await this.getToken(); + } catch (error) { + this.permissionDeniedHandler(`Failed to get token during sendToken(): ${error}`); + return; + } + this.send(AuthenticationMessage, { + token: token !== null && token !== void 0 ? token : "", + documentName: this.configuration.name + }); + } + documentUpdateHandler(update, origin) { + if (origin === this) { + return; + } + this.incrementUnsyncedChanges(); + this.send(UpdateMessage, { update, documentName: this.configuration.name }); + } + awarenessUpdateHandler({ added, updated, removed }, origin) { + const changedClients = added.concat(updated).concat(removed); + this.send(AwarenessMessage, { + awareness: this.awareness, + clients: changedClients, + documentName: this.configuration.name + }); + } + /** + * Indicates whether a first handshake with the server has been established + * + * Note: this does not mean all updates from the client have been persisted to the backend. For this, + * use `hasUnsyncedChanges`. + */ + get synced() { + return this.isSynced; + } + set synced(state) { + if (this.isSynced === state) { + return; + } + this.isSynced = state; + if (state) { + this.emit("synced", { state }); + } + } + receiveStateless(payload) { + this.emit("stateless", { payload }); + } + // not needed, but provides backward compatibility with e.g. lexical/yjs + async connect() { + if (this.manageSocket) { + return this.configuration.websocketProvider.connect(); + } + console.warn("HocuspocusProvider::connect() is deprecated and does not do anything. Please connect/disconnect on the websocketProvider, or attach/deattach providers."); + } + disconnect() { + if (this.manageSocket) { + return this.configuration.websocketProvider.disconnect(); + } + console.warn("HocuspocusProvider::disconnect() is deprecated and does not do anything. Please connect/disconnect on the websocketProvider, or attach/deattach providers."); + } + async onOpen(event) { + this.isAuthenticated = false; + this.emit("open", { event }); + await this.sendToken(); + this.startSync(); + } + async getToken() { + if (typeof this.configuration.token === "function") { + const token = await this.configuration.token(); + return token; + } + return this.configuration.token; + } + startSync() { + this.resetUnsyncedChanges(); + this.send(SyncStepOneMessage, { + document: this.document, + documentName: this.configuration.name + }); + if (this.awareness && this.awareness.getLocalState() !== null) { + this.send(AwarenessMessage, { + awareness: this.awareness, + clients: [this.document.clientID], + documentName: this.configuration.name + }); + } + } + send(message, args2) { + if (!this._isAttached) + return; + const messageSender = new MessageSender(message, args2); + this.emit("outgoingMessage", { message: messageSender.message }); + messageSender.send(this.configuration.websocketProvider); + } + onMessage(event) { + const message = new IncomingMessage(event.data); + const documentName = message.readVarString(); + message.writeVarString(documentName); + this.emit("message", { event, message: new IncomingMessage(event.data) }); + new MessageReceiver(message).apply(this, true); + } + onClose() { + this.isAuthenticated = false; + this.synced = false; + if (this.awareness) { + removeAwarenessStates(this.awareness, Array.from(this.awareness.getStates().keys()).filter((client) => client !== this.document.clientID), this); + } + } + destroy() { + this.emit("destroy"); + if (this.intervals.forceSync) { + clearInterval(this.intervals.forceSync); + } + if (this.awareness) { + removeAwarenessStates(this.awareness, [this.document.clientID], "provider destroy"); + this.awareness.off("update", this.boundAwarenessUpdateHandler); + this.awareness.destroy(); + } + this.document.off("update", this.boundDocumentUpdateHandler); + this.removeAllListeners(); + this.detach(); + if (this.manageSocket) { + this.configuration.websocketProvider.destroy(); + } + if (typeof window === "undefined" || !("removeEventListener" in window)) { + return; + } + window.removeEventListener("pagehide", this.boundPageHide); + } + detach() { + this.configuration.websocketProvider.off("connect", this.configuration.onConnect); + this.configuration.websocketProvider.off("connect", this.forwardConnect); + this.configuration.websocketProvider.off("status", this.forwardStatus); + this.configuration.websocketProvider.off("status", this.configuration.onStatus); + this.configuration.websocketProvider.off("open", this.boundOnOpen); + this.configuration.websocketProvider.off("close", this.boundOnClose); + this.configuration.websocketProvider.off("close", this.configuration.onClose); + this.configuration.websocketProvider.off("close", this.forwardClose); + this.configuration.websocketProvider.off("disconnect", this.configuration.onDisconnect); + this.configuration.websocketProvider.off("disconnect", this.forwardDisconnect); + this.configuration.websocketProvider.off("destroy", this.configuration.onDestroy); + this.configuration.websocketProvider.off("destroy", this.forwardDestroy); + this.configuration.websocketProvider.detach(this); + this._isAttached = false; + } + attach() { + if (this._isAttached) + return; + this.configuration.websocketProvider.on("connect", this.configuration.onConnect); + this.configuration.websocketProvider.on("connect", this.forwardConnect); + this.configuration.websocketProvider.on("status", this.configuration.onStatus); + this.configuration.websocketProvider.on("status", this.forwardStatus); + this.configuration.websocketProvider.on("open", this.boundOnOpen); + this.configuration.websocketProvider.on("close", this.boundOnClose); + this.configuration.websocketProvider.on("close", this.configuration.onClose); + this.configuration.websocketProvider.on("close", this.forwardClose); + this.configuration.websocketProvider.on("disconnect", this.configuration.onDisconnect); + this.configuration.websocketProvider.on("disconnect", this.forwardDisconnect); + this.configuration.websocketProvider.on("destroy", this.configuration.onDestroy); + this.configuration.websocketProvider.on("destroy", this.forwardDestroy); + this.configuration.websocketProvider.attach(this); + this._isAttached = true; + } + permissionDeniedHandler(reason) { + this.emit("authenticationFailed", { reason }); + this.isAuthenticated = false; + } + authenticatedHandler(scope) { + this.isAuthenticated = true; + this.authorizedScope = scope; + this.emit("authenticated", { scope }); + } + setAwarenessField(key, value) { + if (!this.awareness) { + throw new AwarenessError(`Cannot set awareness field "${key}" to ${JSON.stringify(value)}. You have disabled Awareness for this provider by explicitly passing awareness: null in the provider configuration.`); + } + this.awareness.setLocalStateField(key, value); + } + }; + + // src/bridge/cursor-presence/userColor.ts + var CURSOR_PALETTE = [ + "#E53935", + "#1E88E5", + "#43A047", + "#FB8C00", + "#8E24AA", + "#00ACC1", + "#F4511E", + "#3949AB", + "#7CB342", + "#D81B60", + "#6D4C41", + "#546E7A" + ]; + function hashString(input) { + let hash = 2166136261; + for (let i = 0; i < input.length; i += 1) { + hash ^= input.charCodeAt(i); + hash = Math.imul(hash, 16777619); + } + return hash >>> 0; + } + function getUserColor(userId) { + const normalized = userId.trim() || "anonymous"; + return CURSOR_PALETTE[hashString(normalized) % CURSOR_PALETTE.length]; + } + + // src/bridge/pointer-presence/PointerOverlay.ts + var PointerOverlay = class { + constructor() { + this.cursors = /* @__PURE__ */ new Map(); + this.selections = /* @__PURE__ */ new Map(); + this.showRemoteCursors = true; + this.destroyed = false; + this.root = document.createElement("div"); + this.root.id = "lowcoder-pointer-overlay"; + Object.assign(this.root.style, { + position: "fixed", + inset: "0", + pointerEvents: "none", + zIndex: "2147483646", + overflow: "hidden" + }); + document.documentElement.appendChild(this.root); + } + setShowRemoteCursors(show) { + this.showRemoteCursors = show; + if (!show) { + for (const id2 of Array.from(this.cursors.keys())) { + this.removeCursor(id2); + } + } + } + syncFromStates(states) { + if (this.destroyed) return; + const seenCursors = /* @__PURE__ */ new Set(); + const seenSelections = /* @__PURE__ */ new Set(); + for (const state of states) { + const id2 = state.user.id; + if (this.showRemoteCursors && state.pointer) { + seenCursors.add(id2); + this.upsertCursor(id2, state.user.name, state.user.color, state.pointer); + } + if (state.selection && state.selection.rects.length > 0) { + seenSelections.add(id2); + this.upsertSelection(id2, state.user.name, state.user.color, state.selection); + } + } + for (const id2 of Array.from(this.cursors.keys())) { + if (!seenCursors.has(id2)) this.removeCursor(id2); + } + for (const id2 of Array.from(this.selections.keys())) { + if (!seenSelections.has(id2)) this.removeSelection(id2); + } + } + showClickRipple(xRatio, yRatio, color = "#1E88E5") { + if (this.destroyed) return; + const ripple = document.createElement("div"); + const x = clamp(xRatio, 0, 1) * window.innerWidth; + const y = clamp(yRatio, 0, 1) * window.innerHeight; + Object.assign(ripple.style, { + position: "absolute", + left: `${x}px`, + top: `${y}px`, + width: "12px", + height: "12px", + marginLeft: "-6px", + marginTop: "-6px", + borderRadius: "50%", + border: `2px solid ${color}`, + background: `${color}33`, + transform: "scale(0.4)", + opacity: "0.9", + transition: "transform 420ms ease-out, opacity 420ms ease-out", + pointerEvents: "none" + }); + this.root.appendChild(ripple); + requestAnimationFrame(() => { + ripple.style.transform = "scale(3.2)"; + ripple.style.opacity = "0"; + }); + window.setTimeout(() => ripple.remove(), 480); + } + showButtonClickFlash(rect, color, label) { + if (this.destroyed) return; + const flash = document.createElement("div"); + Object.assign(flash.style, { + position: "absolute", + left: `${rect.left}px`, + top: `${rect.top}px`, + width: `${Math.max(rect.width, 8)}px`, + height: `${Math.max(rect.height, 8)}px`, + borderRadius: "6px", + border: `2px solid ${color}`, + background: `${color}33`, + boxShadow: `0 0 0 3px ${color}22`, + pointerEvents: "none", + opacity: "1", + transition: "opacity 500ms ease-out" + }); + if (label) { + const badge = document.createElement("div"); + badge.textContent = label; + Object.assign(badge.style, { + position: "absolute", + left: "0", + top: "-22px", + padding: "1px 6px", + borderRadius: "4px", + background: color, + color: "#fff", + font: "11px/16px system-ui,sans-serif", + whiteSpace: "nowrap", + maxWidth: "180px", + overflow: "hidden", + textOverflow: "ellipsis" + }); + flash.appendChild(badge); + } + this.root.appendChild(flash); + window.setTimeout(() => { + flash.style.opacity = "0"; + }, 40); + window.setTimeout(() => flash.remove(), 560); + } + destroy() { + this.destroyed = true; + this.root.remove(); + this.cursors.clear(); + this.selections.clear(); + } + upsertCursor(id2, name, color, pointer) { + let el = this.cursors.get(id2); + if (!el) { + el = document.createElement("div"); + el.innerHTML = `${escapeHtml(name)}`; + Object.assign(el.style, { + position: "absolute", + left: "0", + top: "0", + transform: "translate(-2px, -2px)", + pointerEvents: "none", + display: "flex", + alignItems: "flex-start", + willChange: "left, top" + }); + this.root.appendChild(el); + this.cursors.set(id2, el); + } + const x = clamp(pointer.xRatio, 0, 1) * window.innerWidth; + const y = clamp(pointer.yRatio, 0, 1) * window.innerHeight; + el.style.left = `${x}px`; + el.style.top = `${y}px`; + } + upsertSelection(id2, name, color, selection) { + let group = this.selections.get(id2); + if (!group) { + group = document.createElement("div"); + group.dataset.selectionUser = id2; + Object.assign(group.style, { + position: "absolute", + inset: "0", + pointerEvents: "none" + }); + this.root.appendChild(group); + this.selections.set(id2, group); + } + group.innerHTML = ""; + for (const rect of selection.rects) { + group.appendChild(this.buildSelectionRect(rect, color)); + } + if (selection.rects[0]) { + const label = document.createElement("div"); + label.textContent = name; + const first = selection.rects[0]; + Object.assign(label.style, { + position: "absolute", + left: `${clamp(first.xRatio, 0, 1) * window.innerWidth}px`, + top: `${Math.max(0, clamp(first.yRatio, 0, 1) * window.innerHeight - 18)}px`, + padding: "0 5px", + borderRadius: "3px", + background: color, + color: "#fff", + font: "10px/16px system-ui,sans-serif", + whiteSpace: "nowrap" + }); + group.appendChild(label); + } + } + buildSelectionRect(rect, color) { + const el = document.createElement("div"); + Object.assign(el.style, { + position: "absolute", + left: `${clamp(rect.xRatio, 0, 1) * window.innerWidth}px`, + top: `${clamp(rect.yRatio, 0, 1) * window.innerHeight}px`, + width: `${Math.max(2, clamp(rect.wRatio, 0, 1) * window.innerWidth)}px`, + height: `${Math.max(2, clamp(rect.hRatio, 0, 1) * window.innerHeight)}px`, + background: `${color}55`, + outline: `1px solid ${color}`, + pointerEvents: "none" + }); + return el; + } + removeCursor(id2) { + const el = this.cursors.get(id2); + if (!el) return; + el.remove(); + this.cursors.delete(id2); + } + removeSelection(id2) { + const el = this.selections.get(id2); + if (!el) return; + el.remove(); + this.selections.delete(id2); + } + }; + function clamp(value, min4, max4) { + return Math.min(max4, Math.max(min4, value)); + } + function escapeHtml(value) { + return value.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); + } + + // src/bridge/pointer-presence/PointerPresenceProvider.ts + var PointerPresenceProvider = class { + constructor(provider, user, onRemoteChange, throttleMs = 33) { + this.provider = provider; + this.pendingFlush = false; + this.lastBroadcastAt = 0; + this.destroyed = false; + this.boundAwarenessChange = () => this.onRemoteChange(); + this.throttleMs = throttleMs; + this.onRemoteChange = onRemoteChange; + this.localState = { + user, + pointer: null, + selection: null + }; + this.provider.awareness?.setLocalState(this.localState); + this.provider.awareness?.on("change", this.boundAwarenessChange); + } + setLocalPointer(pointer) { + if (this.destroyed) return; + this.localState = { ...this.localState, pointer }; + this.scheduleFlush(); + } + setLocalSelection(selection) { + if (this.destroyed) return; + this.localState = { ...this.localState, selection }; + this.scheduleFlush(); + } + getRemoteStates() { + const states = []; + const awareness = this.provider.awareness; + if (!awareness) return states; + const localId = awareness.clientID; + awareness.getStates().forEach((raw, clientId) => { + if (clientId === localId) return; + const state = raw; + if (!state?.user) return; + states.push({ + user: state.user, + pointer: state.pointer ?? null, + selection: state.selection ?? null + }); + }); + return states; + } + destroy() { + this.destroyed = true; + window.clearTimeout(this.throttleTimer); + this.provider.awareness?.off("change", this.boundAwarenessChange); + this.provider.awareness?.setLocalState(null); + } + scheduleFlush() { + this.pendingFlush = true; + const now = Date.now(); + const elapsed = now - this.lastBroadcastAt; + if (elapsed >= this.throttleMs) { + this.flushBroadcast(); + return; + } + if (this.throttleTimer === void 0) { + this.throttleTimer = window.setTimeout( + () => this.flushBroadcast(), + this.throttleMs - elapsed + ); + } + } + flushBroadcast() { + if (this.destroyed) return; + window.clearTimeout(this.throttleTimer); + this.throttleTimer = void 0; + if (!this.pendingFlush) return; + this.pendingFlush = false; + this.lastBroadcastAt = Date.now(); + this.provider.awareness?.setLocalState({ ...this.localState }); + } + }; + + // src/bridge/pointer-presence/initPointerPresence.ts + var SHOW_MICE_STORAGE_KEY = "lowcoder-website-show-remote-mice"; + function initPointerPresence(config) { + const username = config.username || new URLSearchParams(window.location.search).get("username") || document.documentElement.getAttribute("data-lowcoder-username") || config.editorId; + const user = { + id: config.editorId, + name: username, + color: getUserColor(config.editorId), + role: config.role + }; + const overlay = new PointerOverlay(); + const presence = new PointerPresenceProvider(config.provider, user, () => { + overlay.syncFromStates(presence.getRemoteStates()); + }); + let showRemoteMice = readShowMicePreference(); + overlay.setShowRemoteCursors(showRemoteMice); + const chrome = createMiceToggleChrome(showRemoteMice, (next) => { + showRemoteMice = next; + writeShowMicePreference(next); + overlay.setShowRemoteCursors(next); + overlay.syncFromStates(presence.getRemoteStates()); + }); + const onPointerMove = (event) => { + if (!event.isTrusted) return; + const xRatio = window.innerWidth > 0 ? event.clientX / window.innerWidth : 0; + const yRatio = window.innerHeight > 0 ? event.clientY / window.innerHeight : 0; + presence.setLocalPointer({ + xRatio, + yRatio, + updatedAt: Date.now() + }); + }; + const onPointerLeave = () => { + presence.setLocalPointer(null); + }; + const publishLocalSelection = () => { + const selection = window.getSelection(); + if (!selection || selection.isCollapsed || selection.rangeCount === 0) { + presence.setLocalSelection(null); + return; + } + const anchorNode = selection.anchorNode; + if (anchorNode && isInsideEditable(anchorNode)) { + presence.setLocalSelection(null); + return; + } + const range = selection.getRangeAt(0); + const text2 = selection.toString().trim(); + if (!text2) { + presence.setLocalSelection(null); + return; + } + const rects = clientRectsToRatios(range.getClientRects()); + if (rects.length === 0) { + presence.setLocalSelection(null); + return; + } + presence.setLocalSelection({ + text: text2.slice(0, 200), + rects, + updatedAt: Date.now() + }); + }; + const onSelectionChange = () => { + window.requestAnimationFrame(publishLocalSelection); + }; + window.addEventListener("pointermove", onPointerMove, { passive: true }); + window.addEventListener("blur", onPointerLeave); + document.addEventListener("mouseleave", onPointerLeave); + document.addEventListener("selectionchange", onSelectionChange); + return { + showClickRipple: (xRatio, yRatio, color) => overlay.showClickRipple(xRatio, yRatio, color ?? user.color), + showButtonClickFlash: (rect, color, label) => overlay.showButtonClickFlash(rect, color, label), + destroy: () => { + window.removeEventListener("pointermove", onPointerMove); + window.removeEventListener("blur", onPointerLeave); + document.removeEventListener("mouseleave", onPointerLeave); + document.removeEventListener("selectionchange", onSelectionChange); + chrome.remove(); + presence.destroy(); + overlay.destroy(); + } + }; + } + function createMiceToggleChrome(initial, onChange) { + const chrome = document.createElement("div"); + chrome.id = "lowcoder-website-mice-toggle"; + Object.assign(chrome.style, { + position: "fixed", + left: "50%", + top: "12px", + transform: "translateX(-50%)", + zIndex: "2147483647", + display: "flex", + alignItems: "center", + gap: "8px", + padding: "8px 10px", + borderRadius: "8px", + background: "rgba(20, 24, 28, 0.88)", + color: "#fff", + font: "12px/1.3 system-ui,sans-serif", + boxShadow: "0 2px 10px rgba(0,0,0,.28)", + pointerEvents: "auto", + userSelect: "none" + }); + const label = document.createElement("label"); + Object.assign(label.style, { + display: "flex", + alignItems: "center", + gap: "6px", + cursor: "pointer" + }); + const checkbox = document.createElement("input"); + checkbox.type = "checkbox"; + checkbox.checked = initial; + checkbox.addEventListener("change", () => onChange(checkbox.checked)); + const text2 = document.createElement("span"); + text2.textContent = "Show others' mice"; + label.appendChild(checkbox); + label.appendChild(text2); + chrome.appendChild(label); + document.documentElement.appendChild(chrome); + return chrome; + } + function readShowMicePreference() { + try { + const raw = window.localStorage.getItem(SHOW_MICE_STORAGE_KEY); + if (raw === null) return true; + return raw === "1"; + } catch { + return true; + } + } + function writeShowMicePreference(show) { + try { + window.localStorage.setItem(SHOW_MICE_STORAGE_KEY, show ? "1" : "0"); + } catch { + } + } + function isInsideEditable(node) { + const el = node instanceof Element ? node : node.parentElement; + if (!el) return false; + return Boolean( + el.closest("input, textarea, select, [contenteditable=''], [contenteditable='true']") + ); + } + function clientRectsToRatios(clientRects) { + const width = window.innerWidth || 1; + const height = window.innerHeight || 1; + const rects = []; + for (let i = 0; i < clientRects.length; i += 1) { + const rect = clientRects.item(i); + if (!rect || rect.width <= 0 || rect.height <= 0) continue; + rects.push({ + xRatio: rect.left / width, + yRatio: rect.top / height, + wRatio: rect.width / width, + hRatio: rect.height / height + }); + if (rects.length >= 24) break; + } + return rects; + } + + // src/bridge/website-bridge.ts + (() => { + const params2 = new URLSearchParams(window.location.search); + const root = document.documentElement; + const roomId = params2.get("roomId") || root.dataset.lowcoderRoomId || ""; + const role = params2.get("role") || root.dataset.lowcoderRole || "driver"; + const editorId = params2.get("editorId") || root.dataset.lowcoderEditorId || "local"; + const collabId = params2.get("collab") || root.dataset.lowcoderCollabId || ""; + const username = params2.get("username") || root.dataset.lowcoderUsername || editorId; + const debug = params2.get("debug") === "1"; + const isPresenter = role === "driver"; + const peerId = `${editorId}|${role}|${Math.random().toString(36).slice(2, 10)}`; + if (!roomId || !collabId) { + console.error( + "[website-bridge] Missing roomId/collab. Load the page through /proxy/website and create a session with createWebsiteProxySession before Explore Together." + ); + return; + } + if (!window.location.pathname.includes("/proxy/website")) { + console.error( + "[website-bridge] Page left the Lowcoder proxy. Keep browsing through /proxy/website." + ); + return; + } + const hocuspocusConfig = window.__LOWCODER_HOCUSPOCUS__ ?? {}; + const hocuspocusUrl = hocuspocusConfig.url || root.dataset.lowcoderHocuspocusUrl || "ws://localhost:3006"; + const hocuspocusToken = hocuspocusConfig.token || root.dataset.lowcoderHocuspocusToken || ""; + const documentName = `website_${roomId}_${collabId}`; + let isApplyingRemote = false; + let isApplyingRemoteFields = false; + let lastAppliedNavSeq = 0; + let lastAppliedClickTs = 0; + let scrollPublishTimer; + let fieldPublishTimer; + let lastPublishedScroll = ""; + let dismissedNavSeq = 0; + let followPromptEl = null; + const doc2 = new Doc(); + const state = doc2.getMap("state"); + const fields = doc2.getMap("fields"); + const provider = new HocuspocusProvider({ + url: hocuspocusUrl, + name: documentName, + document: doc2, + token: hocuspocusToken || void 0, + onAuthenticationFailed: (data) => { + console.error("[website-bridge] Hocuspocus auth failed", data); + } + }); + const pointer = initPointerPresence({ + provider, + editorId, + role, + username, + debug + }); + function log(...args2) { + if (debug) console.log("[website-bridge]", role, ...args2); + } + function listControls() { + return Array.from( + document.querySelectorAll("input, textarea, select") + ).filter((control) => { + if (control.disabled) return false; + if (control instanceof HTMLInputElement) { + const type = (control.type || "text").toLowerCase(); + return !["hidden", "button", "submit", "reset", "file", "password", "image"].includes( + type + ); + } + return true; + }); + } + function controlIdentity(control) { + const name = control.getAttribute("name")?.trim(); + if (name) return `name:${name}`; + const id2 = control.id?.trim(); + if (id2) return `id:${id2}`; + const ariaLabel = control.getAttribute("aria-label")?.trim(); + if (ariaLabel) return `aria:${ariaLabel}`; + const placeholder = control instanceof HTMLInputElement || control instanceof HTMLTextAreaElement ? control.placeholder?.trim() : ""; + if (placeholder) return `placeholder:${placeholder}`; + return `index:${listControls().indexOf(control)}`; + } + function optionValue(input) { + return input.getAttribute("data-value") || input.value; + } + function controlKey(control) { + const identity = controlIdentity(control); + if (control instanceof HTMLInputElement && control.type === "radio") { + return `radio:${identity}`; + } + if (control instanceof HTMLInputElement && control.type === "checkbox") { + return `checkbox:${identity}:${optionValue(control)}`; + } + const sameIdentity = listControls().filter( + (candidate) => !(candidate instanceof HTMLInputElement && ["radio", "checkbox"].includes(candidate.type)) && controlIdentity(candidate) === identity + ); + return `field:${identity}:${Math.max(0, sameIdentity.indexOf(control))}`; + } + function controlValue(control) { + if (control instanceof HTMLInputElement && control.type === "checkbox") { + return control.checked ? "1" : "0"; + } + if (control instanceof HTMLInputElement && control.type === "radio") { + const group = listControls().filter( + (candidate) => candidate instanceof HTMLInputElement && candidate.type === "radio" && controlIdentity(candidate) === controlIdentity(control) + ); + const checked = group.find((candidate) => candidate.checked); + return checked ? optionValue(checked) : ""; + } + if (control instanceof HTMLSelectElement && control.multiple) { + return JSON.stringify(Array.from(control.selectedOptions).map((option) => option.value)); + } + return control.value; + } + function findControl(key) { + return listControls().find((control) => controlKey(control) === key) ?? null; + } + function setNativeValue(control, value) { + if (control instanceof HTMLInputElement && control.type === "radio") { + const target = listControls().find( + (candidate) => candidate instanceof HTMLInputElement && candidate.type === "radio" && controlKey(candidate) === controlKey(control) && optionValue(candidate) === value + ); + if (target && !target.checked) target.click(); + return; + } + if (control instanceof HTMLInputElement && control.type === "checkbox") { + const checked = value === "1"; + if (control.checked !== checked) control.click(); + return; + } + if (control instanceof HTMLSelectElement) { + if (control.multiple) { + let selected = []; + try { + selected = JSON.parse(value); + } catch { + selected = []; + } + Array.from(control.options).forEach((option) => { + option.selected = selected.includes(option.value); + }); + } else if (control.value !== value) { + control.value = value; + } + control.dispatchEvent(new Event("change", { bubbles: true })); + return; + } + if (control.value === value) return; + const prototype = control instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype; + const setter = Object.getOwnPropertyDescriptor(prototype, "value")?.set; + if (setter) setter.call(control, value); + else control.value = value; + control.dispatchEvent(new InputEvent("input", { bubbles: true, inputType: "insertText" })); + control.dispatchEvent(new Event("change", { bubbles: true })); + } + function publishControl(control) { + if (isApplyingRemoteFields || !provider.isSynced) return; + const key = controlKey(control); + const value = controlValue(control); + if (fields.get(key) === value) return; + doc2.transact(() => fields.set(key, value), peerId); + log("published field", key, value.slice(0, 40)); + } + function schedulePublishControl(control) { + window.clearTimeout(fieldPublishTimer); + fieldPublishTimer = window.setTimeout(() => publishControl(control), 80); + } + function applyField(key) { + const control = findControl(key); + const value = fields.get(key); + if (!control || typeof value !== "string") return; + if (controlValue(control) === value) return; + isApplyingRemoteFields = true; + try { + setNativeValue(control, value); + } finally { + window.setTimeout(() => { + isApplyingRemoteFields = false; + }, 30); + } + } + function applyAllRemoteFields() { + fields.forEach((_value, key) => applyField(key)); + } + function currentUpstreamUrl() { + const fromQuery = params2.get("target"); + if (fromQuery) return fromQuery; + return root.dataset.lowcoderUpstreamUrl || ""; + } + function buildProxiedUrlForTarget(targetUrl) { + const next = new URL(window.location.href); + next.searchParams.set("target", targetUrl); + return `${next.pathname}?${next.searchParams.toString()}`; + } + function readScroll() { + const maxX = Math.max(0, document.documentElement.scrollWidth - window.innerWidth); + const maxY = Math.max(0, document.documentElement.scrollHeight - window.innerHeight); + return { + xRatio: maxX > 0 ? window.scrollX / maxX : 0, + yRatio: maxY > 0 ? window.scrollY / maxY : 0 + }; + } + function applyScroll(scroll) { + const maxX = Math.max(0, document.documentElement.scrollWidth - window.innerWidth); + const maxY = Math.max(0, document.documentElement.scrollHeight - window.innerHeight); + isApplyingRemote = true; + try { + window.scrollTo({ + left: clamp2(scroll.xRatio, 0, 1) * maxX, + top: clamp2(scroll.yRatio, 0, 1) * maxY, + behavior: "auto" + }); + } finally { + window.setTimeout(() => { + isApplyingRemote = false; + }, 50); + } + } + function publishUrl(url) { + if (!isPresenter || isApplyingRemote || !provider.isSynced) return; + const navSeq = Number(state.get("navSeq") || "0") + 1; + doc2.transact(() => { + state.set("url", url); + state.set("navSeq", String(navSeq)); + state.set( + "navOffer", + JSON.stringify({ + url, + navSeq, + editorId, + username + }) + ); + }); + log("publish url", url, navSeq); + } + function publishScroll() { + if (!isPresenter || isApplyingRemote || !provider.isSynced) return; + const scroll = readScroll(); + const encoded = JSON.stringify(scroll); + if (encoded === lastPublishedScroll) return; + lastPublishedScroll = encoded; + state.set("scroll", encoded); + } + function publishClick(event) { + if (!provider.isSynced) return; + const xRatio = window.innerWidth > 0 ? event.clientX / window.innerWidth : 0; + const yRatio = window.innerHeight > 0 ? event.clientY / window.innerHeight : 0; + const button = findButtonTarget(event.target); + const color = getUserColor(editorId); + let click = { + xRatio, + yRatio, + ts: Date.now(), + editorId, + username, + kind: "generic", + label: "" + }; + if (button) { + const rect = button.getBoundingClientRect(); + const label = (button.getAttribute("aria-label") || button.textContent || (button instanceof HTMLInputElement ? button.value : "") || "Button").trim().replace(/\s+/g, " ").slice(0, 80); + click = { + ...click, + kind: "button", + label, + rect: { + leftRatio: rect.left / (window.innerWidth || 1), + topRatio: rect.top / (window.innerHeight || 1), + wRatio: rect.width / (window.innerWidth || 1), + hRatio: rect.height / (window.innerHeight || 1) + } + }; + pointer.showButtonClickFlash( + { left: rect.left, top: rect.top, width: rect.width, height: rect.height }, + color, + `${username}: ${label}` + ); + } else { + pointer.showClickRipple(xRatio, yRatio, color); + } + state.set("click", JSON.stringify(click)); + } + function hideFollowPrompt() { + followPromptEl?.remove(); + followPromptEl = null; + } + function showFollowPrompt(offer) { + if (isPresenter) return; + if (offer.navSeq <= dismissedNavSeq) return; + if (offer.url === currentUpstreamUrl()) return; + hideFollowPrompt(); + const panel = document.createElement("div"); + panel.id = "lowcoder-website-follow-prompt"; + Object.assign(panel.style, { + position: "fixed", + left: "50%", + top: "56px", + transform: "translateX(-50%)", + zIndex: "2147483647", + maxWidth: "min(440px, calc(100vw - 24px))", + padding: "14px 16px", + borderRadius: "10px", + background: "rgba(20, 24, 28, 0.94)", + color: "#fff", + font: "13px/1.4 system-ui,sans-serif", + boxShadow: "0 8px 28px rgba(0,0,0,.35)", + pointerEvents: "auto" + }); + const title = document.createElement("div"); + title.style.fontWeight = "600"; + title.style.marginBottom = "6px"; + title.textContent = `${offer.username || "Presenter"} opened a new page`; + const urlLine = document.createElement("div"); + urlLine.style.opacity = "0.85"; + urlLine.style.fontSize = "12px"; + urlLine.style.wordBreak = "break-all"; + urlLine.style.marginBottom = "12px"; + urlLine.textContent = offer.url; + const actions = document.createElement("div"); + Object.assign(actions.style, { + display: "flex", + gap: "8px", + justifyContent: "flex-end" + }); + const stayBtn = document.createElement("button"); + stayBtn.type = "button"; + stayBtn.textContent = "Stay"; + Object.assign(stayBtn.style, { + padding: "6px 12px", + borderRadius: "6px", + border: "1px solid rgba(255,255,255,.25)", + background: "transparent", + color: "#fff", + cursor: "pointer" + }); + stayBtn.addEventListener("click", () => { + dismissedNavSeq = offer.navSeq; + hideFollowPrompt(); + log("dismissed follow", offer.url); + }); + const followBtn = document.createElement("button"); + followBtn.type = "button"; + followBtn.textContent = "Follow"; + Object.assign(followBtn.style, { + padding: "6px 12px", + borderRadius: "6px", + border: "none", + background: "#1E88E5", + color: "#fff", + cursor: "pointer", + fontWeight: "600" + }); + followBtn.addEventListener("click", () => { + dismissedNavSeq = offer.navSeq; + hideFollowPrompt(); + isApplyingRemote = true; + window.location.assign(buildProxiedUrlForTarget(offer.url)); + }); + actions.appendChild(stayBtn); + actions.appendChild(followBtn); + panel.appendChild(title); + panel.appendChild(urlLine); + panel.appendChild(actions); + document.documentElement.appendChild(panel); + followPromptEl = panel; + } + function applyRemoteClick(click) { + const color = getUserColor(click.editorId); + if (click.kind === "button" && click.rect) { + pointer.showButtonClickFlash( + { + left: click.rect.leftRatio * window.innerWidth, + top: click.rect.topRatio * window.innerHeight, + width: click.rect.wRatio * window.innerWidth, + height: click.rect.hRatio * window.innerHeight + }, + color, + `${click.username || "User"}: ${click.label || "Button"}` + ); + return; + } + pointer.showClickRipple(click.xRatio, click.yRatio, color); + } + function applyRemoteState() { + const navSeq = Number(state.get("navSeq") || "0"); + const offerRaw = state.get("navOffer"); + if (offerRaw && navSeq > lastAppliedNavSeq) { + lastAppliedNavSeq = navSeq; + try { + const offer = JSON.parse(offerRaw); + if (!isPresenter && offer.url && offer.url !== currentUpstreamUrl()) { + showFollowPrompt(offer); + } + } catch { + } + } + if (!isPresenter) { + const scrollRaw = state.get("scroll"); + if (scrollRaw) { + try { + const scroll = JSON.parse(scrollRaw); + const local = readScroll(); + if (Math.abs(local.xRatio - scroll.xRatio) > 0.01 || Math.abs(local.yRatio - scroll.yRatio) > 0.01) { + applyScroll(scroll); + lastPublishedScroll = scrollRaw; + } + } catch { + } + } + } + const clickRaw = state.get("click"); + if (clickRaw) { + try { + const click = JSON.parse(clickRaw); + if (click.ts > lastAppliedClickTs && click.editorId !== editorId) { + lastAppliedClickTs = click.ts; + applyRemoteClick(click); + } + } catch { + } + } + } + document.addEventListener( + "input", + (event) => { + if (!event.isTrusted || isApplyingRemoteFields) return; + const target = event.target; + if (!(target instanceof HTMLInputElement) && !(target instanceof HTMLTextAreaElement) && !(target instanceof HTMLSelectElement)) { + return; + } + schedulePublishControl(target); + }, + true + ); + document.addEventListener( + "change", + (event) => { + if (!event.isTrusted || isApplyingRemoteFields) return; + const target = event.target; + if (!(target instanceof HTMLInputElement) && !(target instanceof HTMLTextAreaElement) && !(target instanceof HTMLSelectElement)) { + return; + } + publishControl(target); + }, + true + ); + fields.observe((event) => { + if (event.transaction.origin === peerId) return; + event.keysChanged.forEach((key) => applyField(key)); + }); + document.addEventListener( + "click", + (event) => { + if (!event.isTrusted) return; + const formControl = event.target; + const isFormControl = formControl instanceof HTMLInputElement || formControl instanceof HTMLTextAreaElement || formControl instanceof HTMLSelectElement; + if (!isFormControl) { + publishClick(event); + } else if (formControl instanceof HTMLInputElement && ["checkbox", "radio"].includes(formControl.type)) { + window.setTimeout(() => publishControl(formControl), 0); + } + const anchor = event.target?.closest?.("a[href]"); + if (!(anchor instanceof HTMLAnchorElement)) return; + const href = anchor.getAttribute("href"); + if (!href || href.startsWith("#") || href.startsWith("javascript:")) return; + try { + const resolved = new URL(anchor.href, window.location.href); + if (resolved.pathname.includes("/proxy/website")) { + const target = resolved.searchParams.get("target"); + if (target && isPresenter) { + publishUrl(target); + } + return; + } + if (resolved.protocol === "http:" || resolved.protocol === "https:") { + event.preventDefault(); + if (isPresenter) { + publishUrl(resolved.toString()); + } + window.location.assign(buildProxiedUrlForTarget(resolved.toString())); + } + } catch { + } + }, + true + ); + const originalPushState = history.pushState.bind(history); + const originalReplaceState = history.replaceState.bind(history); + function onHistoryChange() { + if (!isPresenter) return; + const url = currentUpstreamUrl(); + if (url) publishUrl(url); + } + history.pushState = function(...args2) { + const result = originalPushState(...args2); + onHistoryChange(); + return result; + }; + history.replaceState = function(...args2) { + const result = originalReplaceState(...args2); + onHistoryChange(); + return result; + }; + window.addEventListener("popstate", onHistoryChange); + window.addEventListener("hashchange", onHistoryChange); + window.addEventListener( + "scroll", + () => { + if (isApplyingRemote || !isPresenter) return; + window.clearTimeout(scrollPublishTimer); + scrollPublishTimer = window.setTimeout(() => publishScroll(), 80); + }, + { passive: true } + ); + state.observe(() => applyRemoteState()); + provider.on("synced", () => { + log("synced", documentName); + const localUrl = currentUpstreamUrl(); + if (isPresenter) { + if (localUrl) { + publishUrl(localUrl); + publishScroll(); + } + } else { + applyRemoteState(); + } + applyAllRemoteFields(); + }); + log("ready", { + documentName, + editorId, + username, + isPresenter, + upstream: currentUpstreamUrl() + }); + })(); + function findButtonTarget(target) { + if (!(target instanceof Element)) return null; + return target.closest( + "button, [role='button'], input[type='button'], input[type='submit'], input[type='reset']" + ); + } + function clamp2(value, min4, max4) { + return Math.min(max4, Math.max(min4, value)); + } +})(); diff --git a/server/proxy-service/build/googleCookieJar.js b/server/proxy-service/build/googleCookieJar.js new file mode 100644 index 0000000000..e80fb86532 --- /dev/null +++ b/server/proxy-service/build/googleCookieJar.js @@ -0,0 +1,149 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.storeGoogleCookies = storeGoogleCookies; +exports.buildGoogleCookieHeader = buildGoogleCookieHeader; +const MAX_JARS = 500; +const MAX_COOKIES_PER_JAR = 300; +const JAR_TTL_MS = 12 * 60 * 60 * 1000; +const jars = new Map(); +function defaultPath(pathname) { + if (!pathname.startsWith("/")) + return "/"; + const lastSlash = pathname.lastIndexOf("/"); + return lastSlash <= 0 ? "/" : pathname.slice(0, lastSlash); +} +function domainMatches(host, cookie) { + if (cookie.hostOnly) + return host === cookie.domain; + return host === cookie.domain || host.endsWith(`.${cookie.domain}`); +} +function pathMatches(pathname, cookiePath) { + if (cookiePath === "/") + return true; + if (pathname === cookiePath) + return true; + return pathname.startsWith(cookiePath.endsWith("/") ? cookiePath : `${cookiePath}/`); +} +function parseSetCookie(raw, url) { + const segments = raw.split(";"); + const [nameValue, ...attributes] = segments; + const separator = nameValue.indexOf("="); + if (separator <= 0) + return null; + const name = nameValue.slice(0, separator).trim(); + const value = nameValue.slice(separator + 1).trim(); + if (!name) + return null; + const host = url.hostname.toLowerCase(); + const cookie = { + name, + value, + domain: host, + path: defaultPath(url.pathname), + hostOnly: true, + }; + for (const attribute of attributes) { + const index = attribute.indexOf("="); + const key = (index === -1 ? attribute : attribute.slice(0, index)).trim().toLowerCase(); + const attributeValue = index === -1 ? "" : attribute.slice(index + 1).trim(); + if (key === "domain" && attributeValue) { + const domain = attributeValue.replace(/^\./, "").toLowerCase(); + // A response may not set cookies for an unrelated domain. + if (host !== domain && !host.endsWith(`.${domain}`)) + return null; + cookie.domain = domain; + cookie.hostOnly = false; + continue; + } + if (key === "path" && attributeValue.startsWith("/")) { + cookie.path = attributeValue; + continue; + } + if (key === "max-age" && attributeValue) { + const seconds = Number(attributeValue); + if (!Number.isNaN(seconds)) + cookie.expiresAt = Date.now() + seconds * 1000; + continue; + } + if (key === "expires" && attributeValue && cookie.expiresAt === undefined) { + const parsed = Date.parse(attributeValue); + if (!Number.isNaN(parsed)) + cookie.expiresAt = parsed; + } + } + return cookie; +} +function pruneJars() { + const now = Date.now(); + for (const [key, jar] of jars) { + if (now - jar.lastUsed > JAR_TTL_MS) + jars.delete(key); + } + if (jars.size <= MAX_JARS) + return; + const oldestFirst = [...jars.entries()].sort((a, b) => a[1].lastUsed - b[1].lastUsed); + for (const [key] of oldestFirst.slice(0, jars.size - MAX_JARS)) { + jars.delete(key); + } +} +function getJar(sessionKey, create) { + const existing = jars.get(sessionKey); + if (existing) { + existing.lastUsed = Date.now(); + return existing; + } + if (!create) + return undefined; + pruneJars(); + const jar = { cookies: new Map(), lastUsed: Date.now() }; + jars.set(sessionKey, jar); + return jar; +} +function storeGoogleCookies(sessionKey, url, rawSetCookies) { + if (!sessionKey || !rawSetCookies?.length) + return; + const jar = getJar(sessionKey, true); + if (!jar) + return; + for (const raw of rawSetCookies) { + const cookie = parseSetCookie(raw, url); + if (!cookie) + continue; + const id = `${cookie.domain}|${cookie.path}|${cookie.name}`; + const expired = cookie.expiresAt !== undefined && cookie.expiresAt <= Date.now(); + if (expired || cookie.value === "") { + jar.cookies.delete(id); + continue; + } + jar.cookies.set(id, cookie); + } + if (jar.cookies.size > MAX_COOKIES_PER_JAR) { + const excess = jar.cookies.size - MAX_COOKIES_PER_JAR; + for (const id of [...jar.cookies.keys()].slice(0, excess)) { + jar.cookies.delete(id); + } + } +} +function buildGoogleCookieHeader(sessionKey, url) { + if (!sessionKey) + return ""; + const jar = getJar(sessionKey, false); + if (!jar) + return ""; + const now = Date.now(); + const host = url.hostname.toLowerCase(); + const matching = []; + for (const [id, cookie] of jar.cookies) { + if (cookie.expiresAt !== undefined && cookie.expiresAt <= now) { + jar.cookies.delete(id); + continue; + } + if (domainMatches(host, cookie) && pathMatches(url.pathname, cookie.path)) { + matching.push(cookie); + } + } + return matching + .sort((a, b) => b.path.length - a.path.length) + .map((cookie) => `${cookie.name}=${cookie.value}`) + .join("; "); +} diff --git a/server/proxy-service/build/googleFormUrls.js b/server/proxy-service/build/googleFormUrls.js new file mode 100644 index 0000000000..2e77a52970 --- /dev/null +++ b/server/proxy-service/build/googleFormUrls.js @@ -0,0 +1,127 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.isPublishedViewformUrl = isPublishedViewformUrl; +exports.cleanViewformUrl = cleanViewformUrl; +exports.resolveGoogleFormResponderUrl = resolveGoogleFormResponderUrl; +const node_fetch_1 = __importDefault(require("node-fetch")); +const PUBLISHED_VIEWFORM_RE = /\/forms\/d\/e\/[^/]+\/viewform/i; +const DRIVE_FORM_ID_RE = /\/forms\/d\/([^/]+)\/(edit|viewform|preview)/i; +function isPublishedViewformUrl(url) { + return PUBLISHED_VIEWFORM_RE.test(url.pathname); +} +function cleanViewformUrl(rawUrl) { + const url = new URL(rawUrl.trim()); + if (!url.pathname.startsWith("/forms/")) { + throw new Error("googleFormUrl must point to a Google Form"); + } + if (!url.pathname.includes("/viewform")) { + throw new Error("googleFormUrl must be a published responder URL ending with /viewform. " + + "Do not use webViewLink or /edit URLs from Google Drive."); + } + url.pathname = url.pathname.replace(/\/viewform.*/i, "/viewform"); + url.search = ""; + url.hash = ""; + return url.toString(); +} +function extractViewformFromHtml(html) { + const patterns = [ + /https:\/\/docs\.google\.com\/forms\/d\/e\/[^"'\\\s]+\/viewform/gi, + /"publishedFormUrl":"(https:\\\/\\\/docs\.google\.com\\\/forms\\\/d\\\/e\\\/[^"\\]+\\\/viewform)"/i, + /"responderUri":"(https:\\\/\\\/docs\.google\.com\\\/forms\\\/d\\\/e\\\/[^"\\]+\\\/viewform)"/i, + ]; + for (const pattern of patterns) { + const match = html.match(pattern); + if (!match?.[0]) + continue; + const candidate = match[0] + .replace(/^"publishedFormUrl":"|"responderUri":"/, "") + .replace(/"$/, "") + .replace(/\\\//g, "/"); + try { + return cleanViewformUrl(candidate); + } + catch { + continue; + } + } + return null; +} +async function followToPublishedViewform(startUrl) { + let current = startUrl; + for (let step = 0; step < 12; step += 1) { + const response = await (0, node_fetch_1.default)(current, { + method: "GET", + redirect: "manual", + headers: { + "user-agent": "Mozilla/5.0 (compatible; LowcoderGoogleFormsProxy/1.0; +https://lowcoder.cloud)", + }, + }); + if (response.status >= 300 && response.status < 400) { + const location = response.headers.get("location"); + if (!location) + return null; + current = new URL(location, current).toString(); + const parsed = new URL(current); + if (isPublishedViewformUrl(parsed)) { + return cleanViewformUrl(current); + } + continue; + } + if (response.status === 200) { + const html = await response.text(); + const scraped = extractViewformFromHtml(html); + if (scraped) + return scraped; + const parsed = new URL(current); + if (isPublishedViewformUrl(parsed)) { + return cleanViewformUrl(current); + } + } + return null; + } + return null; +} +/** + * Normalize Drive/webViewLink or draft form URLs to a published responder URL. + * Collaboration only works on public /viewform pages that stay inside the proxy. + */ +async function resolveGoogleFormResponderUrl(rawUrl) { + const value = (rawUrl ?? "").trim(); + if (!value) + throw new Error("googleFormUrl is required"); + let url; + try { + url = new URL(value); + } + catch { + throw new Error("googleFormUrl must be a valid URL"); + } + if (url.protocol !== "https:" || url.hostname !== "docs.google.com") { + throw new Error("googleFormUrl must be an https://docs.google.com/forms URL"); + } + if (!url.pathname.startsWith("/forms/")) { + throw new Error("googleFormUrl must point to a Google Form"); + } + if (isPublishedViewformUrl(url)) { + return cleanViewformUrl(url.toString()); + } + const driveMatch = url.pathname.match(DRIVE_FORM_ID_RE); + if (driveMatch?.[1] && driveMatch[1] !== "e") { + const formId = driveMatch[1]; + const candidates = [ + `https://docs.google.com/forms/d/${formId}/viewform`, + `https://docs.google.com/forms/d/${formId}/preview`, + ]; + for (const candidate of candidates) { + const resolved = await followToPublishedViewform(candidate); + if (resolved) + return resolved; + } + } + throw new Error("Could not resolve a published Google Form responder URL. " + + "Open the form in Google Forms, click Send, copy the public link " + + "(https://docs.google.com/forms/d/e/.../viewform), and use that instead of webViewLink."); +} diff --git a/server/proxy-service/build/googleFormsApi.js b/server/proxy-service/build/googleFormsApi.js new file mode 100644 index 0000000000..6e83ec35a4 --- /dev/null +++ b/server/proxy-service/build/googleFormsApi.js @@ -0,0 +1,151 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.extractGoogleFormId = extractGoogleFormId; +exports.fetchGoogleForm = fetchGoogleForm; +exports.toCollabFormView = toCollabFormView; +exports.updateGoogleFormInfo = updateGoogleFormInfo; +const node_fetch_1 = __importDefault(require("node-fetch")); +const FORMS_API = "https://forms.googleapis.com/v1/forms"; +function extractGoogleFormId(rawUrl) { + const value = (rawUrl ?? "").trim(); + if (!value) + throw new Error("googleFormUrl is required"); + let url; + try { + url = new URL(value); + } + catch { + // Treat bare form IDs as valid. + if (/^[a-zA-Z0-9_-]{10,}$/.test(value)) + return value; + throw new Error("googleFormUrl must be a valid URL or form id"); + } + if (url.hostname !== "docs.google.com" || !url.pathname.startsWith("/forms/")) { + throw new Error("googleFormUrl must be an https://docs.google.com/forms URL"); + } + const published = url.pathname.match(/\/forms\/d\/e\/([^/]+)/i); + if (published?.[1]) { + throw new Error("Published responder IDs (forms/d/e/...) cannot be used with Forms API. " + + "Pass the Drive/edit form URL or form id from Google Drive (forms/d/FORM_ID/edit)."); + } + const drive = url.pathname.match(/\/forms\/d\/([^/]+)/i); + if (drive?.[1] && drive[1] !== "e") + return drive[1]; + throw new Error("Could not extract Google Form id from googleFormUrl"); +} +async function fetchGoogleForm(formId, accessToken) { + const response = await (0, node_fetch_1.default)(`${FORMS_API}/${encodeURIComponent(formId)}`, { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json", + }, + }); + const text = await response.text(); + if (!response.ok) { + let detail = text; + try { + const parsed = JSON.parse(text); + detail = parsed.error?.message || text; + } + catch { + // keep raw text + } + throw new Error(`Google Forms API error (${response.status}): ${detail}. ` + + "Ensure the OAuth token includes the forms.googleapis.com scope " + + "(https://www.googleapis.com/auth/forms.body or forms.body.readonly)."); + } + return JSON.parse(text); +} +function toCollabFormView(form) { + const questions = []; + for (const item of form.items ?? []) { + const question = item.questionItem?.question; + if (!question?.questionId) + continue; + let type = "unknown"; + let options = []; + let scaleLow; + let scaleHigh; + if (question.textQuestion) { + type = "paragraph" in question.textQuestion && question.textQuestion.paragraph ? "paragraph" : "text"; + // Forms API uses textQuestion.paragraph boolean + if (question.textQuestion.paragraph) + type = "paragraph"; + else + type = "text"; + } + else if (question.choiceQuestion) { + const choiceType = (question.choiceQuestion.type || "").toUpperCase(); + type = choiceType === "CHECKBOX" ? "checkbox" : "radio"; + options = (question.choiceQuestion.options ?? []).map((option) => option.value).filter(Boolean); + } + else if (question.scaleQuestion) { + type = "scale"; + scaleLow = question.scaleQuestion.low ?? 1; + scaleHigh = question.scaleQuestion.high ?? 5; + } + else if (question.dateQuestion) { + type = "date"; + } + else if (question.timeQuestion) { + type = "time"; + } + questions.push({ + itemId: item.itemId, + questionId: question.questionId, + title: item.title || "Untitled question", + description: item.description || "", + required: Boolean(question.required), + type, + options, + scaleLow, + scaleHigh, + }); + } + return { + formId: form.formId, + title: form.info?.title || form.info?.documentTitle || "Untitled form", + description: form.info?.description || "", + responderUri: form.responderUri || "", + questions, + }; +} +async function updateGoogleFormInfo(formId, accessToken, info) { + const update = {}; + const mask = []; + if (typeof info.title === "string") { + update.title = info.title; + mask.push("title"); + } + if (typeof info.description === "string") { + update.description = info.description; + mask.push("description"); + } + if (mask.length === 0) + return; + const response = await (0, node_fetch_1.default)(`${FORMS_API}/${encodeURIComponent(formId)}:batchUpdate`, { + method: "POST", + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify({ + requests: [ + { + updateFormInfo: { + info: update, + updateMask: mask.join(","), + }, + }, + ], + }), + }); + if (!response.ok) { + const text = await response.text(); + throw new Error(`Failed to update Google Form (${response.status}): ${text}`); + } +} diff --git a/server/proxy-service/build/googleFormsHosts.js b/server/proxy-service/build/googleFormsHosts.js new file mode 100644 index 0000000000..6975dd9077 --- /dev/null +++ b/server/proxy-service/build/googleFormsHosts.js @@ -0,0 +1,72 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.listGoogleFormsHosts = listGoogleFormsHosts; +exports.isGoogleFormsHostAllowed = isGoogleFormsHostAllowed; +exports.isGoogleFormsPathAllowed = isGoogleFormsPathAllowed; +exports.assertAllowedGoogleFormsUrl = assertAllowedGoogleFormsUrl; +/** + * Hosts the Google Forms proxy may fetch from. + * + * A rendered Google Form pulls markup from docs.google.com but loads its + * freebird modules, fonts and images from several gstatic/googleusercontent + * hosts, so all of them have to be proxyable for the page to work inside the + * Lowcoder iframe. Entries starting with "." match any subdomain. + */ +const DEFAULT_GOOGLE_FORMS_HOSTS = [ + "docs.google.com", + "drive.google.com", + "accounts.google.com", + "apis.google.com", + "clients6.google.com", + "www.google.com", + "www.gstatic.com", + "ssl.gstatic.com", + "fonts.googleapis.com", + "fonts.gstatic.com", + ".googleusercontent.com", + ".gstatic.com", +]; +/** Paths served by docs.google.com that belong to a form and its assets. */ +const GOOGLE_FORMS_PATH_PREFIXES = [ + "/forms/", + "/_/", + "/static/", + "/js/", + "/xjs/", + "/u/", + "/picker", +]; +function parseHostList(raw) { + return (raw ?? "") + .split(",") + .map((value) => value.trim().toLowerCase()) + .filter(Boolean); +} +const ALLOWED_HOSTS = Array.from(new Set([ + ...DEFAULT_GOOGLE_FORMS_HOSTS, + ...parseHostList(process.env.LOWCODER_GOOGLE_FORMS_ALLOWED_HOSTS), +])); +function listGoogleFormsHosts() { + return [...ALLOWED_HOSTS]; +} +function isGoogleFormsHostAllowed(hostname) { + const host = hostname.toLowerCase(); + return ALLOWED_HOSTS.some((allowed) => allowed.startsWith(".") ? host.endsWith(allowed) : host === allowed); +} +/** + * docs.google.com is restricted to form/asset paths; the remaining hosts are + * asset or sign-in origins where any path is fine. + */ +function isGoogleFormsPathAllowed(url) { + const host = url.hostname.toLowerCase(); + if (host !== "docs.google.com" && host !== "drive.google.com") + return true; + return GOOGLE_FORMS_PATH_PREFIXES.some((prefix) => url.pathname.startsWith(prefix)); +} +function assertAllowedGoogleFormsUrl(url) { + if (url.protocol !== "https:" || + !isGoogleFormsHostAllowed(url.hostname) || + !isGoogleFormsPathAllowed(url)) { + throw new Error(`Google Forms URL is not allowed: ${url.toString()}`); + } +} diff --git a/server/proxy-service/build/googleFormsRewrite.js b/server/proxy-service/build/googleFormsRewrite.js new file mode 100644 index 0000000000..829afa95da --- /dev/null +++ b/server/proxy-service/build/googleFormsRewrite.js @@ -0,0 +1,93 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.GOOGLE_ROOT_PATH_PREFIXES = void 0; +exports.rewriteGoogleFormsBody = rewriteGoogleFormsBody; +exports.stripSubresourceIntegrity = stripSubresourceIntegrity; +const googleFormsHosts_1 = require("./googleFormsHosts"); +/** Root-relative prefixes that belong to Google rather than to Lowcoder. */ +exports.GOOGLE_ROOT_PATH_PREFIXES = [ + "forms", + "_", + "static", + "js", + "xjs", + "u", + "images", + "logos", + "css", +]; +function escapeRegex(text) { + return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} +const HOST_PATTERN = (0, googleFormsHosts_1.listGoogleFormsHosts)() + .map((host) => (host.startsWith(".") ? `[a-z0-9-]+${escapeRegex(host)}` : escapeRegex(host))) + .join("|"); +const ROOT_PATH_PATTERN = exports.GOOGLE_ROOT_PATH_PREFIXES.map(escapeRegex).join("|"); +const ABSOLUTE_URL_RE = new RegExp(`https?://(?:${HOST_PATTERN})(?:/[^"'\\s<>()\\\\]*)?`, "gi"); +const ESCAPED_ABSOLUTE_URL_RE = new RegExp(`https?:\\\\/\\\\/(?:${HOST_PATTERN})(?:\\\\/[^"'\\s<>()]*)?`, "gi"); +const PROTOCOL_RELATIVE_URL_RE = new RegExp(`(^|[^:\\w\\\\])//(?:${HOST_PATTERN})(?:/[^"'\\s<>()\\\\]*)?`, "gi"); +const ESCAPED_PROTOCOL_RELATIVE_URL_RE = new RegExp(`\\\\/\\\\/(?:${HOST_PATTERN})(?:\\\\/[^"'\\s<>()]*)?`, "gi"); +const ATTRIBUTE_ROOT_PATH_RE = new RegExp(`(href|src|action|data-src|poster)=(["'])(/(?:${ROOT_PATH_PATTERN})/[^"']*)\\2`, "gi"); +const CSS_ROOT_PATH_RE = new RegExp(`url\\((["']?)(/(?:${ROOT_PATH_PATTERN})/[^"')]+)\\1\\)`, "gi"); +const STRING_ROOT_PATH_RE = new RegExp(`(["'])(/(?:${ROOT_PATH_PATTERN})/[^"'\\s\\\\]*)\\1`, "g"); +const ESCAPED_STRING_ROOT_PATH_RE = new RegExp(`\\\\/(?:${ROOT_PATH_PATTERN})(?:\\\\/[^"'\\s]*)`, "g"); +const INTEGRITY_ATTRIBUTE_RE = /\sintegrity=(["'])[^"']*\1/gi; +function unescapeSlashes(value) { + return value.replace(/\\\//g, "/"); +} +function proxyAbsolute(rawUrl, context) { + try { + const parsed = new URL(rawUrl, context.upstreamUrl); + if (parsed.protocol !== "http:" && parsed.protocol !== "https:") + return null; + if (!(0, googleFormsHosts_1.isGoogleFormsHostAllowed)(parsed.hostname)) + return null; + parsed.protocol = "https:"; + return context.toProxied(parsed.toString()); + } + catch { + return null; + } +} +function proxyRootPath(path, context) { + return proxyAbsolute(`${context.upstreamUrl.origin}${path}`, context); +} +/** Applies every URL shape rewrite. Safe to run on HTML, CSS, JS and JSON. */ +function rewriteGoogleFormsBody(body, context) { + let output = body.replace(ESCAPED_ABSOLUTE_URL_RE, (match) => { + return proxyAbsolute(unescapeSlashes(match), context) ?? match; + }); + output = output.replace(ABSOLUTE_URL_RE, (match) => proxyAbsolute(match, context) ?? match); + output = output.replace(ESCAPED_PROTOCOL_RELATIVE_URL_RE, (match) => { + return proxyAbsolute(`https:${unescapeSlashes(match)}`, context) ?? match; + }); + output = output.replace(PROTOCOL_RELATIVE_URL_RE, (match, prefix) => { + const url = match.slice(prefix.length); + const proxied = proxyAbsolute(`https:${url}`, context); + return proxied ? `${prefix}${proxied}` : match; + }); + output = output.replace(ATTRIBUTE_ROOT_PATH_RE, (match, attribute, quote, path) => { + const proxied = proxyRootPath(path, context); + return proxied ? `${attribute}=${quote}${proxied}${quote}` : match; + }); + output = output.replace(CSS_ROOT_PATH_RE, (match, quote, path) => { + const proxied = proxyRootPath(path, context); + return proxied ? `url(${quote}${proxied}${quote})` : match; + }); + output = output.replace(STRING_ROOT_PATH_RE, (match, quote, path) => { + const proxied = proxyRootPath(path, context); + return proxied ? `${quote}${proxied}${quote}` : match; + }); + output = output.replace(ESCAPED_STRING_ROOT_PATH_RE, (match) => { + const proxied = proxyRootPath(unescapeSlashes(match), context); + return proxied ?? match; + }); + return output; +} +/** + * Rewritten scripts no longer match Google's subresource hashes, so the hashes + * have to go or the browser refuses to execute the proxied modules. + */ +function stripSubresourceIntegrity(html) { + return html.replace(INTEGRITY_ATTRIBUTE_RE, ""); +} diff --git a/server/proxy-service/build/googleProxy.js b/server/proxy-service/build/googleProxy.js new file mode 100644 index 0000000000..9c569ba420 --- /dev/null +++ b/server/proxy-service/build/googleProxy.js @@ -0,0 +1,353 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.registerGoogleFormsProxy = registerGoogleFormsProxy; +const express_1 = __importDefault(require("express")); +const node_fetch_1 = __importDefault(require("node-fetch")); +const node_fs_1 = __importDefault(require("node:fs")); +const node_path_1 = __importDefault(require("node:path")); +const node_url_1 = require("node:url"); +const auth_1 = require("./auth"); +const googleCookieJar_1 = require("./googleCookieJar"); +const googleFormsHosts_1 = require("./googleFormsHosts"); +const googleFormsRewrite_1 = require("./googleFormsRewrite"); +const googleUrls_1 = require("./googleUrls"); +const googleSession_1 = require("./googleSession"); +const BRIDGE_PATH = "/proxy/google-forms-bridge.js"; +const HOCUSPOCUS_URL = (process.env.LOWCODER_HOCUSPOCUS_URL ?? "ws://localhost:3006").trim(); +const FALLBACK_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) " + + "Chrome/125.0.0.0 Safari/537.36"; +/** Query params the proxy owns; they must never be forwarded to Google. */ +const PROXY_CONTROL_PARAMS = new Set([ + "target", + "roomId", + "role", + "editorId", + "token", + "collab", + "username", + "debug", +]); +/** + * Request headers the proxy replaces or that would break the upstream fetch. + * Conditional headers are dropped so Google never answers 304 — a bodyless + * response cannot be URL-rewritten. + */ +const DROPPED_REQUEST_HEADERS = new Set([ + "host", + "connection", + "keep-alive", + "content-length", + "accept-encoding", + "cookie", + "referer", + "origin", + "if-none-match", + "if-modified-since", + "upgrade-insecure-requests", + "x-forwarded-host", + "x-forwarded-proto", + "x-forwarded-for", +]); +/** Response headers that would break framing, caching or body rewriting. */ +const SKIPPED_RESPONSE_HEADERS = new Set([ + "x-frame-options", + "content-security-policy", + "content-security-policy-report-only", + "cross-origin-opener-policy", + "cross-origin-embedder-policy", + "cross-origin-resource-policy", + "permissions-policy", + "strict-transport-security", + "report-to", + "reporting-endpoints", + "transfer-encoding", + "content-length", + "content-encoding", + "connection", + "keep-alive", + "set-cookie", +]); +function resolveHocuspocusUrl(req) { + if (!/localhost|127\.0\.0\.1/.test(HOCUSPOCUS_URL)) { + return HOCUSPOCUS_URL; + } + const forwardedHost = (req.get("x-forwarded-host") || req.get("host") || "localhost") + .split(",")[0] + .trim(); + const hostname = forwardedHost.split(":")[0] || "localhost"; + const port = new node_url_1.URL(HOCUSPOCUS_URL.replace(/^ws/, "http")).port || "3006"; + return `ws://${hostname}:${port}`; +} +const HOCUSPOCUS_SECRET = (process.env.LOWCODER_HOCUSPOCUS_SECRET ?? process.env.HOCUSPOCUS_SECRET ?? "").trim(); +// A single form page fans out into hundreds of proxied asset and XHR requests, +// so the Google proxy needs a much higher ceiling than the Typeform proxy. +const RATE_LIMIT_PER_MINUTE = Number(process.env.LOWCODER_GOOGLE_PROXY_RATE_LIMIT ?? + Math.max(600, Number(process.env.LOWCODER_PROXY_RATE_LIMIT ?? 0) || 0)); +const requestBuckets = new Map(); +function registerGoogleFormsProxy(app) { + app.get(BRIDGE_PATH, (_req, res) => { + const bridgePath = node_path_1.default.join(__dirname, "bridge", "google-forms-bridge.js"); + if (!node_fs_1.default.existsSync(bridgePath)) { + res.status(404).type("text/plain").send("Google Forms bridge script not found"); + return; + } + res.setHeader("Content-Type", "application/javascript; charset=utf-8"); + res.setHeader("Cache-Control", "no-cache"); + res.send(node_fs_1.default.readFileSync(bridgePath, "utf8")); + }); + const router = express_1.default.Router(); + router.post("/session", express_1.default.json({ limit: "1mb" }), googleSession_1.createGoogleFormsProxySession); + router.post("/session/join", express_1.default.json({ limit: "1mb" }), googleSession_1.joinGoogleFormsProxySession); + router.use(express_1.default.raw({ type: "*/*", limit: "25mb" }), async (req, res) => { + if (req.path === "/session" || req.path === "/session/join") { + res.status(405).json({ + message: "Use POST on /proxy/google-forms/session or /proxy/google-forms/session/join", + }); + return; + } + try { + if (!isAuthorized(req)) { + res.status(401).json({ message: "Missing or invalid proxy token" }); + return; + } + if (!checkRateLimit(req)) { + res.status(429).json({ message: "Proxy rate limit exceeded" }); + return; + } + const upstreamUrl = resolveUpstreamUrl(req); + const sessionKey = resolveSessionKey(req); + const upstreamResponse = await (0, node_fetch_1.default)(upstreamUrl.toString(), { + method: req.method, + redirect: "manual", + headers: buildForwardHeaders(req, upstreamUrl, sessionKey), + body: hasBody(req.method) ? req.body : undefined, + }); + relayHeaders(upstreamResponse, res, req, upstreamUrl, sessionKey); + const contentType = upstreamResponse.headers.get("content-type") ?? ""; + if (upstreamResponse.status >= 300 && upstreamResponse.status < 400) { + res.status(upstreamResponse.status).end(); + return; + } + if (contentType.includes("text/html")) { + const html = await upstreamResponse.text(); + res + .status(upstreamResponse.status) + .send(injectBridgeAndRewriteHtml(html, req, upstreamUrl)); + return; + } + if (isRewritableContentType(contentType)) { + const text = await upstreamResponse.text(); + res.status(upstreamResponse.status).send(rewriteBodyUrls(text, req, upstreamUrl)); + return; + } + res.status(upstreamResponse.status).send(await upstreamResponse.buffer()); + } + catch (error) { + console.error("Google Forms proxy request failed", error); + const message = error instanceof Error ? error.message : "Google Forms proxy failed"; + const status = message.includes("not allowed") || message.includes("required") ? 400 : 502; + res.status(status).json({ message }); + } + }); + app.use(googleUrls_1.GOOGLE_FORMS_PROXY_PREFIX, router); +} +function hasBody(method) { + return ["POST", "PUT", "PATCH", "DELETE"].includes(method.toUpperCase()); +} +function isRewritableContentType(contentType) { + return /(text\/html|text\/css|text\/plain|text\/xml|application\/xml|javascript|ecmascript|json)/i.test(contentType); +} +/** + * Resolves the Google URL for this request. Normal traffic carries an explicit + * `target`; requests that Google's own scripts build relative to the proxy path + * fall back to the docs.google.com equivalent of that path. + */ +function resolveUpstreamUrl(req) { + const targetParam = (0, googleUrls_1.firstQueryValue)(req.query.target).trim(); + if (targetParam) { + const upstream = new node_url_1.URL(targetParam); + (0, googleFormsHosts_1.assertAllowedGoogleFormsUrl)(upstream); + return upstream; + } + const relativePath = req.path && req.path !== "/" ? req.path : ""; + if (!relativePath) { + throw new Error("A Google Forms target URL is required"); + } + const upstream = new node_url_1.URL(`https://docs.google.com${relativePath}`); + Object.entries(req.query).forEach(([key, value]) => { + if (PROXY_CONTROL_PARAMS.has(key)) + return; + const first = (0, googleUrls_1.firstQueryValue)(value); + if (first) + upstream.searchParams.set(key, first); + }); + (0, googleFormsHosts_1.assertAllowedGoogleFormsUrl)(upstream); + return upstream; +} +/** Cookie jars are per participant, keyed by the minted proxy token. */ +function resolveSessionKey(req) { + const token = (0, googleUrls_1.firstQueryValue)(req.query.token).trim() || (0, auth_1.getBearerToken)(req.headers.authorization) || ""; + if (token) + return `token:${token}`; + const roomId = (0, googleUrls_1.firstQueryValue)(req.query.roomId).trim(); + const collab = (0, googleUrls_1.firstQueryValue)(req.query.collab).trim(); + const editorId = (0, googleUrls_1.firstQueryValue)(req.query.editorId).trim(); + if (roomId || collab || editorId) + return `room:${roomId}|${collab}|${editorId}`; + return `ip:${req.ip ?? "unknown"}`; +} +/** + * Google validates Referer on its XHR endpoints, and the browser only ever + * sends the proxied page URL. Unwrap that referer back to its Google target. + */ +function resolveUpstreamReferer(req, upstreamUrl) { + const referer = req.get("referer"); + if (referer) { + try { + const parsed = new node_url_1.URL(referer); + if (parsed.pathname.startsWith(googleUrls_1.GOOGLE_FORMS_PROXY_PREFIX)) { + const target = parsed.searchParams.get("target"); + if (target) { + const targetUrl = new node_url_1.URL(target); + if ((0, googleFormsHosts_1.isGoogleFormsHostAllowed)(targetUrl.hostname)) + return targetUrl.toString(); + } + } + } + catch { + // fall through to the upstream default + } + } + return `${upstreamUrl.origin}/`; +} +function buildForwardHeaders(req, upstreamUrl, sessionKey) { + const normalized = new Map(); + Object.entries(req.headers).forEach(([key, value]) => { + if (typeof value !== "string") + return; + const lower = key.toLowerCase(); + if (DROPPED_REQUEST_HEADERS.has(lower)) + return; + normalized.set(lower, value); + }); + normalized.set("host", upstreamUrl.host); + // Rewriting needs an uncompressed body, and brotli support varies by runtime. + normalized.set("accept-encoding", "identity"); + normalized.set("referer", resolveUpstreamReferer(req, upstreamUrl)); + if (!normalized.has("user-agent")) { + normalized.set("user-agent", FALLBACK_USER_AGENT); + } + if (req.get("origin") || hasBody(req.method)) { + normalized.set("origin", upstreamUrl.origin); + normalized.set("sec-fetch-site", "same-origin"); + } + const cookieHeader = (0, googleCookieJar_1.buildGoogleCookieHeader)(sessionKey, upstreamUrl); + if (cookieHeader) { + normalized.set("cookie", cookieHeader); + } + return Object.fromEntries(normalized.entries()); +} +function relayHeaders(upstreamResponse, res, req, upstreamUrl, sessionKey) { + // The proxy is Google's HTTP client, so its cookies stay server-side. + (0, googleCookieJar_1.storeGoogleCookies)(sessionKey, upstreamUrl, upstreamResponse.headers.raw()["set-cookie"]); + upstreamResponse.headers.forEach((value, key) => { + const lower = key.toLowerCase(); + if (SKIPPED_RESPONSE_HEADERS.has(lower)) + return; + if (lower === "location") { + res.setHeader(key, rewriteAbsoluteUrl(value, req, upstreamUrl)); + return; + } + res.setHeader(key, value); + }); + res.setHeader("Content-Security-Policy", "frame-ancestors 'self';"); +} +function injectBridgeAndRewriteHtml(html, req, upstreamUrl) { + const rewritten = rewriteBodyUrls((0, googleFormsRewrite_1.stripSubresourceIntegrity)(html), req, upstreamUrl); + const hocuspocusUrl = resolveHocuspocusUrl(req); + const collab = (0, googleUrls_1.firstQueryValue)(req.query.collab).trim(); + const attrs = ` data-lowcoder-room-id="${escapeHtml((0, googleUrls_1.firstQueryValue)(req.query.roomId))}"` + + ` data-lowcoder-role="${escapeHtml((0, googleUrls_1.firstQueryValue)(req.query.role) || "driver")}"` + + ` data-lowcoder-editor-id="${escapeHtml((0, googleUrls_1.firstQueryValue)(req.query.editorId) || "local")}"` + + ` data-lowcoder-collab-id="${escapeHtml(collab)}"` + + ` data-lowcoder-username="${escapeHtml((0, googleUrls_1.firstQueryValue)(req.query.username))}"` + + ` data-lowcoder-upstream-url="${escapeHtml(upstreamUrl.toString())}"` + + ` data-lowcoder-hocuspocus-url="${escapeHtml(hocuspocusUrl)}"` + + (HOCUSPOCUS_SECRET + ? ` data-lowcoder-hocuspocus-token="${escapeHtml(HOCUSPOCUS_SECRET)}"` + : ""); + const withRootAttrs = rewritten.replace(/)/i, `window.__LOWCODER_HOCUSPOCUS__=${hocuspocusConfig};` + + `window.__LOWCODER_GOOGLE_PROXY__=${proxyRuntimeConfig};` + + ``; + // The bridge has to run before Google's scripts so it can patch fetch/XHR. + if (/]*>/i.test(withRootAttrs)) { + return withRootAttrs.replace(/]*>/i, (match) => `${match}${bridgeTag}`); + } + return /<\/head>/i.test(withRootAttrs) + ? withRootAttrs.replace(/<\/head>/i, `${bridgeTag}`) + : `${bridgeTag}${withRootAttrs}`; +} +function rewriteBodyUrls(body, req, upstreamUrl) { + return (0, googleFormsRewrite_1.rewriteGoogleFormsBody)(body, { + upstreamUrl, + toProxied: (absoluteUrl) => (0, googleUrls_1.buildGoogleFormsProxiedUrlFromRequest)(absoluteUrl, req), + }); +} +function rewriteAbsoluteUrl(value, req, upstreamUrl) { + try { + const parsed = new node_url_1.URL(value, upstreamUrl); + (0, googleFormsHosts_1.assertAllowedGoogleFormsUrl)(parsed); + return (0, googleUrls_1.buildGoogleFormsProxiedUrlFromRequest)(parsed.toString(), req); + } + catch { + return value; + } +} +function toInlineJson(value) { + return JSON.stringify(value).replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} +function isAuthorized(req) { + const token = (0, googleUrls_1.firstQueryValue)(req.query.token) || (0, auth_1.getBearerToken)(req.headers.authorization); + return (0, auth_1.verifyProxyToken)(token, "google-forms-proxy"); +} +function checkRateLimit(req) { + const key = req.ip || "unknown"; + const now = Date.now(); + const existing = requestBuckets.get(key); + if (!existing || now > existing.resetAt) { + requestBuckets.set(key, { count: 1, resetAt: now + 60_000 }); + return true; + } + existing.count += 1; + return existing.count <= RATE_LIMIT_PER_MINUTE; +} diff --git a/server/proxy-service/build/googleSession.js b/server/proxy-service/build/googleSession.js new file mode 100644 index 0000000000..6e3e066510 --- /dev/null +++ b/server/proxy-service/build/googleSession.js @@ -0,0 +1,88 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.createGoogleFormsProxySession = createGoogleFormsProxySession; +exports.joinGoogleFormsProxySession = joinGoogleFormsProxySession; +const auth_1 = require("./auth"); +const googleFormUrls_1 = require("./googleFormUrls"); +const googleUrls_1 = require("./googleUrls"); +function normalizeRole(role, fallback = "driver") { + return (role ?? fallback).trim().toLowerCase() === "follower" ? "follower" : "driver"; +} +function normalizeCollab(rawCollab, role) { + const collab = (rawCollab ?? "").trim(); + if (collab) + return collab; + if (role === "driver") { + return String(Date.now()); + } + throw new Error("collab is required for followers. Pass broadcast.collab from the driver's session response."); +} +async function mintGoogleFormsSession(req, body, role) { + const googleFormUrl = await (0, googleFormUrls_1.resolveGoogleFormResponderUrl)(body.googleFormUrl); + const roomId = (body.roomId ?? "").trim(); + const collab = normalizeCollab(body.collab, role); + const username = (body.username ?? "").trim(); + if (!roomId) + throw new Error("roomId is required"); + const participantId = await (0, auth_1.resolveParticipantId)(req, { + editorId: body.editorId?.trim() || undefined, + guestId: body.guestId?.trim() || undefined, + roomId, + role, + }); + const token = (0, auth_1.createProxyToken)(participantId, roomId, role, "google-forms-proxy"); + const proxiedUrl = (0, googleUrls_1.buildGoogleFormsProxiedUrl)(googleFormUrl, { + roomId, + role, + editorId: participantId, + token, + collab, + username, + }); + return { + token, + proxiedUrl, + roomId, + role, + editorId: participantId, + participantId, + googleFormUrl, + collab, + username, + broadcast: { roomId, collab, googleFormUrl, editorId: participantId, username }, + }; +} +function sendSessionResponse(res, data) { + res.status(200).json({ code: 1, message: "", data }); +} +function sendSessionError(res, error) { + console.error("Google Forms proxy session error", error); + const message = error instanceof Error ? error.message : "Unauthorized"; + const status = message.includes("required") || + message.includes("must be") || + message.includes("Invalid URL") || + message.includes("Could not resolve") || + message.includes("webViewLink") || + message.includes("collab") + ? 400 + : 401; + res.status(status).json({ code: status, message }); +} +async function createGoogleFormsProxySession(req, res) { + try { + const body = (req.body ?? {}); + sendSessionResponse(res, await mintGoogleFormsSession(req, body, normalizeRole(body.role))); + } + catch (error) { + sendSessionError(res, error); + } +} +async function joinGoogleFormsProxySession(req, res) { + try { + const body = (req.body ?? {}); + sendSessionResponse(res, await mintGoogleFormsSession(req, body, normalizeRole(body.role, "follower"))); + } + catch (error) { + sendSessionError(res, error); + } +} diff --git a/server/proxy-service/build/googleUrls.js b/server/proxy-service/build/googleUrls.js new file mode 100644 index 0000000000..37f51c465d --- /dev/null +++ b/server/proxy-service/build/googleUrls.js @@ -0,0 +1,45 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.GOOGLE_FORMS_PROXY_PREFIX = void 0; +exports.buildGoogleFormsProxiedUrl = buildGoogleFormsProxiedUrl; +exports.firstQueryValue = firstQueryValue; +exports.buildGoogleFormsProxiedUrlFromRequest = buildGoogleFormsProxiedUrlFromRequest; +exports.GOOGLE_FORMS_PROXY_PREFIX = "/proxy/google-forms"; +function buildGoogleFormsProxiedUrl(targetUrl, options) { + const params = new URLSearchParams(); + params.set("target", targetUrl); + if (options.roomId) + params.set("roomId", options.roomId); + if (options.role) + params.set("role", options.role); + if (options.editorId) + params.set("editorId", options.editorId); + if (options.token) + params.set("token", options.token); + if (options.collab) + params.set("collab", options.collab); + if (options.username) + params.set("username", options.username); + return `${exports.GOOGLE_FORMS_PROXY_PREFIX}?${params.toString()}`; +} +/** + * Express parses repeated query params into arrays, and Google URLs regularly + * carry their own params, so every control param is read defensively. + */ +function firstQueryValue(value) { + if (Array.isArray(value)) { + const first = value.find((entry) => typeof entry === "string"); + return typeof first === "string" ? first : ""; + } + return typeof value === "string" ? value : ""; +} +function buildGoogleFormsProxiedUrlFromRequest(targetUrl, req) { + return buildGoogleFormsProxiedUrl(targetUrl, { + roomId: firstQueryValue(req.query.roomId), + role: firstQueryValue(req.query.role) || "driver", + editorId: firstQueryValue(req.query.editorId), + token: firstQueryValue(req.query.token), + collab: firstQueryValue(req.query.collab), + username: firstQueryValue(req.query.username), + }); +} diff --git a/server/proxy-service/build/server.js b/server/proxy-service/build/server.js new file mode 100644 index 0000000000..cc407464f2 --- /dev/null +++ b/server/proxy-service/build/server.js @@ -0,0 +1,242 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const express_1 = __importDefault(require("express")); +const cors_1 = __importDefault(require("cors")); +const node_fetch_1 = __importDefault(require("node-fetch")); +const node_fs_1 = __importDefault(require("node:fs")); +const node_path_1 = __importDefault(require("node:path")); +const node_url_1 = require("node:url"); +const node_http_1 = require("node:http"); +const session_1 = require("./session"); +const auth_1 = require("./auth"); +const urls_1 = require("./urls"); +const googleProxy_1 = require("./googleProxy"); +const websiteProxy_1 = require("./websiteProxy"); +const PORT = Number(process.env.PROXY_SERVICE_PORT ?? 6070); +const LOWCODER_PUBLIC_URL = (process.env.LOWCODER_PUBLIC_URL ?? "http://localhost:3000").replace(/\/$/, ""); +const HOCUSPOCUS_URL = (process.env.LOWCODER_HOCUSPOCUS_URL ?? "ws://localhost:3006").trim(); +const HOCUSPOCUS_SECRET = (process.env.LOWCODER_HOCUSPOCUS_SECRET ?? process.env.HOCUSPOCUS_SECRET ?? "").trim(); +const RATE_LIMIT_PER_MINUTE = Number(process.env.LOWCODER_PROXY_RATE_LIMIT ?? 120); +const ALLOWED_TYPEFORM_HOSTS = new Set((process.env.LOWCODER_PROXY_ALLOWED_HOSTS ?? "form.typeform.com,embed.typeform.com,admin.typeform.com") + .split(",") + .map((value) => value.trim()) + .filter(Boolean)); +const SESSION_PATH = `${urls_1.PROXY_PREFIX}/session`; +const JOIN_SESSION_PATH = `${urls_1.PROXY_PREFIX}/session/join`; +const BRIDGE_PATH = "/proxy/typeform-bridge.js"; +const requestBuckets = new Map(); +const app = (0, express_1.default)(); +app.disable("x-powered-by"); +app.use((0, cors_1.default)({ credentials: true, origin: true })); +app.get("/", (_req, res) => { + res.status(200).json({ code: 1, message: "Lowcoder Proxy Service is up and running", success: true }); +}); +(0, googleProxy_1.registerGoogleFormsProxy)(app); +(0, websiteProxy_1.registerWebsiteProxy)(app); +app.get(BRIDGE_PATH, (_req, res) => { + const bridgePath = node_path_1.default.join(__dirname, "bridge", "typeform-bridge.js"); + if (!node_fs_1.default.existsSync(bridgePath)) { + res.status(404).type("text/plain").send("Bridge script not found"); + return; + } + res.setHeader("Content-Type", "application/javascript; charset=utf-8"); + res.setHeader("Cache-Control", "no-cache"); + res.send(node_fs_1.default.readFileSync(bridgePath, "utf8")); +}); +app.post(SESSION_PATH, express_1.default.json({ limit: "1mb" }), session_1.createProxySession); +app.post(JOIN_SESSION_PATH, express_1.default.json({ limit: "1mb" }), session_1.joinProxySession); +app.use(urls_1.PROXY_PREFIX, express_1.default.raw({ type: "*/*", limit: "25mb" }), async (req, res) => { + if (req.path === "/session" || req.path === "/session/join") { + res.status(405).json({ message: "Use POST on /proxy/typeform/session or /proxy/typeform/session/join" }); + return; + } + try { + if (!isAuthorized(req)) { + res.status(401).json({ message: "Missing or invalid proxy token" }); + return; + } + if (!checkRateLimit(req)) { + res.status(429).json({ message: "Proxy rate limit exceeded" }); + return; + } + const upstreamUrl = resolveUpstreamUrl(req); + const upstreamResponse = await (0, node_fetch_1.default)(upstreamUrl.toString(), { + method: req.method, + redirect: "manual", + headers: buildForwardHeaders(req, upstreamUrl), + body: hasBody(req.method) ? req.body : undefined, + }); + relayHeaders(upstreamResponse, res, req); + const contentType = upstreamResponse.headers.get("content-type") ?? ""; + if (contentType.includes("text/html")) { + const html = await upstreamResponse.text(); + res.status(upstreamResponse.status).send(injectBridgeAndRewriteHtml(html, req, upstreamUrl)); + return; + } + if (contentType.includes("application/json") || contentType.includes("text/javascript")) { + const text = await upstreamResponse.text(); + res.status(upstreamResponse.status).send(rewriteBodyUrls(text, req, upstreamUrl)); + return; + } + const buffer = await upstreamResponse.buffer(); + res.status(upstreamResponse.status).send(buffer); + } + catch (error) { + console.error("Proxy request failed", error); + res.status(502).json({ message: "Typeform proxy request failed" }); + } +}); +const httpServer = (0, node_http_1.createServer)(app); +httpServer.listen(PORT, () => { + console.log(`Proxy service listening on ${PORT}`); +}); +function hasBody(method) { + return ["POST", "PUT", "PATCH", "DELETE"].includes(method.toUpperCase()); +} +function resolveUpstreamUrl(req) { + const targetParam = req.query.target?.trim(); + if (targetParam) { + const upstream = new node_url_1.URL(targetParam); + assertAllowedHost(upstream.hostname); + return upstream; + } + const rawPath = req.originalUrl.replace(urls_1.PROXY_PREFIX, ""); + const [pathname, query = ""] = rawPath.split("?"); + const cleanPath = pathname.startsWith("/") ? pathname : `/${pathname}`; + const upstream = new node_url_1.URL(`https://form.typeform.com${cleanPath}${query ? `?${query}` : ""}`); + assertAllowedHost(upstream.hostname); + return upstream; +} +function assertAllowedHost(hostname) { + if (!ALLOWED_TYPEFORM_HOSTS.has(hostname)) { + throw new Error(`Host is not allowed: ${hostname}`); + } +} +function buildForwardHeaders(req, upstreamUrl) { + const normalized = new Map(); + Object.entries(req.headers).forEach(([key, value]) => { + if (typeof value !== "string") + return; + const lower = key.toLowerCase(); + if (["host", "content-length", "x-forwarded-host", "x-forwarded-proto", "connection"].includes(lower)) + return; + normalized.set(lower, value); + }); + normalized.set("host", upstreamUrl.host); + normalized.set("origin", `${upstreamUrl.protocol}//${upstreamUrl.host}`); + return Object.fromEntries(normalized.entries()); +} +function relayHeaders(upstreamResponse, res, req) { + const skipHeaders = new Set([ + "x-frame-options", + "content-security-policy", + "transfer-encoding", + "content-length", + "content-encoding", + ]); + upstreamResponse.headers.forEach((value, key) => { + const lower = key.toLowerCase(); + if (skipHeaders.has(lower)) + return; + if (lower === "set-cookie") { + const cookies = rewriteSetCookie(value); + if (cookies.length > 0) + res.setHeader("Set-Cookie", cookies); + return; + } + if (lower === "location") { + res.setHeader(key, rewriteAbsoluteUrl(value, req)); + return; + } + res.setHeader(key, value); + }); + res.setHeader("Content-Security-Policy", "frame-ancestors 'self';"); +} +function rewriteSetCookie(rawValue) { + return rawValue + .split(/,(?=[^;]+=[^;]+)/) + .map((cookie) => cookie + .replace(/;\s*Domain=[^;]+/gi, "") + .replace(/;\s*SameSite=None/gi, "; SameSite=Lax") + .replace(/;\s*Path=[^;]+/gi, "; Path=/proxy/typeform")); +} +function injectBridgeAndRewriteHtml(html, req, upstreamUrl) { + const rewritten = rewriteBodyUrls(html, req, upstreamUrl); + const roomId = String(req.query.roomId ?? ""); + const role = String(req.query.role ?? "driver"); + const editorId = String(req.query.editorId ?? "local"); + const collabId = String(req.query.collab ?? ""); + const username = String(req.query.username ?? ""); + const attrs = ` data-lowcoder-room-id="${escapeHtml(roomId)}"` + + ` data-lowcoder-role="${escapeHtml(role)}"` + + ` data-lowcoder-editor-id="${escapeHtml(editorId)}"` + + ` data-lowcoder-collab-id="${escapeHtml(collabId)}"` + + ` data-lowcoder-username="${escapeHtml(username)}"` + + ` data-lowcoder-hocuspocus-url="${escapeHtml(HOCUSPOCUS_URL)}"` + + (HOCUSPOCUS_SECRET ? ` data-lowcoder-hocuspocus-token="${escapeHtml(HOCUSPOCUS_SECRET)}"` : ""); + const withRootAttrs = rewritten.replace("window.__LOWCODER_HOCUSPOCUS__=${hocuspocusConfig};` + + ``; + if (withRootAttrs.includes("")) { + return withRootAttrs.replace("", `${bridgeTag}`); + } + return `${bridgeTag}${withRootAttrs}`; +} +function rewriteBodyUrls(body, req, upstreamUrl) { + const base = upstreamUrl ?? new node_url_1.URL("https://form.typeform.com"); + const origin = base.origin; + let output = body.replace(new RegExp(`${escapeRegex(origin)}([^"'\\s]*)`, "g"), (_match, suffix) => (0, urls_1.buildProxiedUrlFromRequest)(`${origin}${suffix ?? ""}`, req)); + output = output.replace(/(href|src|action)=["']\/([^"']+)["']/g, (_m, attr, route) => { + const target = `${base.origin}/${route}`; + return `${attr}="${(0, urls_1.buildProxiedUrlFromRequest)(target, req)}"`; + }); + output = output.replace(/url\(["']?\/([^"')]+)["']?\)/g, (_m, route) => { + const target = `${base.origin}/${route}`; + return `url("${(0, urls_1.buildProxiedUrlFromRequest)(target, req)}")`; + }); + return output; +} +function rewriteAbsoluteUrl(value, req) { + try { + const parsed = new node_url_1.URL(value); + if (!ALLOWED_TYPEFORM_HOSTS.has(parsed.hostname)) + return value; + return (0, urls_1.buildProxiedUrlFromRequest)(parsed.toString(), req); + } + catch { + return value; + } +} +function escapeRegex(text) { + return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} +function escapeHtml(value) { + return value + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} +function isAuthorized(req) { + const token = req.query.token || (0, auth_1.getBearerToken)(req.headers.authorization); + return (0, auth_1.verifyProxyToken)(token, "typeform-proxy"); +} +function checkRateLimit(req) { + const key = req.ip || "unknown"; + const now = Date.now(); + const existing = requestBuckets.get(key); + if (!existing || now > existing.resetAt) { + requestBuckets.set(key, { count: 1, resetAt: now + 60_000 }); + return true; + } + existing.count += 1; + return existing.count <= RATE_LIMIT_PER_MINUTE; +} diff --git a/server/proxy-service/build/session.js b/server/proxy-service/build/session.js new file mode 100644 index 0000000000..59ea56a0b3 --- /dev/null +++ b/server/proxy-service/build/session.js @@ -0,0 +1,88 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.createProxySession = createProxySession; +exports.joinProxySession = joinProxySession; +const auth_1 = require("./auth"); +const urls_1 = require("./urls"); +function normalizeRole(role, fallback = "driver") { + const value = (role ?? fallback).trim().toLowerCase(); + return value === "follower" ? "follower" : "driver"; +} +async function mintSession(req, body, role) { + const typeformUrl = (body.typeformUrl ?? "").trim(); + const roomId = (body.roomId ?? "").trim(); + const collab = (body.collab ?? "").trim(); + const username = (body.username ?? "").trim(); + if (!typeformUrl) { + throw new Error("typeformUrl is required"); + } + if (!roomId) { + throw new Error("roomId is required"); + } + const participantId = await (0, auth_1.resolveParticipantId)(req, { + editorId: body.editorId?.trim() || undefined, + guestId: body.guestId?.trim() || undefined, + roomId, + role, + }); + const token = (0, auth_1.createProxyToken)(participantId, roomId, role); + const proxiedUrl = (0, urls_1.buildProxiedUrl)(typeformUrl, { + roomId, + role, + editorId: participantId, + token, + collab, + username, + }); + return { + token, + proxiedUrl, + roomId, + role, + editorId: participantId, + participantId, + typeformUrl, + collab, + username, + broadcast: { roomId, collab, typeformUrl, editorId: participantId, username }, + }; +} +function sendSessionResponse(res, data) { + res.status(200).json({ + code: 1, + message: "", + data, + }); +} +function sendSessionError(res, error) { + console.error("Proxy session error", error); + const status = error instanceof Error && error.message.includes("required") ? 400 : 401; + res.status(status).json({ + code: status, + message: error instanceof Error ? error.message : "Unauthorized", + }); +} +/** Creates a proxy session. Respects `body.role` (`driver` or `follower`). */ +async function createProxySession(req, res) { + try { + const body = (req.body ?? {}); + const role = normalizeRole(body.role, "driver"); + const data = await mintSession(req, body, role); + sendSessionResponse(res, data); + } + catch (error) { + sendSessionError(res, error); + } +} +/** Join an existing collab room as follower (same as session with role=follower). */ +async function joinProxySession(req, res) { + try { + const body = (req.body ?? {}); + const role = normalizeRole(body.role, "follower"); + const data = await mintSession(req, body, role); + sendSessionResponse(res, data); + } + catch (error) { + sendSessionError(res, error); + } +} diff --git a/server/proxy-service/build/urls.js b/server/proxy-service/build/urls.js new file mode 100644 index 0000000000..6ed900e6e0 --- /dev/null +++ b/server/proxy-service/build/urls.js @@ -0,0 +1,33 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.PROXY_PREFIX = void 0; +exports.buildProxiedUrl = buildProxiedUrl; +exports.buildProxiedUrlFromRequest = buildProxiedUrlFromRequest; +exports.PROXY_PREFIX = "/proxy/typeform"; +function buildProxiedUrl(targetUrl, options) { + const params = new URLSearchParams(); + params.set("target", targetUrl); + if (options.roomId) + params.set("roomId", options.roomId); + if (options.role) + params.set("role", options.role); + if (options.editorId) + params.set("editorId", options.editorId); + if (options.token) + params.set("token", options.token); + if (options.collab) + params.set("collab", options.collab); + if (options.username) + params.set("username", options.username); + return `${exports.PROXY_PREFIX}?${params.toString()}`; +} +function buildProxiedUrlFromRequest(targetUrl, req) { + return buildProxiedUrl(targetUrl, { + roomId: String(req.query.roomId ?? ""), + role: String(req.query.role ?? "driver"), + editorId: String(req.query.editorId ?? ""), + token: String(req.query.token ?? ""), + collab: String(req.query.collab ?? ""), + username: String(req.query.username ?? ""), + }); +} diff --git a/server/proxy-service/build/websiteAllowlist.js b/server/proxy-service/build/websiteAllowlist.js new file mode 100644 index 0000000000..a8762bdd33 --- /dev/null +++ b/server/proxy-service/build/websiteAllowlist.js @@ -0,0 +1,100 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.normalizeWebsiteUrl = normalizeWebsiteUrl; +exports.assertAllowedWebsiteUrl = assertAllowedWebsiteUrl; +exports.isWebsiteHostAllowed = isWebsiteHostAllowed; +const node_net_1 = require("node:net"); +const ALLOWED_WEBSITE_HOSTS = new Set((process.env.LOWCODER_WEBSITE_ALLOWED_HOSTS ?? "") + .split(",") + .map((value) => value.trim().toLowerCase()) + .filter(Boolean)); +const BLOCKED_HOSTNAMES = new Set([ + "localhost", + "localhost.localdomain", + "metadata.google.internal", + "metadata", +]); +function isPrivateOrReservedIp(ip) { + const version = (0, node_net_1.isIP)(ip); + if (version === 4) { + const parts = ip.split(".").map(Number); + const [a, b] = parts; + if (a === 10) + return true; + if (a === 127) + return true; + if (a === 0) + return true; + if (a === 169 && b === 254) + return true; + if (a === 172 && b >= 16 && b <= 31) + return true; + if (a === 192 && b === 168) + return true; + if (a === 100 && b >= 64 && b <= 127) + return true; // CGNAT + if (a >= 224) + return true; // multicast / reserved + return false; + } + if (version === 6) { + const normalized = ip.toLowerCase(); + if (normalized === "::1" || normalized === "::") + return true; + if (normalized.startsWith("fc") || normalized.startsWith("fd")) + return true; // ULA + if (normalized.startsWith("fe80")) + return true; // link-local + if (normalized.startsWith("ff")) + return true; // multicast + // IPv4-mapped IPv6 + if (normalized.startsWith("::ffff:")) { + const mapped = normalized.slice("::ffff:".length); + if ((0, node_net_1.isIP)(mapped) === 4) + return isPrivateOrReservedIp(mapped); + } + return false; + } + return true; +} +function normalizeWebsiteUrl(raw) { + const trimmed = (raw ?? "").trim(); + if (!trimmed) { + throw new Error("websiteUrl is required"); + } + let parsed; + try { + parsed = new URL(trimmed); + } + catch { + throw new Error(`Invalid URL: ${trimmed}`); + } + if (parsed.protocol !== "http:" && parsed.protocol !== "https:") { + throw new Error(`Website URL must be http(s): ${trimmed}`); + } + return parsed.toString(); +} +function assertAllowedWebsiteUrl(url) { + if (url.protocol !== "http:" && url.protocol !== "https:") { + throw new Error(`Website URL is not allowed: ${url.toString()}`); + } + const hostname = url.hostname.toLowerCase(); + if (!hostname) { + throw new Error(`Website URL is not allowed: ${url.toString()}`); + } + if (BLOCKED_HOSTNAMES.has(hostname) || hostname.endsWith(".localhost")) { + throw new Error(`Website URL is not allowed: ${url.toString()}`); + } + if ((0, node_net_1.isIP)(hostname) && isPrivateOrReservedIp(hostname)) { + throw new Error(`Website URL is not allowed: ${url.toString()}`); + } + if (ALLOWED_WEBSITE_HOSTS.size > 0 && !ALLOWED_WEBSITE_HOSTS.has(hostname)) { + throw new Error(`Website URL is not allowed: ${url.toString()}`); + } +} +function isWebsiteHostAllowed(hostname) { + const lower = hostname.toLowerCase(); + if (ALLOWED_WEBSITE_HOSTS.size === 0) + return true; + return ALLOWED_WEBSITE_HOSTS.has(lower); +} diff --git a/server/proxy-service/build/websiteProxy.js b/server/proxy-service/build/websiteProxy.js new file mode 100644 index 0000000000..8681180932 --- /dev/null +++ b/server/proxy-service/build/websiteProxy.js @@ -0,0 +1,235 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.registerWebsiteProxy = registerWebsiteProxy; +const express_1 = __importDefault(require("express")); +const node_fetch_1 = __importDefault(require("node-fetch")); +const node_fs_1 = __importDefault(require("node:fs")); +const node_path_1 = __importDefault(require("node:path")); +const node_url_1 = require("node:url"); +const auth_1 = require("./auth"); +const websiteAllowlist_1 = require("./websiteAllowlist"); +const websiteUrls_1 = require("./websiteUrls"); +const websiteSession_1 = require("./websiteSession"); +const BRIDGE_PATH = "/proxy/website-bridge.js"; +const HOCUSPOCUS_URL = (process.env.LOWCODER_HOCUSPOCUS_URL ?? "ws://localhost:3006").trim(); +function resolveHocuspocusUrl(req) { + if (!/localhost|127\.0\.0\.1/.test(HOCUSPOCUS_URL)) { + return HOCUSPOCUS_URL; + } + const forwardedHost = (req.get("x-forwarded-host") || req.get("host") || "localhost") + .split(",")[0] + .trim(); + const hostname = forwardedHost.split(":")[0] || "localhost"; + const port = new node_url_1.URL(HOCUSPOCUS_URL.replace(/^ws/, "http")).port || "3006"; + return `ws://${hostname}:${port}`; +} +const HOCUSPOCUS_SECRET = (process.env.LOWCODER_HOCUSPOCUS_SECRET ?? process.env.HOCUSPOCUS_SECRET ?? "").trim(); +const RATE_LIMIT_PER_MINUTE = Number(process.env.LOWCODER_PROXY_RATE_LIMIT ?? 120); +const requestBuckets = new Map(); +function registerWebsiteProxy(app) { + app.get(BRIDGE_PATH, (_req, res) => { + const bridgePath = node_path_1.default.join(__dirname, "bridge", "website-bridge.js"); + if (!node_fs_1.default.existsSync(bridgePath)) { + res.status(404).type("text/plain").send("Website bridge script not found"); + return; + } + res.setHeader("Content-Type", "application/javascript; charset=utf-8"); + res.setHeader("Cache-Control", "no-cache"); + res.send(node_fs_1.default.readFileSync(bridgePath, "utf8")); + }); + const router = express_1.default.Router(); + router.post("/session", express_1.default.json({ limit: "1mb" }), websiteSession_1.createWebsiteProxySession); + router.post("/session/join", express_1.default.json({ limit: "1mb" }), websiteSession_1.joinWebsiteProxySession); + router.use(express_1.default.raw({ type: "*/*", limit: "25mb" }), async (req, res) => { + if (req.path === "/session" || req.path === "/session/join") { + res.status(405).json({ + message: "Use POST on /proxy/website/session or /proxy/website/session/join", + }); + return; + } + try { + if (!isAuthorized(req)) { + res.status(401).json({ message: "Missing or invalid proxy token" }); + return; + } + if (!checkRateLimit(req)) { + res.status(429).json({ message: "Proxy rate limit exceeded" }); + return; + } + const upstreamUrl = resolveUpstreamUrl(req); + const upstreamResponse = await (0, node_fetch_1.default)(upstreamUrl.toString(), { + method: req.method, + redirect: "manual", + headers: buildForwardHeaders(req, upstreamUrl), + body: hasBody(req.method) ? req.body : undefined, + }); + relayHeaders(upstreamResponse, res, req, upstreamUrl); + const contentType = upstreamResponse.headers.get("content-type") ?? ""; + if (contentType.includes("text/html")) { + const html = await upstreamResponse.text(); + res + .status(upstreamResponse.status) + .send(injectBridgeAndRewriteHtml(html, req, upstreamUrl)); + return; + } + if (/javascript|json|text\/css/.test(contentType)) { + const text = await upstreamResponse.text(); + res.status(upstreamResponse.status).send(rewriteBodyUrls(text, req, upstreamUrl)); + return; + } + res.status(upstreamResponse.status).send(await upstreamResponse.buffer()); + } + catch (error) { + console.error("Website proxy request failed", error); + const message = error instanceof Error ? error.message : "Website proxy request failed"; + const status = message.includes("not allowed") || message.includes("required") ? 400 : 502; + res.status(status).json({ message }); + } + }); + app.use(websiteUrls_1.WEBSITE_PROXY_PREFIX, router); +} +function hasBody(method) { + return ["POST", "PUT", "PATCH", "DELETE"].includes(method.toUpperCase()); +} +function resolveUpstreamUrl(req) { + const targetParam = req.query.target?.trim(); + if (!targetParam) { + throw new Error("A website target URL is required"); + } + const upstream = new node_url_1.URL(targetParam); + (0, websiteAllowlist_1.assertAllowedWebsiteUrl)(upstream); + return upstream; +} +function buildForwardHeaders(req, upstreamUrl) { + const normalized = new Map(); + Object.entries(req.headers).forEach(([key, value]) => { + if (typeof value !== "string") + return; + const lower = key.toLowerCase(); + if (["host", "content-length", "x-forwarded-host", "x-forwarded-proto", "connection"].includes(lower)) { + return; + } + normalized.set(lower, value); + }); + normalized.set("host", upstreamUrl.host); + normalized.set("origin", upstreamUrl.origin); + normalized.set("referer", upstreamUrl.toString()); + return Object.fromEntries(normalized.entries()); +} +function relayHeaders(upstreamResponse, res, req, upstreamUrl) { + const skipHeaders = new Set([ + "x-frame-options", + "content-security-policy", + "transfer-encoding", + "content-length", + "content-encoding", + ]); + upstreamResponse.headers.forEach((value, key) => { + const lower = key.toLowerCase(); + if (skipHeaders.has(lower)) + return; + if (lower === "set-cookie") { + const cookies = rewriteSetCookie(value); + if (cookies.length > 0) + res.setHeader("Set-Cookie", cookies); + return; + } + if (lower === "location") { + res.setHeader(key, rewriteAbsoluteUrl(value, req, upstreamUrl)); + return; + } + res.setHeader(key, value); + }); + res.setHeader("Content-Security-Policy", "frame-ancestors 'self';"); +} +function rewriteSetCookie(rawValue) { + return rawValue + .split(/,(?=[^;]+=[^;]+)/) + .map((cookie) => cookie + .replace(/;\s*Domain=[^;]+/gi, "") + .replace(/;\s*SameSite=None/gi, "; SameSite=Lax") + .replace(/;\s*Path=[^;]+/gi, `; Path=${websiteUrls_1.WEBSITE_PROXY_PREFIX}`)); +} +function injectBridgeAndRewriteHtml(html, req, upstreamUrl) { + const rewritten = rewriteBodyUrls(html, req, upstreamUrl); + const hocuspocusUrl = resolveHocuspocusUrl(req); + const collab = String(req.query.collab ?? "").trim(); + const attrs = ` data-lowcoder-room-id="${escapeHtml(String(req.query.roomId ?? ""))}"` + + ` data-lowcoder-role="${escapeHtml(String(req.query.role ?? "driver"))}"` + + ` data-lowcoder-editor-id="${escapeHtml(String(req.query.editorId ?? "local"))}"` + + ` data-lowcoder-collab-id="${escapeHtml(collab)}"` + + ` data-lowcoder-username="${escapeHtml(String(req.query.username ?? ""))}"` + + ` data-lowcoder-upstream-url="${escapeHtml(upstreamUrl.toString())}"` + + ` data-lowcoder-hocuspocus-url="${escapeHtml(hocuspocusUrl)}"` + + (HOCUSPOCUS_SECRET + ? ` data-lowcoder-hocuspocus-token="${escapeHtml(HOCUSPOCUS_SECRET)}"` + : ""); + const withRootAttrs = rewritten.replace(/)/i, `window.__LOWCODER_HOCUSPOCUS__=${hocuspocusConfig};` + + ``; + return /<\/head>/i.test(withRootAttrs) + ? withRootAttrs.replace(/<\/head>/i, `${bridgeTag}`) + : `${bridgeTag}${withRootAttrs}`; +} +function rewriteBodyUrls(body, req, upstreamUrl) { + const origin = upstreamUrl.origin; + let output = body.replace(new RegExp(`${escapeRegex(origin)}([^"'\\\\\\s<]*)`, "g"), (_match, suffix) => (0, websiteUrls_1.buildWebsiteProxiedUrlFromRequest)(`${origin}${suffix ?? ""}`, req)); + // Root-relative paths (same origin under the proxy) + output = output.replace(/(href|src|action)=["'](\/[^"'#?]*(?:\?[^"']*)?(?:#[^"']*)?)["']/gi, (_match, attr, route) => { + if (route.startsWith("//") || route.startsWith("/proxy/")) + return _match; + return `${attr}="${(0, websiteUrls_1.buildWebsiteProxiedUrlFromRequest)(`${origin}${route}`, req)}"`; + }); + output = output.replace(/url\(["']?(\/[^"')]+)["']?\)/gi, (_match, route) => { + if (route.startsWith("//") || route.startsWith("/proxy/")) + return _match; + return `url("${(0, websiteUrls_1.buildWebsiteProxiedUrlFromRequest)(`${origin}${route}`, req)}")`; + }); + return output; +} +function rewriteAbsoluteUrl(value, req, upstreamUrl) { + try { + const parsed = new node_url_1.URL(value, upstreamUrl); + if (parsed.origin !== upstreamUrl.origin) { + return value; + } + (0, websiteAllowlist_1.assertAllowedWebsiteUrl)(parsed); + return (0, websiteUrls_1.buildWebsiteProxiedUrlFromRequest)(parsed.toString(), req); + } + catch { + return value; + } +} +function escapeRegex(text) { + return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} +function escapeHtml(value) { + return value + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} +function isAuthorized(req) { + const token = req.query.token || (0, auth_1.getBearerToken)(req.headers.authorization); + return (0, auth_1.verifyProxyToken)(token, "website-proxy"); +} +function checkRateLimit(req) { + const key = req.ip || "unknown"; + const now = Date.now(); + const existing = requestBuckets.get(key); + if (!existing || now > existing.resetAt) { + requestBuckets.set(key, { count: 1, resetAt: now + 60_000 }); + return true; + } + existing.count += 1; + return existing.count <= RATE_LIMIT_PER_MINUTE; +} diff --git a/server/proxy-service/build/websiteSession.js b/server/proxy-service/build/websiteSession.js new file mode 100644 index 0000000000..b6e4cce2ac --- /dev/null +++ b/server/proxy-service/build/websiteSession.js @@ -0,0 +1,88 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.createWebsiteProxySession = createWebsiteProxySession; +exports.joinWebsiteProxySession = joinWebsiteProxySession; +const auth_1 = require("./auth"); +const websiteAllowlist_1 = require("./websiteAllowlist"); +const websiteUrls_1 = require("./websiteUrls"); +function normalizeRole(role, fallback = "driver") { + return (role ?? fallback).trim().toLowerCase() === "follower" ? "follower" : "driver"; +} +function normalizeCollab(rawCollab, role) { + const collab = (rawCollab ?? "").trim(); + if (collab) + return collab; + if (role === "driver") { + return String(Date.now()); + } + throw new Error("collab is required for followers. Pass broadcast.collab from the driver's session response."); +} +async function mintWebsiteSession(req, body, role) { + const websiteUrl = (0, websiteAllowlist_1.normalizeWebsiteUrl)(body.websiteUrl); + (0, websiteAllowlist_1.assertAllowedWebsiteUrl)(new URL(websiteUrl)); + const roomId = (body.roomId ?? "").trim(); + const collab = normalizeCollab(body.collab, role); + const username = (body.username ?? "").trim(); + if (!roomId) + throw new Error("roomId is required"); + const participantId = await (0, auth_1.resolveParticipantId)(req, { + editorId: body.editorId?.trim() || undefined, + guestId: body.guestId?.trim() || undefined, + roomId, + role, + }); + const token = (0, auth_1.createProxyToken)(participantId, roomId, role, "website-proxy"); + const proxiedUrl = (0, websiteUrls_1.buildWebsiteProxiedUrl)(websiteUrl, { + roomId, + role, + editorId: participantId, + token, + collab, + username, + }); + return { + token, + proxiedUrl, + roomId, + role, + editorId: participantId, + participantId, + websiteUrl, + collab, + username, + broadcast: { roomId, collab, websiteUrl, editorId: participantId, username }, + }; +} +function sendSessionResponse(res, data) { + res.status(200).json({ code: 1, message: "", data }); +} +function sendSessionError(res, error) { + console.error("Website proxy session error", error); + const message = error instanceof Error ? error.message : "Unauthorized"; + const status = message.includes("required") || + message.includes("must be") || + message.includes("Invalid URL") || + message.includes("not allowed") || + message.includes("collab") + ? 400 + : 401; + res.status(status).json({ code: status, message }); +} +async function createWebsiteProxySession(req, res) { + try { + const body = (req.body ?? {}); + sendSessionResponse(res, await mintWebsiteSession(req, body, normalizeRole(body.role))); + } + catch (error) { + sendSessionError(res, error); + } +} +async function joinWebsiteProxySession(req, res) { + try { + const body = (req.body ?? {}); + sendSessionResponse(res, await mintWebsiteSession(req, body, normalizeRole(body.role, "follower"))); + } + catch (error) { + sendSessionError(res, error); + } +} diff --git a/server/proxy-service/build/websiteUrls.js b/server/proxy-service/build/websiteUrls.js new file mode 100644 index 0000000000..f6935e34a4 --- /dev/null +++ b/server/proxy-service/build/websiteUrls.js @@ -0,0 +1,33 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.WEBSITE_PROXY_PREFIX = void 0; +exports.buildWebsiteProxiedUrl = buildWebsiteProxiedUrl; +exports.buildWebsiteProxiedUrlFromRequest = buildWebsiteProxiedUrlFromRequest; +exports.WEBSITE_PROXY_PREFIX = "/proxy/website"; +function buildWebsiteProxiedUrl(targetUrl, options) { + const params = new URLSearchParams(); + params.set("target", targetUrl); + if (options.roomId) + params.set("roomId", options.roomId); + if (options.role) + params.set("role", options.role); + if (options.editorId) + params.set("editorId", options.editorId); + if (options.token) + params.set("token", options.token); + if (options.collab) + params.set("collab", options.collab); + if (options.username) + params.set("username", options.username); + return `${exports.WEBSITE_PROXY_PREFIX}?${params.toString()}`; +} +function buildWebsiteProxiedUrlFromRequest(targetUrl, req) { + return buildWebsiteProxiedUrl(targetUrl, { + roomId: String(req.query.roomId ?? ""), + role: String(req.query.role ?? "driver"), + editorId: String(req.query.editorId ?? ""), + token: String(req.query.token ?? ""), + collab: String(req.query.collab ?? ""), + username: String(req.query.username ?? ""), + }); +} diff --git a/server/proxy-service/package.json b/server/proxy-service/package.json new file mode 100644 index 0000000000..168f6e711a --- /dev/null +++ b/server/proxy-service/package.json @@ -0,0 +1,30 @@ +{ + "name": "lowcoder-proxy-service", + "version": "0.1.0", + "private": true, + "main": "build/server.js", + "scripts": { + "dev": "nodemon src/server.ts", + "build": "rm -rf build/ && tsc && node scripts/build-bridge.mjs", + "start": "node build/server.js" + }, + "dependencies": { + "@hocuspocus/provider": "^3.4.4", + "cors": "^2.8.5", + "express": "^4.21.1", + "jsonwebtoken": "^9.0.2", + "node-fetch": "2", + "yjs": "^13.6.27" + }, + "devDependencies": { + "@types/cors": "^2.8.17", + "@types/express": "^4.17.21", + "@types/jsonwebtoken": "^9.0.7", + "@types/node": "^22.10.5", + "@types/node-fetch": "^2.6.12", + "esbuild": "^0.25.0", + "nodemon": "^3.1.9", + "ts-node": "^10.9.2", + "typescript": "^5.6.2" + } +} diff --git a/server/proxy-service/scripts/build-bridge.mjs b/server/proxy-service/scripts/build-bridge.mjs new file mode 100644 index 0000000000..de6bff6cd0 --- /dev/null +++ b/server/proxy-service/scripts/build-bridge.mjs @@ -0,0 +1,22 @@ +import esbuild from "esbuild"; +import { mkdirSync } from "node:fs"; + +mkdirSync("build/bridge", { recursive: true }); + +await Promise.all( + [ + ["src/bridge/typeform-bridge.ts", "build/bridge/typeform-bridge.js"], + ["src/bridge/google-forms-bridge.ts", "build/bridge/google-forms-bridge.js"], + ["src/bridge/website-bridge.ts", "build/bridge/website-bridge.js"], + ].map(([entryPoint, outfile]) => + esbuild.build({ + entryPoints: [entryPoint], + bundle: true, + format: "iife", + platform: "browser", + target: ["es2020"], + outfile, + logLevel: "info", + }) + ) +); diff --git a/server/proxy-service/src/auth.ts b/server/proxy-service/src/auth.ts new file mode 100644 index 0000000000..872f2ae199 --- /dev/null +++ b/server/proxy-service/src/auth.ts @@ -0,0 +1,102 @@ +import jwt from "jsonwebtoken"; +import fetch from "node-fetch"; +import { randomUUID } from "node:crypto"; +import { Request } from "express"; + +const API_KEY_SECRET = process.env.LOWCODER_API_KEY_SECRET ?? ""; +const TOKEN_TTL_MS = 60 * 60 * 1000; +const API_SERVICE_URL = (process.env.LOWCODER_API_SERVICE_URL ?? "http://localhost:8080").replace(/\/$/, ""); + +export interface ProxyTokenPayload { + userId: string; + roomId: string; + role: string; + scope: string; +} + +export interface ParticipantIdentityOptions { + editorId?: string; + guestId?: string; + roomId?: string; + role?: string; +} + +export function getSigningSecret(): string | null { + if (!API_KEY_SECRET) return null; + return Buffer.from(API_KEY_SECRET).toString("base64"); +} + +export function createProxyToken( + userId: string, + roomId: string, + role: string, + scope = "typeform-proxy" +): string { + const secret = getSigningSecret(); + if (!secret) { + return jwt.sign({ userId, roomId, role, scope }, "dev-proxy-secret", { + expiresIn: "1h", + }); + } + return jwt.sign({ sub: userId, userId, roomId, role, scope }, secret, { + algorithm: "HS256", + expiresIn: "1h", + }); +} + +export function verifyProxyToken( + token: string | null | undefined, + expectedScope?: string +): boolean { + if (!token) return false; + const secret = getSigningSecret(); + if (!secret) return true; + try { + const payload = jwt.verify(token, secret) as ProxyTokenPayload; + return !expectedScope || payload.scope === expectedScope; + } catch { + return false; + } +} + +export function getBearerToken(authHeader: string | undefined): string | null { + if (!authHeader || !authHeader.startsWith("Bearer ")) return null; + return authHeader.slice("Bearer ".length); +} + +export async function resolveParticipantId( + req: Request, + options: ParticipantIdentityOptions = {} +): Promise { + const editorId = options.editorId?.trim(); + if (editorId) return editorId; + + const guestId = options.guestId?.trim(); + if (guestId) return guestId; + + const cookie = req.headers.cookie; + if (cookie) { + const response = await fetch(`${API_SERVICE_URL}/api/users/me`, { + headers: { cookie }, + }); + + if (response.ok) { + const payload = (await response.json()) as { data?: { id?: string } }; + const userId = payload?.data?.id?.trim(); + if (userId) return userId; + } + } + + const roomId = options.roomId?.trim(); + const role = (options.role?.trim() || "driver").trim() || "driver"; + if (roomId) { + return `guest-${roomId}-${role}`; + } + + return `guest-${randomUUID()}`; +} + +/** @deprecated Use resolveParticipantId */ +export async function resolveEditorId(req: Request, fallbackEditorId?: string): Promise { + return resolveParticipantId(req, { editorId: fallbackEditorId }); +} diff --git a/server/proxy-service/src/bridge/cursor-presence/CursorOverlay.ts b/server/proxy-service/src/bridge/cursor-presence/CursorOverlay.ts new file mode 100644 index 0000000000..4a5ab218a5 --- /dev/null +++ b/server/proxy-service/src/bridge/cursor-presence/CursorOverlay.ts @@ -0,0 +1,192 @@ +import type { + AwarenessPresenceState, + CursorPresenceData, + CursorPresenceUser, + RemoteCursorRenderState, +} from "./types"; +import { + getCaretCoordinatesForField, + getFieldFallbackCaret, + getSelectionRectsForField, + destroyCaretMirror, +} from "./caretMetrics"; +import { + findFieldByCursorKey, + type TextFieldElement, +} from "./textField"; +import { RemoteCursor, ensureCursorStyles } from "./RemoteCursor"; + +interface AwarenessLike { + clientID: number; + getStates(): Map; +} + +interface CursorOverlayOptions { + getCurrentStep: () => number; + findFieldByKey: (key: string) => HTMLInputElement | HTMLTextAreaElement | null; + /** Local user id — never render a collaborative cursor for this user. */ + localUserId: string; +} + +const LERP_FACTOR = 0.35; + +export class CursorOverlay { + private container: HTMLDivElement; + private cursors = new Map(); + private renderStates = new Map(); + private rafId: number | null = null; + private destroyed = false; + + constructor(private readonly options: CursorOverlayOptions) { + ensureCursorStyles(); + this.container = document.createElement("div"); + this.container.id = "lowcoder-cursor-overlay"; + this.container.style.cssText = + "position:fixed;inset:0;pointer-events:none;z-index:2147483647;overflow:visible;"; + document.documentElement.appendChild(this.container); + this.startAnimationLoop(); + } + + /** + * Render cursors for other connected users only. + * The local typist never sees their own collaborative caret/label. + * Null / inactive remote cursors are hidden (no time-based timeout). + */ + syncFromAwareness(awareness: AwarenessLike): void { + const localClientId = awareness.clientID; + const localUserId = this.options.localUserId; + const active = new Set(); + + awareness.getStates().forEach((rawState, clientId) => { + if (clientId === localClientId) return; + + const state = rawState as AwarenessPresenceState | null; + if (!state?.user) return; + if (state.user.id === localUserId) return; + + if (!this.isActiveRemoteCursor(state.cursor)) { + this.removeRemote(clientId); + return; + } + + active.add(clientId); + this.upsertRemoteState(clientId, state.user, state.cursor!); + }); + + for (const clientId of this.cursors.keys()) { + if (!active.has(clientId)) this.removeRemote(clientId); + } + this.renderAll(); + } + + private isActiveRemoteCursor(cursor: CursorPresenceData | null | undefined): boolean { + return cursor != null && cursor.typing === true; + } + + private upsertRemoteState( + clientId: number, + user: CursorPresenceUser, + cursor: CursorPresenceData | null + ): void { + const existing = this.renderStates.get(clientId); + const metrics = this.resolveCursorMetrics(cursor); + const hasCursor = cursor?.typing === true && metrics != null; + + this.renderStates.set(clientId, { + clientId, + user, + cursor, + x: existing?.x ?? metrics?.x ?? 0, + y: existing?.y ?? metrics?.y ?? 0, + targetX: metrics?.x ?? existing?.targetX ?? 0, + targetY: metrics?.y ?? existing?.targetY ?? 0, + height: metrics?.height ?? existing?.height ?? 16, + selectionRects: metrics?.selectionRects ?? [], + online: hasCursor, + }); + + if (!this.cursors.has(clientId)) { + const remoteCursor = new RemoteCursor(clientId); + remoteCursor.mount(this.container); + this.cursors.set(clientId, remoteCursor); + } + } + + private removeRemote(clientId: number): void { + this.renderStates.delete(clientId); + this.cursors.get(clientId)?.destroy(); + this.cursors.delete(clientId); + } + + private resolveField(key: string): TextFieldElement | null { + return findFieldByCursorKey( + key, + this.options.getCurrentStep(), + this.options.findFieldByKey + ); + } + + private resolveCursorMetrics(cursor: CursorPresenceData | null) { + if (!this.isActiveRemoteCursor(cursor)) return null; + if (cursor!.step !== this.options.getCurrentStep()) return null; + + const field = this.resolveField(cursor!.fieldKey); + if (!field?.isConnected) return null; + + let caret = + getCaretCoordinatesForField(field, cursor!.selection.head) ?? + getFieldFallbackCaret(field); + + if (!caret || !Number.isFinite(caret.left)) { + caret = getFieldFallbackCaret(field); + } + + return { + x: caret.left, + y: caret.top, + height: caret.height, + selectionRects: getSelectionRectsForField( + field, + cursor!.selection.anchor, + cursor!.selection.head + ), + }; + } + + private renderAll(): void { + for (const state of this.renderStates.values()) { + this.cursors.get(state.clientId)?.update(state, this.container); + } + } + + private startAnimationLoop(): void { + const tick = () => { + if (this.destroyed) return; + for (const state of this.renderStates.values()) { + const dx = state.targetX - state.x; + const dy = state.targetY - state.y; + if (Math.abs(dx) > 0.5 || Math.abs(dy) > 0.5) { + state.x += dx * LERP_FACTOR; + state.y += dy * LERP_FACTOR; + } else { + state.x = state.targetX; + state.y = state.targetY; + } + this.cursors.get(state.clientId)?.updatePosition(state.x, state.y); + } + this.rafId = window.requestAnimationFrame(tick); + }; + this.rafId = window.requestAnimationFrame(tick); + } + + destroy(): void { + if (this.destroyed) return; + this.destroyed = true; + if (this.rafId != null) window.cancelAnimationFrame(this.rafId); + for (const cursor of this.cursors.values()) cursor.destroy(); + this.cursors.clear(); + this.renderStates.clear(); + this.container.remove(); + destroyCaretMirror(); + } +} diff --git a/server/proxy-service/src/bridge/cursor-presence/CursorPresenceProvider.ts b/server/proxy-service/src/bridge/cursor-presence/CursorPresenceProvider.ts new file mode 100644 index 0000000000..35437f7fbf --- /dev/null +++ b/server/proxy-service/src/bridge/cursor-presence/CursorPresenceProvider.ts @@ -0,0 +1,80 @@ +import type { HocuspocusProvider } from "@hocuspocus/provider"; +import type { + AwarenessPresenceState, + CursorPresenceData, + CursorPresenceUser, +} from "./types"; + +const DEFAULT_THROTTLE_MS = 33; + +export class CursorPresenceProvider { + private pendingCursor: CursorPresenceData | null | undefined; + private throttleTimer: number | undefined; + private lastBroadcastAt = 0; + private destroyed = false; + private readonly throttleMs: number; + private readonly onRemoteChange: () => void; + private boundAwarenessChange = (): void => this.onRemoteChange(); + + constructor( + private readonly provider: HocuspocusProvider, + user: CursorPresenceUser, + onRemoteChange: () => void, + throttleMs = DEFAULT_THROTTLE_MS + ) { + this.onRemoteChange = onRemoteChange; + this.throttleMs = throttleMs; + this.provider.setAwarenessField("user", user); + this.provider.setAwarenessField("cursor", null); + this.provider.awareness?.on("change", this.boundAwarenessChange); + } + + /** + * Broadcast typing caret to peers via awareness. + * Does not create any local DOM — peers render it; the typist does not. + * Passing `null` clears immediately (no throttle) so inactive cursors vanish. + */ + setLocalCursor(cursor: CursorPresenceData | null): void { + if (this.destroyed) return; + + if (cursor === null) { + window.clearTimeout(this.throttleTimer); + this.pendingCursor = null; + this.flushBroadcast(); + return; + } + + this.pendingCursor = cursor; + const elapsed = performance.now() - this.lastBroadcastAt; + if (elapsed >= this.throttleMs) { + this.flushBroadcast(); + return; + } + window.clearTimeout(this.throttleTimer); + this.throttleTimer = window.setTimeout(() => this.flushBroadcast(), this.throttleMs - elapsed); + } + + private flushBroadcast(): void { + if (this.destroyed || this.pendingCursor === undefined) return; + this.lastBroadcastAt = performance.now(); + this.provider.setAwarenessField("cursor", this.pendingCursor); + this.pendingCursor = undefined; + } + + syncOverlayFromAwareness( + sync: (awareness: NonNullable) => void + ): void { + const awareness = this.provider.awareness; + if (awareness) sync(awareness); + } + + destroy(): void { + if (this.destroyed) return; + this.destroyed = true; + window.clearTimeout(this.throttleTimer); + this.provider.setAwarenessField("cursor", null); + this.provider.awareness?.off("change", this.boundAwarenessChange); + } +} + +export type { AwarenessPresenceState }; diff --git a/server/proxy-service/src/bridge/cursor-presence/RemoteCursor.ts b/server/proxy-service/src/bridge/cursor-presence/RemoteCursor.ts new file mode 100644 index 0000000000..50eb5cb37a --- /dev/null +++ b/server/proxy-service/src/bridge/cursor-presence/RemoteCursor.ts @@ -0,0 +1,122 @@ +import type { RemoteCursorRenderState } from "./types"; + +const CURSOR_CLASS = "lowcoder-remote-cursor"; +const LABEL_CLASS = "lowcoder-remote-cursor-label"; +const CARET_CLASS = "lowcoder-remote-cursor-caret"; +const SELECTION_CLASS = "lowcoder-remote-cursor-selection"; +const CARET_VERTICAL_OFFSET_PX = -6; + +export class RemoteCursor { + readonly clientId: number; + private root: HTMLDivElement; + private label: HTMLDivElement; + private caret: HTMLDivElement; + private selectionHighlights: HTMLDivElement[] = []; + private selectionKey = ""; + private visible = false; + + constructor(clientId: number) { + this.clientId = clientId; + this.root = document.createElement("div"); + this.root.className = CURSOR_CLASS; + this.root.dataset.clientId = String(clientId); + this.root.style.cssText = + "position:fixed;pointer-events:none;z-index:2147483646;transition:opacity 120ms ease;"; + + this.label = document.createElement("div"); + this.label.className = LABEL_CLASS; + this.label.style.cssText = + "position:absolute;transform:translate(-2px,calc(-100% - 4px));" + + "padding:1px 6px;border-radius:3px;font:500 11px/16px system-ui,sans-serif;" + + "color:#fff;white-space:nowrap;max-width:160px;overflow:hidden;text-overflow:ellipsis;"; + + this.caret = document.createElement("div"); + this.caret.className = CARET_CLASS; + this.caret.style.cssText = + "position:absolute;width:2px;border-radius:1px;transform:translateX(-1px);"; + + this.root.append(this.caret, this.label); + this.hide(); + } + + mount(container: HTMLElement): void { + if (!this.root.isConnected) container.appendChild(this.root); + } + + update(state: RemoteCursorRenderState, overlayContainer: HTMLElement): void { + if (!state.online || !state.cursor?.typing) { + this.hide(); + return; + } + + this.visible = true; + this.root.style.opacity = "1"; + this.root.style.display = "block"; + + const { user, x, y, height, selectionRects } = state; + this.root.style.transform = `translate(${x}px, ${y}px)`; + this.label.textContent = user.name; + this.label.style.backgroundColor = user.color; + this.caret.style.backgroundColor = user.color; + this.caret.style.height = `${Math.max(6, height)}px`; + this.caret.style.top = `${CARET_VERTICAL_OFFSET_PX}px`; + this.renderSelectionHighlights(user.color, selectionRects, overlayContainer); + } + + updatePosition(x: number, y: number): void { + if (!this.visible) return; + this.root.style.transform = `translate(${x}px, ${y}px)`; + } + + hide(): void { + this.visible = false; + this.root.style.opacity = "0"; + this.root.style.display = "none"; + this.selectionKey = ""; + this.clearSelectionHighlights(); + } + + destroy(): void { + this.clearSelectionHighlights(); + this.root.remove(); + } + + private renderSelectionHighlights( + color: string, + selectionRects: RemoteCursorRenderState["selectionRects"], + overlayContainer: HTMLElement + ): void { + const key = selectionRects.map((r) => `${r.left},${r.top},${r.width},${r.height}`).join("|"); + if (key === this.selectionKey) return; + this.selectionKey = key; + this.clearSelectionHighlights(); + for (const rect of selectionRects) { + const highlight = document.createElement("div"); + highlight.className = SELECTION_CLASS; + highlight.style.cssText = + `position:fixed;left:${rect.left}px;top:${rect.top}px;` + + `width:${rect.width}px;height:${rect.height}px;` + + `background:${color};opacity:0.28;border-radius:2px;pointer-events:none;z-index:2147483644;`; + overlayContainer.appendChild(highlight); + this.selectionHighlights.push(highlight); + } + } + + private clearSelectionHighlights(): void { + for (const el of this.selectionHighlights) el.remove(); + this.selectionHighlights = []; + } +} + +export function ensureCursorStyles(): void { + if (document.getElementById("lowcoder-cursor-presence-styles")) return; + const style = document.createElement("style"); + style.id = "lowcoder-cursor-presence-styles"; + style.textContent = ` + .${CURSOR_CLASS} { contain: layout style; } + .${LABEL_CLASS} { box-shadow: 0 1px 3px rgba(0,0,0,0.25); } + .${CARET_CLASS} { animation: lowcoder-cursor-blink 1s step-end infinite; } + @keyframes lowcoder-cursor-blink { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } } + `; + document.head.appendChild(style); +} diff --git a/server/proxy-service/src/bridge/cursor-presence/caretMetrics.ts b/server/proxy-service/src/bridge/cursor-presence/caretMetrics.ts new file mode 100644 index 0000000000..4a76cf90c9 --- /dev/null +++ b/server/proxy-service/src/bridge/cursor-presence/caretMetrics.ts @@ -0,0 +1,216 @@ +import type { TextFieldElement } from "./textField"; + +const MIRROR_PROPERTIES = [ + "direction", "boxSizing", "width", "height", "overflowX", "overflowY", + "borderTopWidth", "borderRightWidth", "borderBottomWidth", "borderLeftWidth", + "paddingTop", "paddingRight", "paddingBottom", "paddingLeft", + "fontStyle", "fontVariant", "fontWeight", "fontStretch", "fontSize", + "fontSizeAdjust", "lineHeight", "fontFamily", "textAlign", "textTransform", + "textIndent", "textDecoration", "letterSpacing", "wordSpacing", "tabSize", + "whiteSpace", "wordWrap", "wordBreak", +] as const; + +let mirrorDiv: HTMLDivElement | null = null; + +function getMirrorDiv(): HTMLDivElement { + if (!mirrorDiv) { + mirrorDiv = document.createElement("div"); + mirrorDiv.id = "lowcoder-cursor-mirror"; + mirrorDiv.setAttribute("aria-hidden", "true"); + mirrorDiv.style.cssText = + "position:absolute;visibility:hidden;white-space:pre-wrap;word-wrap:break-word;top:0;left:-9999px;"; + document.body.appendChild(mirrorDiv); + } + return mirrorDiv; +} + +function toKebabCase(prop: string): string { + return prop.replace(/([A-Z])/g, "-$1").toLowerCase(); +} + +function copyInputStyles(element: HTMLInputElement | HTMLTextAreaElement, div: HTMLDivElement): void { + const computed = window.getComputedStyle(element); + for (const prop of MIRROR_PROPERTIES) { + const kebab = toKebabCase(prop); + div.style.setProperty(kebab, computed.getPropertyValue(kebab)); + } + div.style.width = `${element.clientWidth}px`; + div.style.whiteSpace = element instanceof HTMLTextAreaElement ? "pre-wrap" : "nowrap"; +} + +export interface CaretCoordinates { + left: number; + top: number; + height: number; +} + +export interface SelectionRect { + left: number; + top: number; + width: number; + height: number; +} + +function fieldLineHeight(field: TextFieldElement): number { + const style = window.getComputedStyle(field); + return parseFloat(style.lineHeight) || parseFloat(style.fontSize) * 1.2 || 20; +} + +/** Fallback caret at the start of the visible field bounds. */ +export function getFieldFallbackCaret(field: TextFieldElement): CaretCoordinates { + const rect = field.getBoundingClientRect(); + const height = fieldLineHeight(field); + const style = window.getComputedStyle(field); + const padL = parseFloat(style.paddingLeft || "0"); + const padT = parseFloat(style.paddingTop || "0"); + return { + left: rect.left + padL + 4, + top: rect.top + padT + 2, + height, + }; +} + +export function getContentEditableCaret(field: HTMLElement): CaretCoordinates | null { + const sel = window.getSelection(); + if (!sel || sel.rangeCount === 0) return getFieldFallbackCaret(field); + const range = sel.getRangeAt(0); + if (!field.contains(range.startContainer)) return getFieldFallbackCaret(field); + const collapsed = range.cloneRange(); + collapsed.collapse(true); + const rects = collapsed.getClientRects(); + const rect = rects.length > 0 ? rects[0] : collapsed.getBoundingClientRect(); + if (rect.width === 0 && rect.height === 0) return getFieldFallbackCaret(field); + return { left: rect.left, top: rect.top, height: Math.max(rect.height, fieldLineHeight(field)) }; +} + +export function getCaretCoordinatesForField( + field: TextFieldElement, + position?: number +): CaretCoordinates | null { + if (field instanceof HTMLElement && field.isContentEditable && !(field instanceof HTMLInputElement) && !(field instanceof HTMLTextAreaElement)) { + return getContentEditableCaret(field); + } + if (position == null) return getFieldFallbackCaret(field); + const exact = getCaretCoordinates(field as HTMLInputElement | HTMLTextAreaElement, position); + return exact ?? getFieldFallbackCaret(field); +} + +export function getCaretCoordinates( + element: HTMLInputElement | HTMLTextAreaElement, + position: number +): CaretCoordinates | null { + if (!element.isConnected) return null; + + const div = getMirrorDiv(); + copyInputStyles(element, div); + + const value = element.value; + const clamped = Math.max(0, Math.min(position, value.length)); + const before = value.slice(0, clamped); + const after = value.slice(clamped) || "."; + + div.textContent = before; + const span = document.createElement("span"); + span.textContent = after; + div.appendChild(span); + + const elementRect = element.getBoundingClientRect(); + const spanRect = span.getBoundingClientRect(); + const divRect = div.getBoundingClientRect(); + const style = window.getComputedStyle(element); + const lineHeight = parseFloat(style.lineHeight) || parseFloat(style.fontSize) * 1.2; + + const left = + elementRect.left - + element.scrollLeft + + (spanRect.left - divRect.left) + + parseFloat(style.borderLeftWidth || "0") + + parseFloat(style.paddingLeft || "0"); + const top = + elementRect.top - + element.scrollTop + + (spanRect.top - divRect.top) + + parseFloat(style.borderTopWidth || "0") + + parseFloat(style.paddingTop || "0"); + + div.textContent = ""; + const coords = { left, top, height: lineHeight }; + if (!Number.isFinite(coords.left) || !Number.isFinite(coords.top)) { + return null; + } + return coords; +} + +export function getSelectionRectsForField( + field: TextFieldElement, + anchor: number, + head: number +): SelectionRect[] { + if (field instanceof HTMLElement && field.isContentEditable && !(field instanceof HTMLInputElement) && !(field instanceof HTMLTextAreaElement)) { + const sel = window.getSelection(); + if (!sel || sel.rangeCount === 0 || anchor === head) return []; + const range = sel.getRangeAt(0); + if (!field.contains(range.startContainer)) return []; + const rects: SelectionRect[] = []; + for (const r of Array.from(range.getClientRects())) { + rects.push({ left: r.left, top: r.top, width: r.width, height: r.height }); + } + return rects; + } + return getSelectionRects(field as HTMLInputElement | HTMLTextAreaElement, anchor, head); +} + +export function getSelectionRects( + element: HTMLInputElement | HTMLTextAreaElement, + anchor: number, + head: number +): SelectionRect[] { + const start = Math.min(anchor, head); + const end = Math.max(anchor, head); + if (start === end) return []; + + const startCoords = getCaretCoordinates(element, start); + const endCoords = getCaretCoordinates(element, end); + if (!startCoords || !endCoords) return []; + + const height = startCoords.height; + if (Math.abs(startCoords.top - endCoords.top) < height * 0.5) { + return [{ + left: startCoords.left, + top: startCoords.top, + width: Math.max(2, endCoords.left - startCoords.left), + height, + }]; + } + + const value = element.value; + const lineStart = value.lastIndexOf("\n", start) + 1; + const lineEnd = value.indexOf("\n", end); + const lineEndIndex = lineEnd === -1 ? value.length : lineEnd; + const lineEndCoords = getCaretCoordinates(element, lineEndIndex); + const lineStartCoords = getCaretCoordinates(element, lineStart); + const rects: SelectionRect[] = []; + + if (lineEndCoords) { + rects.push({ + left: startCoords.left, + top: startCoords.top, + width: Math.max(2, lineEndCoords.left - startCoords.left), + height, + }); + } + if (lineStartCoords) { + rects.push({ + left: lineStartCoords.left, + top: endCoords.top, + width: Math.max(2, endCoords.left - lineStartCoords.left), + height, + }); + } + return rects; +} + +export function destroyCaretMirror(): void { + mirrorDiv?.remove(); + mirrorDiv = null; +} diff --git a/server/proxy-service/src/bridge/cursor-presence/initTypeformCursorPresence.ts b/server/proxy-service/src/bridge/cursor-presence/initTypeformCursorPresence.ts new file mode 100644 index 0000000000..6695a15ee6 --- /dev/null +++ b/server/proxy-service/src/bridge/cursor-presence/initTypeformCursorPresence.ts @@ -0,0 +1,207 @@ +/** + * Self-contained cursor presence bootstrap for the Typeform bridge. + * + * - Broadcast caret while THIS user is actively editing. + * - Do not clear during form-sync (avoids per-keystroke flicker). + * - Clear after idle / blur so peers stop seeing a stuck caret. + * - Render only REMOTE users' cursors. + */ + +import type { TypeformCursorPresenceInit } from "./types"; +import { getUserColor } from "./userColor"; +import { CursorPresenceProvider } from "./CursorPresenceProvider"; +import { CursorOverlay } from "./CursorOverlay"; +import { + getCursorFieldKey, + getFocusedTextField, + getFieldSelection, + isTextFieldElement, +} from "./textField"; + +/** Hide collaborative caret this long after the last real keystroke. */ +const TYPING_IDLE_MS = 2500; + +function readUserName(editorId: string): string { + const params = new URLSearchParams(window.location.search); + return ( + params.get("username") || + document.documentElement.getAttribute("data-lowcoder-username") || + editorId + ); +} + +/** Real user input only — ignore synthetic events from form sync. */ +function isRealUserActivity(event: Event): boolean { + return event.isTrusted === true; +} + +export function initTypeformCursorPresence(config: TypeformCursorPresenceInit): () => void { + const userName = readUserName(config.editorId); + const user = { + id: config.editorId, + name: userName, + color: getUserColor(config.editorId), + role: config.role, + }; + + const canBroadcast = (): boolean => + !config.isWelcomeScreen() && !(config.isSyncing?.() ?? false); + + const overlay = new CursorOverlay({ + findFieldByKey: config.findFieldByKey, + getCurrentStep: config.getCurrentStep, + localUserId: config.editorId, + }); + + const presence = new CursorPresenceProvider( + config.provider, + user, + () => { + presence.syncOverlayFromAwareness((awareness) => overlay.syncFromAwareness(awareness)); + }, + 33 + ); + + let isActive = false; + let idleTimer: number | undefined; + + const clearCursor = (): void => { + isActive = false; + window.clearTimeout(idleTimer); + idleTimer = undefined; + presence.setLocalCursor(null); + }; + + const syncOverlay = (): void => { + presence.syncOverlayFromAwareness((awareness) => overlay.syncFromAwareness(awareness)); + }; + + const scheduleIdleClear = (): void => { + window.clearTimeout(idleTimer); + idleTimer = window.setTimeout(() => { + clearCursor(); + syncOverlay(); + }, TYPING_IDLE_MS); + }; + + const publishCursor = (): void => { + if (!isActive) return; + // During form sync, keep the last published cursor — do not clear. + if (!canBroadcast()) return; + const field = getFocusedTextField(); + if (!field) return; + const step = config.getCurrentStep(); + presence.setLocalCursor({ + fieldKey: getCursorFieldKey(field, step, config.getFieldKey), + step, + selection: getFieldSelection(field), + typing: true, + updatedAt: Date.now(), + }); + }; + + const activateCursor = (event: Event): void => { + if (!isRealUserActivity(event)) return; + // During form sync, ignore — but never clear an existing cursor. + if (!canBroadcast()) return; + + const target = event.target; + if (target instanceof Element && !isTextFieldElement(target) && !getFocusedTextField()) { + return; + } + if (!getFocusedTextField()) return; + + isActive = true; + publishCursor(); + // Only real typing resets idle — poll/sync must not keep the cursor forever. + scheduleIdleClear(); + }; + + const listenerOpts: AddEventListenerOptions = { capture: true, passive: true }; + + const onInput = (event: Event): void => activateCursor(event); + const onCompositionUpdate = (event: Event): void => activateCursor(event); + const onKeyDown = (event: Event): void => { + if (!isRealUserActivity(event)) return; + if (!getFocusedTextField()) return; + activateCursor(event); + }; + const onSelectionChange = (): void => { + if (!isActive) return; + publishCursor(); + }; + + const onFocusOut = (): void => { + window.setTimeout(() => { + if (!getFocusedTextField()) clearCursor(); + }, 0); + }; + + const onScroll = (): void => { + if (isActive) publishCursor(); + syncOverlay(); + }; + const onResize = (): void => syncOverlay(); + + // Typing only — not focusin (Typeform keeps focus and would leave a stuck caret). + document.addEventListener("input", onInput, listenerOpts); + document.addEventListener("compositionupdate", onCompositionUpdate, listenerOpts); + document.addEventListener("keydown", onKeyDown, listenerOpts); + document.addEventListener("selectionchange", onSelectionChange); + document.addEventListener("focusout", onFocusOut, listenerOpts); + document.addEventListener("scroll", onScroll, listenerOpts); + window.addEventListener("resize", onResize, { passive: true }); + + let layoutTimer: number | undefined; + const domObserver = new MutationObserver(() => { + window.clearTimeout(layoutTimer); + layoutTimer = window.setTimeout(() => { + if (isActive) publishCursor(); + syncOverlay(); + }, 100); + }); + domObserver.observe(document.documentElement, { + childList: true, + subtree: true, + attributes: true, + }); + + const pollTimer = window.setInterval(() => { + if (isActive) publishCursor(); + syncOverlay(); + }, 100); + + const onProviderStatus = (): void => syncOverlay(); + config.provider.on("synced", onProviderStatus); + + presence.setLocalCursor(null); + syncOverlay(); + + if (config.debug) { + console.log("[typeform-cursor-presence] started (idle-clear, no sync-clear)", { + userName, + editorId: config.editorId, + }); + } + + const destroy = (): void => { + window.clearInterval(pollTimer); + window.clearTimeout(layoutTimer); + window.clearTimeout(idleTimer); + config.provider.off("synced", onProviderStatus); + document.removeEventListener("input", onInput, listenerOpts); + document.removeEventListener("compositionupdate", onCompositionUpdate, listenerOpts); + document.removeEventListener("keydown", onKeyDown, listenerOpts); + document.removeEventListener("selectionchange", onSelectionChange); + document.removeEventListener("focusout", onFocusOut, listenerOpts); + document.removeEventListener("scroll", onScroll, listenerOpts); + window.removeEventListener("resize", onResize); + domObserver.disconnect(); + clearCursor(); + presence.destroy(); + overlay.destroy(); + }; + + window.addEventListener("beforeunload", destroy, { once: true }); + return destroy; +} diff --git a/server/proxy-service/src/bridge/cursor-presence/textField.ts b/server/proxy-service/src/bridge/cursor-presence/textField.ts new file mode 100644 index 0000000000..315015ca91 --- /dev/null +++ b/server/proxy-service/src/bridge/cursor-presence/textField.ts @@ -0,0 +1,155 @@ +/** + * Unified text-field target helpers for cursor presence. + * Supports input, textarea, and contenteditable (used by some Typeform builds). + */ + +export type TextFieldElement = + | HTMLInputElement + | HTMLTextAreaElement + | (HTMLElement & { contentEditable: "true" }); + +const IGNORED_INPUT_TYPES = new Set([ + "hidden", "checkbox", "radio", "button", "submit", "file", "password", +]); + +export function isTextFieldElement(el: Element | null): el is TextFieldElement { + if (!el) return false; + if (el instanceof HTMLTextAreaElement) return true; + if (el instanceof HTMLInputElement) { + const type = (el.getAttribute("type") || el.type || "text").toLowerCase(); + return !IGNORED_INPUT_TYPES.has(type); + } + if (el instanceof HTMLElement && el.isContentEditable) return true; + return false; +} + +export function getFocusedTextField(): TextFieldElement | null { + const el = document.activeElement; + if (isTextFieldElement(el)) return el; + // Typeform sometimes focuses a wrapper; look for editable descendant. + if (el instanceof HTMLElement) { + const inner = el.querySelector( + 'input:not([type="hidden"]):not([type="checkbox"]):not([type="radio"]), textarea, [contenteditable="true"]' + ); + if (isTextFieldElement(inner)) return inner; + } + return null; +} + +export function listEditableFields(container: ParentNode = document): TextFieldElement[] { + const nodes = container.querySelectorAll( + [ + 'input[type="text"]', + 'input[type="email"]', + 'input[type="number"]', + 'input[type="tel"]', + 'input[type="url"]', + 'input[type="search"]', + 'input[type="short_text"]', + 'input[type="long_text"]', + 'input[type="phone_number"]', + 'input[name]', + 'input:not([type])', + "textarea", + '[contenteditable="true"]', + '[role="textbox"]', + ].join(", ") + ); + return Array.from(nodes).filter((field) => { + if (!isTextFieldElement(field)) return false; + const rect = field.getBoundingClientRect(); + return rect.width > 0 && rect.height > 0; + }); +} + +export function getFieldText(field: TextFieldElement): string { + if (field instanceof HTMLInputElement || field instanceof HTMLTextAreaElement) { + return field.value; + } + return field.textContent ?? ""; +} + +export function getFieldSelection(field: TextFieldElement): { anchor: number; head: number } { + if (field instanceof HTMLInputElement || field instanceof HTMLTextAreaElement) { + return { + anchor: field.selectionStart ?? 0, + head: field.selectionEnd ?? 0, + }; + } + const sel = window.getSelection(); + if (!sel || sel.rangeCount === 0) { + const len = getFieldText(field).length; + return { anchor: len, head: len }; + } + const range = sel.getRangeAt(0); + if (!field.contains(range.startContainer)) { + const len = getFieldText(field).length; + return { anchor: len, head: len }; + } + const pre = range.cloneRange(); + pre.selectNodeContents(field); + pre.setEnd(range.startContainer, range.startOffset); + const anchor = pre.toString().length; + pre.setEnd(range.endContainer, range.endOffset); + const head = pre.toString().length; + return { anchor, head }; +} + +export function extractQuestionUuid(value: string): string | null { + const match = value.match( + /([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i + ); + return match?.[1] ?? null; +} + +/** Field key compatible with the Typeform bridge key scheme. */ +export function getCursorFieldKey( + field: TextFieldElement, + step: number, + bridgeGetFieldKey?: (f: HTMLInputElement | HTMLTextAreaElement) => string +): string { + if ( + bridgeGetFieldKey && + (field instanceof HTMLInputElement || field instanceof HTMLTextAreaElement) + ) { + return bridgeGetFieldKey(field); + } + const name = field.getAttribute("name")?.trim(); + if (name) return `name:${name}`; + const labelledBy = field.getAttribute("aria-labelledby") || ""; + const fromLabel = extractQuestionUuid(labelledBy); + if (fromLabel) return `qid:${fromLabel}`; + const id = field.getAttribute("id") || ""; + const fromId = extractQuestionUuid(id); + if (fromId) return `qid:${fromId}`; + const qa = field.getAttribute("data-qa"); + if (qa) return `qa:${qa}:step:${step}`; + return `ce:step:${step}`; +} + +export function findFieldByCursorKey( + key: string, + step: number, + bridgeFindFieldByKey?: (key: string) => HTMLInputElement | HTMLTextAreaElement | null +): TextFieldElement | null { + if (bridgeFindFieldByKey) { + const bridged = bridgeFindFieldByKey(key); + if (bridged) return bridged; + } + for (const field of listEditableFields()) { + if (getCursorFieldKey(field, step) === key) return field; + } + if (key.startsWith("name:")) { + const name = key.slice("name:".length); + const el = document.querySelector(`input[name="${CSS.escape(name)}"], textarea[name="${CSS.escape(name)}"]`); + if (isTextFieldElement(el)) return el; + } + if (key.startsWith("qid:")) { + const qid = key.slice("qid:".length); + const sel = `[aria-labelledby*="${CSS.escape(qid)}"], [id*="${CSS.escape(qid)}"]`; + for (const el of document.querySelectorAll(sel)) { + if (isTextFieldElement(el)) return el; + } + } + return null; +} diff --git a/server/proxy-service/src/bridge/cursor-presence/types.ts b/server/proxy-service/src/bridge/cursor-presence/types.ts new file mode 100644 index 0000000000..705a76bf2d --- /dev/null +++ b/server/proxy-service/src/bridge/cursor-presence/types.ts @@ -0,0 +1,62 @@ +/** + * Yjs Awareness state shapes for real-time cursor presence. + * Stored in awareness only — never written to the shared Y.Doc. + */ + +export interface CursorPresenceUser { + id: string; + name: string; + color: string; + avatar?: string; + role?: string; +} + +export interface CursorSelection { + anchor: number; + head: number; +} + +export interface CursorPresenceData { + fieldKey: string; + step: number; + selection: CursorSelection; + /** True while the user is actively typing; caret hidden when false. */ + typing: boolean; + /** Epoch ms when this cursor was last updated by a real user action. */ + updatedAt: number; +} + +export interface AwarenessPresenceState { + user: CursorPresenceUser; + cursor: CursorPresenceData | null; +} + +export interface RemoteCursorRenderState { + clientId: number; + user: CursorPresenceUser; + cursor: CursorPresenceData | null; + x: number; + y: number; + targetX: number; + targetY: number; + height: number; + selectionRects: Array<{ left: number; top: number; width: number; height: number }>; + online: boolean; +} + +export interface CursorPresenceFieldResolver { + getFieldKey: (field: HTMLInputElement | HTMLTextAreaElement) => string; + findFieldByKey: (key: string) => HTMLInputElement | HTMLTextAreaElement | null; + getCurrentStep: () => number; +} + +export interface TypeformCursorPresenceInit extends CursorPresenceFieldResolver { + provider: import("@hocuspocus/provider").HocuspocusProvider; + editorId: string; + role: string; + debug?: boolean; + getSessionStarted: () => boolean; + isWelcomeScreen: () => boolean; + /** When true, pause local cursor broadcasts (read-only peek at sync flags). */ + isSyncing?: () => boolean; +} diff --git a/server/proxy-service/src/bridge/cursor-presence/userColor.ts b/server/proxy-service/src/bridge/cursor-presence/userColor.ts new file mode 100644 index 0000000000..bb4bd5954c --- /dev/null +++ b/server/proxy-service/src/bridge/cursor-presence/userColor.ts @@ -0,0 +1,28 @@ +const CURSOR_PALETTE = [ + "#E53935", + "#1E88E5", + "#43A047", + "#FB8C00", + "#8E24AA", + "#00ACC1", + "#F4511E", + "#3949AB", + "#7CB342", + "#D81B60", + "#6D4C41", + "#546E7A", +] as const; + +function hashString(input: string): number { + let hash = 2166136261; + for (let i = 0; i < input.length; i += 1) { + hash ^= input.charCodeAt(i); + hash = Math.imul(hash, 16777619); + } + return hash >>> 0; +} + +export function getUserColor(userId: string): string { + const normalized = userId.trim() || "anonymous"; + return CURSOR_PALETTE[hashString(normalized) % CURSOR_PALETTE.length]; +} diff --git a/server/proxy-service/src/bridge/google-forms-bridge.ts b/server/proxy-service/src/bridge/google-forms-bridge.ts new file mode 100644 index 0000000000..acade533d2 --- /dev/null +++ b/server/proxy-service/src/bridge/google-forms-bridge.ts @@ -0,0 +1,577 @@ +import * as Y from "yjs"; +import { HocuspocusProvider, WebSocketStatus } from "@hocuspocus/provider"; +import { initTypeformCursorPresence } from "./cursor-presence/initTypeformCursorPresence"; + +declare global { + interface Window { + __LOWCODER_HOCUSPOCUS__?: { url?: string; token?: string }; + } +} + +type FormControl = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement; +type ChoiceWidget = HTMLElement; +type NavigationAction = "next" | "back" | "submit"; + +interface NavigationCommand { + id: string; + action: NavigationAction; + fromPage: string; + editorId: string; +} + +(() => { + const params = new URLSearchParams(window.location.search); + const root = document.documentElement; + const roomId = params.get("roomId") || root.dataset.lowcoderRoomId || ""; + const role = params.get("role") || root.dataset.lowcoderRole || "driver"; + const editorId = params.get("editorId") || root.dataset.lowcoderEditorId || "local"; + const collabId = params.get("collab") || root.dataset.lowcoderCollabId || ""; + const debug = params.get("debug") === "1"; + const peerId = `${editorId}|${role}|${Math.random().toString(36).slice(2, 10)}`; + + if (!roomId || !collabId) { + console.error( + "[google-forms-bridge] Missing roomId/collab. Load the form through /proxy/google-forms " + + "and create a session with createGoogleFormsProxySession before Fill Together." + ); + return; + } + + if (!window.location.pathname.includes("/proxy/google-forms")) { + console.error( + "[google-forms-bridge] Form left the Lowcoder proxy (often after Google sign-in on an /edit URL). " + + "Use the published .../viewform responder URL instead of webViewLink." + ); + return; + } + const hocuspocusConfig = window.__LOWCODER_HOCUSPOCUS__ ?? {}; + const hocuspocusUrl = + hocuspocusConfig.url || root.dataset.lowcoderHocuspocusUrl || "ws://localhost:3006"; + const hocuspocusToken = + hocuspocusConfig.token || root.dataset.lowcoderHocuspocusToken || ""; + const documentName = `googleform_${roomId}_${collabId}`; + + let providerReady = false; + let isApplyingRemoteState = false; + let lastNavigationId = ""; + let navigationTimer: number | undefined; + + const doc = new Y.Doc(); + const fields = doc.getMap("fields"); + const state = doc.getMap("state"); + const provider = new HocuspocusProvider({ + url: hocuspocusUrl, + name: documentName, + document: doc, + token: hocuspocusToken || undefined, + onAuthenticationFailed: (data) => { + console.error("[google-forms-bridge] Hocuspocus auth failed", data); + }, + }); + + function log(...args: unknown[]): void { + if (debug) console.log("[google-forms-bridge]", role, ...args); + } + + function listControls(): FormControl[] { + return Array.from( + document.querySelectorAll("input, textarea, select") + ).filter((control) => { + if (control.disabled) return false; + if (control.getAttribute("name") === "g-recaptcha-response") return false; + if (control instanceof HTMLInputElement) { + const type = (control.type || "text").toLowerCase(); + return !["hidden", "button", "submit", "reset", "file", "password", "image"].includes(type); + } + const rect = control.getBoundingClientRect(); + return rect.width > 0 && rect.height > 0; + }); + } + + function listChoiceWidgets(): ChoiceWidget[] { + return Array.from( + document.querySelectorAll( + '[role="radio"], [role="checkbox"], [role="listbox"]' + ) + ).filter((widget) => widget.getAttribute("aria-disabled") !== "true"); + } + + function stableHash(value: string): string { + let hash = 5381; + for (let index = 0; index < value.length; index += 1) { + hash = (hash * 33) ^ value.charCodeAt(index); + } + return (hash >>> 0).toString(36); + } + + function widgetGroup(widget: ChoiceWidget): HTMLElement { + return ( + widget.closest('[role="radiogroup"], [role="group"]') || + widget.closest('[role="listitem"], [data-params]') || + widget.parentElement || + widget + ); + } + + function widgetIdentity(widget: ChoiceWidget): string { + const group = widgetGroup(widget); + const question = widget.closest("[data-params]"); + const dataParams = question?.getAttribute("data-params"); + if (dataParams) { + const groups = Array.from( + question.querySelectorAll('[role="radiogroup"], [role="group"], [role="listbox"]') + ); + return `question:${stableHash(dataParams)}:group:${Math.max(0, groups.indexOf(group))}`; + } + const listItems = Array.from(document.querySelectorAll('[role="listitem"]')); + const listItem = widget.closest('[role="listitem"]'); + if (listItem) return `listitem:${Math.max(0, listItems.indexOf(listItem))}`; + return `widget:${Math.max(0, listChoiceWidgets().indexOf(widget))}`; + } + + function widgetKey(widget: ChoiceWidget): string { + const identity = widgetIdentity(widget); + if (widget.getAttribute("role") === "checkbox") { + return `widget-checkbox:${identity}:${widget.dataset.value || widget.getAttribute("aria-label") || ""}`; + } + return `widget-${widget.getAttribute("role")}:${identity}`; + } + + function widgetValue(widget: ChoiceWidget): string { + const role = widget.getAttribute("role"); + if (role === "checkbox") return widget.getAttribute("aria-checked") === "true" ? "1" : "0"; + if (role === "radio") { + const selected = widgetGroup(widget).querySelector( + '[role="radio"][aria-checked="true"]' + ); + return selected?.dataset.value || selected?.getAttribute("aria-label") || ""; + } + const selected = widget.querySelector('[role="option"][aria-selected="true"]'); + return selected?.dataset.value || selected?.textContent?.trim() || ""; + } + + function controlIdentity(control: FormControl): string { + const name = control.getAttribute("name")?.trim(); + if (name) return `name:${name}`; + const id = control.id?.trim(); + if (id) return `id:${id}`; + const ariaLabel = control.getAttribute("aria-label")?.trim(); + if (ariaLabel) return `aria:${ariaLabel}`; + const all = listControls(); + return `index:${all.indexOf(control)}`; + } + + function controlValue(control: FormControl): string { + if (control instanceof HTMLInputElement && control.type === "checkbox") { + return control.checked ? "1" : "0"; + } + if (control instanceof HTMLInputElement && control.type === "radio") { + const group = listControls().filter( + (candidate) => + candidate instanceof HTMLInputElement && + candidate.type === "radio" && + controlIdentity(candidate) === controlIdentity(control) + ) as HTMLInputElement[]; + const checked = group.find((candidate) => candidate.checked); + return checked ? optionValue(checked) : ""; + } + if (control instanceof HTMLSelectElement && control.multiple) { + return JSON.stringify(Array.from(control.selectedOptions).map((option) => option.value)); + } + return control.value; + } + + function optionValue(input: HTMLInputElement): string { + return ( + input.closest("[data-value]")?.dataset.value || + input.getAttribute("data-value") || + input.value + ); + } + + function controlKey(control: FormControl): string { + const identity = controlIdentity(control); + if (control instanceof HTMLInputElement && control.type === "radio") { + return `radio:${identity}`; + } + if (control instanceof HTMLInputElement && control.type === "checkbox") { + const peers = listControls().filter( + (candidate) => + candidate instanceof HTMLInputElement && + candidate.type === "checkbox" && + controlIdentity(candidate) === identity + ) as HTMLInputElement[]; + const sameValueIndex = peers + .filter((candidate) => optionValue(candidate) === optionValue(control)) + .indexOf(control); + return `checkbox:${identity}:${optionValue(control)}:${Math.max(0, sameValueIndex)}`; + } + const sameIdentity = listControls().filter( + (candidate) => + !(candidate instanceof HTMLInputElement && ["radio", "checkbox"].includes(candidate.type)) && + controlIdentity(candidate) === identity + ); + return `field:${identity}:${Math.max(0, sameIdentity.indexOf(control))}`; + } + + function findControl(key: string): FormControl | null { + return listControls().find((control) => controlKey(control) === key) ?? null; + } + + function setNativeValue(control: FormControl, value: string): void { + if (control instanceof HTMLInputElement && control.type === "radio") { + const target = listControls().find( + (candidate) => + candidate instanceof HTMLInputElement && + candidate.type === "radio" && + controlKey(candidate) === controlKey(control) && + optionValue(candidate) === value + ) as HTMLInputElement | undefined; + if (target && !target.checked) target.click(); + return; + } + + if (control instanceof HTMLInputElement && control.type === "checkbox") { + const checked = value === "1"; + if (control.checked !== checked) control.click(); + return; + } + + if (control instanceof HTMLSelectElement) { + if (control.multiple) { + let selected: string[] = []; + try { + selected = JSON.parse(value) as string[]; + } catch { + selected = []; + } + Array.from(control.options).forEach((option) => { + option.selected = selected.includes(option.value); + }); + } else { + control.value = value; + } + control.dispatchEvent(new Event("change", { bubbles: true })); + return; + } + + if (control.value === value) return; + const prototype = + control instanceof HTMLTextAreaElement + ? HTMLTextAreaElement.prototype + : HTMLInputElement.prototype; + const setter = Object.getOwnPropertyDescriptor(prototype, "value")?.set; + if (setter) setter.call(control, value); + else control.value = value; + control.dispatchEvent(new InputEvent("input", { bubbles: true, inputType: "insertText" })); + control.dispatchEvent(new Event("change", { bubbles: true })); + } + + function publishControl(control: FormControl): void { + if (isApplyingRemoteState) return; + const key = controlKey(control); + const value = controlValue(control); + if (fields.get(key) === value) return; + doc.transact(() => fields.set(key, value), peerId); + log("published field", key); + } + + function publishAllControls(onlyMissing = false): void { + doc.transact(() => { + listControls().forEach((control) => { + const key = controlKey(control); + if (onlyMissing && fields.has(key)) return; + fields.set(key, controlValue(control)); + }); + }, peerId); + } + + function publishWidget(widget: ChoiceWidget): void { + if (isApplyingRemoteState) return; + const key = widgetKey(widget); + const value = widgetValue(widget); + if (fields.get(key) === value) return; + doc.transact(() => fields.set(key, value), peerId); + log("published widget", key); + } + + function publishAllWidgets(onlyMissing = false): void { + const seen = new Set(); + doc.transact(() => { + listChoiceWidgets().forEach((widget) => { + const key = widgetKey(widget); + if (seen.has(key) || (onlyMissing && fields.has(key))) return; + seen.add(key); + fields.set(key, widgetValue(widget)); + }); + }, peerId); + } + + function findWidget(key: string): ChoiceWidget | null { + return listChoiceWidgets().find((widget) => widgetKey(widget) === key) ?? null; + } + + function applyWidget(key: string): void { + const widget = findWidget(key); + const value = fields.get(key); + if (!widget || typeof value !== "string" || widgetValue(widget) === value) return; + + let target: HTMLElement | null = null; + const role = widget.getAttribute("role"); + if (role === "checkbox") { + target = widget; + } else if (role === "radio") { + target = + Array.from(widgetGroup(widget).querySelectorAll('[role="radio"]')).find( + (option) => + (option.dataset.value || option.getAttribute("aria-label") || "") === value + ) ?? null; + } else { + target = + Array.from(widget.querySelectorAll('[role="option"]')).find( + (option) => (option.dataset.value || option.textContent?.trim() || "") === value + ) ?? null; + } + if (!target) return; + + isApplyingRemoteState = true; + try { + target.click(); + log("applied widget", key); + } finally { + window.setTimeout(() => { + isApplyingRemoteState = false; + }, 0); + } + } + + function applyField(key: string): void { + if (key.startsWith("widget-")) { + applyWidget(key); + return; + } + const control = findControl(key); + const value = fields.get(key); + if (!control || typeof value !== "string" || controlValue(control) === value) return; + isApplyingRemoteState = true; + try { + setNativeValue(control, value); + log("applied field", key); + } finally { + isApplyingRemoteState = false; + } + } + + function applyAllFields(): void { + fields.forEach((_value, key) => applyField(key)); + } + + function pageMarker(): string { + const history = document.querySelector( + 'input[name="pageHistory"], input[name="pagehistory"]' + )?.value; + if (history) return history; + const visibleQuestion = Array.from( + document.querySelectorAll('[role="listitem"], [data-params]') + ).find((element) => { + const rect = element.getBoundingClientRect(); + return rect.width > 0 && rect.height > 0; + }); + return visibleQuestion?.getAttribute("data-params")?.slice(0, 200) || window.location.pathname; + } + + function navigationAction(target: Element): NavigationAction | null { + const button = target.closest( + 'button, input[type="submit"], [role="button"], [jsname]' + ); + if (!button) return null; + const jsname = button.getAttribute("jsname") || ""; + const text = (button.textContent || (button as HTMLInputElement).value || "") + .trim() + .toLowerCase(); + if (jsname === "e19J0b" || /^(back|previous)$/.test(text)) return "back"; + if (jsname === "OCpkoe" || /^(next|continue)$/.test(text)) return "next"; + if (jsname === "M2UYVd" || /^(submit|send)$/.test(text)) return "submit"; + return null; + } + + function findNavigationButton(action: "next" | "back"): HTMLElement | null { + const jsname = action === "next" ? "OCpkoe" : "e19J0b"; + const byJsName = document.querySelector(`[jsname="${jsname}"]`); + if (byJsName) return byJsName; + return ( + Array.from( + document.querySelectorAll('button, input[type="submit"], [role="button"]') + ).find((button) => navigationAction(button) === action) ?? null + ); + } + + function publishNavigation(action: NavigationAction): void { + publishAllControls(); + publishAllWidgets(); + const command: NavigationCommand = { + id: `${peerId}:${Date.now()}:${Math.random().toString(36).slice(2, 7)}`, + action, + fromPage: pageMarker(), + editorId: peerId, + }; + lastNavigationId = command.id; + doc.transact(() => state.set("navigationJson", JSON.stringify(command)), peerId); + log("published navigation", command); + } + + function applyRemoteNavigation(): void { + const raw = state.get("navigationJson"); + if (typeof raw !== "string" || !raw) return; + let command: NavigationCommand; + try { + command = JSON.parse(raw) as NavigationCommand; + } catch { + return; + } + if ( + !command.id || + command.id === lastNavigationId || + command.editorId === peerId || + command.action === "submit" || + command.fromPage !== pageMarker() + ) { + return; + } + lastNavigationId = command.id; + window.clearTimeout(navigationTimer); + navigationTimer = window.setTimeout(() => { + applyAllFields(); + const button = findNavigationButton(command.action); + if (!button) { + log("navigation button not found", command.action); + return; + } + isApplyingRemoteState = true; + button.click(); + window.setTimeout(() => { + isApplyingRemoteState = false; + }, 500); + log("applied navigation", command.action); + }, 100); + } + + fields.observe((event) => { + if (event.transaction.origin === peerId) return; + event.keysChanged.forEach((key) => applyField(key)); + }); + state.observe((event) => { + if (event.transaction.origin === peerId) return; + if (event.keysChanged.has("navigationJson")) applyRemoteNavigation(); + }); + + function onProviderReady(): void { + if (!providerReady) { + providerReady = true; + publishAllControls(true); + publishAllWidgets(true); + } + applyAllFields(); + applyRemoteNavigation(); + } + + provider.on("status", ({ status }) => { + if (status === WebSocketStatus.Connected) onProviderReady(); + }); + provider.on("synced", onProviderReady); + + document.addEventListener( + "input", + (event) => { + if (!event.isTrusted || isApplyingRemoteState) return; + if ( + event.target instanceof HTMLInputElement || + event.target instanceof HTMLTextAreaElement || + event.target instanceof HTMLSelectElement + ) { + publishControl(event.target); + } + }, + true + ); + document.addEventListener( + "change", + (event) => { + if (!event.isTrusted || isApplyingRemoteState) return; + if ( + event.target instanceof HTMLInputElement || + event.target instanceof HTMLTextAreaElement || + event.target instanceof HTMLSelectElement + ) { + publishControl(event.target); + } + }, + true + ); + document.addEventListener( + "click", + (event) => { + if (!event.isTrusted || isApplyingRemoteState || !(event.target instanceof Element)) return; + const action = navigationAction(event.target); + if (action) { + publishNavigation(action); + return; + } + const widget = event.target.closest( + '[role="radio"], [role="checkbox"], [role="listbox"], [role="option"]' + ); + if (widget) { + const root = + widget.getAttribute("role") === "option" + ? widget.closest('[role="listbox"]') + : widget; + if (root) window.setTimeout(() => publishWidget(root), 0); + } + }, + true + ); + + let mutationTimer: number | undefined; + const observer = new MutationObserver(() => { + window.clearTimeout(mutationTimer); + mutationTimer = window.setTimeout(() => { + if (!isApplyingRemoteState) { + publishAllControls(true); + publishAllWidgets(true); + applyAllFields(); + } + }, 100); + }); + observer.observe(document.documentElement, { childList: true, subtree: true }); + + initTypeformCursorPresence({ + provider, + editorId, + role, + debug, + getFieldKey: (field) => controlKey(field), + findFieldByKey: (key) => { + const field = findControl(key); + return field instanceof HTMLInputElement || field instanceof HTMLTextAreaElement ? field : null; + }, + getCurrentStep: () => { + const marker = pageMarker(); + const last = marker.split(",").pop(); + return Number(last) || 0; + }, + getSessionStarted: () => true, + isWelcomeScreen: () => false, + isSyncing: () => isApplyingRemoteState, + }); + + console.info("[google-forms-bridge] ready", { roomId, collabId, role, editorId, documentName }); + log("ready", { roomId, collabId, role, editorId, documentName }); + + window.addEventListener("beforeunload", () => { + window.clearTimeout(mutationTimer); + window.clearTimeout(navigationTimer); + observer.disconnect(); + provider.destroy(); + doc.destroy(); + }); +})(); diff --git a/server/proxy-service/src/bridge/pointer-presence/PointerOverlay.ts b/server/proxy-service/src/bridge/pointer-presence/PointerOverlay.ts new file mode 100644 index 0000000000..ca8203778a --- /dev/null +++ b/server/proxy-service/src/bridge/pointer-presence/PointerOverlay.ts @@ -0,0 +1,263 @@ +import type { + AwarenessPointerState, + PointerPresenceData, + SelectionRectRatio, + TextSelectionPresenceData, +} from "./types"; + +export class PointerOverlay { + private readonly root: HTMLDivElement; + private readonly cursors = new Map(); + private readonly selections = new Map(); + private showRemoteCursors = true; + private destroyed = false; + + constructor() { + this.root = document.createElement("div"); + this.root.id = "lowcoder-pointer-overlay"; + Object.assign(this.root.style, { + position: "fixed", + inset: "0", + pointerEvents: "none", + zIndex: "2147483646", + overflow: "hidden", + }); + document.documentElement.appendChild(this.root); + } + + setShowRemoteCursors(show: boolean): void { + this.showRemoteCursors = show; + if (!show) { + for (const id of Array.from(this.cursors.keys())) { + this.removeCursor(id); + } + } + } + + syncFromStates(states: AwarenessPointerState[]): void { + if (this.destroyed) return; + const seenCursors = new Set(); + const seenSelections = new Set(); + + for (const state of states) { + const id = state.user.id; + + if (this.showRemoteCursors && state.pointer) { + seenCursors.add(id); + this.upsertCursor(id, state.user.name, state.user.color, state.pointer); + } + + if (state.selection && state.selection.rects.length > 0) { + seenSelections.add(id); + this.upsertSelection(id, state.user.name, state.user.color, state.selection); + } + } + + for (const id of Array.from(this.cursors.keys())) { + if (!seenCursors.has(id)) this.removeCursor(id); + } + for (const id of Array.from(this.selections.keys())) { + if (!seenSelections.has(id)) this.removeSelection(id); + } + } + + showClickRipple(xRatio: number, yRatio: number, color = "#1E88E5"): void { + if (this.destroyed) return; + const ripple = document.createElement("div"); + const x = clamp(xRatio, 0, 1) * window.innerWidth; + const y = clamp(yRatio, 0, 1) * window.innerHeight; + Object.assign(ripple.style, { + position: "absolute", + left: `${x}px`, + top: `${y}px`, + width: "12px", + height: "12px", + marginLeft: "-6px", + marginTop: "-6px", + borderRadius: "50%", + border: `2px solid ${color}`, + background: `${color}33`, + transform: "scale(0.4)", + opacity: "0.9", + transition: "transform 420ms ease-out, opacity 420ms ease-out", + pointerEvents: "none", + }); + this.root.appendChild(ripple); + requestAnimationFrame(() => { + ripple.style.transform = "scale(3.2)"; + ripple.style.opacity = "0"; + }); + window.setTimeout(() => ripple.remove(), 480); + } + + showButtonClickFlash( + rect: { left: number; top: number; width: number; height: number }, + color: string, + label: string + ): void { + if (this.destroyed) return; + const flash = document.createElement("div"); + Object.assign(flash.style, { + position: "absolute", + left: `${rect.left}px`, + top: `${rect.top}px`, + width: `${Math.max(rect.width, 8)}px`, + height: `${Math.max(rect.height, 8)}px`, + borderRadius: "6px", + border: `2px solid ${color}`, + background: `${color}33`, + boxShadow: `0 0 0 3px ${color}22`, + pointerEvents: "none", + opacity: "1", + transition: "opacity 500ms ease-out", + }); + if (label) { + const badge = document.createElement("div"); + badge.textContent = label; + Object.assign(badge.style, { + position: "absolute", + left: "0", + top: "-22px", + padding: "1px 6px", + borderRadius: "4px", + background: color, + color: "#fff", + font: "11px/16px system-ui,sans-serif", + whiteSpace: "nowrap", + maxWidth: "180px", + overflow: "hidden", + textOverflow: "ellipsis", + }); + flash.appendChild(badge); + } + this.root.appendChild(flash); + window.setTimeout(() => { + flash.style.opacity = "0"; + }, 40); + window.setTimeout(() => flash.remove(), 560); + } + + destroy(): void { + this.destroyed = true; + this.root.remove(); + this.cursors.clear(); + this.selections.clear(); + } + + private upsertCursor( + id: string, + name: string, + color: string, + pointer: PointerPresenceData + ): void { + let el = this.cursors.get(id); + if (!el) { + el = document.createElement("div"); + el.innerHTML = + `` + + `` + + `` + + `${escapeHtml(name)}`; + Object.assign(el.style, { + position: "absolute", + left: "0", + top: "0", + transform: "translate(-2px, -2px)", + pointerEvents: "none", + display: "flex", + alignItems: "flex-start", + willChange: "left, top", + }); + this.root.appendChild(el); + this.cursors.set(id, el); + } + const x = clamp(pointer.xRatio, 0, 1) * window.innerWidth; + const y = clamp(pointer.yRatio, 0, 1) * window.innerHeight; + el.style.left = `${x}px`; + el.style.top = `${y}px`; + } + + private upsertSelection( + id: string, + name: string, + color: string, + selection: TextSelectionPresenceData + ): void { + let group = this.selections.get(id); + if (!group) { + group = document.createElement("div"); + group.dataset.selectionUser = id; + Object.assign(group.style, { + position: "absolute", + inset: "0", + pointerEvents: "none", + }); + this.root.appendChild(group); + this.selections.set(id, group); + } + group.innerHTML = ""; + for (const rect of selection.rects) { + group.appendChild(this.buildSelectionRect(rect, color)); + } + if (selection.rects[0]) { + const label = document.createElement("div"); + label.textContent = name; + const first = selection.rects[0]; + Object.assign(label.style, { + position: "absolute", + left: `${clamp(first.xRatio, 0, 1) * window.innerWidth}px`, + top: `${Math.max(0, clamp(first.yRatio, 0, 1) * window.innerHeight - 18)}px`, + padding: "0 5px", + borderRadius: "3px", + background: color, + color: "#fff", + font: "10px/16px system-ui,sans-serif", + whiteSpace: "nowrap", + }); + group.appendChild(label); + } + } + + private buildSelectionRect(rect: SelectionRectRatio, color: string): HTMLDivElement { + const el = document.createElement("div"); + Object.assign(el.style, { + position: "absolute", + left: `${clamp(rect.xRatio, 0, 1) * window.innerWidth}px`, + top: `${clamp(rect.yRatio, 0, 1) * window.innerHeight}px`, + width: `${Math.max(2, clamp(rect.wRatio, 0, 1) * window.innerWidth)}px`, + height: `${Math.max(2, clamp(rect.hRatio, 0, 1) * window.innerHeight)}px`, + background: `${color}55`, + outline: `1px solid ${color}`, + pointerEvents: "none", + }); + return el; + } + + private removeCursor(id: string): void { + const el = this.cursors.get(id); + if (!el) return; + el.remove(); + this.cursors.delete(id); + } + + private removeSelection(id: string): void { + const el = this.selections.get(id); + if (!el) return; + el.remove(); + this.selections.delete(id); + } +} + +function clamp(value: number, min: number, max: number): number { + return Math.min(max, Math.max(min, value)); +} + +function escapeHtml(value: string): string { + return value + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """); +} diff --git a/server/proxy-service/src/bridge/pointer-presence/PointerPresenceProvider.ts b/server/proxy-service/src/bridge/pointer-presence/PointerPresenceProvider.ts new file mode 100644 index 0000000000..b7b655697c --- /dev/null +++ b/server/proxy-service/src/bridge/pointer-presence/PointerPresenceProvider.ts @@ -0,0 +1,98 @@ +import type { HocuspocusProvider } from "@hocuspocus/provider"; +import type { + AwarenessPointerState, + PointerPresenceData, + PointerPresenceUser, + TextSelectionPresenceData, +} from "./types"; + +export class PointerPresenceProvider { + private localState: AwarenessPointerState; + private pendingFlush = false; + private throttleTimer: number | undefined; + private lastBroadcastAt = 0; + private destroyed = false; + private readonly throttleMs: number; + private readonly onRemoteChange: () => void; + private boundAwarenessChange = (): void => this.onRemoteChange(); + + constructor( + private readonly provider: HocuspocusProvider, + user: PointerPresenceUser, + onRemoteChange: () => void, + throttleMs = 33 + ) { + this.throttleMs = throttleMs; + this.onRemoteChange = onRemoteChange; + this.localState = { + user, + pointer: null, + selection: null, + }; + this.provider.awareness?.setLocalState(this.localState); + this.provider.awareness?.on("change", this.boundAwarenessChange); + } + + setLocalPointer(pointer: PointerPresenceData | null): void { + if (this.destroyed) return; + this.localState = { ...this.localState, pointer }; + this.scheduleFlush(); + } + + setLocalSelection(selection: TextSelectionPresenceData | null): void { + if (this.destroyed) return; + this.localState = { ...this.localState, selection }; + this.scheduleFlush(); + } + + getRemoteStates(): AwarenessPointerState[] { + const states: AwarenessPointerState[] = []; + const awareness = this.provider.awareness; + if (!awareness) return states; + const localId = awareness.clientID; + awareness.getStates().forEach((raw, clientId) => { + if (clientId === localId) return; + const state = raw as AwarenessPointerState; + if (!state?.user) return; + states.push({ + user: state.user, + pointer: state.pointer ?? null, + selection: state.selection ?? null, + }); + }); + return states; + } + + destroy(): void { + this.destroyed = true; + window.clearTimeout(this.throttleTimer); + this.provider.awareness?.off("change", this.boundAwarenessChange); + this.provider.awareness?.setLocalState(null); + } + + private scheduleFlush(): void { + this.pendingFlush = true; + const now = Date.now(); + const elapsed = now - this.lastBroadcastAt; + if (elapsed >= this.throttleMs) { + this.flushBroadcast(); + return; + } + if (this.throttleTimer === undefined) { + this.throttleTimer = window.setTimeout( + () => this.flushBroadcast(), + this.throttleMs - elapsed + ); + } + } + + private flushBroadcast(): void { + if (this.destroyed) return; + window.clearTimeout(this.throttleTimer); + this.throttleTimer = undefined; + if (!this.pendingFlush) return; + this.pendingFlush = false; + this.lastBroadcastAt = Date.now(); + this.provider.awareness?.setLocalState({ ...this.localState }); + } +} diff --git a/server/proxy-service/src/bridge/pointer-presence/initPointerPresence.ts b/server/proxy-service/src/bridge/pointer-presence/initPointerPresence.ts new file mode 100644 index 0000000000..99e2bf1819 --- /dev/null +++ b/server/proxy-service/src/bridge/pointer-presence/initPointerPresence.ts @@ -0,0 +1,209 @@ +import { getUserColor } from "../cursor-presence/userColor"; +import { PointerOverlay } from "./PointerOverlay"; +import { PointerPresenceProvider } from "./PointerPresenceProvider"; +import type { PointerPresenceInit, SelectionRectRatio } from "./types"; + +const SHOW_MICE_STORAGE_KEY = "lowcoder-website-show-remote-mice"; + +export function initPointerPresence(config: PointerPresenceInit): { + destroy: () => void; + showClickRipple: (xRatio: number, yRatio: number, color?: string) => void; + showButtonClickFlash: ( + rect: { left: number; top: number; width: number; height: number }, + color: string, + label: string + ) => void; +} { + const username = + config.username || + new URLSearchParams(window.location.search).get("username") || + document.documentElement.getAttribute("data-lowcoder-username") || + config.editorId; + + const user = { + id: config.editorId, + name: username, + color: getUserColor(config.editorId), + role: config.role, + }; + + const overlay = new PointerOverlay(); + const presence = new PointerPresenceProvider(config.provider, user, () => { + overlay.syncFromStates(presence.getRemoteStates()); + }); + + let showRemoteMice = readShowMicePreference(); + overlay.setShowRemoteCursors(showRemoteMice); + + const chrome = createMiceToggleChrome(showRemoteMice, (next) => { + showRemoteMice = next; + writeShowMicePreference(next); + overlay.setShowRemoteCursors(next); + overlay.syncFromStates(presence.getRemoteStates()); + }); + + const onPointerMove = (event: PointerEvent): void => { + if (!event.isTrusted) return; + const xRatio = window.innerWidth > 0 ? event.clientX / window.innerWidth : 0; + const yRatio = window.innerHeight > 0 ? event.clientY / window.innerHeight : 0; + presence.setLocalPointer({ + xRatio, + yRatio, + updatedAt: Date.now(), + }); + }; + + const onPointerLeave = (): void => { + presence.setLocalPointer(null); + }; + + const publishLocalSelection = (): void => { + const selection = window.getSelection(); + if (!selection || selection.isCollapsed || selection.rangeCount === 0) { + presence.setLocalSelection(null); + return; + } + + const anchorNode = selection.anchorNode; + if (anchorNode && isInsideEditable(anchorNode)) { + presence.setLocalSelection(null); + return; + } + + const range = selection.getRangeAt(0); + const text = selection.toString().trim(); + if (!text) { + presence.setLocalSelection(null); + return; + } + + const rects = clientRectsToRatios(range.getClientRects()); + if (rects.length === 0) { + presence.setLocalSelection(null); + return; + } + + presence.setLocalSelection({ + text: text.slice(0, 200), + rects, + updatedAt: Date.now(), + }); + }; + + const onSelectionChange = (): void => { + window.requestAnimationFrame(publishLocalSelection); + }; + + window.addEventListener("pointermove", onPointerMove, { passive: true }); + window.addEventListener("blur", onPointerLeave); + document.addEventListener("mouseleave", onPointerLeave); + document.addEventListener("selectionchange", onSelectionChange); + + return { + showClickRipple: (xRatio, yRatio, color) => + overlay.showClickRipple(xRatio, yRatio, color ?? user.color), + showButtonClickFlash: (rect, color, label) => + overlay.showButtonClickFlash(rect, color, label), + destroy: () => { + window.removeEventListener("pointermove", onPointerMove); + window.removeEventListener("blur", onPointerLeave); + document.removeEventListener("mouseleave", onPointerLeave); + document.removeEventListener("selectionchange", onSelectionChange); + chrome.remove(); + presence.destroy(); + overlay.destroy(); + }, + }; +} + +function createMiceToggleChrome( + initial: boolean, + onChange: (show: boolean) => void +): HTMLDivElement { + const chrome = document.createElement("div"); + chrome.id = "lowcoder-website-mice-toggle"; + Object.assign(chrome.style, { + position: "fixed", + left: "50%", + top: "12px", + transform: "translateX(-50%)", + zIndex: "2147483647", + display: "flex", + alignItems: "center", + gap: "8px", + padding: "8px 10px", + borderRadius: "8px", + background: "rgba(20, 24, 28, 0.88)", + color: "#fff", + font: "12px/1.3 system-ui,sans-serif", + boxShadow: "0 2px 10px rgba(0,0,0,.28)", + pointerEvents: "auto", + userSelect: "none", + }); + + const label = document.createElement("label"); + Object.assign(label.style, { + display: "flex", + alignItems: "center", + gap: "6px", + cursor: "pointer", + }); + + const checkbox = document.createElement("input"); + checkbox.type = "checkbox"; + checkbox.checked = initial; + checkbox.addEventListener("change", () => onChange(checkbox.checked)); + + const text = document.createElement("span"); + text.textContent = "Show others' mice"; + + label.appendChild(checkbox); + label.appendChild(text); + chrome.appendChild(label); + document.documentElement.appendChild(chrome); + return chrome; +} + +function readShowMicePreference(): boolean { + try { + const raw = window.localStorage.getItem(SHOW_MICE_STORAGE_KEY); + if (raw === null) return true; + return raw === "1"; + } catch { + return true; + } +} + +function writeShowMicePreference(show: boolean): void { + try { + window.localStorage.setItem(SHOW_MICE_STORAGE_KEY, show ? "1" : "0"); + } catch { + // ignore + } +} + +function isInsideEditable(node: Node): boolean { + const el = node instanceof Element ? node : node.parentElement; + if (!el) return false; + return Boolean( + el.closest("input, textarea, select, [contenteditable=''], [contenteditable='true']") + ); +} + +function clientRectsToRatios(clientRects: DOMRectList): SelectionRectRatio[] { + const width = window.innerWidth || 1; + const height = window.innerHeight || 1; + const rects: SelectionRectRatio[] = []; + for (let i = 0; i < clientRects.length; i += 1) { + const rect = clientRects.item(i); + if (!rect || rect.width <= 0 || rect.height <= 0) continue; + rects.push({ + xRatio: rect.left / width, + yRatio: rect.top / height, + wRatio: rect.width / width, + hRatio: rect.height / height, + }); + if (rects.length >= 24) break; + } + return rects; +} diff --git a/server/proxy-service/src/bridge/pointer-presence/types.ts b/server/proxy-service/src/bridge/pointer-presence/types.ts new file mode 100644 index 0000000000..c95cd6b27e --- /dev/null +++ b/server/proxy-service/src/bridge/pointer-presence/types.ts @@ -0,0 +1,44 @@ +/** + * Page-level presence via Yjs Awareness (not Y.Doc): + * mouse pointer + text selection highlights. + */ + +export interface PointerPresenceUser { + id: string; + name: string; + color: string; + role?: string; +} + +export interface PointerPresenceData { + xRatio: number; + yRatio: number; + updatedAt: number; +} + +export interface SelectionRectRatio { + xRatio: number; + yRatio: number; + wRatio: number; + hRatio: number; +} + +export interface TextSelectionPresenceData { + text: string; + rects: SelectionRectRatio[]; + updatedAt: number; +} + +export interface AwarenessPointerState { + user: PointerPresenceUser; + pointer: PointerPresenceData | null; + selection: TextSelectionPresenceData | null; +} + +export interface PointerPresenceInit { + provider: import("@hocuspocus/provider").HocuspocusProvider; + editorId: string; + role: string; + username?: string; + debug?: boolean; +} diff --git a/server/proxy-service/src/bridge/typeform-bridge.ts b/server/proxy-service/src/bridge/typeform-bridge.ts new file mode 100644 index 0000000000..5660a6ec72 --- /dev/null +++ b/server/proxy-service/src/bridge/typeform-bridge.ts @@ -0,0 +1,1061 @@ +import * as Y from "yjs"; +import { HocuspocusProvider, WebSocketStatus } from "@hocuspocus/provider"; +import { initTypeformCursorPresence } from "./cursor-presence/initTypeformCursorPresence"; + +declare global { + interface Window { + __LOWCODER_HOCUSPOCUS__?: { url?: string; token?: string }; + } +} + +(() => { + type TypeformPatch = { + formId?: string; + answers: Record; + /** Absolute step index the peer is on after this action. */ + currentStep: number; + /** Answers were collected for this question key. */ + questionKey: string; + version: number; + lastEditor?: string; + submitted?: boolean; + started?: boolean; + /** Optional explicit navigation hint. */ + nav?: "answer" | "next" | "prev" | "start"; + }; + + const pageParams = new URLSearchParams(window.location.search); + const roomId = + pageParams.get("roomId") || + document.documentElement.getAttribute("data-lowcoder-room-id") || + ""; + const role = + pageParams.get("role") || + document.documentElement.getAttribute("data-lowcoder-role") || + "driver"; + const editorId = + pageParams.get("editorId") || + document.documentElement.getAttribute("data-lowcoder-editor-id") || + "local"; + // Unique per iframe load so two tabs of the same user still sync. + const peerId = `${editorId}|${role}|${Math.random().toString(36).slice(2, 10)}`; + const collabId = + pageParams.get("collab") || + document.documentElement.getAttribute("data-lowcoder-collab-id") || + "default"; + const debug = pageParams.get("debug") === "1"; + + const hocuspocusConfig = window.__LOWCODER_HOCUSPOCUS__ ?? {}; + const hocuspocusUrl = + hocuspocusConfig.url || + document.documentElement.getAttribute("data-lowcoder-hocuspocus-url") || + "ws://localhost:3006"; + const hocuspocusToken = + hocuspocusConfig.token || + document.documentElement.getAttribute("data-lowcoder-hocuspocus-token") || + ""; + + const documentName = `typeform_${roomId}_${collabId}`; + + let version = 0; + let lastAppliedVersion = 0; + let localStep = 0; + let isApplyingRemoteState = false; + let lastSentPayload = ""; + let sessionStarted = false; + let welcomeClickPending = false; + let providerReady = false; + let lastNavAt = 0; + let lastLocalInputAt = 0; + let isApplyingInputText = false; + let publishInputTimer: number | undefined; + let applyingGeneration = 0; + const outboundQueue: TypeformPatch[] = []; + let allAnswers: Record = {}; + + const doc = new Y.Doc(); + const stateMap = doc.getMap("state"); + + const provider = new HocuspocusProvider({ + url: hocuspocusUrl, + name: documentName, + document: doc, + token: hocuspocusToken || undefined, + onAuthenticationFailed: (data) => { + console.error("[typeform-bridge] Hocuspocus auth failed", data); + }, + }); + + function log(...args: unknown[]) { + if (debug) console.log("[typeform-bridge]", role, ...args); + } + + function nextVersion(): number { + const remote = Number(stateMap.get("version") || 0); + version = Math.max(version, remote) + 1; + return version; + } + + function publishPatch(patch: TypeformPatch): void { + if (!providerReady) { + outboundQueue.push(patch); + return; + } + doc.transact(() => { + if (patch.started) { + stateMap.set("started", true); + } + stateMap.set("version", patch.version); + stateMap.set("patchJson", JSON.stringify(patch)); + }); + log("published", { + version: patch.version, + step: patch.currentStep, + nav: patch.nav, + q: patch.questionKey, + }); + } + + function flushOutboundQueue(): void { + while (outboundQueue.length > 0) { + const patch = outboundQueue.shift(); + if (patch) publishPatch(patch); + } + } + + function parseRemotePatch(): TypeformPatch | null { + const raw = stateMap.get("patchJson"); + if (typeof raw !== "string" || !raw) return null; + try { + return JSON.parse(raw) as TypeformPatch; + } catch { + return null; + } + } + + function shouldApplyPatch(patch: TypeformPatch): boolean { + if (!patch) return false; + if (patch.lastEditor === peerId) return false; + if ((patch.version ?? 0) <= lastAppliedVersion && patch.currentStep === localStep) { + return false; + } + return true; + } + + function syncFromRemoteState(): void { + const started = Boolean(stateMap.get("started")); + const patch = parseRemotePatch(); + + // Only followers mirror a remote start; driver always lands on welcome until local Start. + if (started && !sessionStarted && role === "follower") { + onRemoteSessionStarted(); + } + + // Apply navigation/answers first; inputTextsJson wins for in-progress typing. + if (patch && shouldApplyPatch(patch)) { + applyRemoteState(patch); + } + + applyRemoteInputText(); + } + + provider.on("status", ({ status }) => { + log("status", status, documentName); + if (status === WebSocketStatus.Connected) { + providerReady = true; + flushOutboundQueue(); + syncFromRemoteState(); + } + }); + + provider.on("synced", () => { + providerReady = true; + flushOutboundQueue(); + syncFromRemoteState(); + }); + + stateMap.observe((event) => { + if ( + event.keysChanged.has("patchJson") || + event.keysChanged.has("version") || + event.keysChanged.has("started") || + event.keysChanged.has("inputTextJson") || + event.keysChanged.has("inputTextsJson") + ) { + syncFromRemoteState(); + } + }); + + function isVisible(el: Element): boolean { + const node = el as HTMLElement; + if (!node.getBoundingClientRect) return true; + const rect = node.getBoundingClientRect(); + return rect.width > 0 && rect.height > 0; + } + + function listVisibleTextFields( + container: Element | Document = document + ): Array { + return Array.from( + container.querySelectorAll( + [ + 'input[type="text"]', + 'input[type="email"]', + 'input[type="number"]', + 'input[type="tel"]', + 'input[type="url"]', + 'input[type="search"]', + 'input[type="short_text"]', + 'input[type="long_text"]', + 'input[type="phone_number"]', + 'input[name]', + 'input:not([type])', + "textarea", + ].join(", ") + ) + ).filter((field) => { + const type = (field.getAttribute("type") || field.type || "").toLowerCase(); + if (["hidden", "checkbox", "radio", "button", "submit", "file", "password"].includes(type)) { + return false; + } + return isVisible(field); + }); + } + + function getFocusedTextField(): HTMLInputElement | HTMLTextAreaElement | null { + const el = document.activeElement; + if (!el) return null; + if (el instanceof HTMLInputElement) { + const type = (el.getAttribute("type") || el.type || "text").toLowerCase(); + if (["hidden", "checkbox", "radio", "button", "submit", "file", "password"].includes(type)) { + return null; + } + return el; + } + if (el instanceof HTMLTextAreaElement) return el; + return null; + } + + function extractQuestionUuid(value: string): string | null { + const match = value.match( + /([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i + ); + return match?.[1] ?? null; + } + + /** Stable field key shared across peers (name / Typeform question uuid). */ + function getFieldKey(field: HTMLInputElement | HTMLTextAreaElement): string { + const name = field.getAttribute("name")?.trim(); + if (name) return `name:${name}`; + + const labelledBy = field.getAttribute("aria-labelledby") || ""; + const fromLabel = extractQuestionUuid(labelledBy); + if (fromLabel) return `qid:${fromLabel}`; + + const id = field.getAttribute("id") || ""; + const fromId = extractQuestionUuid(id); + if (fromId) return `qid:${fromId}`; + + const typeAttr = (field.getAttribute("type") || field.type || "text").toLowerCase(); + return `type:${typeAttr}:step:${localStep}`; + } + + function findFieldByKey(key: string): HTMLInputElement | HTMLTextAreaElement | null { + const visible = listVisibleTextFields(document); + for (const field of visible) { + if (getFieldKey(field) === key) return field; + } + + if (key.startsWith("name:")) { + const name = key.slice("name:".length); + const el = document.querySelector(`input[name="${CSS.escape(name)}"]`); + if (el && isVisible(el)) return el; + } + + if (key.startsWith("qid:")) { + const qid = key.slice("qid:".length); + const matches = Array.from( + document.querySelectorAll( + `input[aria-labelledby*="${CSS.escape(qid)}"], input[id*="${CSS.escape(qid)}"], textarea[aria-labelledby*="${CSS.escape(qid)}"]` + ) + ).filter(isVisible); + if (matches[0]) return matches[0]; + } + + return null; + } + + function getActiveQuestionContainer(): Element | null { + const candidates = [ + '[data-qa="question-container"]', + '[data-qa="question"]', + "fieldset", + '[role="group"]', + ]; + for (const selector of candidates) { + const nodes = Array.from(document.querySelectorAll(selector)).filter(isVisible); + if (nodes.length > 0) { + return nodes[nodes.length - 1]; + } + } + return null; + } + + function readInputTextsMap(): Record { + const raw = stateMap.get("inputTextsJson"); + if (typeof raw !== "string" || !raw) return {}; + try { + const parsed = JSON.parse(raw) as Record; + return parsed && typeof parsed === "object" ? parsed : {}; + } catch { + return {}; + } + } + + function syncAllAnswersForField(fieldKey: string, text: string): void { + const qKey = stepKey(localStep); + const container = getActiveQuestionContainer() || document; + listVisibleTextFields(container).forEach((field, index) => { + if (getFieldKey(field) === fieldKey) { + allAnswers[`${qKey}::field-${index}`] = text; + } + }); + } + + function publishInputText(): void { + if (!canPublish()) return; + if (isApplyingInputText || isApplyingRemoteState) return; + const field = getFocusedTextField() || listVisibleTextFields(document)[0]; + if (!field) return; + + const fieldKey = getFieldKey(field); + const text = field.value; + const remoteMap = readInputTextsMap(); + // Skip only when Yjs already has this exact value (allows re-publishing after delete-back). + if ((remoteMap[fieldKey] ?? "") === text) return; + + if (!providerReady) return; + + syncAllAnswersForField(fieldKey, text); + const nextMap = { ...remoteMap, [fieldKey]: text }; + const payload = { + fieldKey, + step: localStep, + text, + peerId, + version: nextVersion(), + }; + doc.transact(() => { + stateMap.set("inputTextJson", JSON.stringify(payload)); + stateMap.set("inputTextsJson", JSON.stringify(nextMap)); + stateMap.set("version", payload.version); + }); + log("published input text", payload); + } + + /** Debounce publish so fast typing doesn't race with remote echoes. */ + function schedulePublishInputText(): void { + lastLocalInputAt = Date.now(); + window.clearTimeout(publishInputTimer); + publishInputTimer = window.setTimeout(() => { + publishInputText(); + }, 120); + } + + function flushPublishInputText(): void { + window.clearTimeout(publishInputTimer); + publishInputText(); + } + + function readLatestInputPayload(): { + fieldKey?: string; + text?: string; + peerId?: string; + } | null { + const raw = stateMap.get("inputTextJson"); + if (typeof raw !== "string" || !raw) return null; + try { + return JSON.parse(raw) as { fieldKey?: string; text?: string; peerId?: string }; + } catch { + return null; + } + } + + /** Apply stored text only onto matching visible fields (by name / question uuid). */ + function applyRemoteInputText(): void { + if (isOnWelcomeScreen()) return; + if (isApplyingInputText) return; + + const map = readInputTextsMap(); + const visible = listVisibleTextFields(document); + const latestInput = readLatestInputPayload(); + // Only protect against overwrite while THIS client is actively typing. + // Typeform auto-focuses inputs, so "has focus" alone must not block follower sync. + const locallyTyping = Date.now() - lastLocalInputAt < 800; + + isApplyingInputText = true; + try { + for (const field of visible) { + const key = getFieldKey(field); + if (!Object.prototype.hasOwnProperty.call(map, key)) continue; + // Never overwrite the field the user is actively typing in. + if (locallyTyping && document.activeElement === field) continue; + const next = map[key] ?? ""; + // Ignore stale self-echo while the DOM is ahead of our last publish. + if ( + latestInput?.peerId === peerId && + latestInput.fieldKey === key && + document.activeElement === field && + field.value !== next + ) { + continue; + } + if (field.value === next) continue; + setNativeInputValue(field, next); + log("applied field text", key, next); + } + + const payload = latestInput; + if ( + payload && + payload.peerId !== peerId && + typeof payload.text === "string" && + payload.fieldKey + ) { + const target = findFieldByKey(payload.fieldKey); + if ( + target && + !(locallyTyping && document.activeElement === target) && + target.value !== payload.text + ) { + setNativeInputValue(target, payload.text); + log("applied field text (payload)", payload.fieldKey, payload.text); + } + } + } finally { + window.setTimeout(() => { + isApplyingInputText = false; + }, 50); + } + } + + function stepKey(step: number): string { + return `question-${Math.max(0, step)}`; + } + + function collectVisibleAnswersForKey(qKey: string): Record { + const result: Record = {}; + const container = getActiveQuestionContainer() || document; + + listVisibleTextFields(container).forEach((field, index) => { + result[`${qKey}::field-${index}`] = field.value; + }); + + container + .querySelectorAll( + '[data-qa*="choice"], [role="radio"], [role="checkbox"], [role="option"], button[data-qa]' + ) + .forEach((el, index) => { + const selected = + el.getAttribute("aria-checked") === "true" || + el.getAttribute("aria-pressed") === "true" || + el.getAttribute("aria-selected") === "true" || + el.classList.contains("selected"); + if (!selected) return; + const value = el.getAttribute("data-qa") || el.textContent?.trim() || String(index); + result[`${qKey}::choice`] = value; + }); + + return result; + } + + function collectAnswers(forStep = localStep): Record { + const visible = collectVisibleAnswersForKey(stepKey(forStep)); + allAnswers = { ...allAnswers, ...visible }; + return allAnswers; + } + + function getFormId(): string { + const target = pageParams.get("target"); + if (target) { + try { + const match = new URL(target).pathname.match(/\/to\/([^/?#]+)/); + if (match?.[1]) return match[1]; + } catch { + // ignore + } + } + const match = window.location.pathname.match(/\/to\/([^/?#]+)/); + return match?.[1] ?? ""; + } + + function isOnWelcomeScreen(): boolean { + return !!( + document.querySelector('[data-qa="start-button"]') || + document.querySelector('[data-qa="welcome-screen"]') || + document.querySelector('[data-qa="landing-wrapper"]') || + document.querySelector('[data-qa="welcome-screen-paragraph"]') + ); + } + + function buttonText(el: Element): string { + return (el.textContent ?? "").trim().toLowerCase(); + } + + function isStartButton(el: Element | null): boolean { + if (!el) return false; + const button = el.closest('button, [role="button"], a'); + if (!button) return false; + const qa = button.getAttribute("data-qa") ?? ""; + if (/start/i.test(qa)) return true; + const text = buttonText(button); + return text === "start" || text === "begin" || text === "get started" || text.includes("start"); + } + + function isSubmitButton(el: Element | null): boolean { + if (!el) return false; + const button = el.closest('button, [role="button"], a, input'); + if (!button) return false; + const qa = (button.getAttribute("data-qa") ?? "").toLowerCase(); + const aria = (button.getAttribute("aria-label") ?? "").toLowerCase(); + const type = (button.getAttribute("type") ?? "").toLowerCase(); + if (/submit/i.test(qa) || /submit/i.test(aria) || type === "submit") return true; + const text = buttonText(button); + return text === "submit" || text === "done" || text === "send" || text === "finish"; + } + + function isOkButton(el: Element | null): boolean { + if (!el) return false; + const button = el.closest('button, [role="button"]'); + if (!button) return false; + if (isBackButton(button)) return false; + const qa = button.getAttribute("data-qa") ?? ""; + if (/ok-button|submit-button|next/i.test(qa)) return true; + const text = buttonText(button); + return ["ok", "next", "continue", "submit", "done"].includes(text); + } + + function isBackButton(el: Element | null): boolean { + if (!el) return false; + const button = el.closest('button, [role="button"], a'); + if (!button) return false; + const qa = (button.getAttribute("data-qa") ?? "").toLowerCase(); + const aria = (button.getAttribute("aria-label") ?? "").toLowerCase(); + const title = (button.getAttribute("title") ?? "").toLowerCase(); + if (/prev|previous|back/.test(qa) || /prev|previous|back/.test(aria) || /prev|previous|back/.test(title)) { + return true; + } + const text = buttonText(button); + return text === "previous" || text === "prev" || text === "back" || text === "←"; + } + + /** + * Advance to the next question. By default never clicks Submit — that is only + * allowed when the remote peer explicitly marked the form as submitted. + */ + function clickOkButton(allowSubmit = false): boolean { + const selectors = [ + '[data-qa="ok-button-visible"]', + '[data-qa="ok-button"]', + '[data-qa*="next"]', + ]; + if (allowSubmit) { + selectors.splice(1, 0, '[data-qa="submit-button"]'); + } + for (const selector of selectors) { + const btn = document.querySelector(selector); + if (btn && isVisible(btn) && !isBackButton(btn)) { + if (!allowSubmit && isSubmitButton(btn)) continue; + btn.click(); + return true; + } + } + const fallback = Array.from(document.querySelectorAll("button, [role='button']")).find( + (btn) => { + if (!isVisible(btn) || isBackButton(btn)) return false; + if (!allowSubmit && isSubmitButton(btn)) return false; + const text = buttonText(btn); + return ["ok", "next", "continue"].includes(text) || + (allowSubmit && ["submit", "done"].includes(text)); + } + ); + if (fallback) { + fallback.click(); + return true; + } + // Do not synthesize Enter — it often triggers Typeform's final Submit. + return false; + } + + function clickBackButton(): boolean { + const selectors = [ + '[data-qa*="previous"]', + '[data-qa*="prev"]', + '[data-qa*="back"]', + '[aria-label*="Previous" i]', + '[aria-label*="Back" i]', + '[title*="Previous" i]', + '[title*="Back" i]', + ]; + for (const selector of selectors) { + try { + const btn = document.querySelector(selector); + if (btn && isVisible(btn)) { + btn.click(); + return true; + } + } catch { + // Some browsers don't support i flag in querySelector; ignore. + } + } + const fallback = Array.from(document.querySelectorAll("button, [role='button'], a")).find( + (btn) => isVisible(btn) && isBackButton(btn) + ); + if (fallback) { + fallback.click(); + return true; + } + return false; + } + + function advancePastWelcomeIfNeeded(): void { + if (!isOnWelcomeScreen()) return; + const startButton = document.querySelector( + '[data-qa="start-button"]' + ) as HTMLButtonElement | null; + if (startButton) { + startButton.click(); + return; + } + const fallback = Array.from(document.querySelectorAll("button, [role='button']")).find((node) => + isStartButton(node) + ) as HTMLButtonElement | undefined; + fallback?.click(); + } + + function buildPatch(opts: { + submitted?: boolean; + answeredStep?: number; + currentStep?: number; + nav?: TypeformPatch["nav"]; + } = {}): TypeformPatch { + const answeredStep = opts.answeredStep ?? localStep; + const current = opts.currentStep ?? localStep; + return { + formId: getFormId(), + answers: collectAnswers(answeredStep), + currentStep: current, + questionKey: stepKey(answeredStep), + version: nextVersion(), + lastEditor: peerId, + submitted: Boolean(opts.submitted), + started: true, + nav: opts.nav ?? "answer", + }; + } + + function markSessionStarted(): void { + if (sessionStarted) return; + // Only the driver (or first Start click) announces start; follower mirrors remotely. + sessionStarted = true; + welcomeClickPending = false; + localStep = 0; + + if (role === "driver") { + const startedPatch = buildPatch({ currentStep: 0, answeredStep: 0, nav: "start" }); + publishPatch(startedPatch); + log("session started (local)"); + } + } + + function onRemoteSessionStarted(): void { + if (sessionStarted) return; + sessionStarted = true; + localStep = 0; + // Follower mirrors the driver's Start click; driver navigates via native click. + if (role === "follower") { + advancePastWelcomeIfNeeded(); + log("session started (remote)"); + } + } + + function canPublish(): boolean { + return sessionStarted && !isApplyingRemoteState; + } + + /** Publish in-progress answers on the current step (no navigation). */ + function sendPatch(submitted = false) { + if (!canPublish()) return; + + const payload = buildPatch({ + submitted, + answeredStep: localStep, + currentStep: localStep, + nav: "answer", + }); + const serialized = JSON.stringify({ + answers: payload.answers, + currentStep: payload.currentStep, + submitted: payload.submitted, + nav: payload.nav, + }); + if (serialized === lastSentPayload && !submitted) return; + lastSentPayload = serialized; + publishPatch(payload); + } + + function publishNext(fromStep: number, submitted = false): void { + if (!canPublish()) return; + const now = Date.now(); + if (!submitted && now - lastNavAt < 350) return; + lastNavAt = now; + + const payload = buildPatch({ + submitted, + answeredStep: fromStep, + currentStep: fromStep + 1, + nav: "next", + }); + localStep = fromStep + 1; + lastSentPayload = ""; + lastAppliedVersion = Math.max(lastAppliedVersion, payload.version); + publishPatch(payload); + log("next", fromStep, "->", localStep); + window.setTimeout(() => applyRemoteInputText(), 300); + } + + function publishPrev(fromStep: number): void { + if (!canPublish()) return; + if (fromStep <= 0) return; + const now = Date.now(); + if (now - lastNavAt < 350) return; + lastNavAt = now; + + const payload = buildPatch({ + answeredStep: fromStep, + currentStep: fromStep - 1, + nav: "prev", + }); + localStep = fromStep - 1; + lastSentPayload = ""; + lastAppliedVersion = Math.max(lastAppliedVersion, payload.version); + publishPatch(payload); + log("prev", fromStep, "->", localStep); + window.setTimeout(() => applyRemoteInputText(), 300); + } + + function applyChoice(value: string): boolean { + const container = getActiveQuestionContainer() || document; + const choices = container.querySelectorAll( + '[data-qa*="choice"], [role="radio"], [role="checkbox"], [role="option"], button[data-qa]' + ); + for (const el of choices) { + if (!isVisible(el)) continue; + const label = el.textContent?.trim() || ""; + const qa = el.getAttribute("data-qa") || ""; + if (qa === value || label === value || qa.includes(value) || label.includes(value)) { + el.click(); + return true; + } + } + return false; + } + + function setNativeInputValue(field: HTMLInputElement | HTMLTextAreaElement, nextValue: string) { + if (field.value === nextValue) return; + const previous = field.value; + const proto = + field instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype; + const setter = Object.getOwnPropertyDescriptor(proto, "value")?.set; + const tracker = (field as HTMLInputElement & { _valueTracker?: { setValue: (v: string) => void } }) + ._valueTracker; + if (tracker) { + tracker.setValue(previous); + } + if (setter) { + setter.call(field, nextValue); + } else { + field.value = nextValue; + } + const inputType = + nextValue.length < previous.length + ? "deleteContentBackward" + : nextValue.length > previous.length + ? "insertText" + : "insertReplacementText"; + field.dispatchEvent( + new InputEvent("input", { + bubbles: true, + cancelable: true, + data: inputType === "insertText" ? nextValue.slice(previous.length) : nextValue, + inputType, + }) + ); + field.dispatchEvent(new Event("change", { bubbles: true })); + } + + function applyAnswersForQuestion(answers: Record, questionKey: string) { + const container = getActiveQuestionContainer() || document; + + Object.entries(answers).forEach(([key, value]) => { + if (!key.startsWith(`${questionKey}::`)) return; + + if (key.endsWith("::choice")) { + applyChoice(String(value ?? "")); + return; + } + + if (key.includes("::field-")) { + const index = Number(key.split("::field-")[1] ?? 0); + const field = listVisibleTextFields(container)[index]; + if (!field) return; + const fieldKey = getFieldKey(field); + const inputTexts = readInputTextsMap(); + // Real-time typing uses inputTextsJson; avoid stale patch answers overwriting deletes. + if (Object.prototype.hasOwnProperty.call(inputTexts, fieldKey)) return; + if (document.activeElement === field && Date.now() - lastLocalInputAt < 800) return; + setNativeInputValue(field, value == null ? "" : String(value)); + } + }); + } + + function alignToRemoteStep(patch: TypeformPatch, generation: number) { + if (generation !== applyingGeneration) return; + + const remoteStep = Math.max(0, patch.currentStep ?? 0); + + if (localStep === remoteStep) { + applyAnswersForQuestion(patch.answers, stepKey(localStep)); + lastAppliedVersion = Math.max(lastAppliedVersion, patch.version ?? 0); + isApplyingRemoteState = false; + log("aligned on step", localStep); + window.setTimeout(() => applyRemoteInputText(), 250); + return; + } + + if (localStep < remoteStep) { + applyAnswersForQuestion(patch.answers, stepKey(localStep)); + window.setTimeout(() => { + if (generation !== applyingGeneration) return; + // Only click Submit when the peer explicitly submitted the form. + const allowSubmit = + Boolean(patch.submitted) && localStep + 1 >= remoteStep; + const advanced = clickOkButton(allowSubmit); + if (!advanced) { + // No OK control found — stop catching up instead of forcing Submit/Enter. + lastAppliedVersion = Math.max(lastAppliedVersion, patch.version ?? 0); + isApplyingRemoteState = false; + log("catch-up stopped: no next control", localStep, "target", remoteStep); + return; + } + localStep += 1; + log("catch-up next ->", localStep, "target", remoteStep); + window.setTimeout(() => alignToRemoteStep(patch, generation), 450); + }, 180); + return; + } + + // localStep > remoteStep — go back + window.setTimeout(() => { + if (generation !== applyingGeneration) return; + const moved = clickBackButton(); + if (moved) { + localStep = Math.max(0, localStep - 1); + log("catch-up prev ->", localStep, "target", remoteStep); + } else { + // Cannot find back control — snap step bookkeeping and apply answers. + localStep = remoteStep; + applyAnswersForQuestion(patch.answers, stepKey(localStep)); + lastAppliedVersion = Math.max(lastAppliedVersion, patch.version ?? 0); + isApplyingRemoteState = false; + return; + } + window.setTimeout(() => alignToRemoteStep(patch, generation), 450); + }, 180); + } + + function applyRemoteStateInner(patch: TypeformPatch) { + if (!patch?.answers && patch.nav === "answer") return; + + applyingGeneration += 1; + const generation = applyingGeneration; + isApplyingRemoteState = true; + allAnswers = { ...allAnswers, ...(patch.answers || {}) }; + + alignToRemoteStep(patch, generation); + } + + function applyRemoteState(patch: TypeformPatch) { + if (!sessionStarted) { + // Driver ignores remote patches until they click Start locally. + if (role === "driver") return; + if (!patch.started && !Boolean(stateMap.get("started"))) return; + sessionStarted = true; + } + if (isOnWelcomeScreen()) { + // Stay on welcome until the driver starts; follower auto-clicks Start then catches up. + if (role === "follower" && sessionStarted) { + advancePastWelcomeIfNeeded(); + window.setTimeout(() => applyRemoteStateInner(patch), 400); + } + return; + } + applyRemoteStateInner(patch); + } + + function maybeMarkSessionStartedAfterWelcomeClick(): void { + if (role !== "driver" || sessionStarted || !welcomeClickPending) return; + if (!isOnWelcomeScreen()) { + markSessionStarted(); + } + } + + const debounce = (() => { + let timer: number | undefined; + return () => { + window.clearTimeout(timer); + timer = window.setTimeout(() => { + maybeMarkSessionStartedAfterWelcomeClick(); + sendPatch(false); + }, 150); + }; + })(); + + document.addEventListener( + "click", + (event) => { + const target = event.target as Element; + + if (!sessionStarted) { + if (role === "driver" && isOnWelcomeScreen()) { + welcomeClickPending = true; + if (isStartButton(target)) { + window.setTimeout(() => markSessionStarted(), 0); + } + } + return; + } + + if (isApplyingRemoteState) return; + + if (isBackButton(target)) { + publishPrev(localStep); + return; + } + + if (isOkButton(target)) { + // Explicit Submit stays submitted; OK/Next must not mark the form submitted. + publishNext(localStep, isSubmitButton(target)); + } + }, + true + ); + + document.addEventListener( + "keydown", + (event) => { + if (!canPublish()) return; + if (isOnWelcomeScreen()) return; + + if (event.key === "Enter") { + window.setTimeout(() => publishNext(localStep, false), 0); + return; + } + + // Typeform supports Up/Left-style go-back in some builds via Alt/Meta+Arrow, but + // also exposed as dedicated previous control. ArrowUp often goes previous. + if (event.key === "ArrowUp") { + window.setTimeout(() => publishPrev(localStep), 0); + } + }, + true + ); + + new MutationObserver(debounce).observe(document.documentElement, { + childList: true, + attributes: true, + subtree: true, + }); + + document.addEventListener( + "input", + () => { + if (isApplyingInputText || isApplyingRemoteState) return; + schedulePublishInputText(); + debounce(); + }, + true + ); + document.addEventListener( + "keyup", + (event) => { + if (isApplyingInputText || isApplyingRemoteState) return; + const key = event.key; + if (key === "Enter" || key === "ArrowUp" || key === "ArrowDown") return; + schedulePublishInputText(); + }, + true + ); + document.addEventListener( + "blur", + (event) => { + if (isApplyingInputText || isApplyingRemoteState) return; + const target = event.target; + if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement) { + flushPublishInputText(); + } + }, + true + ); + document.addEventListener("change", debounce, true); + document.addEventListener( + "submit", + (event) => { + // Never auto-submit while applying remote sync. + if (isApplyingRemoteState) { + event.preventDefault(); + event.stopPropagation(); + log("blocked auto-submit during remote sync"); + return; + } + if (canPublish()) { + publishNext(localStep, true); + } + }, + true + ); + + const originalFetch = window.fetch.bind(window); + window.fetch = async (...args: Parameters) => { + const response = await originalFetch(...args); + debounce(); + return response; + }; + + const originalOpen = XMLHttpRequest.prototype.open; + XMLHttpRequest.prototype.open = function patchedOpen(this: XMLHttpRequest, ...args: any[]) { + this.addEventListener("loadend", debounce); + return (originalOpen as any).apply(this, args); + }; + + log("ready", { role, roomId, collabId, documentName, editorId, peerId }); + + // Cursor presence add-on — awareness only; does not modify sync handlers above. + initTypeformCursorPresence({ + provider, + editorId, + role, + debug, + getFieldKey, + findFieldByKey, + getCurrentStep: () => localStep, + getSessionStarted: () => sessionStarted, + isWelcomeScreen: isOnWelcomeScreen, + isSyncing: () => isApplyingRemoteState || isApplyingInputText, + }); + + window.addEventListener("beforeunload", () => { + provider.destroy(); + doc.destroy(); + }); +})(); diff --git a/server/proxy-service/src/bridge/website-bridge.ts b/server/proxy-service/src/bridge/website-bridge.ts new file mode 100644 index 0000000000..862182b0fa --- /dev/null +++ b/server/proxy-service/src/bridge/website-bridge.ts @@ -0,0 +1,700 @@ +import * as Y from "yjs"; +import { HocuspocusProvider } from "@hocuspocus/provider"; +import { getUserColor } from "./cursor-presence/userColor"; +import { initPointerPresence } from "./pointer-presence/initPointerPresence"; + +declare global { + interface Window { + __LOWCODER_HOCUSPOCUS__?: { url?: string; token?: string }; + } +} + +interface ScrollState { + xRatio: number; + yRatio: number; +} + +interface ClickState { + xRatio: number; + yRatio: number; + ts: number; + editorId: string; + username: string; + kind: "button" | "generic"; + label: string; + rect?: { leftRatio: number; topRatio: number; wRatio: number; hRatio: number }; +} + +interface NavOffer { + url: string; + navSeq: number; + editorId: string; + username: string; +} + +type FormControl = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement; + +(() => { + const params = new URLSearchParams(window.location.search); + const root = document.documentElement; + const roomId = params.get("roomId") || root.dataset.lowcoderRoomId || ""; + const role = params.get("role") || root.dataset.lowcoderRole || "driver"; + const editorId = params.get("editorId") || root.dataset.lowcoderEditorId || "local"; + const collabId = params.get("collab") || root.dataset.lowcoderCollabId || ""; + const username = + params.get("username") || root.dataset.lowcoderUsername || editorId; + const debug = params.get("debug") === "1"; + const isPresenter = role === "driver"; + const peerId = `${editorId}|${role}|${Math.random().toString(36).slice(2, 10)}`; + + if (!roomId || !collabId) { + console.error( + "[website-bridge] Missing roomId/collab. Load the page through /proxy/website " + + "and create a session with createWebsiteProxySession before Explore Together." + ); + return; + } + + if (!window.location.pathname.includes("/proxy/website")) { + console.error( + "[website-bridge] Page left the Lowcoder proxy. Keep browsing through /proxy/website." + ); + return; + } + + const hocuspocusConfig = window.__LOWCODER_HOCUSPOCUS__ ?? {}; + const hocuspocusUrl = + hocuspocusConfig.url || root.dataset.lowcoderHocuspocusUrl || "ws://localhost:3006"; + const hocuspocusToken = + hocuspocusConfig.token || root.dataset.lowcoderHocuspocusToken || ""; + const documentName = `website_${roomId}_${collabId}`; + + let isApplyingRemote = false; + let isApplyingRemoteFields = false; + let lastAppliedNavSeq = 0; + let lastAppliedClickTs = 0; + let scrollPublishTimer: number | undefined; + let fieldPublishTimer: number | undefined; + let lastPublishedScroll = ""; + let dismissedNavSeq = 0; + let followPromptEl: HTMLDivElement | null = null; + + const doc = new Y.Doc(); + const state = doc.getMap("state"); + const fields = doc.getMap("fields"); + const provider = new HocuspocusProvider({ + url: hocuspocusUrl, + name: documentName, + document: doc, + token: hocuspocusToken || undefined, + onAuthenticationFailed: (data) => { + console.error("[website-bridge] Hocuspocus auth failed", data); + }, + }); + + const pointer = initPointerPresence({ + provider, + editorId, + role, + username, + debug, + }); + + function log(...args: unknown[]): void { + if (debug) console.log("[website-bridge]", role, ...args); + } + + function listControls(): FormControl[] { + return Array.from( + document.querySelectorAll("input, textarea, select") + ).filter((control) => { + if (control.disabled) return false; + if (control instanceof HTMLInputElement) { + const type = (control.type || "text").toLowerCase(); + return !["hidden", "button", "submit", "reset", "file", "password", "image"].includes( + type + ); + } + return true; + }); + } + + function controlIdentity(control: FormControl): string { + const name = control.getAttribute("name")?.trim(); + if (name) return `name:${name}`; + const id = control.id?.trim(); + if (id) return `id:${id}`; + const ariaLabel = control.getAttribute("aria-label")?.trim(); + if (ariaLabel) return `aria:${ariaLabel}`; + const placeholder = + control instanceof HTMLInputElement || control instanceof HTMLTextAreaElement + ? control.placeholder?.trim() + : ""; + if (placeholder) return `placeholder:${placeholder}`; + return `index:${listControls().indexOf(control)}`; + } + + function optionValue(input: HTMLInputElement): string { + return input.getAttribute("data-value") || input.value; + } + + function controlKey(control: FormControl): string { + const identity = controlIdentity(control); + if (control instanceof HTMLInputElement && control.type === "radio") { + return `radio:${identity}`; + } + if (control instanceof HTMLInputElement && control.type === "checkbox") { + return `checkbox:${identity}:${optionValue(control)}`; + } + const sameIdentity = listControls().filter( + (candidate) => + !(candidate instanceof HTMLInputElement && ["radio", "checkbox"].includes(candidate.type)) && + controlIdentity(candidate) === identity + ); + return `field:${identity}:${Math.max(0, sameIdentity.indexOf(control))}`; + } + + function controlValue(control: FormControl): string { + if (control instanceof HTMLInputElement && control.type === "checkbox") { + return control.checked ? "1" : "0"; + } + if (control instanceof HTMLInputElement && control.type === "radio") { + const group = listControls().filter( + (candidate) => + candidate instanceof HTMLInputElement && + candidate.type === "radio" && + controlIdentity(candidate) === controlIdentity(control) + ) as HTMLInputElement[]; + const checked = group.find((candidate) => candidate.checked); + return checked ? optionValue(checked) : ""; + } + if (control instanceof HTMLSelectElement && control.multiple) { + return JSON.stringify(Array.from(control.selectedOptions).map((option) => option.value)); + } + return control.value; + } + + function findControl(key: string): FormControl | null { + return listControls().find((control) => controlKey(control) === key) ?? null; + } + + function setNativeValue(control: FormControl, value: string): void { + if (control instanceof HTMLInputElement && control.type === "radio") { + const target = listControls().find( + (candidate) => + candidate instanceof HTMLInputElement && + candidate.type === "radio" && + controlKey(candidate) === controlKey(control) && + optionValue(candidate) === value + ) as HTMLInputElement | undefined; + if (target && !target.checked) target.click(); + return; + } + + if (control instanceof HTMLInputElement && control.type === "checkbox") { + const checked = value === "1"; + if (control.checked !== checked) control.click(); + return; + } + + if (control instanceof HTMLSelectElement) { + if (control.multiple) { + let selected: string[] = []; + try { + selected = JSON.parse(value) as string[]; + } catch { + selected = []; + } + Array.from(control.options).forEach((option) => { + option.selected = selected.includes(option.value); + }); + } else if (control.value !== value) { + control.value = value; + } + control.dispatchEvent(new Event("change", { bubbles: true })); + return; + } + + if (control.value === value) return; + const prototype = + control instanceof HTMLTextAreaElement + ? HTMLTextAreaElement.prototype + : HTMLInputElement.prototype; + const setter = Object.getOwnPropertyDescriptor(prototype, "value")?.set; + if (setter) setter.call(control, value); + else control.value = value; + control.dispatchEvent(new InputEvent("input", { bubbles: true, inputType: "insertText" })); + control.dispatchEvent(new Event("change", { bubbles: true })); + } + + function publishControl(control: FormControl): void { + if (isApplyingRemoteFields || !provider.isSynced) return; + const key = controlKey(control); + const value = controlValue(control); + if (fields.get(key) === value) return; + doc.transact(() => fields.set(key, value), peerId); + log("published field", key, value.slice(0, 40)); + } + + function schedulePublishControl(control: FormControl): void { + window.clearTimeout(fieldPublishTimer); + fieldPublishTimer = window.setTimeout(() => publishControl(control), 80); + } + + function applyField(key: string): void { + const control = findControl(key); + const value = fields.get(key); + if (!control || typeof value !== "string") return; + if (controlValue(control) === value) return; + isApplyingRemoteFields = true; + try { + setNativeValue(control, value); + } finally { + window.setTimeout(() => { + isApplyingRemoteFields = false; + }, 30); + } + } + + function applyAllRemoteFields(): void { + fields.forEach((_value, key) => applyField(key)); + } + + function currentUpstreamUrl(): string { + const fromQuery = params.get("target"); + if (fromQuery) return fromQuery; + return root.dataset.lowcoderUpstreamUrl || ""; + } + + function buildProxiedUrlForTarget(targetUrl: string): string { + const next = new URL(window.location.href); + next.searchParams.set("target", targetUrl); + return `${next.pathname}?${next.searchParams.toString()}`; + } + + function readScroll(): ScrollState { + const maxX = Math.max(0, document.documentElement.scrollWidth - window.innerWidth); + const maxY = Math.max(0, document.documentElement.scrollHeight - window.innerHeight); + return { + xRatio: maxX > 0 ? window.scrollX / maxX : 0, + yRatio: maxY > 0 ? window.scrollY / maxY : 0, + }; + } + + function applyScroll(scroll: ScrollState): void { + const maxX = Math.max(0, document.documentElement.scrollWidth - window.innerWidth); + const maxY = Math.max(0, document.documentElement.scrollHeight - window.innerHeight); + isApplyingRemote = true; + try { + window.scrollTo({ + left: clamp(scroll.xRatio, 0, 1) * maxX, + top: clamp(scroll.yRatio, 0, 1) * maxY, + behavior: "auto", + }); + } finally { + window.setTimeout(() => { + isApplyingRemote = false; + }, 50); + } + } + + /** Presenter-only: publish navigation for attendees to accept/decline. */ + function publishUrl(url: string): void { + if (!isPresenter || isApplyingRemote || !provider.isSynced) return; + const navSeq = Number(state.get("navSeq") || "0") + 1; + doc.transact(() => { + state.set("url", url); + state.set("navSeq", String(navSeq)); + state.set( + "navOffer", + JSON.stringify({ + url, + navSeq, + editorId, + username, + } satisfies NavOffer) + ); + }); + log("publish url", url, navSeq); + } + + /** Presenter-only: followers apply this scroll. */ + function publishScroll(): void { + if (!isPresenter || isApplyingRemote || !provider.isSynced) return; + const scroll = readScroll(); + const encoded = JSON.stringify(scroll); + if (encoded === lastPublishedScroll) return; + lastPublishedScroll = encoded; + state.set("scroll", encoded); + } + + function publishClick(event: MouseEvent): void { + if (!provider.isSynced) return; + const xRatio = window.innerWidth > 0 ? event.clientX / window.innerWidth : 0; + const yRatio = window.innerHeight > 0 ? event.clientY / window.innerHeight : 0; + const button = findButtonTarget(event.target); + const color = getUserColor(editorId); + + let click: ClickState = { + xRatio, + yRatio, + ts: Date.now(), + editorId, + username, + kind: "generic", + label: "", + }; + + if (button) { + const rect = button.getBoundingClientRect(); + const label = + (button.getAttribute("aria-label") || + button.textContent || + (button instanceof HTMLInputElement ? button.value : "") || + "Button") + .trim() + .replace(/\s+/g, " ") + .slice(0, 80); + click = { + ...click, + kind: "button", + label, + rect: { + leftRatio: rect.left / (window.innerWidth || 1), + topRatio: rect.top / (window.innerHeight || 1), + wRatio: rect.width / (window.innerWidth || 1), + hRatio: rect.height / (window.innerHeight || 1), + }, + }; + pointer.showButtonClickFlash( + { left: rect.left, top: rect.top, width: rect.width, height: rect.height }, + color, + `${username}: ${label}` + ); + } else { + pointer.showClickRipple(xRatio, yRatio, color); + } + + state.set("click", JSON.stringify(click)); + } + + function hideFollowPrompt(): void { + followPromptEl?.remove(); + followPromptEl = null; + } + + function showFollowPrompt(offer: NavOffer): void { + if (isPresenter) return; + if (offer.navSeq <= dismissedNavSeq) return; + if (offer.url === currentUpstreamUrl()) return; + + hideFollowPrompt(); + + const panel = document.createElement("div"); + panel.id = "lowcoder-website-follow-prompt"; + Object.assign(panel.style, { + position: "fixed", + left: "50%", + top: "56px", + transform: "translateX(-50%)", + zIndex: "2147483647", + maxWidth: "min(440px, calc(100vw - 24px))", + padding: "14px 16px", + borderRadius: "10px", + background: "rgba(20, 24, 28, 0.94)", + color: "#fff", + font: "13px/1.4 system-ui,sans-serif", + boxShadow: "0 8px 28px rgba(0,0,0,.35)", + pointerEvents: "auto", + }); + + const title = document.createElement("div"); + title.style.fontWeight = "600"; + title.style.marginBottom = "6px"; + title.textContent = `${offer.username || "Presenter"} opened a new page`; + + const urlLine = document.createElement("div"); + urlLine.style.opacity = "0.85"; + urlLine.style.fontSize = "12px"; + urlLine.style.wordBreak = "break-all"; + urlLine.style.marginBottom = "12px"; + urlLine.textContent = offer.url; + + const actions = document.createElement("div"); + Object.assign(actions.style, { + display: "flex", + gap: "8px", + justifyContent: "flex-end", + }); + + const stayBtn = document.createElement("button"); + stayBtn.type = "button"; + stayBtn.textContent = "Stay"; + Object.assign(stayBtn.style, { + padding: "6px 12px", + borderRadius: "6px", + border: "1px solid rgba(255,255,255,.25)", + background: "transparent", + color: "#fff", + cursor: "pointer", + }); + stayBtn.addEventListener("click", () => { + dismissedNavSeq = offer.navSeq; + hideFollowPrompt(); + log("dismissed follow", offer.url); + }); + + const followBtn = document.createElement("button"); + followBtn.type = "button"; + followBtn.textContent = "Follow"; + Object.assign(followBtn.style, { + padding: "6px 12px", + borderRadius: "6px", + border: "none", + background: "#1E88E5", + color: "#fff", + cursor: "pointer", + fontWeight: "600", + }); + followBtn.addEventListener("click", () => { + dismissedNavSeq = offer.navSeq; + hideFollowPrompt(); + isApplyingRemote = true; + window.location.assign(buildProxiedUrlForTarget(offer.url)); + }); + + actions.appendChild(stayBtn); + actions.appendChild(followBtn); + panel.appendChild(title); + panel.appendChild(urlLine); + panel.appendChild(actions); + document.documentElement.appendChild(panel); + followPromptEl = panel; + } + + function applyRemoteClick(click: ClickState): void { + const color = getUserColor(click.editorId); + if (click.kind === "button" && click.rect) { + pointer.showButtonClickFlash( + { + left: click.rect.leftRatio * window.innerWidth, + top: click.rect.topRatio * window.innerHeight, + width: click.rect.wRatio * window.innerWidth, + height: click.rect.hRatio * window.innerHeight, + }, + color, + `${click.username || "User"}: ${click.label || "Button"}` + ); + return; + } + pointer.showClickRipple(click.xRatio, click.yRatio, color); + } + + function applyRemoteState(): void { + const navSeq = Number(state.get("navSeq") || "0"); + const offerRaw = state.get("navOffer"); + if (offerRaw && navSeq > lastAppliedNavSeq) { + lastAppliedNavSeq = navSeq; + try { + const offer = JSON.parse(offerRaw) as NavOffer; + if (!isPresenter && offer.url && offer.url !== currentUpstreamUrl()) { + showFollowPrompt(offer); + } + } catch { + // ignore malformed nav offer + } + } + + // Presenter scroll → attendees follow (approximate by viewport ratio) + if (!isPresenter) { + const scrollRaw = state.get("scroll"); + if (scrollRaw) { + try { + const scroll = JSON.parse(scrollRaw) as ScrollState; + const local = readScroll(); + if ( + Math.abs(local.xRatio - scroll.xRatio) > 0.01 || + Math.abs(local.yRatio - scroll.yRatio) > 0.01 + ) { + applyScroll(scroll); + lastPublishedScroll = scrollRaw; + } + } catch { + // ignore malformed scroll + } + } + } + + const clickRaw = state.get("click"); + if (clickRaw) { + try { + const click = JSON.parse(clickRaw) as ClickState; + if (click.ts > lastAppliedClickTs && click.editorId !== editorId) { + lastAppliedClickTs = click.ts; + applyRemoteClick(click); + } + } catch { + // ignore malformed click + } + } + } + + // --- Form field sync (mutual): text / textarea / select / checkbox / radio --- + document.addEventListener( + "input", + (event) => { + if (!event.isTrusted || isApplyingRemoteFields) return; + const target = event.target; + if ( + !(target instanceof HTMLInputElement) && + !(target instanceof HTMLTextAreaElement) && + !(target instanceof HTMLSelectElement) + ) { + return; + } + schedulePublishControl(target); + }, + true + ); + + document.addEventListener( + "change", + (event) => { + if (!event.isTrusted || isApplyingRemoteFields) return; + const target = event.target; + if ( + !(target instanceof HTMLInputElement) && + !(target instanceof HTMLTextAreaElement) && + !(target instanceof HTMLSelectElement) + ) { + return; + } + publishControl(target); + }, + true + ); + + fields.observe((event) => { + if (event.transaction.origin === peerId) return; + event.keysChanged.forEach((key) => applyField(key)); + }); + + // --- Clicks: button highlight for all; navigation publish for presenter --- + document.addEventListener( + "click", + (event) => { + if (!event.isTrusted) return; + + // Checkbox / radio clicks are handled via change; still show ripple for other clicks + const formControl = event.target; + const isFormControl = + formControl instanceof HTMLInputElement || + formControl instanceof HTMLTextAreaElement || + formControl instanceof HTMLSelectElement; + if (!isFormControl) { + publishClick(event); + } else if ( + formControl instanceof HTMLInputElement && + ["checkbox", "radio"].includes(formControl.type) + ) { + // ensure value publishes even if change is delayed + window.setTimeout(() => publishControl(formControl), 0); + } + + const anchor = (event.target as Element | null)?.closest?.("a[href]"); + if (!(anchor instanceof HTMLAnchorElement)) return; + + const href = anchor.getAttribute("href"); + if (!href || href.startsWith("#") || href.startsWith("javascript:")) return; + + try { + const resolved = new URL(anchor.href, window.location.href); + if (resolved.pathname.includes("/proxy/website")) { + const target = resolved.searchParams.get("target"); + if (target && isPresenter) { + publishUrl(target); + } + return; + } + if (resolved.protocol === "http:" || resolved.protocol === "https:") { + event.preventDefault(); + if (isPresenter) { + publishUrl(resolved.toString()); + } + window.location.assign(buildProxiedUrlForTarget(resolved.toString())); + } + } catch { + // ignore invalid href + } + }, + true + ); + + const originalPushState = history.pushState.bind(history); + const originalReplaceState = history.replaceState.bind(history); + + function onHistoryChange(): void { + if (!isPresenter) return; + const url = currentUpstreamUrl(); + if (url) publishUrl(url); + } + + history.pushState = function (...args) { + const result = originalPushState(...args); + onHistoryChange(); + return result; + }; + history.replaceState = function (...args) { + const result = originalReplaceState(...args); + onHistoryChange(); + return result; + }; + window.addEventListener("popstate", onHistoryChange); + window.addEventListener("hashchange", onHistoryChange); + + // --- Scroll: presenter publishes; attendees apply --- + window.addEventListener( + "scroll", + () => { + if (isApplyingRemote || !isPresenter) return; + window.clearTimeout(scrollPublishTimer); + scrollPublishTimer = window.setTimeout(() => publishScroll(), 80); + }, + { passive: true } + ); + + state.observe(() => applyRemoteState()); + + provider.on("synced", () => { + log("synced", documentName); + const localUrl = currentUpstreamUrl(); + if (isPresenter) { + if (localUrl) { + publishUrl(localUrl); + publishScroll(); + } + } else { + applyRemoteState(); + } + applyAllRemoteFields(); + }); + + log("ready", { + documentName, + editorId, + username, + isPresenter, + upstream: currentUpstreamUrl(), + }); +})(); + +function findButtonTarget(target: EventTarget | null): Element | null { + if (!(target instanceof Element)) return null; + return target.closest( + "button, [role='button'], input[type='button'], input[type='submit'], input[type='reset']" + ); +} + +function clamp(value: number, min: number, max: number): number { + return Math.min(max, Math.max(min, value)); +} diff --git a/server/proxy-service/src/googleFormUrls.ts b/server/proxy-service/src/googleFormUrls.ts new file mode 100644 index 0000000000..514b86747a --- /dev/null +++ b/server/proxy-service/src/googleFormUrls.ts @@ -0,0 +1,136 @@ +import fetch from "node-fetch"; + +const PUBLISHED_VIEWFORM_RE = /\/forms\/d\/e\/[^/]+\/viewform/i; +const DRIVE_FORM_ID_RE = /\/forms\/d\/([^/]+)\/(edit|viewform|preview)/i; + +export function isPublishedViewformUrl(url: URL): boolean { + return PUBLISHED_VIEWFORM_RE.test(url.pathname); +} + +export function cleanViewformUrl(rawUrl: string): string { + const url = new URL(rawUrl.trim()); + if (!url.pathname.startsWith("/forms/")) { + throw new Error("googleFormUrl must point to a Google Form"); + } + if (!url.pathname.includes("/viewform")) { + throw new Error( + "googleFormUrl must be a published responder URL ending with /viewform. " + + "Do not use webViewLink or /edit URLs from Google Drive." + ); + } + url.pathname = url.pathname.replace(/\/viewform.*/i, "/viewform"); + url.search = ""; + url.hash = ""; + return url.toString(); +} + +function extractViewformFromHtml(html: string): string | null { + const patterns = [ + /https:\/\/docs\.google\.com\/forms\/d\/e\/[^"'\\\s]+\/viewform/gi, + /"publishedFormUrl":"(https:\\\/\\\/docs\.google\.com\\\/forms\\\/d\\\/e\\\/[^"\\]+\\\/viewform)"/i, + /"responderUri":"(https:\\\/\\\/docs\.google\.com\\\/forms\\\/d\\\/e\\\/[^"\\]+\\\/viewform)"/i, + ]; + + for (const pattern of patterns) { + const match = html.match(pattern); + if (!match?.[0]) continue; + const candidate = match[0] + .replace(/^"publishedFormUrl":"|"responderUri":"/, "") + .replace(/"$/, "") + .replace(/\\\//g, "/"); + try { + return cleanViewformUrl(candidate); + } catch { + continue; + } + } + return null; +} + +async function followToPublishedViewform(startUrl: string): Promise { + let current = startUrl; + + for (let step = 0; step < 12; step += 1) { + const response = await fetch(current, { + method: "GET", + redirect: "manual", + headers: { + "user-agent": + "Mozilla/5.0 (compatible; LowcoderGoogleFormsProxy/1.0; +https://lowcoder.cloud)", + }, + }); + + if (response.status >= 300 && response.status < 400) { + const location = response.headers.get("location"); + if (!location) return null; + current = new URL(location, current).toString(); + const parsed = new URL(current); + if (isPublishedViewformUrl(parsed)) { + return cleanViewformUrl(current); + } + continue; + } + + if (response.status === 200) { + const html = await response.text(); + const scraped = extractViewformFromHtml(html); + if (scraped) return scraped; + + const parsed = new URL(current); + if (isPublishedViewformUrl(parsed)) { + return cleanViewformUrl(current); + } + } + + return null; + } + + return null; +} + +/** + * Normalize Drive/webViewLink or draft form URLs to a published responder URL. + * Collaboration only works on public /viewform pages that stay inside the proxy. + */ +export async function resolveGoogleFormResponderUrl(rawUrl?: string): Promise { + const value = (rawUrl ?? "").trim(); + if (!value) throw new Error("googleFormUrl is required"); + + let url: URL; + try { + url = new URL(value); + } catch { + throw new Error("googleFormUrl must be a valid URL"); + } + + if (url.protocol !== "https:" || url.hostname !== "docs.google.com") { + throw new Error("googleFormUrl must be an https://docs.google.com/forms URL"); + } + if (!url.pathname.startsWith("/forms/")) { + throw new Error("googleFormUrl must point to a Google Form"); + } + + if (isPublishedViewformUrl(url)) { + return cleanViewformUrl(url.toString()); + } + + const driveMatch = url.pathname.match(DRIVE_FORM_ID_RE); + if (driveMatch?.[1] && driveMatch[1] !== "e") { + const formId = driveMatch[1]; + const candidates = [ + `https://docs.google.com/forms/d/${formId}/viewform`, + `https://docs.google.com/forms/d/${formId}/preview`, + ]; + + for (const candidate of candidates) { + const resolved = await followToPublishedViewform(candidate); + if (resolved) return resolved; + } + } + + throw new Error( + "Could not resolve a published Google Form responder URL. " + + "Open the form in Google Forms, click Send, copy the public link " + + "(https://docs.google.com/forms/d/e/.../viewform), and use that instead of webViewLink." + ); +} diff --git a/server/proxy-service/src/googleFormsApi.ts b/server/proxy-service/src/googleFormsApi.ts new file mode 100644 index 0000000000..2f2ec5a0db --- /dev/null +++ b/server/proxy-service/src/googleFormsApi.ts @@ -0,0 +1,219 @@ +import fetch from "node-fetch"; + +const FORMS_API = "https://forms.googleapis.com/v1/forms"; + +export interface GoogleFormChoiceOption { + value: string; +} + +export interface GoogleFormQuestion { + questionId: string; + required?: boolean; + textQuestion?: Record; + choiceQuestion?: { + type?: string; + options?: GoogleFormChoiceOption[]; + }; + scaleQuestion?: { + low?: number; + high?: number; + }; + dateQuestion?: Record; + timeQuestion?: Record; +} + +export interface GoogleFormItem { + itemId: string; + title?: string; + description?: string; + questionItem?: { + question?: GoogleFormQuestion; + }; +} + +export interface GoogleFormResource { + formId: string; + info?: { + title?: string; + documentTitle?: string; + description?: string; + }; + items?: GoogleFormItem[]; + responderUri?: string; +} + +export function extractGoogleFormId(rawUrl?: string): string { + const value = (rawUrl ?? "").trim(); + if (!value) throw new Error("googleFormUrl is required"); + + let url: URL; + try { + url = new URL(value); + } catch { + // Treat bare form IDs as valid. + if (/^[a-zA-Z0-9_-]{10,}$/.test(value)) return value; + throw new Error("googleFormUrl must be a valid URL or form id"); + } + + if (url.hostname !== "docs.google.com" || !url.pathname.startsWith("/forms/")) { + throw new Error("googleFormUrl must be an https://docs.google.com/forms URL"); + } + + const published = url.pathname.match(/\/forms\/d\/e\/([^/]+)/i); + if (published?.[1]) { + throw new Error( + "Published responder IDs (forms/d/e/...) cannot be used with Forms API. " + + "Pass the Drive/edit form URL or form id from Google Drive (forms/d/FORM_ID/edit)." + ); + } + + const drive = url.pathname.match(/\/forms\/d\/([^/]+)/i); + if (drive?.[1] && drive[1] !== "e") return drive[1]; + + throw new Error("Could not extract Google Form id from googleFormUrl"); +} + +export async function fetchGoogleForm( + formId: string, + accessToken: string +): Promise { + const response = await fetch(`${FORMS_API}/${encodeURIComponent(formId)}`, { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json", + }, + }); + + const text = await response.text(); + if (!response.ok) { + let detail = text; + try { + const parsed = JSON.parse(text) as { error?: { message?: string } }; + detail = parsed.error?.message || text; + } catch { + // keep raw text + } + throw new Error( + `Google Forms API error (${response.status}): ${detail}. ` + + "Ensure the OAuth token includes the forms.googleapis.com scope " + + "(https://www.googleapis.com/auth/forms.body or forms.body.readonly)." + ); + } + + return JSON.parse(text) as GoogleFormResource; +} + +export interface CollabQuestionView { + itemId: string; + questionId: string; + title: string; + description: string; + required: boolean; + type: "text" | "paragraph" | "radio" | "checkbox" | "scale" | "date" | "time" | "unknown"; + options: string[]; + scaleLow?: number; + scaleHigh?: number; +} + +export interface CollabFormView { + formId: string; + title: string; + description: string; + responderUri: string; + questions: CollabQuestionView[]; +} + +export function toCollabFormView(form: GoogleFormResource): CollabFormView { + const questions: CollabQuestionView[] = []; + + for (const item of form.items ?? []) { + const question = item.questionItem?.question; + if (!question?.questionId) continue; + + let type: CollabQuestionView["type"] = "unknown"; + let options: string[] = []; + let scaleLow: number | undefined; + let scaleHigh: number | undefined; + + if (question.textQuestion) { + type = "paragraph" in question.textQuestion && question.textQuestion.paragraph ? "paragraph" : "text"; + // Forms API uses textQuestion.paragraph boolean + if ((question.textQuestion as { paragraph?: boolean }).paragraph) type = "paragraph"; + else type = "text"; + } else if (question.choiceQuestion) { + const choiceType = (question.choiceQuestion.type || "").toUpperCase(); + type = choiceType === "CHECKBOX" ? "checkbox" : "radio"; + options = (question.choiceQuestion.options ?? []).map((option) => option.value).filter(Boolean); + } else if (question.scaleQuestion) { + type = "scale"; + scaleLow = question.scaleQuestion.low ?? 1; + scaleHigh = question.scaleQuestion.high ?? 5; + } else if (question.dateQuestion) { + type = "date"; + } else if (question.timeQuestion) { + type = "time"; + } + + questions.push({ + itemId: item.itemId, + questionId: question.questionId, + title: item.title || "Untitled question", + description: item.description || "", + required: Boolean(question.required), + type, + options, + scaleLow, + scaleHigh, + }); + } + + return { + formId: form.formId, + title: form.info?.title || form.info?.documentTitle || "Untitled form", + description: form.info?.description || "", + responderUri: form.responderUri || "", + questions, + }; +} + +export async function updateGoogleFormInfo( + formId: string, + accessToken: string, + info: { title?: string; description?: string } +): Promise { + const update: Record = {}; + const mask: string[] = []; + if (typeof info.title === "string") { + update.title = info.title; + mask.push("title"); + } + if (typeof info.description === "string") { + update.description = info.description; + mask.push("description"); + } + if (mask.length === 0) return; + + const response = await fetch(`${FORMS_API}/${encodeURIComponent(formId)}:batchUpdate`, { + method: "POST", + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify({ + requests: [ + { + updateFormInfo: { + info: update, + updateMask: mask.join(","), + }, + }, + ], + }), + }); + + if (!response.ok) { + const text = await response.text(); + throw new Error(`Failed to update Google Form (${response.status}): ${text}`); + } +} diff --git a/server/proxy-service/src/googleProxy.ts b/server/proxy-service/src/googleProxy.ts new file mode 100644 index 0000000000..3f62e8463e --- /dev/null +++ b/server/proxy-service/src/googleProxy.ts @@ -0,0 +1,285 @@ +import express, { Express, Request, Response } from "express"; +import fetch, { HeadersInit, Response as FetchResponse } from "node-fetch"; +import fs from "node:fs"; +import path from "node:path"; +import { URL } from "node:url"; +import { getBearerToken, verifyProxyToken } from "./auth"; +import { + GOOGLE_FORMS_PROXY_PREFIX, + buildGoogleFormsProxiedUrlFromRequest, +} from "./googleUrls"; +import { + createGoogleFormsProxySession, + joinGoogleFormsProxySession, +} from "./googleSession"; + +const BRIDGE_PATH = "/proxy/google-forms-bridge.js"; +const HOCUSPOCUS_URL = (process.env.LOWCODER_HOCUSPOCUS_URL ?? "ws://localhost:3006").trim(); + +function resolveHocuspocusUrl(req: Request): string { + if (!/localhost|127\.0\.0\.1/.test(HOCUSPOCUS_URL)) { + return HOCUSPOCUS_URL; + } + const forwardedHost = (req.get("x-forwarded-host") || req.get("host") || "localhost") + .split(",")[0] + .trim(); + const hostname = forwardedHost.split(":")[0] || "localhost"; + const port = new URL(HOCUSPOCUS_URL.replace(/^ws/, "http")).port || "3006"; + return `ws://${hostname}:${port}`; +} +const HOCUSPOCUS_SECRET = ( + process.env.LOWCODER_HOCUSPOCUS_SECRET ?? process.env.HOCUSPOCUS_SECRET ?? "" +).trim(); +const RATE_LIMIT_PER_MINUTE = Number(process.env.LOWCODER_PROXY_RATE_LIMIT ?? 120); +const ALLOWED_GOOGLE_FORMS_HOSTS = new Set( + (process.env.LOWCODER_GOOGLE_FORMS_ALLOWED_HOSTS ?? "docs.google.com") + .split(",") + .map((value) => value.trim().toLowerCase()) + .filter(Boolean) +); +const requestBuckets = new Map(); + +export function registerGoogleFormsProxy(app: Express): void { + app.get(BRIDGE_PATH, (_req, res) => { + const bridgePath = path.join(__dirname, "bridge", "google-forms-bridge.js"); + if (!fs.existsSync(bridgePath)) { + res.status(404).type("text/plain").send("Google Forms bridge script not found"); + return; + } + res.setHeader("Content-Type", "application/javascript; charset=utf-8"); + res.setHeader("Cache-Control", "no-cache"); + res.send(fs.readFileSync(bridgePath, "utf8")); + }); + + const router = express.Router(); + router.post("/session", express.json({ limit: "1mb" }), createGoogleFormsProxySession); + router.post("/session/join", express.json({ limit: "1mb" }), joinGoogleFormsProxySession); + + router.use(express.raw({ type: "*/*", limit: "25mb" }), async (req, res) => { + if (req.path === "/session" || req.path === "/session/join") { + res.status(405).json({ + message: + "Use POST on /proxy/google-forms/session or /proxy/google-forms/session/join", + }); + return; + } + + try { + if (!isAuthorized(req)) { + res.status(401).json({ message: "Missing or invalid proxy token" }); + return; + } + if (!checkRateLimit(req)) { + res.status(429).json({ message: "Proxy rate limit exceeded" }); + return; + } + + const upstreamUrl = resolveUpstreamUrl(req); + const upstreamResponse = await fetch(upstreamUrl.toString(), { + method: req.method, + redirect: "manual", + headers: buildForwardHeaders(req, upstreamUrl), + body: hasBody(req.method) ? req.body : undefined, + }); + + relayHeaders(upstreamResponse, res, req); + const contentType = upstreamResponse.headers.get("content-type") ?? ""; + + if (contentType.includes("text/html")) { + const html = await upstreamResponse.text(); + res + .status(upstreamResponse.status) + .send(injectBridgeAndRewriteHtml(html, req, upstreamUrl)); + return; + } + + if (/javascript|json|text\/css/.test(contentType)) { + const text = await upstreamResponse.text(); + res.status(upstreamResponse.status).send(rewriteBodyUrls(text, req, upstreamUrl)); + return; + } + + res.status(upstreamResponse.status).send(await upstreamResponse.buffer()); + } catch (error) { + console.error("Google Forms proxy request failed", error); + res.status(502).json({ message: "Google Forms proxy request failed" }); + } + }); + + app.use(GOOGLE_FORMS_PROXY_PREFIX, router); +} + +function hasBody(method: string): boolean { + return ["POST", "PUT", "PATCH", "DELETE"].includes(method.toUpperCase()); +} + +function resolveUpstreamUrl(req: Request): URL { + const targetParam = (req.query.target as string | undefined)?.trim(); + if (!targetParam) { + throw new Error("A Google Forms target URL is required"); + } + const upstream = new URL(targetParam); + assertAllowedGoogleFormsUrl(upstream); + return upstream; +} + +function assertAllowedGoogleFormsUrl(url: URL): void { + if ( + url.protocol !== "https:" || + !ALLOWED_GOOGLE_FORMS_HOSTS.has(url.hostname.toLowerCase()) || + !url.pathname.startsWith("/forms/") + ) { + throw new Error(`Google Forms URL is not allowed: ${url.toString()}`); + } +} + +function buildForwardHeaders(req: Request, upstreamUrl: URL): HeadersInit { + const normalized = new Map(); + Object.entries(req.headers).forEach(([key, value]) => { + if (typeof value !== "string") return; + const lower = key.toLowerCase(); + if ( + ["host", "content-length", "x-forwarded-host", "x-forwarded-proto", "connection"].includes( + lower + ) + ) { + return; + } + normalized.set(lower, value); + }); + normalized.set("host", upstreamUrl.host); + normalized.set("origin", upstreamUrl.origin); + normalized.set("referer", upstreamUrl.toString()); + return Object.fromEntries(normalized.entries()); +} + +function relayHeaders(upstreamResponse: FetchResponse, res: Response, req: Request): void { + const skipHeaders = new Set([ + "x-frame-options", + "content-security-policy", + "transfer-encoding", + "content-length", + "content-encoding", + ]); + + upstreamResponse.headers.forEach((value, key) => { + const lower = key.toLowerCase(); + if (skipHeaders.has(lower)) return; + if (lower === "set-cookie") { + const cookies = rewriteSetCookie(value); + if (cookies.length > 0) res.setHeader("Set-Cookie", cookies); + return; + } + if (lower === "location") { + res.setHeader(key, rewriteAbsoluteUrl(value, req)); + return; + } + res.setHeader(key, value); + }); + res.setHeader("Content-Security-Policy", "frame-ancestors 'self';"); +} + +function rewriteSetCookie(rawValue: string): string[] { + return rawValue + .split(/,(?=[^;]+=[^;]+)/) + .map((cookie) => + cookie + .replace(/;\s*Domain=[^;]+/gi, "") + .replace(/;\s*SameSite=None/gi, "; SameSite=Lax") + .replace(/;\s*Path=[^;]+/gi, `; Path=${GOOGLE_FORMS_PROXY_PREFIX}`) + ); +} + +function injectBridgeAndRewriteHtml(html: string, req: Request, upstreamUrl: URL): string { + const rewritten = rewriteBodyUrls(html, req, upstreamUrl); + const hocuspocusUrl = resolveHocuspocusUrl(req); + const collab = String(req.query.collab ?? "").trim(); + const attrs = + ` data-lowcoder-room-id="${escapeHtml(String(req.query.roomId ?? ""))}"` + + ` data-lowcoder-role="${escapeHtml(String(req.query.role ?? "driver"))}"` + + ` data-lowcoder-editor-id="${escapeHtml(String(req.query.editorId ?? "local"))}"` + + ` data-lowcoder-collab-id="${escapeHtml(collab)}"` + + ` data-lowcoder-username="${escapeHtml(String(req.query.username ?? ""))}"` + + ` data-lowcoder-hocuspocus-url="${escapeHtml(hocuspocusUrl)}"` + + (HOCUSPOCUS_SECRET + ? ` data-lowcoder-hocuspocus-token="${escapeHtml(HOCUSPOCUS_SECRET)}"` + : ""); + const withRootAttrs = rewritten.replace(/)/i, `window.__LOWCODER_HOCUSPOCUS__=${hocuspocusConfig};` + + ``; + + return /<\/head>/i.test(withRootAttrs) + ? withRootAttrs.replace(/<\/head>/i, `${bridgeTag}`) + : `${bridgeTag}${withRootAttrs}`; +} + +function rewriteBodyUrls(body: string, req: Request, upstreamUrl: URL): string { + const origin = upstreamUrl.origin; + let output = body.replace( + new RegExp(`${escapeRegex(origin)}([^"'\\\\\\s<]*)`, "g"), + (_match, suffix: string) => + buildGoogleFormsProxiedUrlFromRequest(`${origin}${suffix ?? ""}`, req) + ); + + output = output.replace( + /(href|src|action)=["'](\/forms\/[^"']*)["']/gi, + (_match, attr: string, route: string) => + `${attr}="${buildGoogleFormsProxiedUrlFromRequest(`${origin}${route}`, req)}"` + ); + output = output.replace( + /url\(["']?(\/forms\/[^"')]+)["']?\)/gi, + (_match, route: string) => + `url("${buildGoogleFormsProxiedUrlFromRequest(`${origin}${route}`, req)}")` + ); + output = output.replace( + /(["'])(\/forms\/[^"'\\\s]*)\1/g, + (_match, quote: string, route: string) => + `${quote}${buildGoogleFormsProxiedUrlFromRequest(`${origin}${route}`, req)}${quote}` + ); + return output; +} + +function rewriteAbsoluteUrl(value: string, req: Request): string { + try { + const parsed = new URL(value, "https://docs.google.com"); + assertAllowedGoogleFormsUrl(parsed); + return buildGoogleFormsProxiedUrlFromRequest(parsed.toString(), req); + } catch { + return value; + } +} + +function escapeRegex(text: string): string { + return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + +function escapeHtml(value: string): string { + return value + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + +function isAuthorized(req: Request): boolean { + const token = (req.query.token as string | undefined) || getBearerToken(req.headers.authorization); + return verifyProxyToken(token, "google-forms-proxy"); +} + +function checkRateLimit(req: Request): boolean { + const key = req.ip || "unknown"; + const now = Date.now(); + const existing = requestBuckets.get(key); + if (!existing || now > existing.resetAt) { + requestBuckets.set(key, { count: 1, resetAt: now + 60_000 }); + return true; + } + existing.count += 1; + return existing.count <= RATE_LIMIT_PER_MINUTE; +} diff --git a/server/proxy-service/src/googleSession.ts b/server/proxy-service/src/googleSession.ts new file mode 100644 index 0000000000..b2a5a836c7 --- /dev/null +++ b/server/proxy-service/src/googleSession.ts @@ -0,0 +1,133 @@ +import { Request, Response } from "express"; +import { createProxyToken, resolveParticipantId } from "./auth"; +import { resolveGoogleFormResponderUrl } from "./googleFormUrls"; +import { buildGoogleFormsProxiedUrl } from "./googleUrls"; + +interface GoogleFormsSessionRequestBody { + googleFormUrl?: string; + roomId?: string; + role?: string; + editorId?: string; + guestId?: string; + collab?: string; + username?: string; +} + +export interface GoogleFormsSessionData { + token: string; + proxiedUrl: string; + roomId: string; + role: string; + editorId: string; + participantId: string; + googleFormUrl: string; + collab: string; + username: string; + broadcast: { + roomId: string; + collab: string; + googleFormUrl: string; + editorId: string; + username: string; + }; +} + +function normalizeRole( + role?: string, + fallback: "driver" | "follower" = "driver" +): "driver" | "follower" { + return (role ?? fallback).trim().toLowerCase() === "follower" ? "follower" : "driver"; +} + +function normalizeCollab(rawCollab: string | undefined, role: "driver" | "follower"): string { + const collab = (rawCollab ?? "").trim(); + if (collab) return collab; + if (role === "driver") { + return String(Date.now()); + } + throw new Error( + "collab is required for followers. Pass broadcast.collab from the driver's session response." + ); +} + +async function mintGoogleFormsSession( + req: Request, + body: GoogleFormsSessionRequestBody, + role: "driver" | "follower" +): Promise { + const googleFormUrl = await resolveGoogleFormResponderUrl(body.googleFormUrl); + const roomId = (body.roomId ?? "").trim(); + const collab = normalizeCollab(body.collab, role); + const username = (body.username ?? "").trim(); + + if (!roomId) throw new Error("roomId is required"); + + const participantId = await resolveParticipantId(req, { + editorId: body.editorId?.trim() || undefined, + guestId: body.guestId?.trim() || undefined, + roomId, + role, + }); + const token = createProxyToken(participantId, roomId, role, "google-forms-proxy"); + const proxiedUrl = buildGoogleFormsProxiedUrl(googleFormUrl, { + roomId, + role, + editorId: participantId, + token, + collab, + username, + }); + + return { + token, + proxiedUrl, + roomId, + role, + editorId: participantId, + participantId, + googleFormUrl, + collab, + username, + broadcast: { roomId, collab, googleFormUrl, editorId: participantId, username }, + }; +} + +function sendSessionResponse(res: Response, data: GoogleFormsSessionData): void { + res.status(200).json({ code: 1, message: "", data }); +} + +function sendSessionError(res: Response, error: unknown): void { + console.error("Google Forms proxy session error", error); + const message = error instanceof Error ? error.message : "Unauthorized"; + const status = + message.includes("required") || + message.includes("must be") || + message.includes("Invalid URL") || + message.includes("Could not resolve") || + message.includes("webViewLink") || + message.includes("collab") + ? 400 + : 401; + res.status(status).json({ code: status, message }); +} + +export async function createGoogleFormsProxySession(req: Request, res: Response): Promise { + try { + const body = (req.body ?? {}) as GoogleFormsSessionRequestBody; + sendSessionResponse(res, await mintGoogleFormsSession(req, body, normalizeRole(body.role))); + } catch (error) { + sendSessionError(res, error); + } +} + +export async function joinGoogleFormsProxySession(req: Request, res: Response): Promise { + try { + const body = (req.body ?? {}) as GoogleFormsSessionRequestBody; + sendSessionResponse( + res, + await mintGoogleFormsSession(req, body, normalizeRole(body.role, "follower")) + ); + } catch (error) { + sendSessionError(res, error); + } +} diff --git a/server/proxy-service/src/googleUrls.ts b/server/proxy-service/src/googleUrls.ts new file mode 100644 index 0000000000..f8a771c16e --- /dev/null +++ b/server/proxy-service/src/googleUrls.ts @@ -0,0 +1,41 @@ +import { Request } from "express"; + +export const GOOGLE_FORMS_PROXY_PREFIX = "/proxy/google-forms"; + +export interface GoogleFormsProxyOptions { + roomId?: string; + role?: string; + editorId?: string; + token?: string; + collab?: string; + username?: string; +} + +export function buildGoogleFormsProxiedUrl( + targetUrl: string, + options: GoogleFormsProxyOptions +): string { + const params = new URLSearchParams(); + params.set("target", targetUrl); + if (options.roomId) params.set("roomId", options.roomId); + if (options.role) params.set("role", options.role); + if (options.editorId) params.set("editorId", options.editorId); + if (options.token) params.set("token", options.token); + if (options.collab) params.set("collab", options.collab); + if (options.username) params.set("username", options.username); + return `${GOOGLE_FORMS_PROXY_PREFIX}?${params.toString()}`; +} + +export function buildGoogleFormsProxiedUrlFromRequest( + targetUrl: string, + req: Request +): string { + return buildGoogleFormsProxiedUrl(targetUrl, { + roomId: String(req.query.roomId ?? ""), + role: String(req.query.role ?? "driver"), + editorId: String(req.query.editorId ?? ""), + token: String(req.query.token ?? ""), + collab: String(req.query.collab ?? ""), + username: String(req.query.username ?? ""), + }); +} diff --git a/server/proxy-service/src/server.ts b/server/proxy-service/src/server.ts new file mode 100644 index 0000000000..0d95616e71 --- /dev/null +++ b/server/proxy-service/src/server.ts @@ -0,0 +1,274 @@ +import express, { Request, Response } from "express"; +import cors from "cors"; +import fetch, { HeadersInit, Response as FetchResponse } from "node-fetch"; +import fs from "node:fs"; +import path from "node:path"; +import { URL } from "node:url"; +import { createServer } from "node:http"; +import { createProxySession, joinProxySession } from "./session"; +import { getBearerToken, verifyProxyToken } from "./auth"; +import { PROXY_PREFIX, buildProxiedUrlFromRequest } from "./urls"; +import { registerGoogleFormsProxy } from "./googleProxy"; +import { registerWebsiteProxy } from "./websiteProxy"; + +const PORT = Number(process.env.PROXY_SERVICE_PORT ?? 6070); +const LOWCODER_PUBLIC_URL = (process.env.LOWCODER_PUBLIC_URL ?? "http://localhost:3000").replace(/\/$/, ""); +const HOCUSPOCUS_URL = (process.env.LOWCODER_HOCUSPOCUS_URL ?? "ws://localhost:3006").trim(); +const HOCUSPOCUS_SECRET = ( + process.env.LOWCODER_HOCUSPOCUS_SECRET ?? process.env.HOCUSPOCUS_SECRET ?? "" +).trim(); +const RATE_LIMIT_PER_MINUTE = Number(process.env.LOWCODER_PROXY_RATE_LIMIT ?? 120); +const ALLOWED_TYPEFORM_HOSTS = new Set( + (process.env.LOWCODER_PROXY_ALLOWED_HOSTS ?? "form.typeform.com,embed.typeform.com,admin.typeform.com") + .split(",") + .map((value) => value.trim()) + .filter(Boolean) +); + +const SESSION_PATH = `${PROXY_PREFIX}/session`; +const JOIN_SESSION_PATH = `${PROXY_PREFIX}/session/join`; +const BRIDGE_PATH = "/proxy/typeform-bridge.js"; +const requestBuckets = new Map(); + +const app = express(); +app.disable("x-powered-by"); +app.use(cors({ credentials: true, origin: true })); + +app.get("/", (_req, res) => { + res.status(200).json({ code: 1, message: "Lowcoder Proxy Service is up and running", success: true }); +}); + +registerGoogleFormsProxy(app); +registerWebsiteProxy(app); + +app.get(BRIDGE_PATH, (_req, res) => { + const bridgePath = path.join(__dirname, "bridge", "typeform-bridge.js"); + if (!fs.existsSync(bridgePath)) { + res.status(404).type("text/plain").send("Bridge script not found"); + return; + } + res.setHeader("Content-Type", "application/javascript; charset=utf-8"); + res.setHeader("Cache-Control", "no-cache"); + res.send(fs.readFileSync(bridgePath, "utf8")); +}); + +app.post(SESSION_PATH, express.json({ limit: "1mb" }), createProxySession); +app.post(JOIN_SESSION_PATH, express.json({ limit: "1mb" }), joinProxySession); + +app.use(PROXY_PREFIX, express.raw({ type: "*/*", limit: "25mb" }), async (req, res) => { + if (req.path === "/session" || req.path === "/session/join") { + res.status(405).json({ message: "Use POST on /proxy/typeform/session or /proxy/typeform/session/join" }); + return; + } + + try { + if (!isAuthorized(req)) { + res.status(401).json({ message: "Missing or invalid proxy token" }); + return; + } + if (!checkRateLimit(req)) { + res.status(429).json({ message: "Proxy rate limit exceeded" }); + return; + } + + const upstreamUrl = resolveUpstreamUrl(req); + const upstreamResponse = await fetch(upstreamUrl.toString(), { + method: req.method, + redirect: "manual", + headers: buildForwardHeaders(req, upstreamUrl), + body: hasBody(req.method) ? req.body : undefined, + }); + + relayHeaders(upstreamResponse, res, req); + const contentType = upstreamResponse.headers.get("content-type") ?? ""; + + if (contentType.includes("text/html")) { + const html = await upstreamResponse.text(); + res.status(upstreamResponse.status).send(injectBridgeAndRewriteHtml(html, req, upstreamUrl)); + return; + } + + if (contentType.includes("application/json") || contentType.includes("text/javascript")) { + const text = await upstreamResponse.text(); + res.status(upstreamResponse.status).send(rewriteBodyUrls(text, req, upstreamUrl)); + return; + } + + const buffer = await upstreamResponse.buffer(); + res.status(upstreamResponse.status).send(buffer); + } catch (error) { + console.error("Proxy request failed", error); + res.status(502).json({ message: "Typeform proxy request failed" }); + } +}); + +const httpServer = createServer(app); + +httpServer.listen(PORT, () => { + console.log(`Proxy service listening on ${PORT}`); +}); + +function hasBody(method: string): boolean { + return ["POST", "PUT", "PATCH", "DELETE"].includes(method.toUpperCase()); +} + +function resolveUpstreamUrl(req: Request): URL { + const targetParam = (req.query.target as string | undefined)?.trim(); + if (targetParam) { + const upstream = new URL(targetParam); + assertAllowedHost(upstream.hostname); + return upstream; + } + + const rawPath = req.originalUrl.replace(PROXY_PREFIX, ""); + const [pathname, query = ""] = rawPath.split("?"); + const cleanPath = pathname.startsWith("/") ? pathname : `/${pathname}`; + const upstream = new URL(`https://form.typeform.com${cleanPath}${query ? `?${query}` : ""}`); + assertAllowedHost(upstream.hostname); + return upstream; +} + +function assertAllowedHost(hostname: string) { + if (!ALLOWED_TYPEFORM_HOSTS.has(hostname)) { + throw new Error(`Host is not allowed: ${hostname}`); + } +} + +function buildForwardHeaders(req: Request, upstreamUrl: URL): HeadersInit { + const normalized = new Map(); + Object.entries(req.headers).forEach(([key, value]) => { + if (typeof value !== "string") return; + const lower = key.toLowerCase(); + if (["host", "content-length", "x-forwarded-host", "x-forwarded-proto", "connection"].includes(lower)) return; + normalized.set(lower, value); + }); + normalized.set("host", upstreamUrl.host); + normalized.set("origin", `${upstreamUrl.protocol}//${upstreamUrl.host}`); + return Object.fromEntries(normalized.entries()); +} + +function relayHeaders(upstreamResponse: FetchResponse, res: Response, req: Request) { + const skipHeaders = new Set([ + "x-frame-options", + "content-security-policy", + "transfer-encoding", + "content-length", + "content-encoding", + ]); + + upstreamResponse.headers.forEach((value, key) => { + const lower = key.toLowerCase(); + if (skipHeaders.has(lower)) return; + if (lower === "set-cookie") { + const cookies = rewriteSetCookie(value); + if (cookies.length > 0) res.setHeader("Set-Cookie", cookies); + return; + } + if (lower === "location") { + res.setHeader(key, rewriteAbsoluteUrl(value, req)); + return; + } + res.setHeader(key, value); + }); + res.setHeader("Content-Security-Policy", "frame-ancestors 'self';"); +} + +function rewriteSetCookie(rawValue: string): string[] { + return rawValue + .split(/,(?=[^;]+=[^;]+)/) + .map((cookie) => + cookie + .replace(/;\s*Domain=[^;]+/gi, "") + .replace(/;\s*SameSite=None/gi, "; SameSite=Lax") + .replace(/;\s*Path=[^;]+/gi, "; Path=/proxy/typeform") + ); +} + +function injectBridgeAndRewriteHtml(html: string, req: Request, upstreamUrl: URL): string { + const rewritten = rewriteBodyUrls(html, req, upstreamUrl); + const roomId = String(req.query.roomId ?? ""); + const role = String(req.query.role ?? "driver"); + const editorId = String(req.query.editorId ?? "local"); + const collabId = String(req.query.collab ?? ""); + const username = String(req.query.username ?? ""); + const attrs = + ` data-lowcoder-room-id="${escapeHtml(roomId)}"` + + ` data-lowcoder-role="${escapeHtml(role)}"` + + ` data-lowcoder-editor-id="${escapeHtml(editorId)}"` + + ` data-lowcoder-collab-id="${escapeHtml(collabId)}"` + + ` data-lowcoder-username="${escapeHtml(username)}"` + + ` data-lowcoder-hocuspocus-url="${escapeHtml(HOCUSPOCUS_URL)}"` + + (HOCUSPOCUS_SECRET ? ` data-lowcoder-hocuspocus-token="${escapeHtml(HOCUSPOCUS_SECRET)}"` : ""); + const withRootAttrs = rewritten.replace("window.__LOWCODER_HOCUSPOCUS__=${hocuspocusConfig};` + + ``; + if (withRootAttrs.includes("")) { + return withRootAttrs.replace("", `${bridgeTag}`); + } + return `${bridgeTag}${withRootAttrs}`; +} + +function rewriteBodyUrls(body: string, req: Request, upstreamUrl?: URL): string { + const base = upstreamUrl ?? new URL("https://form.typeform.com"); + const origin = base.origin; + + let output = body.replace( + new RegExp(`${escapeRegex(origin)}([^"'\\s]*)`, "g"), + (_match, suffix: string) => buildProxiedUrlFromRequest(`${origin}${suffix ?? ""}`, req) + ); + + output = output.replace(/(href|src|action)=["']\/([^"']+)["']/g, (_m, attr, route) => { + const target = `${base.origin}/${route}`; + return `${attr}="${buildProxiedUrlFromRequest(target, req)}"`; + }); + output = output.replace(/url\(["']?\/([^"')]+)["']?\)/g, (_m, route) => { + const target = `${base.origin}/${route}`; + return `url("${buildProxiedUrlFromRequest(target, req)}")`; + }); + return output; +} + +function rewriteAbsoluteUrl(value: string, req: Request): string { + try { + const parsed = new URL(value); + if (!ALLOWED_TYPEFORM_HOSTS.has(parsed.hostname)) return value; + return buildProxiedUrlFromRequest(parsed.toString(), req); + } catch { + return value; + } +} + +function escapeRegex(text: string): string { + return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + +function escapeHtml(value: string): string { + return value + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + +function isAuthorized(req: Request): boolean { + const token = (req.query.token as string | undefined) || getBearerToken(req.headers.authorization); + return verifyProxyToken(token, "typeform-proxy"); +} + +function checkRateLimit(req: Request): boolean { + const key = req.ip || "unknown"; + const now = Date.now(); + const existing = requestBuckets.get(key); + if (!existing || now > existing.resetAt) { + requestBuckets.set(key, { count: 1, resetAt: now + 60_000 }); + return true; + } + existing.count += 1; + return existing.count <= RATE_LIMIT_PER_MINUTE; +} diff --git a/server/proxy-service/src/session.ts b/server/proxy-service/src/session.ts new file mode 100644 index 0000000000..18fa03f0e2 --- /dev/null +++ b/server/proxy-service/src/session.ts @@ -0,0 +1,122 @@ +import { Request, Response } from "express"; +import { createProxyToken, resolveParticipantId } from "./auth"; +import { buildProxiedUrl } from "./urls"; + +interface SessionRequestBody { + typeformUrl?: string; + roomId?: string; + role?: string; + editorId?: string; + guestId?: string; + collab?: string; + username?: string; +} + +export interface SessionData { + token: string; + proxiedUrl: string; + roomId: string; + role: string; + editorId: string; + participantId: string; + typeformUrl: string; + collab: string; + username: string; + broadcast: { + roomId: string; + collab: string; + typeformUrl: string; + editorId: string; + username: string; + }; +} + +function normalizeRole(role?: string, fallback: "driver" | "follower" = "driver"): "driver" | "follower" { + const value = (role ?? fallback).trim().toLowerCase(); + return value === "follower" ? "follower" : "driver"; +} + +async function mintSession(req: Request, body: SessionRequestBody, role: "driver" | "follower"): Promise { + const typeformUrl = (body.typeformUrl ?? "").trim(); + const roomId = (body.roomId ?? "").trim(); + const collab = (body.collab ?? "").trim(); + const username = (body.username ?? "").trim(); + + if (!typeformUrl) { + throw new Error("typeformUrl is required"); + } + if (!roomId) { + throw new Error("roomId is required"); + } + + const participantId = await resolveParticipantId(req, { + editorId: body.editorId?.trim() || undefined, + guestId: body.guestId?.trim() || undefined, + roomId, + role, + }); + const token = createProxyToken(participantId, roomId, role); + const proxiedUrl = buildProxiedUrl(typeformUrl, { + roomId, + role, + editorId: participantId, + token, + collab, + username, + }); + + return { + token, + proxiedUrl, + roomId, + role, + editorId: participantId, + participantId, + typeformUrl, + collab, + + username, + broadcast: { roomId, collab, typeformUrl, editorId: participantId, username }, + }; +} + +function sendSessionResponse(res: Response, data: SessionData) { + res.status(200).json({ + code: 1, + message: "", + data, + }); +} + +function sendSessionError(res: Response, error: unknown) { + console.error("Proxy session error", error); + const status = error instanceof Error && error.message.includes("required") ? 400 : 401; + res.status(status).json({ + code: status, + message: error instanceof Error ? error.message : "Unauthorized", + }); +} + +/** Creates a proxy session. Respects `body.role` (`driver` or `follower`). */ +export async function createProxySession(req: Request, res: Response) { + try { + const body = (req.body ?? {}) as SessionRequestBody; + const role = normalizeRole(body.role, "driver"); + const data = await mintSession(req, body, role); + sendSessionResponse(res, data); + } catch (error) { + sendSessionError(res, error); + } +} + +/** Join an existing collab room as follower (same as session with role=follower). */ +export async function joinProxySession(req: Request, res: Response) { + try { + const body = (req.body ?? {}) as SessionRequestBody; + const role = normalizeRole(body.role, "follower"); + const data = await mintSession(req, body, role); + sendSessionResponse(res, data); + } catch (error) { + sendSessionError(res, error); + } +} diff --git a/server/proxy-service/src/urls.ts b/server/proxy-service/src/urls.ts new file mode 100644 index 0000000000..e6d1a120b3 --- /dev/null +++ b/server/proxy-service/src/urls.ts @@ -0,0 +1,36 @@ +import { Request } from "express"; + +export const PROXY_PREFIX = "/proxy/typeform"; + +export function buildProxiedUrl( + targetUrl: string, + options: { + roomId?: string; + role?: string; + editorId?: string; + token?: string; + collab?: string; + username?: string; + } +): string { + const params = new URLSearchParams(); + params.set("target", targetUrl); + if (options.roomId) params.set("roomId", options.roomId); + if (options.role) params.set("role", options.role); + if (options.editorId) params.set("editorId", options.editorId); + if (options.token) params.set("token", options.token); + if (options.collab) params.set("collab", options.collab); + if (options.username) params.set("username", options.username); + return `${PROXY_PREFIX}?${params.toString()}`; +} + +export function buildProxiedUrlFromRequest(targetUrl: string, req: Request): string { + return buildProxiedUrl(targetUrl, { + roomId: String(req.query.roomId ?? ""), + role: String(req.query.role ?? "driver"), + editorId: String(req.query.editorId ?? ""), + token: String(req.query.token ?? ""), + collab: String(req.query.collab ?? ""), + username: String(req.query.username ?? ""), + }); +} diff --git a/server/proxy-service/src/websiteAllowlist.ts b/server/proxy-service/src/websiteAllowlist.ts new file mode 100644 index 0000000000..7c944ca552 --- /dev/null +++ b/server/proxy-service/src/websiteAllowlist.ts @@ -0,0 +1,92 @@ +import { isIP } from "node:net"; + +const ALLOWED_WEBSITE_HOSTS = new Set( + (process.env.LOWCODER_WEBSITE_ALLOWED_HOSTS ?? "") + .split(",") + .map((value) => value.trim().toLowerCase()) + .filter(Boolean) +); + +const BLOCKED_HOSTNAMES = new Set([ + "localhost", + "localhost.localdomain", + "metadata.google.internal", + "metadata", +]); + +function isPrivateOrReservedIp(ip: string): boolean { + const version = isIP(ip); + if (version === 4) { + const parts = ip.split(".").map(Number); + const [a, b] = parts; + if (a === 10) return true; + if (a === 127) return true; + if (a === 0) return true; + if (a === 169 && b === 254) return true; + if (a === 172 && b >= 16 && b <= 31) return true; + if (a === 192 && b === 168) return true; + if (a === 100 && b >= 64 && b <= 127) return true; // CGNAT + if (a >= 224) return true; // multicast / reserved + return false; + } + if (version === 6) { + const normalized = ip.toLowerCase(); + if (normalized === "::1" || normalized === "::") return true; + if (normalized.startsWith("fc") || normalized.startsWith("fd")) return true; // ULA + if (normalized.startsWith("fe80")) return true; // link-local + if (normalized.startsWith("ff")) return true; // multicast + // IPv4-mapped IPv6 + if (normalized.startsWith("::ffff:")) { + const mapped = normalized.slice("::ffff:".length); + if (isIP(mapped) === 4) return isPrivateOrReservedIp(mapped); + } + return false; + } + return true; +} + +export function normalizeWebsiteUrl(raw?: string): string { + const trimmed = (raw ?? "").trim(); + if (!trimmed) { + throw new Error("websiteUrl is required"); + } + let parsed: URL; + try { + parsed = new URL(trimmed); + } catch { + throw new Error(`Invalid URL: ${trimmed}`); + } + if (parsed.protocol !== "http:" && parsed.protocol !== "https:") { + throw new Error(`Website URL must be http(s): ${trimmed}`); + } + return parsed.toString(); +} + +export function assertAllowedWebsiteUrl(url: URL): void { + if (url.protocol !== "http:" && url.protocol !== "https:") { + throw new Error(`Website URL is not allowed: ${url.toString()}`); + } + + const hostname = url.hostname.toLowerCase(); + if (!hostname) { + throw new Error(`Website URL is not allowed: ${url.toString()}`); + } + + if (BLOCKED_HOSTNAMES.has(hostname) || hostname.endsWith(".localhost")) { + throw new Error(`Website URL is not allowed: ${url.toString()}`); + } + + if (isIP(hostname) && isPrivateOrReservedIp(hostname)) { + throw new Error(`Website URL is not allowed: ${url.toString()}`); + } + + if (ALLOWED_WEBSITE_HOSTS.size > 0 && !ALLOWED_WEBSITE_HOSTS.has(hostname)) { + throw new Error(`Website URL is not allowed: ${url.toString()}`); + } +} + +export function isWebsiteHostAllowed(hostname: string): boolean { + const lower = hostname.toLowerCase(); + if (ALLOWED_WEBSITE_HOSTS.size === 0) return true; + return ALLOWED_WEBSITE_HOSTS.has(lower); +} diff --git a/server/proxy-service/src/websiteProxy.ts b/server/proxy-service/src/websiteProxy.ts new file mode 100644 index 0000000000..da53dc10d6 --- /dev/null +++ b/server/proxy-service/src/websiteProxy.ts @@ -0,0 +1,281 @@ +import express, { Express, Request, Response } from "express"; +import fetch, { HeadersInit, Response as FetchResponse } from "node-fetch"; +import fs from "node:fs"; +import path from "node:path"; +import { URL } from "node:url"; +import { getBearerToken, verifyProxyToken } from "./auth"; +import { assertAllowedWebsiteUrl } from "./websiteAllowlist"; +import { + WEBSITE_PROXY_PREFIX, + buildWebsiteProxiedUrlFromRequest, +} from "./websiteUrls"; +import { + createWebsiteProxySession, + joinWebsiteProxySession, +} from "./websiteSession"; + +const BRIDGE_PATH = "/proxy/website-bridge.js"; +const HOCUSPOCUS_URL = (process.env.LOWCODER_HOCUSPOCUS_URL ?? "ws://localhost:3006").trim(); + +function resolveHocuspocusUrl(req: Request): string { + if (!/localhost|127\.0\.0\.1/.test(HOCUSPOCUS_URL)) { + return HOCUSPOCUS_URL; + } + const forwardedHost = (req.get("x-forwarded-host") || req.get("host") || "localhost") + .split(",")[0] + .trim(); + const hostname = forwardedHost.split(":")[0] || "localhost"; + const port = new URL(HOCUSPOCUS_URL.replace(/^ws/, "http")).port || "3006"; + return `ws://${hostname}:${port}`; +} + +const HOCUSPOCUS_SECRET = ( + process.env.LOWCODER_HOCUSPOCUS_SECRET ?? process.env.HOCUSPOCUS_SECRET ?? "" +).trim(); +const RATE_LIMIT_PER_MINUTE = Number(process.env.LOWCODER_PROXY_RATE_LIMIT ?? 120); +const requestBuckets = new Map(); + +export function registerWebsiteProxy(app: Express): void { + app.get(BRIDGE_PATH, (_req, res) => { + const bridgePath = path.join(__dirname, "bridge", "website-bridge.js"); + if (!fs.existsSync(bridgePath)) { + res.status(404).type("text/plain").send("Website bridge script not found"); + return; + } + res.setHeader("Content-Type", "application/javascript; charset=utf-8"); + res.setHeader("Cache-Control", "no-cache"); + res.send(fs.readFileSync(bridgePath, "utf8")); + }); + + const router = express.Router(); + router.post("/session", express.json({ limit: "1mb" }), createWebsiteProxySession); + router.post("/session/join", express.json({ limit: "1mb" }), joinWebsiteProxySession); + + router.use(express.raw({ type: "*/*", limit: "25mb" }), async (req, res) => { + if (req.path === "/session" || req.path === "/session/join") { + res.status(405).json({ + message: "Use POST on /proxy/website/session or /proxy/website/session/join", + }); + return; + } + + try { + if (!isAuthorized(req)) { + res.status(401).json({ message: "Missing or invalid proxy token" }); + return; + } + if (!checkRateLimit(req)) { + res.status(429).json({ message: "Proxy rate limit exceeded" }); + return; + } + + const upstreamUrl = resolveUpstreamUrl(req); + const upstreamResponse = await fetch(upstreamUrl.toString(), { + method: req.method, + redirect: "manual", + headers: buildForwardHeaders(req, upstreamUrl), + body: hasBody(req.method) ? req.body : undefined, + }); + + relayHeaders(upstreamResponse, res, req, upstreamUrl); + const contentType = upstreamResponse.headers.get("content-type") ?? ""; + + if (contentType.includes("text/html")) { + const html = await upstreamResponse.text(); + res + .status(upstreamResponse.status) + .send(injectBridgeAndRewriteHtml(html, req, upstreamUrl)); + return; + } + + if (/javascript|json|text\/css/.test(contentType)) { + const text = await upstreamResponse.text(); + res.status(upstreamResponse.status).send(rewriteBodyUrls(text, req, upstreamUrl)); + return; + } + + res.status(upstreamResponse.status).send(await upstreamResponse.buffer()); + } catch (error) { + console.error("Website proxy request failed", error); + const message = error instanceof Error ? error.message : "Website proxy request failed"; + const status = message.includes("not allowed") || message.includes("required") ? 400 : 502; + res.status(status).json({ message }); + } + }); + + app.use(WEBSITE_PROXY_PREFIX, router); +} + +function hasBody(method: string): boolean { + return ["POST", "PUT", "PATCH", "DELETE"].includes(method.toUpperCase()); +} + +function resolveUpstreamUrl(req: Request): URL { + const targetParam = (req.query.target as string | undefined)?.trim(); + if (!targetParam) { + throw new Error("A website target URL is required"); + } + const upstream = new URL(targetParam); + assertAllowedWebsiteUrl(upstream); + return upstream; +} + +function buildForwardHeaders(req: Request, upstreamUrl: URL): HeadersInit { + const normalized = new Map(); + Object.entries(req.headers).forEach(([key, value]) => { + if (typeof value !== "string") return; + const lower = key.toLowerCase(); + if ( + ["host", "content-length", "x-forwarded-host", "x-forwarded-proto", "connection"].includes( + lower + ) + ) { + return; + } + normalized.set(lower, value); + }); + normalized.set("host", upstreamUrl.host); + normalized.set("origin", upstreamUrl.origin); + normalized.set("referer", upstreamUrl.toString()); + return Object.fromEntries(normalized.entries()); +} + +function relayHeaders( + upstreamResponse: FetchResponse, + res: Response, + req: Request, + upstreamUrl: URL +): void { + const skipHeaders = new Set([ + "x-frame-options", + "content-security-policy", + "transfer-encoding", + "content-length", + "content-encoding", + ]); + + upstreamResponse.headers.forEach((value, key) => { + const lower = key.toLowerCase(); + if (skipHeaders.has(lower)) return; + if (lower === "set-cookie") { + const cookies = rewriteSetCookie(value); + if (cookies.length > 0) res.setHeader("Set-Cookie", cookies); + return; + } + if (lower === "location") { + res.setHeader(key, rewriteAbsoluteUrl(value, req, upstreamUrl)); + return; + } + res.setHeader(key, value); + }); + res.setHeader("Content-Security-Policy", "frame-ancestors 'self';"); +} + +function rewriteSetCookie(rawValue: string): string[] { + return rawValue + .split(/,(?=[^;]+=[^;]+)/) + .map((cookie) => + cookie + .replace(/;\s*Domain=[^;]+/gi, "") + .replace(/;\s*SameSite=None/gi, "; SameSite=Lax") + .replace(/;\s*Path=[^;]+/gi, `; Path=${WEBSITE_PROXY_PREFIX}`) + ); +} + +function injectBridgeAndRewriteHtml(html: string, req: Request, upstreamUrl: URL): string { + const rewritten = rewriteBodyUrls(html, req, upstreamUrl); + const hocuspocusUrl = resolveHocuspocusUrl(req); + const collab = String(req.query.collab ?? "").trim(); + const attrs = + ` data-lowcoder-room-id="${escapeHtml(String(req.query.roomId ?? ""))}"` + + ` data-lowcoder-role="${escapeHtml(String(req.query.role ?? "driver"))}"` + + ` data-lowcoder-editor-id="${escapeHtml(String(req.query.editorId ?? "local"))}"` + + ` data-lowcoder-collab-id="${escapeHtml(collab)}"` + + ` data-lowcoder-username="${escapeHtml(String(req.query.username ?? ""))}"` + + ` data-lowcoder-upstream-url="${escapeHtml(upstreamUrl.toString())}"` + + ` data-lowcoder-hocuspocus-url="${escapeHtml(hocuspocusUrl)}"` + + (HOCUSPOCUS_SECRET + ? ` data-lowcoder-hocuspocus-token="${escapeHtml(HOCUSPOCUS_SECRET)}"` + : ""); + const withRootAttrs = rewritten.replace(/)/i, `window.__LOWCODER_HOCUSPOCUS__=${hocuspocusConfig};` + + ``; + + return /<\/head>/i.test(withRootAttrs) + ? withRootAttrs.replace(/<\/head>/i, `${bridgeTag}`) + : `${bridgeTag}${withRootAttrs}`; +} + +function rewriteBodyUrls(body: string, req: Request, upstreamUrl: URL): string { + const origin = upstreamUrl.origin; + let output = body.replace( + new RegExp(`${escapeRegex(origin)}([^"'\\\\\\s<]*)`, "g"), + (_match, suffix: string) => + buildWebsiteProxiedUrlFromRequest(`${origin}${suffix ?? ""}`, req) + ); + + // Root-relative paths (same origin under the proxy) + output = output.replace( + /(href|src|action)=["'](\/[^"'#?]*(?:\?[^"']*)?(?:#[^"']*)?)["']/gi, + (_match, attr: string, route: string) => { + if (route.startsWith("//") || route.startsWith("/proxy/")) return _match; + return `${attr}="${buildWebsiteProxiedUrlFromRequest(`${origin}${route}`, req)}"`; + } + ); + output = output.replace( + /url\(["']?(\/[^"')]+)["']?\)/gi, + (_match, route: string) => { + if (route.startsWith("//") || route.startsWith("/proxy/")) return _match; + return `url("${buildWebsiteProxiedUrlFromRequest(`${origin}${route}`, req)}")`; + } + ); + return output; +} + +function rewriteAbsoluteUrl(value: string, req: Request, upstreamUrl: URL): string { + try { + const parsed = new URL(value, upstreamUrl); + if (parsed.origin !== upstreamUrl.origin) { + return value; + } + assertAllowedWebsiteUrl(parsed); + return buildWebsiteProxiedUrlFromRequest(parsed.toString(), req); + } catch { + return value; + } +} + +function escapeRegex(text: string): string { + return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + +function escapeHtml(value: string): string { + return value + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + +function isAuthorized(req: Request): boolean { + const token = (req.query.token as string | undefined) || getBearerToken(req.headers.authorization); + return verifyProxyToken(token, "website-proxy"); +} + +function checkRateLimit(req: Request): boolean { + const key = req.ip || "unknown"; + const now = Date.now(); + const existing = requestBuckets.get(key); + if (!existing || now > existing.resetAt) { + requestBuckets.set(key, { count: 1, resetAt: now + 60_000 }); + return true; + } + existing.count += 1; + return existing.count <= RATE_LIMIT_PER_MINUTE; +} diff --git a/server/proxy-service/src/websiteSession.ts b/server/proxy-service/src/websiteSession.ts new file mode 100644 index 0000000000..b84ebcfb9b --- /dev/null +++ b/server/proxy-service/src/websiteSession.ts @@ -0,0 +1,134 @@ +import { Request, Response } from "express"; +import { createProxyToken, resolveParticipantId } from "./auth"; +import { assertAllowedWebsiteUrl, normalizeWebsiteUrl } from "./websiteAllowlist"; +import { buildWebsiteProxiedUrl } from "./websiteUrls"; + +interface WebsiteSessionRequestBody { + websiteUrl?: string; + roomId?: string; + role?: string; + editorId?: string; + guestId?: string; + collab?: string; + username?: string; +} + +export interface WebsiteSessionData { + token: string; + proxiedUrl: string; + roomId: string; + role: string; + editorId: string; + participantId: string; + websiteUrl: string; + collab: string; + username: string; + broadcast: { + roomId: string; + collab: string; + websiteUrl: string; + editorId: string; + username: string; + }; +} + +function normalizeRole( + role?: string, + fallback: "driver" | "follower" = "driver" +): "driver" | "follower" { + return (role ?? fallback).trim().toLowerCase() === "follower" ? "follower" : "driver"; +} + +function normalizeCollab(rawCollab: string | undefined, role: "driver" | "follower"): string { + const collab = (rawCollab ?? "").trim(); + if (collab) return collab; + if (role === "driver") { + return String(Date.now()); + } + throw new Error( + "collab is required for followers. Pass broadcast.collab from the driver's session response." + ); +} + +async function mintWebsiteSession( + req: Request, + body: WebsiteSessionRequestBody, + role: "driver" | "follower" +): Promise { + const websiteUrl = normalizeWebsiteUrl(body.websiteUrl); + assertAllowedWebsiteUrl(new URL(websiteUrl)); + + const roomId = (body.roomId ?? "").trim(); + const collab = normalizeCollab(body.collab, role); + const username = (body.username ?? "").trim(); + + if (!roomId) throw new Error("roomId is required"); + + const participantId = await resolveParticipantId(req, { + editorId: body.editorId?.trim() || undefined, + guestId: body.guestId?.trim() || undefined, + roomId, + role, + }); + const token = createProxyToken(participantId, roomId, role, "website-proxy"); + const proxiedUrl = buildWebsiteProxiedUrl(websiteUrl, { + roomId, + role, + editorId: participantId, + token, + collab, + username, + }); + + return { + token, + proxiedUrl, + roomId, + role, + editorId: participantId, + participantId, + websiteUrl, + collab, + username, + broadcast: { roomId, collab, websiteUrl, editorId: participantId, username }, + }; +} + +function sendSessionResponse(res: Response, data: WebsiteSessionData): void { + res.status(200).json({ code: 1, message: "", data }); +} + +function sendSessionError(res: Response, error: unknown): void { + console.error("Website proxy session error", error); + const message = error instanceof Error ? error.message : "Unauthorized"; + const status = + message.includes("required") || + message.includes("must be") || + message.includes("Invalid URL") || + message.includes("not allowed") || + message.includes("collab") + ? 400 + : 401; + res.status(status).json({ code: status, message }); +} + +export async function createWebsiteProxySession(req: Request, res: Response): Promise { + try { + const body = (req.body ?? {}) as WebsiteSessionRequestBody; + sendSessionResponse(res, await mintWebsiteSession(req, body, normalizeRole(body.role))); + } catch (error) { + sendSessionError(res, error); + } +} + +export async function joinWebsiteProxySession(req: Request, res: Response): Promise { + try { + const body = (req.body ?? {}) as WebsiteSessionRequestBody; + sendSessionResponse( + res, + await mintWebsiteSession(req, body, normalizeRole(body.role, "follower")) + ); + } catch (error) { + sendSessionError(res, error); + } +} diff --git a/server/proxy-service/src/websiteUrls.ts b/server/proxy-service/src/websiteUrls.ts new file mode 100644 index 0000000000..9cc5e0b253 --- /dev/null +++ b/server/proxy-service/src/websiteUrls.ts @@ -0,0 +1,41 @@ +import { Request } from "express"; + +export const WEBSITE_PROXY_PREFIX = "/proxy/website"; + +export interface WebsiteProxyOptions { + roomId?: string; + role?: string; + editorId?: string; + token?: string; + collab?: string; + username?: string; +} + +export function buildWebsiteProxiedUrl( + targetUrl: string, + options: WebsiteProxyOptions +): string { + const params = new URLSearchParams(); + params.set("target", targetUrl); + if (options.roomId) params.set("roomId", options.roomId); + if (options.role) params.set("role", options.role); + if (options.editorId) params.set("editorId", options.editorId); + if (options.token) params.set("token", options.token); + if (options.collab) params.set("collab", options.collab); + if (options.username) params.set("username", options.username); + return `${WEBSITE_PROXY_PREFIX}?${params.toString()}`; +} + +export function buildWebsiteProxiedUrlFromRequest( + targetUrl: string, + req: Request +): string { + return buildWebsiteProxiedUrl(targetUrl, { + roomId: String(req.query.roomId ?? ""), + role: String(req.query.role ?? "driver"), + editorId: String(req.query.editorId ?? ""), + token: String(req.query.token ?? ""), + collab: String(req.query.collab ?? ""), + username: String(req.query.username ?? ""), + }); +} diff --git a/server/proxy-service/tsconfig.json b/server/proxy-service/tsconfig.json new file mode 100644 index 0000000000..ead4845709 --- /dev/null +++ b/server/proxy-service/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2021", + "module": "commonjs", + "lib": ["ES2021", "DOM"], + "outDir": "build", + "rootDir": "src", + "strict": false, + "esModuleInterop": true, + "resolveJsonModule": true, + "skipLibCheck": true + }, + "include": ["src/**/*.ts"], + "exclude": ["src/bridge/**"] +} diff --git a/server/proxy-service/yarn.lock b/server/proxy-service/yarn.lock new file mode 100644 index 0000000000..e6b4963cf1 --- /dev/null +++ b/server/proxy-service/yarn.lock @@ -0,0 +1,1315 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@cspotcode/source-map-support@^0.8.0": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" + integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== + dependencies: + "@jridgewell/trace-mapping" "0.3.9" + +"@esbuild/aix-ppc64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz#80fcbe36130e58b7670511e888b8e88a259ed76c" + integrity sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA== + +"@esbuild/android-arm64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz#8aa4965f8d0a7982dc21734bf6601323a66da752" + integrity sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg== + +"@esbuild/android-arm@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.12.tgz#300712101f7f50f1d2627a162e6e09b109b6767a" + integrity sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg== + +"@esbuild/android-x64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.12.tgz#87dfb27161202bdc958ef48bb61b09c758faee16" + integrity sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg== + +"@esbuild/darwin-arm64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz#79197898ec1ff745d21c071e1c7cc3c802f0c1fd" + integrity sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg== + +"@esbuild/darwin-x64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz#146400a8562133f45c4d2eadcf37ddd09718079e" + integrity sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA== + +"@esbuild/freebsd-arm64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz#1c5f9ba7206e158fd2b24c59fa2d2c8bb47ca0fe" + integrity sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg== + +"@esbuild/freebsd-x64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz#ea631f4a36beaac4b9279fa0fcc6ca29eaeeb2b3" + integrity sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ== + +"@esbuild/linux-arm64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz#e1066bce58394f1b1141deec8557a5f0a22f5977" + integrity sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ== + +"@esbuild/linux-arm@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz#452cd66b20932d08bdc53a8b61c0e30baf4348b9" + integrity sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw== + +"@esbuild/linux-ia32@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz#b24f8acc45bcf54192c7f2f3be1b53e6551eafe0" + integrity sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA== + +"@esbuild/linux-loong64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz#f9cfffa7fc8322571fbc4c8b3268caf15bd81ad0" + integrity sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng== + +"@esbuild/linux-mips64el@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz#575a14bd74644ffab891adc7d7e60d275296f2cd" + integrity sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw== + +"@esbuild/linux-ppc64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz#75b99c70a95fbd5f7739d7692befe60601591869" + integrity sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA== + +"@esbuild/linux-riscv64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz#2e3259440321a44e79ddf7535c325057da875cd6" + integrity sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w== + +"@esbuild/linux-s390x@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz#17676cabbfe5928da5b2a0d6df5d58cd08db2663" + integrity sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg== + +"@esbuild/linux-x64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz#0583775685ca82066d04c3507f09524d3cd7a306" + integrity sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw== + +"@esbuild/netbsd-arm64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz#f04c4049cb2e252fe96b16fed90f70746b13f4a4" + integrity sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg== + +"@esbuild/netbsd-x64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz#77da0d0a0d826d7c921eea3d40292548b258a076" + integrity sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ== + +"@esbuild/openbsd-arm64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz#6296f5867aedef28a81b22ab2009c786a952dccd" + integrity sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A== + +"@esbuild/openbsd-x64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz#f8d23303360e27b16cf065b23bbff43c14142679" + integrity sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw== + +"@esbuild/openharmony-arm64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz#49e0b768744a3924be0d7fd97dd6ce9b2923d88d" + integrity sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg== + +"@esbuild/sunos-x64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz#a6ed7d6778d67e528c81fb165b23f4911b9b13d6" + integrity sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w== + +"@esbuild/win32-arm64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz#9ac14c378e1b653af17d08e7d3ce34caef587323" + integrity sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg== + +"@esbuild/win32-ia32@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz#918942dcbbb35cc14fca39afb91b5e6a3d127267" + integrity sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ== + +"@esbuild/win32-x64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz#9bdad8176be7811ad148d1f8772359041f46c6c5" + integrity sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA== + +"@hocuspocus/common@^3.4.4": + version "3.4.4" + resolved "https://registry.yarnpkg.com/@hocuspocus/common/-/common-3.4.4.tgz#a888fbd6dff2f0b8947c76b7841bddb89eb4d795" + integrity sha512-RykIJ0tsHHMP4Xk+4UCbc7SO5LgGxGUSTdbh6anJEsaALAyqinf1Nn5HYuMjLPolAmsar1v++m9zufR09NLpXA== + dependencies: + lib0 "^0.2.87" + +"@hocuspocus/provider@^3.4.4": + version "3.4.4" + resolved "https://registry.yarnpkg.com/@hocuspocus/provider/-/provider-3.4.4.tgz#ab4ff0b55f9faf848ddbc5775956afee440a4e97" + integrity sha512-KbsMAfdYcIJD8eMU/5QnpXcSOvIWAcCNI33FSRSaKCIpYBFtAwkYIwWnZJmPZ8a1BMAtqQc+uvy9+UQf7GHnGQ== + dependencies: + "@hocuspocus/common" "^3.4.4" + "@lifeomic/attempt" "^3.0.2" + lib0 "^0.2.87" + ws "^8.17.1" + +"@jridgewell/resolve-uri@^3.0.3": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== + +"@jridgewell/sourcemap-codec@^1.4.10": + version "1.5.5" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba" + integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og== + +"@jridgewell/trace-mapping@0.3.9": + version "0.3.9" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" + integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + +"@lifeomic/attempt@^3.0.2": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@lifeomic/attempt/-/attempt-3.1.0.tgz#7fc703559177b81a008b9d263e3d9a001d11d08a" + integrity sha512-QZqem4QuAnAyzfz+Gj5/+SLxqwCAw2qmt7732ZXodr6VDWGeYLG6w1i/vYLa55JQM9wRuBKLmXmiZ2P0LtE5rw== + +"@tsconfig/node10@^1.0.7": + version "1.0.12" + resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.12.tgz#be57ceac1e4692b41be9de6be8c32a106636dba4" + integrity sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ== + +"@tsconfig/node12@^1.0.7": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d" + integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== + +"@tsconfig/node14@^1.0.0": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1" + integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== + +"@tsconfig/node16@^1.0.2": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9" + integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA== + +"@types/body-parser@*": + version "1.19.6" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.6.tgz#1859bebb8fd7dac9918a45d54c1971ab8b5af474" + integrity sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g== + dependencies: + "@types/connect" "*" + "@types/node" "*" + +"@types/connect@*": + version "3.4.38" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.38.tgz#5ba7f3bc4fbbdeaff8dded952e5ff2cc53f8d858" + integrity sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug== + dependencies: + "@types/node" "*" + +"@types/cookie-parser@^1.4.8": + version "1.4.10" + resolved "https://registry.yarnpkg.com/@types/cookie-parser/-/cookie-parser-1.4.10.tgz#a045272a383a30597a01955d4f9c790018f214e4" + integrity sha512-B4xqkqfZ8Wek+rCOeRxsjMS9OgvzebEzzLYw7NHYuvzb7IdxOkI0ZHGgeEBX4PUM7QGVvNSK60T3OvWj3YfBRg== + +"@types/cors@^2.8.17": + version "2.8.19" + resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.19.tgz#d93ea2673fd8c9f697367f5eeefc2bbfa94f0342" + integrity sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg== + dependencies: + "@types/node" "*" + +"@types/express-serve-static-core@^4.17.33": + version "4.19.9" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.19.9.tgz#b746a8bb6c389af7a31141397bb539f775b0ae84" + integrity sha512-QP2ESEe/ImWY0HDwNAnK9PvEffUyhLTnWkk7KXzHfyeWAnlrDe1fN77bXl6ia8KT3wPlmA7t9/VPRpnf4Ex9sg== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + "@types/send" "*" + +"@types/express@^4.17.21": + version "4.17.25" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.25.tgz#070c8c73a6fee6936d65c195dbbfb7da5026649b" + integrity sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^4.17.33" + "@types/qs" "*" + "@types/serve-static" "^1" + +"@types/http-errors@*": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.5.tgz#5b749ab2b16ba113423feb1a64a95dcd30398472" + integrity sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg== + +"@types/jsonwebtoken@^9.0.7": + version "9.0.10" + resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz#a7932a47177dcd4283b6146f3bd5c26d82647f09" + integrity sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA== + dependencies: + "@types/ms" "*" + "@types/node" "*" + +"@types/mime@^1": + version "1.3.5" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690" + integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w== + +"@types/ms@*": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@types/ms/-/ms-2.1.0.tgz#052aa67a48eccc4309d7f0191b7e41434b90bb78" + integrity sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA== + +"@types/node-fetch@^2.6.12": + version "2.6.13" + resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.13.tgz#e0c9b7b5edbdb1b50ce32c127e85e880872d56ee" + integrity sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw== + dependencies: + "@types/node" "*" + form-data "^4.0.4" + +"@types/node@*": + version "26.1.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-26.1.1.tgz#bad758d601e97d6cf457d204ee76a35fce7bd119" + integrity sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw== + dependencies: + undici-types "~8.3.0" + +"@types/node@^22.10.5": + version "22.20.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.20.1.tgz#84e7cdf63cdaa20c134aa317ccc901aa21e16f0e" + integrity sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q== + dependencies: + undici-types "~6.21.0" + +"@types/qs@*": + version "6.15.1" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.15.1.tgz#8606884272c63f0db96986bd3548650d8a9388bf" + integrity sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw== + +"@types/range-parser@*": + version "1.2.7" + resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb" + integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ== + +"@types/send@*": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@types/send/-/send-1.2.1.tgz#6a784e45543c18c774c049bff6d3dbaf045c9c74" + integrity sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ== + dependencies: + "@types/node" "*" + +"@types/send@<1": + version "0.17.6" + resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.6.tgz#aeb5385be62ff58a52cd5459daa509ae91651d25" + integrity sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og== + dependencies: + "@types/mime" "^1" + "@types/node" "*" + +"@types/serve-static@^1": + version "1.15.10" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.10.tgz#768169145a778f8f5dfcb6360aead414a3994fee" + integrity sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw== + dependencies: + "@types/http-errors" "*" + "@types/node" "*" + "@types/send" "<1" + +accepts@~1.3.8: + version "1.3.8" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + dependencies: + mime-types "~2.1.34" + negotiator "0.6.3" + +acorn-walk@^8.1.1: + version "8.3.5" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.5.tgz#8a6b8ca8fc5b34685af15dabb44118663c296496" + integrity sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw== + dependencies: + acorn "^8.11.0" + +acorn@^8.11.0, acorn@^8.4.1: + version "8.17.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.17.0.tgz#1785adb84faf8d8add10369b93826fc2bd08f1fe" + integrity sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg== + +anymatch@~3.1.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +arg@^4.1.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" + integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + +balanced-match@^4.0.2: + version "4.0.4" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-4.0.4.tgz#bfb10662feed8196a2c62e7c68e17720c274179a" + integrity sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA== + +binary-extensions@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" + integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== + +body-parser@~1.20.5: + version "1.20.6" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.6.tgz#60c789c78e0992d906da0a29d71ae01d15c1ed76" + integrity sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g== + dependencies: + bytes "~3.1.2" + content-type "~1.0.5" + debug "2.6.9" + depd "2.0.0" + destroy "~1.2.0" + http-errors "~2.0.1" + iconv-lite "~0.4.24" + on-finished "~2.4.1" + qs "~6.15.1" + raw-body "~2.5.3" + type-is "~1.6.18" + unpipe "~1.0.0" + +brace-expansion@^5.0.5: + version "5.0.7" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-5.0.7.tgz#1b0e46965b479dad65af737b4a02790a05498337" + integrity sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA== + dependencies: + balanced-match "^4.0.2" + +braces@~3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== + dependencies: + fill-range "^7.1.1" + +buffer-equal-constant-time@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA== + +bytes@~3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + +call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" + integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + +call-bound@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a" + integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg== + dependencies: + call-bind-apply-helpers "^1.0.2" + get-intrinsic "^1.3.0" + +chokidar@^3.5.2: + version "3.6.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" + integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +content-disposition@~0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== + dependencies: + safe-buffer "5.2.1" + +content-type@~1.0.4, content-type@~1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + +cookie-parser@^1.4.7: + version "1.4.7" + resolved "https://registry.yarnpkg.com/cookie-parser/-/cookie-parser-1.4.7.tgz#e2125635dfd766888ffe90d60c286404fa0e7b26" + integrity sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw== + dependencies: + cookie "0.7.2" + cookie-signature "1.0.6" + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== + +cookie-signature@~1.0.6: + version "1.0.7" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.7.tgz#ab5dd7ab757c54e60f37ef6550f481c426d10454" + integrity sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA== + +cookie@0.7.2, cookie@~0.7.1: + version "0.7.2" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7" + integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w== + +cors@^2.8.5: + version "2.8.6" + resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.6.tgz#ff5dd69bd95e547503820d29aba4f8faf8dfec96" + integrity sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw== + dependencies: + object-assign "^4" + vary "^1" + +create-require@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" + integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== + +debug@2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@^4: + version "4.4.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" + integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== + dependencies: + ms "^2.1.3" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + +depd@2.0.0, depd@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +destroy@1.2.0, destroy@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + +diff@^4.0.1: + version "4.0.4" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.4.tgz#7a6dbfda325f25f07517e9b518f897c08332e07d" + integrity sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ== + +dunder-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" + integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== + dependencies: + call-bind-apply-helpers "^1.0.1" + es-errors "^1.3.0" + gopd "^1.2.0" + +ecdsa-sig-formatter@1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" + integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== + dependencies: + safe-buffer "^5.0.1" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== + +encodeurl@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58" + integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== + +es-define-property@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" + integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + +es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.2.tgz#a2d0b373205724dfa525d23b0c3e1b1ca582c99b" + integrity sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw== + dependencies: + es-errors "^1.3.0" + +es-set-tostringtag@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz#f31dbbe0c183b00a6d26eb6325c810c0fd18bd4d" + integrity sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA== + dependencies: + es-errors "^1.3.0" + get-intrinsic "^1.2.6" + has-tostringtag "^1.0.2" + hasown "^2.0.2" + +esbuild@^0.25.0: + version "0.25.12" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.25.12.tgz#97a1d041f4ab00c2fce2f838d2b9969a2d2a97a5" + integrity sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg== + optionalDependencies: + "@esbuild/aix-ppc64" "0.25.12" + "@esbuild/android-arm" "0.25.12" + "@esbuild/android-arm64" "0.25.12" + "@esbuild/android-x64" "0.25.12" + "@esbuild/darwin-arm64" "0.25.12" + "@esbuild/darwin-x64" "0.25.12" + "@esbuild/freebsd-arm64" "0.25.12" + "@esbuild/freebsd-x64" "0.25.12" + "@esbuild/linux-arm" "0.25.12" + "@esbuild/linux-arm64" "0.25.12" + "@esbuild/linux-ia32" "0.25.12" + "@esbuild/linux-loong64" "0.25.12" + "@esbuild/linux-mips64el" "0.25.12" + "@esbuild/linux-ppc64" "0.25.12" + "@esbuild/linux-riscv64" "0.25.12" + "@esbuild/linux-s390x" "0.25.12" + "@esbuild/linux-x64" "0.25.12" + "@esbuild/netbsd-arm64" "0.25.12" + "@esbuild/netbsd-x64" "0.25.12" + "@esbuild/openbsd-arm64" "0.25.12" + "@esbuild/openbsd-x64" "0.25.12" + "@esbuild/openharmony-arm64" "0.25.12" + "@esbuild/sunos-x64" "0.25.12" + "@esbuild/win32-arm64" "0.25.12" + "@esbuild/win32-ia32" "0.25.12" + "@esbuild/win32-x64" "0.25.12" + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== + +express@^4.21.1: + version "4.22.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.22.2.tgz#c17ae0981e5efc24b22272f0e041c4662503b700" + integrity sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q== + dependencies: + accepts "~1.3.8" + array-flatten "1.1.1" + body-parser "~1.20.5" + content-disposition "~0.5.4" + content-type "~1.0.4" + cookie "~0.7.1" + cookie-signature "~1.0.6" + debug "2.6.9" + depd "2.0.0" + encodeurl "~2.0.0" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "~1.3.1" + fresh "~0.5.2" + http-errors "~2.0.0" + merge-descriptors "1.0.3" + methods "~1.1.2" + on-finished "~2.4.1" + parseurl "~1.3.3" + path-to-regexp "~0.1.12" + proxy-addr "~2.0.7" + qs "~6.15.1" + range-parser "~1.2.1" + safe-buffer "5.2.1" + send "~0.19.0" + serve-static "~1.16.2" + setprototypeof "1.2.0" + statuses "~2.0.1" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== + dependencies: + to-regex-range "^5.0.1" + +finalhandler@~1.3.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.3.2.tgz#1ebc2228fc7673aac4a472c310cc05b77d852b88" + integrity sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg== + dependencies: + debug "2.6.9" + encodeurl "~2.0.0" + escape-html "~1.0.3" + on-finished "~2.4.1" + parseurl "~1.3.3" + statuses "~2.0.2" + unpipe "~1.0.0" + +form-data@^4.0.4: + version "4.0.6" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.6.tgz#28e864e1b786dbebb68db1f452f9635278665827" + integrity sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + es-set-tostringtag "^2.1.0" + hasown "^2.0.4" + mime-types "^2.1.35" + +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== + +fresh@~0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== + +fsevents@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +get-intrinsic@^1.2.5, get-intrinsic@^1.2.6, get-intrinsic@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" + integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== + dependencies: + call-bind-apply-helpers "^1.0.2" + es-define-property "^1.0.1" + es-errors "^1.3.0" + es-object-atoms "^1.1.1" + function-bind "^1.1.2" + get-proto "^1.0.1" + gopd "^1.2.0" + has-symbols "^1.1.0" + hasown "^2.0.2" + math-intrinsics "^1.1.0" + +get-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1" + integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== + dependencies: + dunder-proto "^1.0.1" + es-object-atoms "^1.0.0" + +glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +gopd@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" + integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== + +has-symbols@^1.0.3, has-symbols@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" + integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== + +has-tostringtag@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" + integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== + dependencies: + has-symbols "^1.0.3" + +hasown@^2.0.2, hasown@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.4.tgz#8c62d8cb90beb2aad5d0a5b67581ad9854c3f003" + integrity sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A== + dependencies: + function-bind "^1.1.2" + +http-errors@~2.0.0, http-errors@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.1.tgz#36d2f65bc909c8790018dd36fb4d93da6caae06b" + integrity sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ== + dependencies: + depd "~2.0.0" + inherits "~2.0.4" + setprototypeof "~1.2.0" + statuses "~2.0.2" + toidentifier "~1.0.1" + +iconv-lite@~0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +ignore-by-default@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" + integrity sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA== + +inherits@~2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-glob@^4.0.1, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +isomorphic.js@^0.2.4: + version "0.2.5" + resolved "https://registry.yarnpkg.com/isomorphic.js/-/isomorphic.js-0.2.5.tgz#13eecf36f2dba53e85d355e11bf9d4208c6f7f88" + integrity sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw== + +jsonwebtoken@^9.0.2: + version "9.0.3" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz#6cd57ab01e9b0ac07cb847d53d3c9b6ee31f7ae2" + integrity sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g== + dependencies: + jws "^4.0.1" + lodash.includes "^4.3.0" + lodash.isboolean "^3.0.3" + lodash.isinteger "^4.0.4" + lodash.isnumber "^3.0.3" + lodash.isplainobject "^4.0.6" + lodash.isstring "^4.0.1" + lodash.once "^4.0.0" + ms "^2.1.1" + semver "^7.5.4" + +jwa@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-2.0.1.tgz#bf8176d1ad0cd72e0f3f58338595a13e110bc804" + integrity sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg== + dependencies: + buffer-equal-constant-time "^1.0.1" + ecdsa-sig-formatter "1.0.11" + safe-buffer "^5.0.1" + +jws@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/jws/-/jws-4.0.1.tgz#07edc1be8fac20e677b283ece261498bd38f0690" + integrity sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA== + dependencies: + jwa "^2.0.1" + safe-buffer "^5.0.1" + +lib0@^0.2.87, lib0@^0.2.99: + version "0.2.117" + resolved "https://registry.yarnpkg.com/lib0/-/lib0-0.2.117.tgz#6c3f926475d28904af05b590703cbbbc29475716" + integrity sha512-DeXj9X5xDCjgKLU/7RR+/HQEVzuuEUiwldwOGsHK/sfAfELGWEyTcf0x+uOvCvK3O2zPmZePXWL85vtia6GyZw== + dependencies: + isomorphic.js "^0.2.4" + +lodash.includes@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" + integrity sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w== + +lodash.isboolean@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" + integrity sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg== + +lodash.isinteger@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" + integrity sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA== + +lodash.isnumber@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" + integrity sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw== + +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + integrity sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA== + +lodash.isstring@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" + integrity sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw== + +lodash.once@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" + integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg== + +make-error@^1.1.1: + version "1.3.6" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" + integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + +math-intrinsics@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" + integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== + +merge-descriptors@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz#d80319a65f3c7935351e5cfdac8f9318504dbed5" + integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ== + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.35, mime-types@~2.1.24, mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +minimatch@^10.2.1: + version "10.2.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.2.5.tgz#bd48687a0be38ed2961399105600f832095861d1" + integrity sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg== + dependencies: + brace-expansion "^5.0.5" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== + +ms@2.1.3, ms@^2.1.1, ms@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + +node-fetch@2: + version "2.7.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" + integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== + dependencies: + whatwg-url "^5.0.0" + +nodemon@^3.1.9: + version "3.1.14" + resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-3.1.14.tgz#8487ca379c515301d221ec007f27f24ecafa2b51" + integrity sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw== + dependencies: + chokidar "^3.5.2" + debug "^4" + ignore-by-default "^1.0.1" + minimatch "^10.2.1" + pstree.remy "^1.1.8" + semver "^7.5.3" + simple-update-notifier "^2.0.0" + supports-color "^5.5.0" + touch "^3.1.0" + undefsafe "^2.0.5" + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +object-assign@^4: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + +object-inspect@^1.13.3, object-inspect@^1.13.4: + version "1.13.4" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.4.tgz#8375265e21bc20d0fa582c22e1b13485d6e00213" + integrity sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew== + +on-finished@~2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + +parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +path-to-regexp@~0.1.12: + version "0.1.13" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.13.tgz#9b22ec16bc3ab88d05a0c7e369869421401ab17d" + integrity sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA== + +picomatch@^2.0.4, picomatch@^2.2.1: + version "2.3.2" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.2.tgz#5a942915e26b372dc0f0e6753149a16e6b1c5601" + integrity sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA== + +proxy-addr@~2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + dependencies: + forwarded "0.2.0" + ipaddr.js "1.9.1" + +pstree.remy@^1.1.8: + version "1.1.8" + resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.8.tgz#c242224f4a67c21f686839bbdb4ac282b8373d3a" + integrity sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w== + +qs@~6.15.1: + version "6.15.3" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.15.3.tgz#76852132a58ed5c7c0ef67e4441b9bb5d6061b3b" + integrity sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A== + dependencies: + es-define-property "^1.0.1" + side-channel "^1.1.1" + +range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@~2.5.3: + version "2.5.3" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.3.tgz#11c6650ee770a7de1b494f197927de0c923822e2" + integrity sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA== + dependencies: + bytes "~3.1.2" + http-errors "~2.0.1" + iconv-lite "~0.4.24" + unpipe "~1.0.0" + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +safe-buffer@5.2.1, safe-buffer@^5.0.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +"safer-buffer@>= 2.1.2 < 3": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +semver@^7.5.3, semver@^7.5.4: + version "7.8.5" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.8.5.tgz#39b646037dd50c14fb451e7e4cac58ed8b863f69" + integrity sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA== + +send@~0.19.0, send@~0.19.1: + version "0.19.2" + resolved "https://registry.yarnpkg.com/send/-/send-0.19.2.tgz#59bc0da1b4ea7ad42736fd642b1c4294e114ff29" + integrity sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg== + dependencies: + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + encodeurl "~2.0.0" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "~0.5.2" + http-errors "~2.0.1" + mime "1.6.0" + ms "2.1.3" + on-finished "~2.4.1" + range-parser "~1.2.1" + statuses "~2.0.2" + +serve-static@~1.16.2: + version "1.16.3" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.16.3.tgz#a97b74d955778583f3862a4f0b841eb4d5d78cf9" + integrity sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA== + dependencies: + encodeurl "~2.0.0" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "~0.19.1" + +setprototypeof@1.2.0, setprototypeof@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + +side-channel-list@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.1.tgz#c2e0b5a14a540aebee3bbc6c3f8666cc9b509127" + integrity sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.4" + +side-channel-map@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/side-channel-map/-/side-channel-map-1.0.1.tgz#d6bb6b37902c6fef5174e5f533fab4c732a26f42" + integrity sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + +side-channel-weakmap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz#11dda19d5368e40ce9ec2bdc1fb0ecbc0790ecea" + integrity sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + side-channel-map "^1.0.1" + +side-channel@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.1.tgz#ea02c62e05dc4bea67d4442f0fb71ee192f8e0ab" + integrity sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.4" + side-channel-list "^1.0.1" + side-channel-map "^1.0.1" + side-channel-weakmap "^1.0.2" + +simple-update-notifier@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz#d70b92bdab7d6d90dfd73931195a30b6e3d7cebb" + integrity sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w== + dependencies: + semver "^7.5.3" + +statuses@~2.0.1, statuses@~2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.2.tgz#8f75eecef765b5e1cfcdc080da59409ed424e382" + integrity sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw== + +supports-color@^5.5.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +toidentifier@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + +touch@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.1.tgz#097a23d7b161476435e5c1344a95c0f75b4a5694" + integrity sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA== + +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== + +ts-node@^10.9.2: + version "10.9.2" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.2.tgz#70f021c9e185bccdca820e26dc413805c101c71f" + integrity sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ== + dependencies: + "@cspotcode/source-map-support" "^0.8.0" + "@tsconfig/node10" "^1.0.7" + "@tsconfig/node12" "^1.0.7" + "@tsconfig/node14" "^1.0.0" + "@tsconfig/node16" "^1.0.2" + acorn "^8.4.1" + acorn-walk "^8.1.1" + arg "^4.1.0" + create-require "^1.1.0" + diff "^4.0.1" + make-error "^1.1.1" + v8-compile-cache-lib "^3.0.1" + yn "3.1.1" + +type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +typescript@^5.6.2: + version "5.9.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f" + integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw== + +undefsafe@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c" + integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA== + +undici-types@~6.21.0: + version "6.21.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.21.0.tgz#691d00af3909be93a7faa13be61b3a5b50ef12cb" + integrity sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ== + +undici-types@~8.3.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-8.3.0.tgz#44e9fc9f3244648cdea35e4f9bb2d681e9410809" + integrity sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ== + +unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== + +v8-compile-cache-lib@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" + integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== + +vary@^1, vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== + +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + +ws@^8.17.1: + version "8.21.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.21.0.tgz#012e413fc07429945121b0c153158c4343086951" + integrity sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g== + +yjs@^13.6.27: + version "13.6.31" + resolved "https://registry.yarnpkg.com/yjs/-/yjs-13.6.31.tgz#3a9ddfe6d0c5d788521b70dfdd7b907148610418" + integrity sha512-Eq+5BRfbeGyqGVrTJL3bEcr8gKkxPuyuoHmAwpk52fDb8kOVMrfVSTRPd6yiGgX5Fskb96qCRjzjbRjrL4YEnw== + dependencies: + lib0 "^0.2.99" + +yn@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==