@tldraw/sync-core
Version:
tldraw infinite canvas SDK (multiplayer sync).
119 lines (118 loc) • 4.45 kB
JavaScript
;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var chunk_exports = {};
__export(chunk_exports, {
JsonChunkAssembler: () => JsonChunkAssembler,
chunk: () => chunk
});
module.exports = __toCommonJS(chunk_exports);
const MAX_CLIENT_SENT_MESSAGE_SIZE_BYTES = 1024 * 1024;
const MAX_BYTES_PER_CHAR = 4;
const MAX_SAFE_MESSAGE_SIZE = MAX_CLIENT_SENT_MESSAGE_SIZE_BYTES / MAX_BYTES_PER_CHAR;
function chunk(msg, maxSafeMessageSize = MAX_SAFE_MESSAGE_SIZE) {
if (msg.length < maxSafeMessageSize) {
return [msg];
} else {
const chunks = [];
let chunkNumber = 0;
let offset = msg.length;
while (offset > 0) {
const prefix = `${chunkNumber}_`;
const chunkSize = Math.max(Math.min(maxSafeMessageSize - prefix.length, offset), 1);
chunks.unshift(prefix + msg.slice(offset - chunkSize, offset));
offset -= chunkSize;
chunkNumber++;
}
return chunks;
}
}
const chunkRe = /^(\d+)_(.*)$/;
class JsonChunkAssembler {
/**
* Current assembly state - either 'idle' or tracking chunks being received
*/
state = "idle";
/**
* Processes a single message, which can be either a complete JSON object or a chunk.
* For complete JSON objects (starting with '\{'), parses immediately.
* For chunks (prefixed with "\{number\}_"), accumulates until all chunks received.
*
* @param msg - The message to process, either JSON or chunk format
* @returns Result object with data/stringified on success, error object on failure, or null for incomplete chunks
* - `\{ data: object, stringified: string \}` - Successfully parsed complete message
* - `\{ error: Error \}` - Parse error or invalid chunk sequence
* - `null` - Chunk received but more chunks expected
*
* @example
* ```ts
* const assembler = new JsonChunkAssembler()
*
* // Complete JSON message
* const result = assembler.handleMessage('{"key": "value"}')
* if (result && 'data' in result) {
* console.log(result.data) // { key: "value" }
* }
*
* // Chunked message sequence
* assembler.handleMessage('2_hel') // null - more chunks expected
* assembler.handleMessage('1_lo ') // null - more chunks expected
* assembler.handleMessage('0_wor') // { data: "hello wor", stringified: "hello wor" }
* ```
*/
handleMessage(msg) {
if (msg.startsWith("{")) {
const error = this.state === "idle" ? void 0 : new Error("Unexpected non-chunk message");
this.state = "idle";
return error ? { error } : { data: JSON.parse(msg), stringified: msg };
} else {
const match = chunkRe.exec(msg);
if (!match) {
this.state = "idle";
return { error: new Error("Invalid chunk: " + JSON.stringify(msg.slice(0, 20) + "...")) };
}
const numChunksRemaining = Number(match[1]);
const data = match[2];
if (this.state === "idle") {
this.state = {
chunksReceived: [data],
totalChunks: numChunksRemaining + 1
};
} else {
this.state.chunksReceived.push(data);
if (numChunksRemaining !== this.state.totalChunks - this.state.chunksReceived.length) {
this.state = "idle";
return { error: new Error(`Chunks received in wrong order`) };
}
}
if (this.state.chunksReceived.length === this.state.totalChunks) {
try {
const stringified = this.state.chunksReceived.join("");
const data2 = JSON.parse(stringified);
return { data: data2, stringified };
} catch (e) {
return { error: e };
} finally {
this.state = "idle";
}
}
return null;
}
}
}
//# sourceMappingURL=chunk.js.map