diff --git a/packages/excalidraw/cloudflare/repository.ts b/packages/excalidraw/cloudflare/repository.ts index ab2b4e2e25..2be3390194 100644 --- a/packages/excalidraw/cloudflare/repository.ts +++ b/packages/excalidraw/cloudflare/repository.ts @@ -31,6 +31,7 @@ export class DurableDeltasRepository implements DeltasRepository { } try { + // CFDO: could be also a buffer const payload = JSON.stringify(delta); const payloadSize = new TextEncoder().encode(payload).byteLength; const nextVersion = this.getLastVersion() + 1; @@ -113,6 +114,7 @@ export class DurableDeltasRepository implements DeltasRepository { return restoredDeltas[0]; } + // CFDO: fix types (should be buffer in the first place) private restoreServerDeltas(deltas: SERVER_DELTA[]): SERVER_DELTA[] { return Array.from( deltas @@ -137,6 +139,10 @@ export class DurableDeltasRepository implements DeltasRepository { return acc; }, new Map()) .values(), - ); + // CFDO: temporary + ).map((delta) => ({ + ...delta, + payload: JSON.parse(delta.payload), + })); } } diff --git a/packages/excalidraw/cloudflare/room.ts b/packages/excalidraw/cloudflare/room.ts index f7ac981054..332a993a3f 100644 --- a/packages/excalidraw/cloudflare/room.ts +++ b/packages/excalidraw/cloudflare/room.ts @@ -46,7 +46,7 @@ export class DurableRoom extends DurableObject { public fetch = async (request: Request): Promise => this.connect(request); - public webSocketMessage = (client: WebSocket, message: string) => + public webSocketMessage = (client: WebSocket, message: ArrayBuffer) => this.sync.onMessage(client, message); public webSocketClose = (ws: WebSocket) => this.sync.onDisconnect(ws); diff --git a/packages/excalidraw/package.json b/packages/excalidraw/package.json index fae33ef5cd..daa54c3060 100644 --- a/packages/excalidraw/package.json +++ b/packages/excalidraw/package.json @@ -76,6 +76,7 @@ "jotai-scope": "0.7.2", "lodash.debounce": "4.0.8", "lodash.throttle": "4.1.1", + "msgpack-lite": "0.1.26", "nanoid": "3.3.3", "open-color": "1.9.1", "pako": "1.0.11", @@ -106,6 +107,7 @@ "@testing-library/jest-dom": "5.16.2", "@testing-library/react": "16.0.0", "@types/async-lock": "^1.4.2", + "@types/msgpack-lite": "0.1.11", "@types/pako": "1.0.3", "@types/pica": "5.1.3", "@types/resize-observer-browser": "0.1.7", diff --git a/packages/excalidraw/store.ts b/packages/excalidraw/store.ts index 10a5821050..843ed8453b 100644 --- a/packages/excalidraw/store.ts +++ b/packages/excalidraw/store.ts @@ -12,6 +12,7 @@ import type { OrderedExcalidrawElement, SceneElementsMap, } from "./element/types"; +import type { SERVER_DELTA } from "./sync/protocol"; import { arrayToMap, assertNever } from "./utils"; import { hashElementsVersion } from "./element"; import { syncMovedIndices } from "./fractionalIndex"; @@ -449,14 +450,11 @@ export class StoreDelta { /** * Parse and load the delta from the remote payload. */ - // CFDO: why it would be a string if it can be a DTO? - public static load(payload: string) { + public static load({ + id, + elements: { added, removed, updated }, + }: SERVER_DELTA["payload"]) { // CFDO: ensure typesafety - const { - id, - elements: { added, removed, updated }, - } = JSON.parse(payload); - const elements = ElementsDelta.create(added, removed, updated, { shouldRedistribute: false, }); diff --git a/packages/excalidraw/sync/client.ts b/packages/excalidraw/sync/client.ts index a23d2768d5..ab5e413f6c 100644 --- a/packages/excalidraw/sync/client.ts +++ b/packages/excalidraw/sync/client.ts @@ -1,5 +1,6 @@ /* eslint-disable no-console */ import throttle from "lodash.throttle"; +import msgpack from "msgpack-lite"; import ReconnectingWebSocket, { type Event, type CloseEvent, @@ -14,7 +15,12 @@ import { StoreAction, StoreDelta } from "../store"; import type { StoreChange } from "../store"; import type { ExcalidrawImperativeAPI } from "../types"; import type { ExcalidrawElement, SceneElementsMap } from "../element/types"; -import type { CLIENT_MESSAGE_RAW, SERVER_DELTA, CHANGE } from "./protocol"; +import type { + CLIENT_MESSAGE_RAW, + SERVER_DELTA, + CHANGE, + SERVER_MESSAGE, +} from "./protocol"; import { debounce } from "../utils"; import { randomId } from "../random"; import { orderByFractionalIndex } from "../fractionalIndex"; @@ -22,7 +28,7 @@ import { orderByFractionalIndex } from "../fractionalIndex"; class SocketMessage implements CLIENT_MESSAGE_RAW { constructor( public readonly type: "relay" | "pull" | "push", - public readonly payload: string, + public readonly payload: Uint8Array, public readonly chunkInfo?: { id: string; position: number; @@ -49,7 +55,7 @@ class SocketClient { private readonly handlers: { onOpen: (event: Event) => void; onOnline: () => void; - onMessage: (event: MessageEvent) => void; + onMessage: (message: SERVER_MESSAGE) => void; }, ) {} @@ -138,12 +144,12 @@ class SocketClient { const { type, payload } = message; // CFDO II: could be slowish for large payloads, thing about a better solution (i.e. msgpack 10x faster, 2x smaller) - const stringifiedPayload = JSON.stringify(payload); - const payloadSize = new TextEncoder().encode(stringifiedPayload).byteLength; + const payloadBuffer = msgpack.encode(payload) as Uint8Array; + const payloadSize = payloadBuffer.byteLength; if (payloadSize < SocketClient.MAX_MESSAGE_SIZE) { - const message = new SocketMessage(type, stringifiedPayload); - return this.socket?.send(JSON.stringify(message)); + const message = new SocketMessage(type, payloadBuffer); + return this.sendMessage(message); } const chunkId = randomId(); @@ -153,19 +159,25 @@ class SocketClient { for (let position = 0; position < chunksCount; position++) { const start = position * chunkSize; const end = start + chunkSize; - const chunkedPayload = stringifiedPayload.slice(start, end); + const chunkedPayload = payloadBuffer.subarray(start, end); const message = new SocketMessage(type, chunkedPayload, { id: chunkId, position, count: chunksCount, }); - this.socket?.send(JSON.stringify(message)); + this.sendMessage(message); } } private onMessage = (event: MessageEvent) => { - this.handlers.onMessage(event); + this.receiveMessage(event.data).then((message) => { + if (!message) { + return; + } + + this.handlers.onMessage(message); + }); }; private onOpen = (event: Event) => { @@ -184,6 +196,68 @@ class SocketClient { event, ); }; + + private sendMessage = ({ payload, ...metadata }: CLIENT_MESSAGE_RAW) => { + const metadataBuffer = msgpack.encode(metadata) as Uint8Array; + + // contains the length of the rest of the message, so that we could decode it server side + const headerBuffer = new ArrayBuffer(4); + new DataView(headerBuffer).setUint32(0, metadataBuffer.byteLength); + + // concatenate into [header(4 bytes)][metadata][payload] + const message = Uint8Array.from([ + ...new Uint8Array(headerBuffer), + ...metadataBuffer, + ...payload, + ]); + + // CFDO: add dev-level logging + { + const headerLength = 4; + const header = new Uint8Array(message.buffer, 0, headerLength); + const metadataLength = new DataView( + header.buffer, + header.byteOffset, + ).getUint32(0); + + const metadata = new Uint8Array( + message.buffer, + headerLength, + headerLength + metadataLength, + ); + + const payload = new Uint8Array( + message.buffer, + headerLength + metadataLength, + ); + + console.log({ + ...msgpack.decode(metadata), + payload, + }); + } + + this.socket?.send(message); + }; + + private async receiveMessage( + message: Blob, + ): Promise { + const arrayBuffer = await message.arrayBuffer(); + const uint8Array = new Uint8Array(arrayBuffer); + + const [decodedMessage, decodeError] = Utils.try(() => + msgpack.decode(uint8Array), + ); + + if (decodeError) { + console.error("Failed to decode message:", message); + return; + } + + // CFDO: should be type-safe + return decodedMessage; + } } interface AcknowledgedDelta { @@ -351,23 +425,17 @@ export class SyncClient { this.push(); }; - private onMessage = (event: MessageEvent) => { - // CFDO: could be an array buffer - const [result, error] = Utils.try(() => JSON.parse(event.data as string)); + private onMessage = ({ type, payload }: SERVER_MESSAGE) => { + // CFDO: add dev-level logging + console.log({ type, payload }); - if (error) { - console.error("Failed to parse message:", event.data); - return; - } - - const { type, payload } = result; switch (type) { case "relayed": return this.handleRelayed(payload); case "acknowledged": return this.handleAcknowledged(payload); - case "rejected": - return this.handleRejected(payload); + // case "rejected": + // return this.handleRejected(payload); default: console.error("Unknown message type:", type); } @@ -499,7 +567,7 @@ export class SyncClient { private handleRejected = (payload: { ids: Array; - message: string; + message: Uint8Array; }) => { // handle rejected deltas console.error("Rejected message received:", payload); diff --git a/packages/excalidraw/sync/protocol.ts b/packages/excalidraw/sync/protocol.ts index 844fc078ed..bb9edabe42 100644 --- a/packages/excalidraw/sync/protocol.ts +++ b/packages/excalidraw/sync/protocol.ts @@ -16,7 +16,7 @@ export type CHUNK_INFO = { export type CLIENT_MESSAGE_RAW = { type: "relay" | "pull" | "push"; - payload: string; + payload: Uint8Array; chunkInfo?: CHUNK_INFO; }; @@ -26,7 +26,12 @@ export type CLIENT_MESSAGE = { chunkInfo: CHUNK_INFO } & ( | { type: "push"; payload: PUSH_PAYLOAD } ); -export type SERVER_DELTA = { id: string; version: number; payload: string }; +export type SERVER_DELTA = { + id: string; + version: number; + // CFDO: should be type-safe + payload: Record; +}; export type SERVER_MESSAGE = | { type: "relayed"; diff --git a/packages/excalidraw/sync/server.ts b/packages/excalidraw/sync/server.ts index 934f388610..8a5181ec68 100644 --- a/packages/excalidraw/sync/server.ts +++ b/packages/excalidraw/sync/server.ts @@ -1,4 +1,5 @@ import AsyncLock from "async-lock"; +import msgpack from "msgpack-lite"; import { Utils } from "./utils"; import type { @@ -37,10 +38,35 @@ export class ExcalidrawSyncServer { this.sessions.delete(client); } - public onMessage(client: WebSocket, message: string): Promise | void { + public onMessage( + client: WebSocket, + message: ArrayBuffer, + ): Promise | void { const [parsedMessage, parseMessageError] = Utils.try( () => { - return JSON.parse(message); + const headerLength = 4; + const header = new Uint8Array(message, 0, headerLength); + const metadataLength = new DataView( + header.buffer, + header.byteOffset, + ).getUint32(0); + + const metadata = new Uint8Array( + message, + headerLength, + headerLength + metadataLength, + ); + + const payload = new Uint8Array(message, headerLength + metadataLength); + const parsed = { + ...msgpack.decode(metadata), + payload, + }; + + // CFDO: add dev-level logging + console.log({ parsed }); + + return parsed; }, ); @@ -56,29 +82,7 @@ export class ExcalidrawSyncServer { return this.processChunks(client, { type, payload, chunkInfo }); } - const [parsedPayload, parsePayloadError] = Utils.try< - CLIENT_MESSAGE["payload"] - >(() => JSON.parse(payload)); - - if (parsePayloadError) { - console.error(parsePayloadError); - return; - } - - switch (type) { - case "relay": - return this.relay(client, parsedPayload as RELAY_PAYLOAD); - case "pull": - return this.pull(client, parsedPayload as PULL_PAYLOAD); - case "push": - // apply each one-by-one to avoid race conditions - // CFDO: in theory we do not need to block ephemeral appState changes - return this.lock.acquire("push", () => - this.push(client, parsedPayload as PUSH_PAYLOAD), - ); - default: - console.error(`Unknown message type: ${type}`); - } + return this.processMessage(client, parsedMessage); } /** @@ -118,16 +122,18 @@ export class ExcalidrawSyncServer { // hopefully we can fit into the 128 MiB memory limit const restoredPayload = Array.from(chunks) - .sort((a, b) => (a <= b ? -1 : 1)) - .reduce((acc, [_, payload]) => (acc += payload), ""); + .sort(([positionA], [positionB]) => (positionA <= positionB ? -1 : 1)) + .reduce( + (acc, [_, payload]) => Uint8Array.from([...acc, ...payload]), + new Uint8Array(), + ); - const rawMessage = JSON.stringify({ + const rawMessage = { type, payload: restoredPayload, - } as CLIENT_MESSAGE_RAW); + }; - // process the message - return this.onMessage(client, rawMessage); + return this.processMessage(client, rawMessage); } catch (error) { console.error(`Error while processing chunk "${id}"`, error); } finally { @@ -138,6 +144,35 @@ export class ExcalidrawSyncServer { } } + private processMessage( + client: WebSocket, + { type, payload }: Omit, + ) { + const [parsedPayload, parsePayloadError] = Utils.try< + CLIENT_MESSAGE["payload"] + >(() => msgpack.decode(payload)); + + if (parsePayloadError) { + console.error(parsePayloadError); + return; + } + + switch (type) { + case "relay": + return this.relay(client, parsedPayload as RELAY_PAYLOAD); + case "pull": + return this.pull(client, parsedPayload as PULL_PAYLOAD); + case "push": + // apply each one-by-one to avoid race conditions + // CFDO: in theory we do not need to block ephemeral appState changes + return this.lock.acquire("push", () => + this.push(client, parsedPayload as PUSH_PAYLOAD), + ); + default: + console.error(`Unknown message type: ${type}`); + } + } + private relay(client: WebSocket, payload: RELAY_PAYLOAD) { // CFDO: we should likely apply these to the snapshot return this.broadcast( @@ -205,19 +240,34 @@ export class ExcalidrawSyncServer { } private send(client: WebSocket, message: SERVER_MESSAGE) { - const msg = JSON.stringify(message); - client.send(msg); + const [encodedMessage, encodeError] = Utils.try(() => + msgpack.encode(message), + ); + + if (encodeError) { + console.error(encodeError); + return; + } + + client.send(encodedMessage); } private broadcast(message: SERVER_MESSAGE, exclude?: WebSocket) { - const msg = JSON.stringify(message); + const [encodedMessage, encodeError] = Utils.try(() => + msgpack.encode(message), + ); + + if (encodeError) { + console.error(encodeError); + return; + } for (const ws of this.sessions) { if (ws === exclude) { continue; } - ws.send(msg); + ws.send(encodedMessage); } } } diff --git a/yarn.lock b/yarn.lock index f9b190b362..5caac2433a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3412,6 +3412,13 @@ resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.34.tgz#10964ba0dee6ac4cd462e2795b6bebd407303433" integrity sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g== +"@types/msgpack-lite@0.1.11": + version "0.1.11" + resolved "https://registry.yarnpkg.com/@types/msgpack-lite/-/msgpack-lite-0.1.11.tgz#f618e1fc469577f65f36c474ff3309407afef174" + integrity sha512-cdCZS/gw+jIN22I4SUZUFf1ZZfVv5JM1//Br/MuZcI373sxiy3eSSoiyLu0oz+BPatTbGGGBO5jrcvd0siCdTQ== + dependencies: + "@types/node" "*" + "@types/node-forge@^1.3.0": version "1.3.11" resolved "https://registry.yarnpkg.com/@types/node-forge/-/node-forge-1.3.11.tgz#0972ea538ddb0f4d9c2fa0ec5db5724773a604da" @@ -6440,6 +6447,11 @@ esutils@^2.0.2: resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== +event-lite@^0.1.1: + version "0.1.3" + resolved "https://registry.yarnpkg.com/event-lite/-/event-lite-0.1.3.tgz#3dfe01144e808ac46448f0c19b4ab68e403a901d" + integrity sha512-8qz9nOz5VeD2z96elrEKD2U433+L3DWdUdDkOINLGOJvx1GsMBbMn0aCeu28y8/e85A6mCigBiFlYMnTBEGlSw== + eventemitter3@^4.0.0: version "4.0.7" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" @@ -7158,7 +7170,7 @@ idb@^7.0.1: resolved "https://registry.yarnpkg.com/idb/-/idb-7.1.1.tgz#d910ded866d32c7ced9befc5bfdf36f572ced72b" integrity sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ== -ieee754@^1.1.13: +ieee754@^1.1.13, ieee754@^1.1.8: version "1.2.1" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== @@ -7234,6 +7246,11 @@ inherits@2, inherits@^2.0.3, inherits@^2.0.4: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== +int64-buffer@^0.1.9: + version "0.1.10" + resolved "https://registry.yarnpkg.com/int64-buffer/-/int64-buffer-0.1.10.tgz#277b228a87d95ad777d07c13832022406a473423" + integrity sha512-v7cSY1J8ydZ0GyjUHqF+1bshJ6cnEVLo9EnjB8p+4HDRPZc9N5jjmvUV7NvEsqQOKyH0pmIBFWXVQbiS0+OBbA== + internal-slot@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.7.tgz#c06dcca3ed874249881007b0a5523b172a190802" @@ -7496,6 +7513,11 @@ is-weakset@^2.0.3: call-bind "^1.0.7" get-intrinsic "^1.2.4" +isarray@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== + isarray@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" @@ -8515,6 +8537,16 @@ ms@^2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== +msgpack-lite@0.1.26: + version "0.1.26" + resolved "https://registry.yarnpkg.com/msgpack-lite/-/msgpack-lite-0.1.26.tgz#dd3c50b26f059f25e7edee3644418358e2a9ad89" + integrity sha512-SZ2IxeqZ1oRFGo0xFGbvBJWMp3yLIY9rlIJyxy8CGrwZn1f0ZK4r6jV/AM1r0FZMDUkWkglOk/eeKIL9g77Nxw== + dependencies: + event-lite "^0.1.1" + ieee754 "^1.1.8" + int64-buffer "^0.1.9" + isarray "^1.0.0" + multimath@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/multimath/-/multimath-2.0.0.tgz#0d37acf67c328f30e3d8c6b0d3209e6082710302" @@ -10089,8 +10121,7 @@ string-natural-compare@^3.0.1: resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4" integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw== -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.2.3: - name string-width-cjs +"string-width-cjs@npm:string-width@^4.2.0": version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -10108,6 +10139,15 @@ string-width@^4.1.0, string-width@^4.2.0: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.0" +string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + string-width@^5.0.0, string-width@^5.0.1, string-width@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" @@ -10179,7 +10219,14 @@ stringify-object@^3.3.0: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@6.0.1, strip-ansi@^3.0.0, strip-ansi@^6.0.0, strip-ansi@^6.0.1, strip-ansi@^7.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@6.0.1, strip-ansi@^3.0.0, strip-ansi@^6.0.0, strip-ansi@^6.0.1, strip-ansi@^7.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -11481,8 +11528,7 @@ wrangler@^3.60.3: optionalDependencies: fsevents "~2.3.2" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: - name wrap-ansi-cjs +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -11500,6 +11546,15 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"