UNPKG

@colyseus/schema

Version:

Binary state serializer with delta encoding for games

1 lines 421 kB
{"version":3,"file":"index.mjs","sources":["../../src/encoding/spec.ts","../../src/symbol.shim.ts","../../src/types/symbols.ts","../../src/encoding/encode.ts","../../src/encoding/decode.ts","../../src/types/registry.ts","../../src/types/TypeContext.ts","../../src/Metadata.ts","../../src/encoder/ChangeTree.ts","../../src/encoder/EncodeOperation.ts","../../src/decoder/DecodeOperation.ts","../../src/encoding/assert.ts","../../src/types/custom/ArraySchema.ts","../../src/types/custom/MapSchema.ts","../../src/types/custom/CollectionSchema.ts","../../src/types/custom/SetSchema.ts","../../src/annotations.ts","../../src/utils.ts","../../src/Schema.ts","../../node_modules/tslib/tslib.es6.js","../../src/encoder/Root.ts","../../src/encoder/Encoder.ts","../../src/types/utils.ts","../../src/decoder/ReferenceTracker.ts","../../src/decoder/Decoder.ts","../../src/Reflection.ts","../../src/decoder/strategy/StateCallbacks.ts","../../src/decoder/strategy/RawChanges.ts","../../src/encoder/StateView.ts","../../src/index.ts"],"sourcesContent":["export const SWITCH_TO_STRUCTURE = 255; // (decoding collides with DELETE_AND_ADD + fieldIndex = 63)\nexport const TYPE_ID = 213;\n\n/**\n * Encoding Schema field operations.\n */\nexport enum OPERATION {\n ADD = 128, // (10000000) add new structure/primitive\n REPLACE = 0, // (00000001) replace structure/primitive\n DELETE = 64, // (01000000) delete field\n DELETE_AND_MOVE = 96, // () ArraySchema only\n MOVE_AND_ADD = 160, // () ArraySchema only\n DELETE_AND_ADD = 192, // (11000000) DELETE field, followed by an ADD\n\n /**\n * Collection operations\n */\n CLEAR = 10,\n\n /**\n * ArraySchema operations\n */\n REVERSE = 15,\n MOVE = 32,\n DELETE_BY_REFID = 33, // This operation is only used at ENCODING time. During DECODING, DELETE_BY_REFID is converted to DELETE\n ADD_BY_REFID = 129,\n}\n","\n//\n// Must have Symbol.metadata defined for metadata support on decorators:\n// https://github.com/microsoft/TypeScript/issues/55453#issuecomment-1687496648\n//\nexport {};\ndeclare global {\n interface SymbolConstructor {\n readonly metadata: unique symbol;\n }\n}\n(Symbol as any).metadata ??= Symbol.for(\"Symbol.metadata\");","export const $track = \"~track\";\nexport const $encoder = \"~encoder\";\nexport const $decoder = \"~decoder\";\n\nexport const $filter = \"~filter\";\n\nexport const $getByIndex = \"~getByIndex\";\nexport const $deleteByIndex = \"~deleteByIndex\";\n\n/**\n * Used to hold ChangeTree instances whitin the structures\n */\nexport const $changes = '~changes';\n\n/**\n * Used to keep track of the type of the child elements of a collection\n * (MapSchema, ArraySchema, etc.)\n */\nexport const $childType = '~childType';\n\n/**\n * Optional \"discard\" method for custom types (ArraySchema)\n * (Discards changes for next serialization)\n */\nexport const $onEncodeEnd = '~onEncodeEnd';\n\n/**\n * When decoding, this method is called after the instance is fully decoded\n */\nexport const $onDecodeEnd = \"~onDecodeEnd\";\n\n/**\n * Metadata\n */\nexport const $descriptors = \"~descriptors\";\nexport const $numFields = \"~__numFields\";\nexport const $refTypeFieldIndexes = \"~__refTypeFieldIndexes\";\nexport const $viewFieldIndexes = \"~__viewFieldIndexes\";\nexport const $fieldIndexesByViewTag = \"$__fieldIndexesByViewTag\";","// @ts-nocheck\n\n/**\n * Copyright (c) 2018 Endel Dreyer\n * Copyright (c) 2014 Ion Drive Software Ltd.\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE\n */\n\nimport type { TextEncoder } from \"util\";\nimport type { Iterator } from \"./decode\";\n\nexport type BufferLike = number[] | ArrayBufferLike | Buffer;\n\n/**\n * msgpack implementation highly based on notepack.io\n * https://github.com/darrachequesne/notepack\n */\n\nlet textEncoder: TextEncoder;\n// @ts-ignore\ntry { textEncoder = new TextEncoder(); } catch (e) { }\n\n// force little endian to facilitate decoding on multiple implementations\nconst _isLittleEndian = true; // new Uint16Array(new Uint8Array([1, 0]).buffer)[0] === 1;\nconst _convoBuffer = new ArrayBuffer(8);\nconst _int32 = new Int32Array(_convoBuffer);\nconst _float32 = new Float32Array(_convoBuffer);\nconst _float64 = new Float64Array(_convoBuffer);\nconst _int64 = new BigInt64Array(_convoBuffer);\n\nconst hasBufferByteLength = (typeof Buffer !== 'undefined' && Buffer.byteLength);\n\nconst utf8Length: (str: string, _?: any) => number = (hasBufferByteLength)\n ? Buffer.byteLength // node\n : function (str: string, _?: any) {\n var c = 0, length = 0;\n for (var i = 0, l = str.length; i < l; i++) {\n c = str.charCodeAt(i);\n if (c < 0x80) {\n length += 1;\n }\n else if (c < 0x800) {\n length += 2;\n }\n else if (c < 0xd800 || c >= 0xe000) {\n length += 3;\n }\n else {\n i++;\n length += 4;\n }\n }\n return length;\n }\n\nfunction utf8Write(view: BufferLike, str: string, it: Iterator) {\n var c = 0;\n for (var i = 0, l = str.length; i < l; i++) {\n c = str.charCodeAt(i);\n if (c < 0x80) {\n view[it.offset++] = c;\n }\n else if (c < 0x800) {\n view[it.offset] = 0xc0 | (c >> 6);\n view[it.offset + 1] = 0x80 | (c & 0x3f);\n it.offset += 2;\n }\n else if (c < 0xd800 || c >= 0xe000) {\n view[it.offset] = 0xe0 | (c >> 12);\n view[it.offset+1] = 0x80 | (c >> 6 & 0x3f);\n view[it.offset+2] = 0x80 | (c & 0x3f);\n it.offset += 3;\n }\n else {\n i++;\n c = 0x10000 + (((c & 0x3ff) << 10) | (str.charCodeAt(i) & 0x3ff));\n view[it.offset] = 0xf0 | (c >> 18);\n view[it.offset+1] = 0x80 | (c >> 12 & 0x3f);\n view[it.offset+2] = 0x80 | (c >> 6 & 0x3f);\n view[it.offset+3] = 0x80 | (c & 0x3f);\n it.offset += 4;\n }\n }\n}\n\nfunction int8(bytes: BufferLike, value: number, it: Iterator) {\n bytes[it.offset++] = value & 255;\n};\n\nfunction uint8(bytes: BufferLike, value: number, it: Iterator) {\n bytes[it.offset++] = value & 255;\n};\n\nfunction int16(bytes: BufferLike, value: number, it: Iterator) {\n bytes[it.offset++] = value & 255;\n bytes[it.offset++] = (value >> 8) & 255;\n};\n\nfunction uint16(bytes: BufferLike, value: number, it: Iterator) {\n bytes[it.offset++] = value & 255;\n bytes[it.offset++] = (value >> 8) & 255;\n};\n\nfunction int32(bytes: BufferLike, value: number, it: Iterator) {\n bytes[it.offset++] = value & 255;\n bytes[it.offset++] = (value >> 8) & 255;\n bytes[it.offset++] = (value >> 16) & 255;\n bytes[it.offset++] = (value >> 24) & 255;\n};\n\nfunction uint32(bytes: BufferLike, value: number, it: Iterator) {\n const b4 = value >> 24;\n const b3 = value >> 16;\n const b2 = value >> 8;\n const b1 = value;\n bytes[it.offset++] = b1 & 255;\n bytes[it.offset++] = b2 & 255;\n bytes[it.offset++] = b3 & 255;\n bytes[it.offset++] = b4 & 255;\n};\n\nfunction int64(bytes: BufferLike, value: number, it: Iterator) {\n const high = Math.floor(value / Math.pow(2, 32));\n const low = value >>> 0;\n uint32(bytes, low, it);\n uint32(bytes, high, it);\n};\n\nfunction uint64(bytes: BufferLike, value: number, it: Iterator) {\n const high = (value / Math.pow(2, 32)) >> 0;\n const low = value >>> 0;\n uint32(bytes, low, it);\n uint32(bytes, high, it);\n};\n\nfunction bigint64(bytes: BufferLike, value: bigint, it: Iterator) {\n _int64[0] = BigInt.asIntN(64, value);\n int32(bytes, _int32[0], it);\n int32(bytes, _int32[1], it);\n}\n\nfunction biguint64(bytes: BufferLike, value: bigint, it: Iterator) {\n _int64[0] = BigInt.asIntN(64, value);\n int32(bytes, _int32[0], it);\n int32(bytes, _int32[1], it);\n}\n\nfunction float32(bytes: BufferLike, value: number, it: Iterator) {\n _float32[0] = value;\n int32(bytes, _int32[0], it);\n}\n\nfunction float64(bytes: BufferLike, value: number, it: Iterator) {\n _float64[0] = value;\n int32(bytes, _int32[_isLittleEndian ? 0 : 1], it);\n int32(bytes, _int32[_isLittleEndian ? 1 : 0], it);\n}\n\nfunction boolean(bytes: BufferLike, value: number, it: Iterator) {\n bytes[it.offset++] = value ? 1 : 0; // uint8\n};\n\nfunction string(bytes: BufferLike, value: string, it: Iterator) {\n // encode `null` strings as empty.\n if (!value) { value = \"\"; }\n\n let length = utf8Length(value, \"utf8\");\n let size = 0;\n\n // fixstr\n if (length < 0x20) {\n bytes[it.offset++] = length | 0xa0;\n size = 1;\n }\n // str 8\n else if (length < 0x100) {\n bytes[it.offset++] = 0xd9;\n bytes[it.offset++] = length % 255;\n size = 2;\n }\n // str 16\n else if (length < 0x10000) {\n bytes[it.offset++] = 0xda;\n uint16(bytes, length, it);\n size = 3;\n }\n // str 32\n else if (length < 0x100000000) {\n bytes[it.offset++] = 0xdb;\n uint32(bytes, length, it);\n size = 5;\n } else {\n throw new Error('String too long');\n }\n\n utf8Write(bytes, value, it);\n\n return size + length;\n}\n\nfunction number(bytes: BufferLike, value: number, it: Iterator) {\n if (isNaN(value)) {\n return number(bytes, 0, it);\n\n } else if (!isFinite(value)) {\n return number(bytes, (value > 0) ? Number.MAX_SAFE_INTEGER : -Number.MAX_SAFE_INTEGER, it);\n\n } else if (value !== (value|0)) {\n if (Math.abs(value) <= 3.4028235e+38) { // range check\n _float32[0] = value;\n if (Math.abs(Math.abs(_float32[0]) - Math.abs(value)) < 1e-4) { // precision check; adjust 1e-n (n = precision) to in-/decrease acceptable precision loss\n // now we know value is in range for f32 and has acceptable precision for f32\n bytes[it.offset++] = 0xca;\n float32(bytes, value, it);\n return 5;\n }\n }\n\n bytes[it.offset++] = 0xcb;\n float64(bytes, value, it);\n return 9;\n }\n\n if (value >= 0) {\n // positive fixnum\n if (value < 0x80) {\n bytes[it.offset++] = value & 255; // uint8\n return 1;\n }\n\n // uint 8\n if (value < 0x100) {\n bytes[it.offset++] = 0xcc;\n bytes[it.offset++] = value & 255; // uint8\n return 2;\n }\n\n // uint 16\n if (value < 0x10000) {\n bytes[it.offset++] = 0xcd;\n uint16(bytes, value, it);\n return 3;\n }\n\n // uint 32\n if (value < 0x100000000) {\n bytes[it.offset++] = 0xce;\n uint32(bytes, value, it);\n return 5;\n }\n\n // uint 64\n bytes[it.offset++] = 0xcf;\n uint64(bytes, value, it);\n return 9;\n\n } else {\n\n // negative fixnum\n if (value >= -0x20) {\n bytes[it.offset++] = 0xe0 | (value + 0x20);\n return 1;\n }\n\n // int 8\n if (value >= -0x80) {\n bytes[it.offset++] = 0xd0;\n int8(bytes, value, it);\n return 2;\n }\n\n // int 16\n if (value >= -0x8000) {\n bytes[it.offset++] = 0xd1;\n int16(bytes, value, it);\n return 3;\n }\n\n // int 32\n if (value >= -0x80000000) {\n bytes[it.offset++] = 0xd2;\n int32(bytes, value, it);\n return 5;\n }\n\n // int 64\n bytes[it.offset++] = 0xd3;\n int64(bytes, value, it);\n return 9;\n }\n}\n\nexport const encode = {\n int8,\n uint8,\n int16,\n uint16,\n int32,\n uint32,\n int64,\n uint64,\n bigint64,\n biguint64,\n float32,\n float64,\n boolean,\n string,\n number,\n utf8Write,\n utf8Length,\n}","// @ts-nocheck\n\n/**\n * Copyright (c) 2018 Endel Dreyer\n * Copyright (c) 2014 Ion Drive Software Ltd.\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE\n */\n\nimport type { BufferLike } from \"./encode\";\n\n/**\n * msgpack implementation highly based on notepack.io\n * https://github.com/darrachequesne/notepack\n */\n\nexport interface Iterator { offset: number; }\n\n// force little endian to facilitate decoding on multiple implementations\nconst _isLittleEndian = true; // new Uint16Array(new Uint8Array([1, 0]).buffer)[0] === 1;\nconst _convoBuffer = new ArrayBuffer(8);\n\nconst _int32 = new Int32Array(_convoBuffer);\nconst _float32 = new Float32Array(_convoBuffer);\nconst _float64 = new Float64Array(_convoBuffer);\nconst _uint64 = new BigUint64Array(_convoBuffer);\nconst _int64 = new BigInt64Array(_convoBuffer);\n\nfunction utf8Read(bytes: BufferLike, it: Iterator, length: number) {\n // boundary check\n if (length > bytes.length - it.offset) { length = bytes.length - it.offset; }\n\n var string = '', chr = 0;\n for (var i = it.offset, end = it.offset + length; i < end; i++) {\n var byte = bytes[i];\n if ((byte & 0x80) === 0x00) {\n string += String.fromCharCode(byte);\n continue;\n }\n if ((byte & 0xe0) === 0xc0) {\n string += String.fromCharCode(\n ((byte & 0x1f) << 6) |\n (bytes[++i] & 0x3f)\n );\n continue;\n }\n if ((byte & 0xf0) === 0xe0) {\n string += String.fromCharCode(\n ((byte & 0x0f) << 12) |\n ((bytes[++i] & 0x3f) << 6) |\n ((bytes[++i] & 0x3f) << 0)\n );\n continue;\n }\n if ((byte & 0xf8) === 0xf0) {\n chr = ((byte & 0x07) << 18) |\n ((bytes[++i] & 0x3f) << 12) |\n ((bytes[++i] & 0x3f) << 6) |\n ((bytes[++i] & 0x3f) << 0);\n if (chr >= 0x010000) { // surrogate pair\n chr -= 0x010000;\n string += String.fromCharCode((chr >>> 10) + 0xD800, (chr & 0x3FF) + 0xDC00);\n } else {\n string += String.fromCharCode(chr);\n }\n continue;\n }\n\n // (do not throw error to avoid server/client from crashing due to hack attemps)\n // throw new Error('Invalid byte ' + byte.toString(16));\n\n console.error('decode.utf8Read(): Invalid byte ' + byte + ' at offset ' + i + '. Skip to end of string: ' + (it.offset + length));\n break;\n }\n it.offset += length;\n return string;\n}\n\nfunction int8 (bytes: BufferLike, it: Iterator) {\n return uint8(bytes, it) << 24 >> 24;\n};\n\nfunction uint8 (bytes: BufferLike, it: Iterator) {\n return bytes[it.offset++];\n};\n\nfunction int16 (bytes: BufferLike, it: Iterator) {\n return uint16(bytes, it) << 16 >> 16;\n};\n\nfunction uint16 (bytes: BufferLike, it: Iterator) {\n return bytes[it.offset++] | bytes[it.offset++] << 8;\n};\n\nfunction int32 (bytes: BufferLike, it: Iterator) {\n return bytes[it.offset++] | bytes[it.offset++] << 8 | bytes[it.offset++] << 16 | bytes[it.offset++] << 24;\n};\n\nfunction uint32 (bytes: BufferLike, it: Iterator) {\n return int32(bytes, it) >>> 0;\n};\n\nfunction float32 (bytes: BufferLike, it: Iterator) {\n _int32[0] = int32(bytes, it);\n return _float32[0];\n};\n\nfunction float64 (bytes: BufferLike, it: Iterator) {\n _int32[_isLittleEndian ? 0 : 1] = int32(bytes, it);\n _int32[_isLittleEndian ? 1 : 0] = int32(bytes, it);\n return _float64[0];\n};\n\nfunction int64(bytes: BufferLike, it: Iterator) {\n const low = uint32(bytes, it);\n const high = int32(bytes, it) * Math.pow(2, 32);\n return high + low;\n};\n\nfunction uint64(bytes: BufferLike, it: Iterator) {\n const low = uint32(bytes, it);\n const high = uint32(bytes, it) * Math.pow(2, 32);\n return high + low;\n};\n\nfunction bigint64(bytes: BufferLike, it: Iterator) {\n _int32[0] = int32(bytes, it);\n _int32[1] = int32(bytes, it);\n return _int64[0];\n}\n\nfunction biguint64(bytes: BufferLike, it: Iterator) {\n _int32[0] = int32(bytes, it);\n _int32[1] = int32(bytes, it);\n return _uint64[0];\n}\n\nfunction boolean (bytes: BufferLike, it: Iterator) {\n return uint8(bytes, it) > 0;\n};\n\nfunction string (bytes: BufferLike, it: Iterator) {\n const prefix = bytes[it.offset++];\n let length: number;\n\n if (prefix < 0xc0) {\n // fixstr\n length = prefix & 0x1f;\n\n } else if (prefix === 0xd9) {\n length = uint8(bytes, it);\n\n } else if (prefix === 0xda) {\n length = uint16(bytes, it);\n\n } else if (prefix === 0xdb) {\n length = uint32(bytes, it);\n }\n\n return utf8Read(bytes, it, length);\n}\n\nfunction number (bytes: BufferLike, it: Iterator) {\n const prefix = bytes[it.offset++];\n\n if (prefix < 0x80) {\n // positive fixint\n return prefix;\n\n } else if (prefix === 0xca) {\n // float 32\n return float32(bytes, it);\n\n } else if (prefix === 0xcb) {\n // float 64\n return float64(bytes, it);\n\n } else if (prefix === 0xcc) {\n // uint 8\n return uint8(bytes, it);\n\n } else if (prefix === 0xcd) {\n // uint 16\n return uint16(bytes, it);\n\n } else if (prefix === 0xce) {\n // uint 32\n return uint32(bytes, it);\n\n } else if (prefix === 0xcf) {\n // uint 64\n return uint64(bytes, it);\n\n } else if (prefix === 0xd0) {\n // int 8\n return int8(bytes, it);\n\n } else if (prefix === 0xd1) {\n // int 16\n return int16(bytes, it);\n\n } else if (prefix === 0xd2) {\n // int 32\n return int32(bytes, it);\n\n } else if (prefix === 0xd3) {\n // int 64\n return int64(bytes, it);\n\n } else if (prefix > 0xdf) {\n // negative fixint\n return (0xff - prefix + 1) * -1\n }\n};\n\nexport function stringCheck(bytes: BufferLike, it: Iterator) {\n const prefix = bytes[it.offset];\n return (\n // fixstr\n (prefix < 0xc0 && prefix > 0xa0) ||\n // str 8\n prefix === 0xd9 ||\n // str 16\n prefix === 0xda ||\n // str 32\n prefix === 0xdb\n );\n}\n\nexport const decode = {\n utf8Read,\n int8,\n uint8,\n int16,\n uint16,\n int32,\n uint32,\n float32,\n float64,\n int64,\n uint64,\n bigint64,\n biguint64,\n boolean,\n string,\n number,\n stringCheck,\n};","import { DefinitionType, type } from \"../annotations\";\nimport { BufferLike, encode } from \"../encoding/encode\";\nimport { decode, Iterator } from \"../encoding/decode\";\n\nexport interface TypeDefinition {\n constructor?: any,\n encode?: (bytes: BufferLike, value: any, it: Iterator) => any;\n decode?: (bytes: BufferLike, it: Iterator) => any;\n}\n\nexport const registeredTypes: {[identifier: string] : TypeDefinition} = {};\n\nconst identifiers = new Map<any, string>();\n\nexport function registerType(identifier: string, definition: TypeDefinition) {\n if (definition.constructor) {\n identifiers.set(definition.constructor, identifier);\n registeredTypes[identifier] = definition;\n }\n\n if (definition.encode) { (encode as any)[identifier] = definition.encode; }\n if (definition.decode) { (decode as any)[identifier] = definition.decode; }\n}\n\nexport function getIdentifier(klass: any): string {\n return identifiers.get(klass);\n}\n\nexport function getType(identifier: string): TypeDefinition {\n return registeredTypes[identifier];\n}\n\nexport function defineCustomTypes<T extends {[key: string]: TypeDefinition}>(types: T) {\n for (const identifier in types) {\n registerType(identifier, types[identifier]);\n }\n\n return (t: keyof T) => type(t as DefinitionType);\n}","import { Metadata } from \"../Metadata\";\nimport { Schema } from \"../Schema\";\nimport { $viewFieldIndexes } from \"./symbols\";\n\nexport class TypeContext {\n types: { [id: number]: typeof Schema; } = {};\n schemas = new Map<typeof Schema, number>();\n\n hasFilters: boolean = false;\n parentFiltered: {[typeIdAndParentIndex: string]: boolean} = {};\n\n /**\n * For inheritance support\n * Keeps track of which classes extends which. (parent -> children)\n */\n static inheritedTypes = new Map<typeof Schema, Set<typeof Schema>>();\n static cachedContexts = new Map<typeof Schema, TypeContext>();\n\n static register(target: typeof Schema) {\n const parent = Object.getPrototypeOf(target);\n if (parent !== Schema) {\n let inherits = TypeContext.inheritedTypes.get(parent);\n if (!inherits) {\n inherits = new Set<typeof Schema>();\n TypeContext.inheritedTypes.set(parent, inherits);\n }\n inherits.add(target);\n }\n }\n\n static cache (rootClass: typeof Schema) {\n let context = TypeContext.cachedContexts.get(rootClass);\n if (!context) {\n context = new TypeContext(rootClass);\n TypeContext.cachedContexts.set(rootClass, context);\n }\n return context;\n }\n\n constructor(rootClass?: typeof Schema) {\n if (rootClass) {\n this.discoverTypes(rootClass);\n }\n }\n\n has(schema: typeof Schema) {\n return this.schemas.has(schema);\n }\n\n get(typeid: number) {\n return this.types[typeid];\n }\n\n add(schema: typeof Schema, typeid = this.schemas.size) {\n // skip if already registered\n if (this.schemas.has(schema)) {\n return false;\n }\n\n this.types[typeid] = schema;\n\n //\n // Workaround to allow using an empty Schema (with no `@type()` fields)\n //\n if (schema[Symbol.metadata] === undefined) {\n Metadata.initialize(schema);\n }\n\n this.schemas.set(schema, typeid);\n return true;\n }\n\n getTypeId(klass: typeof Schema) {\n return this.schemas.get(klass);\n }\n\n private discoverTypes(klass: typeof Schema, parentType?: typeof Schema, parentIndex?: number, parentHasViewTag?: boolean) {\n if (parentHasViewTag) {\n this.registerFilteredByParent(klass, parentType, parentIndex);\n }\n\n // skip if already registered\n if (!this.add(klass)) { return; }\n\n // add classes inherited from this base class\n TypeContext.inheritedTypes.get(klass)?.forEach((child) => {\n this.discoverTypes(child, parentType, parentIndex, parentHasViewTag);\n });\n\n // add parent classes\n let parent: any = klass;\n while (\n (parent = Object.getPrototypeOf(parent)) &&\n parent !== Schema && // stop at root (Schema)\n parent !== Function.prototype // stop at root (non-Schema)\n ) {\n this.discoverTypes(parent);\n }\n\n const metadata: Metadata = (klass[Symbol.metadata] ??= {} as Metadata);\n\n // if any schema/field has filters, mark \"context\" as having filters.\n if (metadata[$viewFieldIndexes]) {\n this.hasFilters = true;\n }\n\n for (const fieldIndex in metadata) {\n const index = fieldIndex as any as number;\n\n const fieldType = metadata[index].type;\n const fieldHasViewTag = (metadata[index].tag !== undefined);\n\n if (typeof (fieldType) === \"string\") {\n continue;\n }\n\n if (typeof (fieldType) === \"function\") {\n this.discoverTypes(fieldType as typeof Schema, klass, index, parentHasViewTag || fieldHasViewTag);\n\n } else {\n const type = Object.values(fieldType)[0];\n\n // skip primitive types\n if (typeof (type) === \"string\") {\n continue;\n }\n\n this.discoverTypes(type as typeof Schema, klass, index, parentHasViewTag || fieldHasViewTag);\n }\n }\n }\n\n /**\n * Keep track of which classes have filters applied.\n * Format: `${typeid}-${parentTypeid}-${parentIndex}`\n */\n private registerFilteredByParent(schema: typeof Schema, parentType?: typeof Schema, parentIndex?: number) {\n const typeid = this.schemas.get(schema) ?? this.schemas.size;\n\n let key = `${typeid}`;\n if (parentType) { key += `-${this.schemas.get(parentType)}`; }\n\n key += `-${parentIndex}`;\n this.parentFiltered[key] = true;\n }\n\n debug() {\n let parentFiltered = \"\";\n\n for (const key in this.parentFiltered) {\n const keys: number[] = key.split(\"-\").map(Number);\n const fieldIndex = keys.pop();\n\n parentFiltered += `\\n\\t\\t`;\n parentFiltered += `${key}: ${keys.reverse().map((id, i) => {\n const klass = this.types[id];\n const metadata: Metadata = klass[Symbol.metadata];\n let txt = klass.name;\n if (i === 0) { txt += `[${metadata[fieldIndex].name}]`; }\n return `${txt}`;\n }).join(\" -> \")}`;\n }\n\n return `TypeContext ->\\n` +\n `\\tSchema types: ${this.schemas.size}\\n` +\n `\\thasFilters: ${this.hasFilters}\\n` +\n `\\tparentFiltered:${parentFiltered}`;\n }\n\n}\n","import { DefinitionType, getPropertyDescriptor } from \"./annotations\";\nimport { Schema } from \"./Schema\";\nimport { getType, registeredTypes } from \"./types/registry\";\nimport { $decoder, $descriptors, $encoder, $fieldIndexesByViewTag, $numFields, $refTypeFieldIndexes, $track, $viewFieldIndexes } from \"./types/symbols\";\nimport { TypeContext } from \"./types/TypeContext\";\n\nexport type MetadataField = {\n type: DefinitionType,\n name: string,\n index: number,\n tag?: number,\n unreliable?: boolean,\n deprecated?: boolean,\n};\n\nexport type Metadata =\n { [$numFields]: number; } & // number of fields\n { [$viewFieldIndexes]: number[]; } & // all field indexes with \"view\" tag\n { [$fieldIndexesByViewTag]: {[tag: number]: number[]}; } & // field indexes by \"view\" tag\n { [$refTypeFieldIndexes]: number[]; } & // all field indexes containing Ref types (Schema, ArraySchema, MapSchema, etc)\n { [field: number]: MetadataField; } & // index => field name\n { [field: string]: number; } & // field name => field metadata\n { [$descriptors]: { [field: string]: PropertyDescriptor } } // property descriptors\n\nexport function getNormalizedType(type: any): DefinitionType {\n if (Array.isArray(type)) {\n return { array: getNormalizedType(type[0]) };\n\n } else if (typeof (type['type']) !== \"undefined\") {\n return type['type'];\n\n } else if (isTSEnum(type)) {\n // Detect TS Enum type (either string or number)\n return Object.keys(type).every(key => typeof type[key] === \"string\")\n ? \"string\"\n : \"number\";\n\n } else if (typeof type === \"object\" && type !== null) {\n // Handle collection types\n const collectionType = Object.keys(type).find(k => registeredTypes[k] !== undefined);\n if (collectionType) {\n type[collectionType] = getNormalizedType(type[collectionType]);\n return type;\n }\n }\n return type;\n}\n\nfunction isTSEnum(_enum: any) {\n if (typeof _enum === 'function' && _enum[Symbol.metadata]) {\n return false;\n }\n\n const keys = Object.keys(_enum);\n const numericFields = keys.filter(k => /\\d+/.test(k));\n\n // Check for number enum (has numeric keys and reverse mapping)\n if (numericFields.length > 0 && numericFields.length === (keys.length / 2) && _enum[_enum[numericFields[0]]] == numericFields[0]) {\n return true;\n }\n\n // Check for string enum (all values are strings and keys match values)\n if (keys.length > 0 && keys.every(key => typeof _enum[key] === 'string' && _enum[key] === key)) {\n return true;\n }\n\n return false;\n}\n\nexport const Metadata = {\n\n addField(metadata: any, index: number, name: string, type: DefinitionType, descriptor?: PropertyDescriptor) {\n if (index > 64) {\n throw new Error(`Can't define field '${name}'.\\nSchema instances may only have up to 64 fields.`);\n }\n\n metadata[index] = Object.assign(\n metadata[index] || {}, // avoid overwriting previous field metadata (@owned / @deprecated)\n {\n type: getNormalizedType(type),\n index,\n name,\n }\n );\n\n // create \"descriptors\" map\n Object.defineProperty(metadata, $descriptors, {\n value: metadata[$descriptors] || {},\n enumerable: false,\n configurable: true,\n });\n\n if (descriptor) {\n // for encoder\n metadata[$descriptors][name] = descriptor;\n metadata[$descriptors][`_${name}`] = {\n value: undefined,\n writable: true,\n enumerable: false,\n configurable: true,\n };\n } else {\n // for decoder\n metadata[$descriptors][name] = {\n value: undefined,\n writable: true,\n enumerable: true,\n configurable: true,\n };\n }\n\n // map -1 as last field index\n Object.defineProperty(metadata, $numFields, {\n value: index,\n enumerable: false,\n configurable: true\n });\n\n // map field name => index (non enumerable)\n Object.defineProperty(metadata, name, {\n value: index,\n enumerable: false,\n configurable: true,\n });\n\n // if child Ref/complex type, add to -4\n if (typeof (metadata[index].type) !== \"string\") {\n if (metadata[$refTypeFieldIndexes] === undefined) {\n Object.defineProperty(metadata, $refTypeFieldIndexes, {\n value: [],\n enumerable: false,\n configurable: true,\n });\n }\n metadata[$refTypeFieldIndexes].push(index);\n }\n },\n\n setTag(metadata: Metadata, fieldName: string, tag: number) {\n const index = metadata[fieldName];\n const field = metadata[index];\n\n // add 'tag' to the field\n field.tag = tag;\n\n if (!metadata[$viewFieldIndexes]) {\n // -2: all field indexes with \"view\" tag\n Object.defineProperty(metadata, $viewFieldIndexes, {\n value: [],\n enumerable: false,\n configurable: true\n });\n\n // -3: field indexes by \"view\" tag\n Object.defineProperty(metadata, $fieldIndexesByViewTag, {\n value: {},\n enumerable: false,\n configurable: true\n });\n }\n\n metadata[$viewFieldIndexes].push(index);\n\n if (!metadata[$fieldIndexesByViewTag][tag]) {\n metadata[$fieldIndexesByViewTag][tag] = [];\n }\n\n metadata[$fieldIndexesByViewTag][tag].push(index);\n },\n\n setFields<T extends { new (...args: any[]): InstanceType<T> } = any>(target: T, fields: { [field in keyof InstanceType<T>]?: DefinitionType }) {\n // for inheritance support\n const constructor = target.prototype.constructor;\n TypeContext.register(constructor);\n\n const parentClass = Object.getPrototypeOf(constructor);\n const parentMetadata = parentClass && parentClass[Symbol.metadata];\n const metadata = Metadata.initialize(constructor);\n\n // Use Schema's methods if not defined in the class\n if (!constructor[$track]) { constructor[$track] = Schema[$track]; }\n if (!constructor[$encoder]) { constructor[$encoder] = Schema[$encoder]; }\n if (!constructor[$decoder]) { constructor[$decoder] = Schema[$decoder]; }\n if (!constructor.prototype.toJSON) { constructor.prototype.toJSON = Schema.prototype.toJSON; }\n\n //\n // detect index for this field, considering inheritance\n //\n let fieldIndex = metadata[$numFields] // current structure already has fields defined\n ?? (parentMetadata && parentMetadata[$numFields]) // parent structure has fields defined\n ?? -1; // no fields defined\n\n fieldIndex++;\n\n for (const field in fields) {\n const type = getNormalizedType(fields[field]);\n\n // FIXME: this code is duplicated from @type() annotation\n const complexTypeKlass = typeof(Object.keys(type)[0]) === \"string\" && getType(Object.keys(type)[0]);\n\n const childType = (complexTypeKlass)\n ? Object.values(type)[0]\n : type;\n\n Metadata.addField(\n metadata,\n fieldIndex,\n field,\n type,\n getPropertyDescriptor(`_${field}`, fieldIndex, childType, complexTypeKlass)\n );\n\n fieldIndex++;\n }\n\n return target;\n },\n\n isDeprecated(metadata: any, field: string) {\n return metadata[field].deprecated === true;\n },\n\n init(klass: any) {\n //\n // Used only to initialize an empty Schema (Encoder#constructor)\n // TODO: remove/refactor this...\n //\n const metadata = {};\n klass[Symbol.metadata] = metadata;\n Object.defineProperty(metadata, $numFields, {\n value: 0,\n enumerable: false,\n configurable: true,\n });\n },\n\n initialize(constructor: any) {\n const parentClass = Object.getPrototypeOf(constructor);\n const parentMetadata: Metadata = parentClass[Symbol.metadata];\n\n let metadata: Metadata = constructor[Symbol.metadata] ?? Object.create(null);\n\n // make sure inherited classes have their own metadata object.\n if (parentClass !== Schema && metadata === parentMetadata) {\n metadata = Object.create(null);\n\n if (parentMetadata) {\n //\n // assign parent metadata to current\n //\n Object.setPrototypeOf(metadata, parentMetadata);\n\n // $numFields\n Object.defineProperty(metadata, $numFields, {\n value: parentMetadata[$numFields],\n enumerable: false,\n configurable: true,\n writable: true,\n });\n\n // $viewFieldIndexes / $fieldIndexesByViewTag\n if (parentMetadata[$viewFieldIndexes] !== undefined) {\n Object.defineProperty(metadata, $viewFieldIndexes, {\n value: [...parentMetadata[$viewFieldIndexes]],\n enumerable: false,\n configurable: true,\n writable: true,\n });\n Object.defineProperty(metadata, $fieldIndexesByViewTag, {\n value: { ...parentMetadata[$fieldIndexesByViewTag] },\n enumerable: false,\n configurable: true,\n writable: true,\n });\n }\n\n // $refTypeFieldIndexes\n if (parentMetadata[$refTypeFieldIndexes] !== undefined) {\n Object.defineProperty(metadata, $refTypeFieldIndexes, {\n value: [...parentMetadata[$refTypeFieldIndexes]],\n enumerable: false,\n configurable: true,\n writable: true,\n });\n }\n\n // $descriptors\n Object.defineProperty(metadata, $descriptors, {\n value: { ...parentMetadata[$descriptors] },\n enumerable: false,\n configurable: true,\n writable: true,\n });\n }\n }\n\n constructor[Symbol.metadata] = metadata;\n\n return metadata;\n },\n\n isValidInstance(klass: any) {\n return (\n klass.constructor[Symbol.metadata] &&\n Object.prototype.hasOwnProperty.call(klass.constructor[Symbol.metadata], $numFields) as boolean\n );\n },\n\n getFields(klass: any) {\n const metadata: Metadata = klass[Symbol.metadata];\n const fields: any = {};\n for (let i = 0; i <= metadata[$numFields]; i++) {\n fields[metadata[i].name] = metadata[i].type;\n }\n return fields;\n },\n\n hasViewTagAtIndex(metadata: Metadata, index: number) {\n return metadata?.[$viewFieldIndexes]?.includes(index);\n }\n}","import { OPERATION } from \"../encoding/spec\";\nimport { Schema } from \"../Schema\";\nimport { $changes, $childType, $decoder, $onEncodeEnd, $encoder, $getByIndex, $refTypeFieldIndexes, $viewFieldIndexes, type $deleteByIndex } from \"../types/symbols\";\n\nimport type { MapSchema } from \"../types/custom/MapSchema\";\nimport type { ArraySchema } from \"../types/custom/ArraySchema\";\nimport type { CollectionSchema } from \"../types/custom/CollectionSchema\";\nimport type { SetSchema } from \"../types/custom/SetSchema\";\n\nimport { Root } from \"./Root\";\nimport { Metadata } from \"../Metadata\";\nimport type { EncodeOperation } from \"./EncodeOperation\";\nimport type { DecodeOperation } from \"../decoder/DecodeOperation\";\n\ndeclare global {\n interface Object {\n // FIXME: not a good practice to extend globals here\n [$changes]?: ChangeTree;\n [$encoder]?: EncodeOperation,\n [$decoder]?: DecodeOperation,\n }\n}\n\nexport interface IRef {\n [$changes]?: ChangeTree;\n [$getByIndex]?: (index: number, isEncodeAll?: boolean) => any;\n [$deleteByIndex]?: (index: number) => void;\n}\n\nexport type Ref = Schema\n | ArraySchema\n | MapSchema\n | CollectionSchema\n | SetSchema;\n\nexport type ChangeSetName = \"changes\"\n | \"allChanges\"\n | \"filteredChanges\"\n | \"allFilteredChanges\";\n\nexport interface IndexedOperations {\n [index: number]: OPERATION;\n}\n\n// Linked list node for change trees\nexport interface ChangeTreeNode {\n changeTree: ChangeTree;\n next?: ChangeTreeNode;\n prev?: ChangeTreeNode;\n position: number; // Cached position in the linked list for O(1) lookup\n}\n\n// Linked list for change trees\nexport interface ChangeTreeList {\n next?: ChangeTreeNode;\n tail?: ChangeTreeNode;\n}\n\nexport interface ChangeSet {\n // field index -> operation index\n indexes: { [index: number]: number };\n operations: number[];\n queueRootNode?: ChangeTreeNode; // direct reference to ChangeTreeNode in the linked list\n}\n\nfunction createChangeSet(queueRootNode?: ChangeTreeNode): ChangeSet {\n return { indexes: {}, operations: [], queueRootNode };\n}\n\n// Linked list helper functions\nexport function createChangeTreeList(): ChangeTreeList {\n return { next: undefined, tail: undefined };\n}\n\nexport function setOperationAtIndex(changeSet: ChangeSet, index: number) {\n const operationsIndex = changeSet.indexes[index];\n if (operationsIndex === undefined) {\n changeSet.indexes[index] = changeSet.operations.push(index) - 1;\n } else {\n changeSet.operations[operationsIndex] = index;\n }\n}\n\nexport function deleteOperationAtIndex(changeSet: ChangeSet, index: number | string) {\n let operationsIndex = changeSet.indexes[index as any as number];\n if (operationsIndex === undefined) {\n //\n // if index is not found, we need to find the last operation\n // FIXME: this is not very efficient\n //\n // > See \"should allow consecutive splices (same place)\" tests\n //\n operationsIndex = Object.values(changeSet.indexes).at(-1);\n index = Object.entries(changeSet.indexes).find(([_, value]) => value === operationsIndex)?.[0];\n }\n changeSet.operations[operationsIndex] = undefined;\n delete changeSet.indexes[index as any as number];\n}\n\nexport function debugChangeSet(label: string, changeSet: ChangeSet) {\n let indexes: string[] = [];\n let operations: string[] = [];\n\n for (const index in changeSet.indexes) {\n indexes.push(`\\t${index} => [${changeSet.indexes[index]}]`);\n }\n\n for (let i = 0; i < changeSet.operations.length; i++) {\n const index = changeSet.operations[i];\n if (index !== undefined) {\n operations.push(`\\t[${i}] => ${index}`);\n }\n }\n\n console.log(`${label} =>\\nindexes (${Object.keys(changeSet.indexes).length}) {`);\n console.log(indexes.join(\"\\n\"), \"\\n}\");\n console.log(`operations (${changeSet.operations.filter(op => op !== undefined).length}) {`);\n console.log(operations.join(\"\\n\"), \"\\n}\");\n}\n\nexport interface ParentChain {\n ref: Ref;\n index: number;\n next?: ParentChain;\n}\n\nexport class ChangeTree<T extends Ref = any> {\n ref: T;\n refId: number;\n metadata: Metadata;\n\n root?: Root;\n parentChain?: ParentChain; // Linked list for tracking parents\n\n /**\n * Whether this structure is parent of a filtered structure.\n */\n isFiltered: boolean = false;\n isVisibilitySharedWithParent?: boolean; // See test case: 'should not be required to manually call view.add() items to child arrays without @view() tag'\n\n indexedOperations: IndexedOperations = {};\n\n //\n // TODO:\n // try storing the index + operation per item.\n // example: 1024 & 1025 => ADD, 1026 => DELETE\n //\n // => https://chatgpt.com/share/67107d0c-bc20-8004-8583-83b17dd7c196\n //\n changes: ChangeSet = { indexes: {}, operations: [] };\n allChanges: ChangeSet = { indexes: {}, operations: [] };\n filteredChanges: ChangeSet;\n allFilteredChanges: ChangeSet;\n\n indexes: { [index: string]: any }; // TODO: remove this, only used by MapSchema/SetSchema/CollectionSchema (`encodeKeyValueOperation`)\n\n /**\n * Is this a new instance? Used on ArraySchema to determine OPERATION.MOVE_AND_ADD operation.\n */\n isNew = true;\n\n constructor(ref: T) {\n this.ref = ref;\n this.metadata = (ref.constructor as typeof Schema)[Symbol.metadata];\n\n //\n // Does this structure have \"filters\" declared?\n //\n if (this.metadata?.[$viewFieldIndexes]) {\n this.allFilteredChanges = { indexes: {}, operations: [] };\n this.filteredChanges = { indexes: {}, operations: [] };\n }\n }\n\n setRoot(root: Root) {\n this.root = root;\n\n const isNewChangeTree = this.root.add(this);\n\n this.checkIsFiltered(this.parent, this.parentIndex, isNewChangeTree);\n\n // Recursively set root on child structures\n if (isNewChangeTree) {\n this.forEachChild((child, _) => {\n if (child.root !== root) {\n child.setRoot(root);\n } else {\n root.add(child); // increment refCount\n }\n });\n }\n }\n\n setParent(\n parent: Ref,\n root?: Root,\n parentIndex?: number,\n ) {\n this.addParent(parent, parentIndex);\n\n // avoid setting parents with empty `root`\n if (!root) { return; }\n\n const isNewChangeTree = root.add(this);\n\n // skip if parent is already set\n if (root !== this.root) {\n this.root = root;\n this.checkIsFiltered(parent, parentIndex, isNewChangeTree);\n }\n\n // assign same parent on child structures\n if (isNewChangeTree) {\n //\n // assign same parent on child structures\n //\n this.forEachChild((child, index) => {\n if (child.root === root) {\n //\n // re-assigning a child of the same root, move it next to parent\n // so encoding order is preserved\n //\n root.add(child);\n root.moveNextToParent(child);\n return;\n }\n child.setParent(this.ref, root, index);\n });\n }\n }\n\n forEachChild(callback: (change: ChangeTree, at: any) => void) {\n //\n // assign same parent on child structures\n //\n if ((this.ref as any)[$childType]) {\n if (typeof ((this.ref as any)[$childType]) !== \"string\") {\n // MapSchema / ArraySchema, etc.\n for (const [key, value] of (this.ref as MapSchema).entries()) {\n if (!value) { continue; } // sparse arrays can have undefined values\n callback(value[$changes], this.indexes?.[key] ?? key);\n };\n }\n\n } else {\n for (const index of this.metadata?.[$refTypeFieldIndexes] ?? []) {\n const field = this.metadata[index as any as number];\n const value = this.ref[field.name as keyof Ref];\n if (!value) { continue; }\n callback(value[$changes], index);\n }\n }\n }\n\n operation(op: OPERATION) {\n // operations without index use negative values to represent them\n // this is checked during .encode() time.\n if (this.filteredChanges !== undefined) {\n this.filteredChanges.operations.push(-op);\n this.root?.enqueueChangeTree(this, 'filteredChanges');\n\n } else {\n this.changes.operations.push(-op);\n this.root?.enqueueChangeTree(this, 'changes');\n }\n }\n\n change(index: number, operation: OPERATION = OPERATION.ADD) {\n const isFiltered = this.isFiltered || (this.metadata?.[index]?.tag !== undefined);\n const changeSet = (isFiltered)\n ? this.filteredChanges\n : this.changes;\n\n const previousOperation = this.indexedOperations[index];\n if (!previousOperation || previousOperation === OPERATION.DELETE) {\n const op = (!previousOperation)\n ? operation\n : (previousOperation === OPERATION.DELETE)\n ? OPERATION.DELETE_AND_ADD\n : operation\n //\n // TODO: are DELETE operations being encoded as ADD here ??\n //\n this.indexedOperations[index] = op;\n }\n\n setOperationAtIndex(changeSet, index);\n\n if (isFiltered) {\n setOperationAtIndex(this.allFilteredChanges, index);\n\n if (this.root) {\n this.root.enqueueChangeTree(this, 'filteredChanges');\n this.root.enqueueChangeTree(this, 'allFilteredChanges');\n }\n\n } else {\n setOperationAtIndex(this.allChanges, index);\n this.root?.enqueueChangeTree(this, 'changes');\n }\n }\n\n shiftChangeIndexes(shiftIndex: number) {\n //\n // Used only during:\n //\n // - ArraySchema#unshift()\n //\n const changeSet = (this.isFiltered)\n ? this.filteredChanges\n : this.changes;\n\n const newIndexedOperations: any = {};\n const newIndexes: { [index: number]: number } = {};\n for (const index in this.indexedOperations) {\n newIndexedOperations[Number(index) + shiftIndex] = this.indexedOperations[index];\n newIndexes[Number(index) + shiftIndex] = changeSet.indexes[index];\n }\n this.indexedOperations = newIndexedOperations;\n changeSet.indexes = newIndexes;\n\n changeSet.operations = changeSet.operations.map((index) => index + shiftIndex);\n }\n\n shiftAllChangeIndexes(shiftIndex: number, startIndex: number = 0) {\n //\n // Used only during:\n //\n // - ArraySchema#splice()\n //\n if (this.filteredChanges !== undefined) {\n this