import { Unpackr, mult10, C1Type, typedArrays, addExtension as unpackAddExtension } from './unpack.js'; let textEncoder; try { textEncoder = new TextEncoder(); } catch (error) {} let extensions, extensionClasses; const hasNodeBuffer = typeof Buffer !== 'undefined'; const ByteArrayAllocate = hasNodeBuffer ? function(length) { return Buffer.allocUnsafeSlow(length); } : Uint8Array; const ByteArray = hasNodeBuffer ? Buffer : Uint8Array; const MAX_BUFFER_SIZE = hasNodeBuffer ? 0x100000000 : 0x7fd00000; let target, keysTarget; let targetView; let position = 0; let safeEnd; let bundledStrings = null; const MAX_BUNDLE_SIZE = 0x5500; // maximum characters such that the encoded bytes fits in 16 bits. const hasNonLatin = /[\u0080-\uFFFF]/; export const RECORD_SYMBOL = Symbol('record-id'); export class Packr extends Unpackr { constructor(options) { super(options); this.offset = 0; let typeBuffer; let start; let hasSharedUpdate; let structures; let referenceMap; let encodeUtf8 = ByteArray.prototype.utf8Write ? function(string, position) { return target.utf8Write(string, position, target.byteLength - position); } : (textEncoder && textEncoder.encodeInto) ? function(string, position) { return textEncoder.encodeInto(string, target.subarray(position)).written; } : false; let packr = this; if (!options) options = {}; let isSequential = options && options.sequential; let hasSharedStructures = options.structures || options.saveStructures; let maxSharedStructures = options.maxSharedStructures; if (maxSharedStructures == null) maxSharedStructures = hasSharedStructures ? 32 : 0; if (maxSharedStructures > 8160) throw new Error('Maximum maxSharedStructure is 8160'); if (options.structuredClone && options.moreTypes == undefined) { this.moreTypes = true; } let maxOwnStructures = options.maxOwnStructures; if (maxOwnStructures == null) maxOwnStructures = hasSharedStructures ? 32 : 64; if (!this.structures && options.useRecords != false) this.structures = []; // two byte record ids for shared structures let useTwoByteRecords = maxSharedStructures > 32 || (maxOwnStructures + maxSharedStructures > 64); let sharedLimitId = maxSharedStructures + 0x40; let maxStructureId = maxSharedStructures + maxOwnStructures + 0x40; if (maxStructureId > 8256) { throw new Error('Maximum maxSharedStructure + maxOwnStructure is 8192'); } let recordIdsToRemove = []; let transitionsCount = 0; let serializationsSinceTransitionRebuild = 0; this.pack = this.encode = function(value, encodeOptions) { if (!target) { target = new ByteArrayAllocate(8192); targetView = target.dataView || (target.dataView = new DataView(target.buffer, 0, 8192)); position = 0; } safeEnd = target.length - 10; if (safeEnd - position < 0x800) { // don't start too close to the end, target = new ByteArrayAllocate(target.length); targetView = target.dataView || (target.dataView = new DataView(target.buffer, 0, target.length)); safeEnd = target.length - 10; position = 0; } else position = (position + 7) & 0x7ffffff8; // Word align to make any future copying of this buffer faster start = position; if (encodeOptions & RESERVE_START_SPACE) position += (encodeOptions & 0xff); referenceMap = packr.structuredClone ? new Map() : null; if (packr.bundleStrings && typeof value !== 'string') { bundledStrings = []; bundledStrings.size = Infinity; // force a new bundle start on first string } else bundledStrings = null; structures = packr.structures; if (structures) { if (structures.uninitialized) structures = packr._mergeStructures(packr.getStructures()); let sharedLength = structures.sharedLength || 0; if (sharedLength > maxSharedStructures) { //if (maxSharedStructures <= 32 && structures.sharedLength > 32) // TODO: could support this, but would need to update the limit ids throw new Error('Shared structures is larger than maximum shared structures, try increasing maxSharedStructures to ' + structures.sharedLength); } if (!structures.transitions) { // rebuild our structure transitions structures.transitions = Object.create(null); for (let i = 0; i < sharedLength; i++) { let keys = structures[i]; if (!keys) continue; let nextTransition, transition = structures.transitions; for (let j = 0, l = keys.length; j < l; j++) { let key = keys[j]; nextTransition = transition[key]; if (!nextTransition) { nextTransition = transition[key] = Object.create(null); } transition = nextTransition; } transition[RECORD_SYMBOL] = i + 0x40; } this.lastNamedStructuresLength = sharedLength; } if (!isSequential) { structures.nextId = sharedLength + 0x40; } } if (hasSharedUpdate) hasSharedUpdate = false; let encodingError; try { if (packr._writeStruct && value && typeof value === 'object') { if (value.constructor === Object) writeStruct(value); // simple object else if (value.constructor !== Map && !Array.isArray(value) && !extensionClasses.some(extClass => value instanceof extClass)) { // allow user classes, if they don't need special handling (but do use toJSON if available) writeStruct(value.toJSON ? value.toJSON() : value); } else pack(value); } else pack(value); let lastBundle = bundledStrings; if (bundledStrings) writeBundles(start, pack, 0); if (referenceMap && referenceMap.idsToInsert) { let idsToInsert = referenceMap.idsToInsert.sort((a, b) => a.offset > b.offset ? 1 : -1); let i = idsToInsert.length; let incrementPosition = -1; while (lastBundle && i > 0) { let insertionPoint = idsToInsert[--i].offset + start; if (insertionPoint < (lastBundle.stringsPosition + start) && incrementPosition === -1) incrementPosition = 0; if (insertionPoint > (lastBundle.position + start)) { if (incrementPosition >= 0) incrementPosition += 6; } else { if (incrementPosition >= 0) { // update the bundle reference now targetView.setUint32(lastBundle.position + start, targetView.getUint32(lastBundle.position + start) + incrementPosition); incrementPosition = -1; // reset } lastBundle = lastBundle.previous; i++; } } if (incrementPosition >= 0 && lastBundle) { // update the bundle reference now targetView.setUint32(lastBundle.position + start, targetView.getUint32(lastBundle.position + start) + incrementPosition); } position += idsToInsert.length * 6; if (position > safeEnd) makeRoom(position); packr.offset = position; let serialized = insertIds(target.subarray(start, position), idsToInsert); referenceMap = null; return serialized; } packr.offset = position; // update the offset so next serialization doesn't write over our buffer, but can continue writing to same buffer sequentially if (encodeOptions & REUSE_BUFFER_MODE) { target.start = start; target.end = position; return target; } return target.subarray(start, position); // position can change if we call pack again in saveStructures, so we get the buffer now } catch(error) { encodingError = error; throw error; } finally { if (structures) { resetStructures(); if (hasSharedUpdate && packr.saveStructures) { let sharedLength = structures.sharedLength || 0; // we can't rely on start/end with REUSE_BUFFER_MODE since they will (probably) change when we save let returnBuffer = target.subarray(start, position); let newSharedData = (packr._prepareStructures || prepareStructures)(structures, packr); if (!encodingError) { // TODO: If there is an encoding error, should make the structures as uninitialized so they get rebuilt next time if (packr.saveStructures(newSharedData, newSharedData.isCompatible) === false) { // The save was declined (a concurrent writer updated the shared structures, // or the store transaction did not durably commit). Our in-memory // structures + transition trie may now reference record ids that were // never persisted; re-packing as-is would re-emit the same record pointing // at an unpersisted structure (-> "Record id is not defined" on decode). // Mark structures uninitialized so the re-pack reloads durable structures // via getStructures, rebuilds the transition trie, and re-mints + re-saves. structures.uninitialized = true; return packr.pack(value, encodeOptions); } packr.lastNamedStructuresLength = sharedLength; // don't keep large buffers around if (target.length > 0x40000000) target = null; return returnBuffer; } } } // don't keep large buffers around, they take too much memory and cause problems (limit at 1GB) if (target.length > 0x40000000) target = null; if (encodeOptions & RESET_BUFFER_MODE) position = start; } }; const resetStructures = () => { if (serializationsSinceTransitionRebuild < 10) serializationsSinceTransitionRebuild++; let sharedLength = structures.sharedLength || 0; if (structures.length > sharedLength && !isSequential) structures.length = sharedLength; if (transitionsCount > 10000) { // force a rebuild occasionally after a lot of transitions so it can get cleaned up structures.transitions = null; serializationsSinceTransitionRebuild = 0; transitionsCount = 0; if (recordIdsToRemove.length > 0) recordIdsToRemove = []; } else if (recordIdsToRemove.length > 0 && !isSequential) { for (let i = 0, l = recordIdsToRemove.length; i < l; i++) { recordIdsToRemove[i][RECORD_SYMBOL] = 0; } recordIdsToRemove = []; } }; const packArray = (value) => { var length = value.length; if (length < 0x10) { target[position++] = 0x90 | length; } else if (length < 0x10000) { target[position++] = 0xdc; target[position++] = length >> 8; target[position++] = length & 0xff; } else { target[position++] = 0xdd; targetView.setUint32(position, length); position += 4; } for (let i = 0; i < length; i++) { pack(value[i]); } }; const pack = (value) => { if (position > safeEnd) target = makeRoom(position); var type = typeof value; var length; if (type === 'string') { let strLength = value.length; if (bundledStrings && strLength >= 4 && strLength < 0x1000) { if ((bundledStrings.size += strLength) > MAX_BUNDLE_SIZE) { let extStart; let maxBytes = (bundledStrings[0] ? bundledStrings[0].length * 3 + bundledStrings[1].length : 0) + 10; if (position + maxBytes > safeEnd) target = makeRoom(position + maxBytes); let lastBundle; if (bundledStrings.position) { // here we use the 0x62 extension to write the last bundle and reserve space for the reference pointer to the next/current bundle lastBundle = bundledStrings; target[position] = 0xc8; // ext 16 position += 3; // reserve for the writing bundle size target[position++] = 0x62; // 'b' extStart = position - start; position += 4; // reserve for writing bundle reference writeBundles(start, pack, 0); // write the last bundles targetView.setUint16(extStart + start - 3, position - start - extStart); } else { // here we use the 0x62 extension just to reserve the space for the reference pointer to the bundle (will be updated once the bundle is written) target[position++] = 0xd6; // fixext 4 target[position++] = 0x62; // 'b' extStart = position - start; position += 4; // reserve for writing bundle reference } bundledStrings = ['', '']; // create new ones bundledStrings.previous = lastBundle; bundledStrings.size = 0; bundledStrings.position = extStart; } let twoByte = hasNonLatin.test(value); bundledStrings[twoByte ? 0 : 1] += value; target[position++] = 0xc1; pack(twoByte ? -strLength : strLength); return; } let headerSize; // first we estimate the header size, so we can write to the correct location if (strLength < 0x20) { headerSize = 1; } else if (strLength < 0x100) { headerSize = 2; } else if (strLength < 0x10000) { headerSize = 3; } else { headerSize = 5; } let maxBytes = strLength * 3; if (position + maxBytes > safeEnd) target = makeRoom(position + maxBytes); if (strLength < 0x40 || !encodeUtf8) { let i, c1, c2, strPosition = position + headerSize; for (i = 0; i < strLength; i++) { c1 = value.charCodeAt(i); if (c1 < 0x80) { target[strPosition++] = c1; } else if (c1 < 0x800) { target[strPosition++] = c1 >> 6 | 0xc0; target[strPosition++] = c1 & 0x3f | 0x80; } else if ( (c1 & 0xfc00) === 0xd800 && ((c2 = value.charCodeAt(i + 1)) & 0xfc00) === 0xdc00 ) { c1 = 0x10000 + ((c1 & 0x03ff) << 10) + (c2 & 0x03ff); i++; target[strPosition++] = c1 >> 18 | 0xf0; target[strPosition++] = c1 >> 12 & 0x3f | 0x80; target[strPosition++] = c1 >> 6 & 0x3f | 0x80; target[strPosition++] = c1 & 0x3f | 0x80; } else { target[strPosition++] = c1 >> 12 | 0xe0; target[strPosition++] = c1 >> 6 & 0x3f | 0x80; target[strPosition++] = c1 & 0x3f | 0x80; } } length = strPosition - position - headerSize; } else { length = encodeUtf8(value, position + headerSize); } if (length < 0x20) { target[position++] = 0xa0 | length; } else if (length < 0x100) { if (headerSize < 2) { target.copyWithin(position + 2, position + 1, position + 1 + length); } target[position++] = 0xd9; target[position++] = length; } else if (length < 0x10000) { if (headerSize < 3) { target.copyWithin(position + 3, position + 2, position + 2 + length); } target[position++] = 0xda; target[position++] = length >> 8; target[position++] = length & 0xff; } else { if (headerSize < 5) { target.copyWithin(position + 5, position + 3, position + 3 + length); } target[position++] = 0xdb; targetView.setUint32(position, length); position += 4; } position += length; } else if (type === 'number') { if (value >>> 0 === value) {// positive integer, 32-bit or less // positive uint if (value < 0x20 || (value < 0x80 && this.useRecords === false) || (value < 0x40 && !this._writeStruct)) { target[position++] = value; } else if (value < 0x100) { target[position++] = 0xcc; target[position++] = value; } else if (value < 0x10000) { target[position++] = 0xcd; target[position++] = value >> 8; target[position++] = value & 0xff; } else { target[position++] = 0xce; targetView.setUint32(position, value); position += 4; } } else if (value >> 0 === value) { // negative integer if (value >= -0x20) { target[position++] = 0x100 + value; } else if (value >= -0x80) { target[position++] = 0xd0; target[position++] = value + 0x100; } else if (value >= -0x8000) { target[position++] = 0xd1; targetView.setInt16(position, value); position += 2; } else { target[position++] = 0xd2; targetView.setInt32(position, value); position += 4; } } else { let useFloat32; if ((useFloat32 = this.useFloat32) > 0 && value < 0x100000000 && value >= -0x80000000) { target[position++] = 0xca; targetView.setFloat32(position, value); let xShifted; if (useFloat32 < 4 || // this checks for rounding of numbers that were encoded in 32-bit float to nearest significant decimal digit that could be preserved ((xShifted = value * mult10[((target[position] & 0x7f) << 1) | (target[position + 1] >> 7)]) >> 0) === xShifted) { position += 4; return; } else position--; // move back into position for writing a double } target[position++] = 0xcb; targetView.setFloat64(position, value); position += 8; } } else if (type === 'object' || type === 'function') { if (!value) target[position++] = 0xc0; else { if (referenceMap) { let referee = referenceMap.get(value); if (referee) { if (!referee.id) { let idsToInsert = referenceMap.idsToInsert || (referenceMap.idsToInsert = []); referee.id = idsToInsert.push(referee); } target[position++] = 0xd6; // fixext 4 target[position++] = 0x70; // "p" for pointer targetView.setUint32(position, referee.id); position += 4; return; } else referenceMap.set(value, { offset: position - start }); } let constructor = value.constructor; if (constructor === Object) { writeObject(value); } else if (constructor === Array) { packArray(value); } else if (constructor === Map) { if (this.mapAsEmptyObject) target[position++] = 0x80; else { length = value.size; if (length < 0x10) { target[position++] = 0x80 | length; } else if (length < 0x10000) { target[position++] = 0xde; target[position++] = length >> 8; target[position++] = length & 0xff; } else { target[position++] = 0xdf; targetView.setUint32(position, length); position += 4; } for (let [key, entryValue] of value) { pack(key); pack(entryValue); } } } else { for (let i = 0, l = extensions.length; i < l; i++) { let extensionClass = extensionClasses[i]; if (value instanceof extensionClass) { let extension = extensions[i]; if (extension.write) { if (extension.type) { target[position++] = 0xd4; // one byte "tag" extension target[position++] = extension.type; target[position++] = 0; } let writeResult = extension.write.call(this, value); if (writeResult === value) { // avoid infinite recursion if (Array.isArray(value)) { packArray(value); } else { writeObject(value); } } else { pack(writeResult); } return; } let currentTarget = target; let currentTargetView = targetView; let currentPosition = position; target = null; let result; try { result = extension.pack.call(this, value, (size) => { // restore target and use it target = currentTarget; currentTarget = null; position += size; if (position > safeEnd) makeRoom(position); return { target, targetView, position: position - size }; }, pack); } finally { // restore current target information (unless already restored) if (currentTarget) { target = currentTarget; targetView = currentTargetView; position = currentPosition; safeEnd = target.length - 10; } } if (result) { if (result.length + position > safeEnd) makeRoom(result.length + position); position = writeExtensionData(result, target, position, extension.type); } return; } } // check isArray after extensions, because extensions can extend Array if (Array.isArray(value)) { packArray(value); } else { // use this as an alternate mechanism for expressing how to serialize if (value.toJSON) { const json = value.toJSON(); // if for some reason value.toJSON returns itself it'll loop forever if (json !== value) return pack(json); } // if there is a writeFunction, use it, otherwise just encode as undefined if (type === 'function') return pack(this.writeFunction && this.writeFunction(value)); // no extension found, write as plain object writeObject(value); } } } } else if (type === 'boolean') { target[position++] = value ? 0xc3 : 0xc2; } else if (type === 'bigint') { if (value < 0x8000000000000000 && value >= -0x8000000000000000) { // use a signed int as long as it fits target[position++] = 0xd3; targetView.setBigInt64(position, value); } else if (value < 0x10000000000000000 && value > 0) { // if we can fit an unsigned int, use that target[position++] = 0xcf; targetView.setBigUint64(position, value); } else { // overflow if (this.largeBigIntToFloat) { target[position++] = 0xcb; targetView.setFloat64(position, Number(value)); } else if (this.largeBigIntToString) { return pack(value.toString()); } else if (this.useBigIntExtension || this.moreTypes) { let empty = value < 0 ? BigInt(-1) : BigInt(0); let array; if (value >> BigInt(0x10000) === empty) { let mask = BigInt(0x10000000000000000) - BigInt(1); // literal would overflow let chunks = []; while (true) { chunks.push(value & mask); if ((value >> BigInt(63)) === empty) break; value >>= BigInt(64); } array = new Uint8Array(new BigUint64Array(chunks).buffer); array.reverse(); } else { let invert = value < 0; let string = (invert ? ~value : value).toString(16); if (string.length % 2) { string = '0' + string; } else if (parseInt(string.charAt(0), 16) >= 8) { string = '00' + string; } if (hasNodeBuffer) { array = Buffer.from(string, 'hex'); } else { array = new Uint8Array(string.length / 2); for (let i = 0; i < array.length; i++) { array[i] = parseInt(string.slice(i * 2, i * 2 + 2), 16); } } if (invert) { for (let i = 0; i < array.length; i++) array[i] = ~array[i]; } } if (array.length + position > safeEnd) makeRoom(array.length + position); position = writeExtensionData(array, target, position, 0x42); return; } else { throw new RangeError(value + ' was too large to fit in MessagePack 64-bit integer format, use' + ' useBigIntExtension, or set largeBigIntToFloat to convert to float-64, or set' + ' largeBigIntToString to convert to string'); } } position += 8; } else if (type === 'undefined') { if (this.encodeUndefinedAsNil) target[position++] = 0xc0; else { target[position++] = 0xd4; // a number of implementations use fixext1 with type 0, data 0 to denote undefined, so we follow suite target[position++] = 0; target[position++] = 0; } } else { throw new Error('Unknown type: ' + type); } }; const writePlainObject = (this.variableMapSize || this.coercibleKeyAsNumber || this.skipValues) ? (object) => { // this method is slightly slower, but generates "preferred serialization" (optimally small for smaller objects) let keys; if (this.skipValues) { keys = []; for (let key in object) { if ((typeof object.hasOwnProperty !== 'function' || object.hasOwnProperty(key)) && !this.skipValues.includes(object[key])) keys.push(key); } } else { keys = Object.keys(object); } let length = keys.length; if (length < 0x10) { target[position++] = 0x80 | length; } else if (length < 0x10000) { target[position++] = 0xde; target[position++] = length >> 8; target[position++] = length & 0xff; } else { target[position++] = 0xdf; targetView.setUint32(position, length); position += 4; } let key; if (this.coercibleKeyAsNumber) { for (let i = 0; i < length; i++) { key = keys[i]; let num = Number(key); pack(isNaN(num) ? key : num); pack(object[key]); } } else { for (let i = 0; i < length; i++) { pack(key = keys[i]); pack(object[key]); } } } : (object) => { target[position++] = 0xde; // always using map 16, so we can preallocate and set the length afterwards let objectOffset = position - start; position += 2; let size = 0; for (let key in object) { if (typeof object.hasOwnProperty !== 'function' || object.hasOwnProperty(key)) { pack(key); pack(object[key]); size++; } } if (size > 0xffff) { throw new Error('Object is too large to serialize with fast 16-bit map size,' + ' use the "variableMapSize" option to serialize this object'); } target[objectOffset++ + start] = size >> 8; target[objectOffset + start] = size & 0xff; }; const writeRecord = this.useRecords === false ? writePlainObject : (options.progressiveRecords && !useTwoByteRecords) ? // this is about 2% faster for highly stable structures, since it only requires one for-in loop (but much more expensive when new structure needs to be written) (object) => { let nextTransition, transition = structures.transitions || (structures.transitions = Object.create(null)); let objectOffset = position++ - start; let wroteKeys; for (let key in object) { if (typeof object.hasOwnProperty !== 'function' || object.hasOwnProperty(key)) { nextTransition = transition[key]; if (nextTransition) transition = nextTransition; else { // record doesn't exist, create full new record and insert it let keys = Object.keys(object); let lastTransition = transition; transition = structures.transitions; let newTransitions = 0; for (let i = 0, l = keys.length; i < l; i++) { let key = keys[i]; nextTransition = transition[key]; if (!nextTransition) { nextTransition = transition[key] = Object.create(null); newTransitions++; } transition = nextTransition; } if (objectOffset + start + 1 == position) { // first key, so we don't need to insert, we can just write record directly position--; newRecord(transition, keys, newTransitions); } else // otherwise we need to insert the record, moving existing data after the record insertNewRecord(transition, keys, objectOffset, newTransitions); wroteKeys = true; transition = lastTransition[key]; } pack(object[key]); } } if (!wroteKeys) { let recordId = transition[RECORD_SYMBOL]; if (recordId) target[objectOffset + start] = recordId; else insertNewRecord(transition, Object.keys(object), objectOffset, 0); } } : (object) => { let nextTransition, transition = structures.transitions || (structures.transitions = Object.create(null)); let newTransitions = 0; for (let key in object) if (typeof object.hasOwnProperty !== 'function' || object.hasOwnProperty(key)) { nextTransition = transition[key]; if (!nextTransition) { nextTransition = transition[key] = Object.create(null); newTransitions++; } transition = nextTransition; } let recordId = transition[RECORD_SYMBOL]; if (recordId) { if (recordId >= 0x60 && useTwoByteRecords) { target[position++] = ((recordId -= 0x60) & 0x1f) + 0x60; target[position++] = recordId >> 5; } else target[position++] = recordId; } else { newRecord(transition, transition.__keys__ || Object.keys(object), newTransitions); } // now write the values for (let key in object) if (typeof object.hasOwnProperty !== 'function' || object.hasOwnProperty(key)) { pack(object[key]); } }; // create reference to useRecords if useRecords is a function const checkUseRecords = typeof this.useRecords == 'function' && this.useRecords; const writeObject = checkUseRecords ? (object) => { checkUseRecords(object) ? writeRecord(object) : writePlainObject(object); } : writeRecord; const writeStruct = (object) => { let newPosition = packr._writeStruct(object, target, start, position, structures, makeRoom, (value, newPosition, notifySharedUpdate) => { if (notifySharedUpdate) return hasSharedUpdate = true; position = newPosition; let startTarget = target; pack(value); resetStructures(); if (startTarget !== target) { return { position, targetView, target }; // indicate the buffer was re-allocated } return position; }); if (newPosition === 0) // bail and go to a msgpack object return writeObject(object); position = newPosition; }; const makeRoom = (end) => { let newSize; if (end > 0x1000000) { // special handling for really large buffers if ((end - start) > MAX_BUFFER_SIZE) throw new Error('Packed buffer would be larger than maximum buffer size'); newSize = Math.min(MAX_BUFFER_SIZE, Math.round(Math.max((end - start) * (end > 0x4000000 ? 1.25 : 2), 0x400000) / 0x1000) * 0x1000); } else // faster handling for smaller buffers newSize = ((Math.max((end - start) << 2, target.length - 1) >> 12) + 1) << 12; let newBuffer = new ByteArrayAllocate(newSize); targetView = newBuffer.dataView || (newBuffer.dataView = new DataView(newBuffer.buffer, 0, newSize)); end = Math.min(end, target.length); if (target.copy) target.copy(newBuffer, 0, start, end); else newBuffer.set(target.slice(start, end)); position -= start; start = 0; safeEnd = newBuffer.length - 10; return target = newBuffer; }; const newRecord = (transition, keys, newTransitions) => { let recordId = structures.nextId; if (!recordId) recordId = 0x40; if (recordId < sharedLimitId && this.shouldShareStructure && !this.shouldShareStructure(keys)) { recordId = structures.nextOwnId; if (!(recordId < maxStructureId)) recordId = sharedLimitId; structures.nextOwnId = recordId + 1; } else { if (recordId >= maxStructureId)// cycle back around recordId = sharedLimitId; structures.nextId = recordId + 1; } let highByte = keys.highByte = recordId >= 0x60 && useTwoByteRecords ? (recordId - 0x60) >> 5 : -1; transition[RECORD_SYMBOL] = recordId; transition.__keys__ = keys; structures[recordId - 0x40] = keys; if (recordId < sharedLimitId) { keys.isShared = true; structures.sharedLength = recordId - 0x3f; hasSharedUpdate = true; if (highByte >= 0) { target[position++] = (recordId & 0x1f) + 0x60; target[position++] = highByte; } else { target[position++] = recordId; } } else { if (highByte >= 0) { target[position++] = 0xd5; // fixext 2 target[position++] = 0x72; // "r" record defintion extension type target[position++] = (recordId & 0x1f) + 0x60; target[position++] = highByte; } else { target[position++] = 0xd4; // fixext 1 target[position++] = 0x72; // "r" record defintion extension type target[position++] = recordId; } if (newTransitions) transitionsCount += serializationsSinceTransitionRebuild * newTransitions; // record the removal of the id, we can maintain our shared structure if (recordIdsToRemove.length >= maxOwnStructures) recordIdsToRemove.shift()[RECORD_SYMBOL] = 0; // we are cycling back through, and have to remove old ones recordIdsToRemove.push(transition); pack(keys); } }; const insertNewRecord = (transition, keys, insertionOffset, newTransitions) => { let mainTarget = target; let mainPosition = position; let mainSafeEnd = safeEnd; let mainStart = start; target = keysTarget; position = 0; start = 0; if (!target) keysTarget = target = new ByteArrayAllocate(8192); safeEnd = target.length - 10; newRecord(transition, keys, newTransitions); keysTarget = target; let keysPosition = position; target = mainTarget; position = mainPosition; safeEnd = mainSafeEnd; start = mainStart; if (keysPosition > 1) { let newEnd = position + keysPosition - 1; if (newEnd > safeEnd) makeRoom(newEnd); let insertionPosition = insertionOffset + start; target.copyWithin(insertionPosition + keysPosition, insertionPosition + 1, position); target.set(keysTarget.slice(0, keysPosition), insertionPosition); position = newEnd; } else { target[insertionOffset + start] = keysTarget[0]; } }; } useBuffer(buffer) { // this means we are finished using our own buffer and we can write over it safely target = buffer; target.dataView || (target.dataView = new DataView(target.buffer, target.byteOffset, target.byteLength)); targetView = target.dataView; position = 0; } set position (value) { position = value; } get position() { return position; } clearSharedData() { if (this.structures) this.structures = []; if (this.typedStructs) this.typedStructs = []; } } extensionClasses = [ Date, Set, Error, RegExp, ArrayBuffer, Object.getPrototypeOf(Uint8Array.prototype).constructor /*TypedArray*/, DataView, C1Type ]; extensions = [{ pack(date, allocateForWrite, pack) { let seconds = date.getTime() / 1000; if ((this.useTimestamp32 || date.getMilliseconds() === 0) && seconds >= 0 && seconds < 0x100000000) { // Timestamp 32 let { target, targetView, position} = allocateForWrite(6); target[position++] = 0xd6; target[position++] = 0xff; targetView.setUint32(position, seconds); } else if (seconds > 0 && seconds < 0x100000000) { // Timestamp 64 let { target, targetView, position} = allocateForWrite(10); target[position++] = 0xd7; target[position++] = 0xff; targetView.setUint32(position, date.getMilliseconds() * 4000000 + ((seconds / 1000 / 0x100000000) >> 0)); targetView.setUint32(position + 4, seconds); } else if (isNaN(seconds)) { if (this.onInvalidDate) { allocateForWrite(0); return pack(this.onInvalidDate()); } // Intentionally invalid timestamp let { target, targetView, position} = allocateForWrite(3); target[position++] = 0xd4; target[position++] = 0xff; target[position++] = 0xff; } else { // Timestamp 96 let { target, targetView, position} = allocateForWrite(15); target[position++] = 0xc7; target[position++] = 12; target[position++] = 0xff; targetView.setUint32(position, date.getMilliseconds() * 1000000); targetView.setBigInt64(position + 4, BigInt(Math.floor(seconds))); } } }, { pack(set, allocateForWrite, pack) { if (this.setAsEmptyObject) { allocateForWrite(0); return pack({}); } let array = Array.from(set); let { target, position} = allocateForWrite(this.moreTypes ? 3 : 0); if (this.moreTypes) { target[position++] = 0xd4; target[position++] = 0x73; // 's' for Set target[position++] = 0; } pack(array); } }, { pack(error, allocateForWrite, pack) { let { target, position} = allocateForWrite(this.moreTypes ? 3 : 0); if (this.moreTypes) { target[position++] = 0xd4; target[position++] = 0x65; // 'e' for error target[position++] = 0; } pack([ error.name, error.message, error.cause ]); } }, { pack(regex, allocateForWrite, pack) { let { target, position} = allocateForWrite(this.moreTypes ? 3 : 0); if (this.moreTypes) { target[position++] = 0xd4; target[position++] = 0x78; // 'x' for regeXp target[position++] = 0; } pack([ regex.source, regex.flags ]); } }, { pack(arrayBuffer, allocateForWrite) { if (this.moreTypes) writeExtBuffer(arrayBuffer, 0x10, allocateForWrite); else writeBuffer(hasNodeBuffer ? Buffer.from(arrayBuffer) : new Uint8Array(arrayBuffer), allocateForWrite); } }, { pack(typedArray, allocateForWrite) { let constructor = typedArray.constructor; if (constructor !== ByteArray && this.moreTypes) writeExtBuffer(typedArray, typedArrays.indexOf(constructor.name), allocateForWrite); else writeBuffer(typedArray, allocateForWrite); } }, { pack(arrayBuffer, allocateForWrite) { if (this.moreTypes) writeExtBuffer(arrayBuffer, 0x11, allocateForWrite); else writeBuffer(hasNodeBuffer ? Buffer.from(arrayBuffer) : new Uint8Array(arrayBuffer), allocateForWrite); } }, { pack(c1, allocateForWrite) { // specific 0xC1 object let { target, position} = allocateForWrite(1); target[position] = 0xc1; } }]; function writeExtBuffer(typedArray, type, allocateForWrite, encode) { let length = typedArray.byteLength; if (length + 1 < 0x100) { var { target, position } = allocateForWrite(4 + length); target[position++] = 0xc7; target[position++] = length + 1; } else if (length + 1 < 0x10000) { var { target, position } = allocateForWrite(5 + length); target[position++] = 0xc8; target[position++] = (length + 1) >> 8; target[position++] = (length + 1) & 0xff; } else { var { target, position, targetView } = allocateForWrite(7 + length); target[position++] = 0xc9; targetView.setUint32(position, length + 1); // plus one for the type byte position += 4; } target[position++] = 0x74; // "t" for typed array target[position++] = type; if (!typedArray.buffer) typedArray = new Uint8Array(typedArray); target.set(new Uint8Array(typedArray.buffer, typedArray.byteOffset, typedArray.byteLength), position); } function writeBuffer(buffer, allocateForWrite) { let length = buffer.byteLength; var target, position; if (length < 0x100) { var { target, position } = allocateForWrite(length + 2); target[position++] = 0xc4; target[position++] = length; } else if (length < 0x10000) { var { target, position } = allocateForWrite(length + 3); target[position++] = 0xc5; target[position++] = length >> 8; target[position++] = length & 0xff; } else { var { target, position, targetView } = allocateForWrite(length + 5); target[position++] = 0xc6; targetView.setUint32(position, length); position += 4; } target.set(buffer, position); } function writeExtensionData(result, target, position, type) { let length = result.length; switch (length) { case 1: target[position++] = 0xd4; break; case 2: target[position++] = 0xd5; break; case 4: target[position++] = 0xd6; break; case 8: target[position++] = 0xd7; break; case 16: target[position++] = 0xd8; break; default: if (length < 0x100) { target[position++] = 0xc7; target[position++] = length; } else if (length < 0x10000) { target[position++] = 0xc8; target[position++] = length >> 8; target[position++] = length & 0xff; } else { target[position++] = 0xc9; target[position++] = length >> 24; target[position++] = (length >> 16) & 0xff; target[position++] = (length >> 8) & 0xff; target[position++] = length & 0xff; } } target[position++] = type; target.set(result, position); position += length; return position; } function insertIds(serialized, idsToInsert) { // insert the ids that need to be referenced for structured clones let nextId; let distanceToMove = idsToInsert.length * 6; let lastEnd = serialized.length - distanceToMove; while (nextId = idsToInsert.pop()) { let offset = nextId.offset; let id = nextId.id; serialized.copyWithin(offset + distanceToMove, offset, lastEnd); distanceToMove -= 6; let position = offset + distanceToMove; serialized[position++] = 0xd6; serialized[position++] = 0x69; // 'i' serialized[position++] = id >> 24; serialized[position++] = (id >> 16) & 0xff; serialized[position++] = (id >> 8) & 0xff; serialized[position++] = id & 0xff; lastEnd = offset; } return serialized; } function writeBundles(start, pack, incrementPosition) { if (bundledStrings.length > 0) { targetView.setUint32(bundledStrings.position + start, position + incrementPosition - bundledStrings.position - start); bundledStrings.stringsPosition = position - start; let writeStrings = bundledStrings; bundledStrings = null; pack(writeStrings[0]); pack(writeStrings[1]); } } export function addExtension(extension) { if (extension.Class) { if (!extension.pack && !extension.write) throw new Error('Extension has no pack or write function'); if (extension.pack && !extension.type) throw new Error('Extension has no type (numeric code to identify the extension)'); extensionClasses.unshift(extension.Class); extensions.unshift(extension); } unpackAddExtension(extension); } function prepareStructures(structures, packr) { structures.isCompatible = (existingStructures) => { let compatible = !existingStructures || ((packr.lastNamedStructuresLength || 0) === existingStructures.length); if (!compatible) // we want to merge these existing structures immediately since we already have it and we are in the right transaction packr._mergeStructures(existingStructures); return compatible; }; return structures; } // Marker for downstream libraries (e.g. structon) to detect that this Packr // supports per-instance struct-encoding hooks (this._writeStruct, // this._prepareStructures). See `pack` for the dispatch. Packr.SUPPORTS_STRUCT_HOOKS = true; let defaultPackr = new Packr({ useRecords: false }); export const pack = defaultPackr.pack; export const encode = defaultPackr.pack; export const Encoder = Packr; export { FLOAT32_OPTIONS } from './unpack.js'; import { FLOAT32_OPTIONS } from './unpack.js'; export const { NEVER, ALWAYS, DECIMAL_ROUND, DECIMAL_FIT } = FLOAT32_OPTIONS; export const REUSE_BUFFER_MODE = 512; export const RESET_BUFFER_MODE = 1024; export const RESERVE_START_SPACE = 2048;