{
  "version": 3,
  "sources": ["../../src/lib/chunk.ts"],
  "sourcesContent": ["// quarter of a megabyte, max possible utf-8 string size\n\n// cloudflare workers only accept messages of max 1mb\nconst MAX_CLIENT_SENT_MESSAGE_SIZE_BYTES = 1024 * 1024\n// utf-8 is max 4 bytes per char\nconst MAX_BYTES_PER_CHAR = 4\n\n// in the (admittedly impossible) worst case, the max size is 1/4 of a megabyte\nconst MAX_SAFE_MESSAGE_SIZE = MAX_CLIENT_SENT_MESSAGE_SIZE_BYTES / MAX_BYTES_PER_CHAR\n\n// JsonChunkAssembler holds every chunk of a message until the last one arrives, and the sender\n// decides how many that is \u2014 the count is just a prefix on the first chunk. Nothing in the\n// protocol obliges a sender to ever send the last chunk, so without a ceiling a single connection\n// can make the receiver hold an arbitrarily large buffer for as long as the socket stays open.\n//\n// The cap counts UTF-16 code units, which is what a JS string costs in memory: 16M of them is the\n// 32MB of retained string data a room can afford to hold per session and stay inside a Durable\n// Object's 128MB instance limit with room for the document itself. It is far above anything\n// chunk() produces for a real document push. The character cap is the real bound; the chunk-count\n// cap only rejects an absurd declared count on arrival instead of waiting for the characters to\n// accumulate, and is loose enough not to constrain a sender that chunks more finely than the\n// default.\nexport const MAX_ASSEMBLED_MESSAGE_CHARS = 16 * 1024 * 1024\nexport const MAX_CHUNK_COUNT = 100_000\n\n/**\n * Thrown when a message would exceed MAX_ASSEMBLED_MESSAGE_CHARS, on either side of the wire.\n * Distinct from the other assembly errors because it is fatal: retrying sends the same\n * oversized message, so the session is rejected rather than reset.\n *\n * @internal\n */\nexport class MessageTooLargeError extends Error {\n\tconstructor(chars: number) {\n\t\tsuper(`Message too large: ${chars} characters, max ${MAX_ASSEMBLED_MESSAGE_CHARS}`)\n\t\tthis.name = 'MessageTooLargeError'\n\t}\n}\n\n/**\n * Splits a string into smaller chunks suitable for transmission over WebSockets.\n * This function ensures messages don't exceed size limits imposed by platforms like Cloudflare Workers (1MB max).\n * Each chunk is prefixed with a number indicating how many more chunks follow.\n *\n * @param msg - The string to split into chunks\n * @param maxSafeMessageSize - Maximum safe size for each chunk in characters. Defaults to quarter megabyte to account for UTF-8 encoding\n * @returns Array of chunked strings, each prefixed with \"\\{number\\}_\" where number indicates remaining chunks\n *\n * @example\n * ```ts\n * // Small message - returns as single chunk\n * chunk('hello world') // ['hello world']\n *\n * // Large message - splits into multiple chunks\n * chunk('very long message...', 10)\n * // ['2_very long', '1_ message', '0_...']\n * ```\n *\n * @internal\n */\nexport function chunk(msg: string, maxSafeMessageSize = MAX_SAFE_MESSAGE_SIZE) {\n\tif (msg.length < maxSafeMessageSize) {\n\t\treturn [msg]\n\t} else {\n\t\tconst chunks = []\n\t\tlet chunkNumber = 0\n\t\tlet offset = msg.length\n\t\twhile (offset > 0) {\n\t\t\tconst prefix = `${chunkNumber}_`\n\t\t\tlet chunkSize = Math.max(Math.min(maxSafeMessageSize - prefix.length, offset), 1)\n\t\t\t// Never split a surrogate pair across chunks: WebSocket.send converts each chunk to a\n\t\t\t// USVString, turning a lone surrogate into U+FFFD, so the reassembled JSON silently\n\t\t\t// carries corrupted text. Shrink by one so the pair moves whole into the preceding\n\t\t\t// chunk; a single-character chunk grows by one instead (one over the CH3 bound, see CH9).\n\t\t\tif (\n\t\t\t\tchunkSize < offset &&\n\t\t\t\tisLowSurrogate(msg.charCodeAt(offset - chunkSize)) &&\n\t\t\t\tisHighSurrogate(msg.charCodeAt(offset - chunkSize - 1))\n\t\t\t) {\n\t\t\t\tchunkSize += chunkSize > 1 ? -1 : 1\n\t\t\t}\n\t\t\tchunks.unshift(prefix + msg.slice(offset - chunkSize, offset))\n\t\t\toffset -= chunkSize\n\t\t\tchunkNumber++\n\t\t}\n\t\treturn chunks\n\t}\n}\n\nfunction isHighSurrogate(code: number) {\n\treturn code >= 0xd800 && code <= 0xdbff\n}\n\nfunction isLowSurrogate(code: number) {\n\treturn code >= 0xdc00 && code <= 0xdfff\n}\n\n// The 's' flag (dotAll) makes '.' match any character including line terminators\n// like U+2028 and U+2029, which are commonly introduced via copy/paste from Word\nconst chunkRe = /^(\\d+)_(.*)$/s\n\n/**\n * Assembles chunked JSON messages back into complete objects.\n * Handles both regular JSON messages and chunked messages created by the chunk() function.\n * Maintains internal state to track partially received chunked messages.\n *\n * @example\n * ```ts\n * const assembler = new JsonChunkAssembler()\n *\n * // Handle regular JSON message\n * const result1 = assembler.handleMessage('{\"hello\": \"world\"}')\n * // Returns: { data: { hello: \"world\" }, stringified: '{\"hello\": \"world\"}' }\n *\n * // Handle chunked message\n * assembler.handleMessage('1_hello') // Returns: null (partial)\n * const result2 = assembler.handleMessage('0_ world')\n * // Returns: { data: \"hello world\", stringified: \"hello world\" }\n * ```\n *\n * @public\n */\nexport class JsonChunkAssembler {\n\t/**\n\t * Current assembly state - either 'idle' or tracking chunks being received\n\t */\n\tstate:\n\t\t| 'idle'\n\t\t| {\n\t\t\t\tchunksReceived: string[]\n\t\t\t\ttotalChunks: number\n\t\t\t\tcharsReceived: number\n\t\t  } = 'idle'\n\n\t/**\n\t * Processes a single message, which can be either a complete JSON object or a chunk.\n\t * For complete JSON objects (starting with '\\{'), parses immediately.\n\t * For chunks (prefixed with \"\\{number\\}_\"), accumulates until all chunks received.\n\t *\n\t * @param msg - The message to process, either JSON or chunk format\n\t * @returns Result object with data/stringified on success, error object on failure, or null for incomplete chunks\n\t * \t- `\\{ data: object, stringified: string \\}` - Successfully parsed complete message\n\t * \t- `\\{ error: Error \\}` - Parse error, invalid chunk sequence, or a message exceeding\n\t * \t  MAX_ASSEMBLED_MESSAGE_CHARS / MAX_CHUNK_COUNT\n\t * \t- `null` - Chunk received but more chunks expected\n\t *\n\t * @example\n\t * ```ts\n\t * const assembler = new JsonChunkAssembler()\n\t *\n\t * // Complete JSON message\n\t * const result = assembler.handleMessage('{\"key\": \"value\"}')\n\t * if (result && 'data' in result) {\n\t *   console.log(result.data) // { key: \"value\" }\n\t * }\n\t *\n\t * // Chunked message sequence\n\t * assembler.handleMessage('2_hel') // null - more chunks expected\n\t * assembler.handleMessage('1_lo ') // null - more chunks expected\n\t * assembler.handleMessage('0_wor') // { data: \"hello wor\", stringified: \"hello wor\" }\n\t * ```\n\t */\n\thandleMessage(msg: string): { error: Error } | { stringified: string; data: object } | null {\n\t\tif (msg.startsWith('{')) {\n\t\t\tconst error = this.state === 'idle' ? undefined : new Error('Unexpected non-chunk message')\n\t\t\tthis.state = 'idle'\n\t\t\treturn error ? { error } : { data: JSON.parse(msg), stringified: msg }\n\t\t} else {\n\t\t\tconst match = chunkRe.exec(msg)!\n\t\t\tif (!match) {\n\t\t\t\tthis.state = 'idle'\n\t\t\t\treturn { error: new Error('Invalid chunk: ' + JSON.stringify(msg.slice(0, 20) + '...')) }\n\t\t\t}\n\t\t\tconst numChunksRemaining = Number(match[1])\n\t\t\tconst data = match[2]\n\n\t\t\tif (this.state === 'idle') {\n\t\t\t\tconst totalChunks = numChunksRemaining + 1\n\t\t\t\tif (totalChunks > MAX_CHUNK_COUNT) {\n\t\t\t\t\t// Nothing has been buffered yet, so the state is already idle.\n\t\t\t\t\treturn { error: new Error(`Too many chunks: ${totalChunks}`) }\n\t\t\t\t}\n\t\t\t\tthis.state = {\n\t\t\t\t\tchunksReceived: [data],\n\t\t\t\t\ttotalChunks,\n\t\t\t\t\tcharsReceived: data.length,\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tthis.state.chunksReceived.push(data)\n\t\t\t\tthis.state.charsReceived += data.length\n\t\t\t\tif (numChunksRemaining !== this.state.totalChunks - this.state.chunksReceived.length) {\n\t\t\t\t\tthis.state = 'idle'\n\t\t\t\t\treturn { error: new Error(`Chunks received in wrong order`) }\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (this.state.charsReceived > MAX_ASSEMBLED_MESSAGE_CHARS) {\n\t\t\t\t// Drop what was buffered rather than keeping it until the socket closes.\n\t\t\t\tconst charsReceived = this.state.charsReceived\n\t\t\t\tthis.state = 'idle'\n\t\t\t\treturn { error: new MessageTooLargeError(charsReceived) }\n\t\t\t}\n\t\t\tif (this.state.chunksReceived.length === this.state.totalChunks) {\n\t\t\t\ttry {\n\t\t\t\t\tconst stringified = this.state.chunksReceived.join('')\n\t\t\t\t\tconst data = JSON.parse(stringified)\n\t\t\t\t\treturn { data, stringified }\n\t\t\t\t} catch (e) {\n\t\t\t\t\treturn { error: e as Error }\n\t\t\t\t} finally {\n\t\t\t\t\tthis.state = 'idle'\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn null\n\t\t}\n\t}\n}\n"],
  "mappings": "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",
  "names": ["data"]
}
