UNPKG

colyseus.js

Version:

Colyseus Multiplayer SDK for JavaScript/TypeScript

1,158 lines (1,134 loc) 403 kB
// colyseus.js@0.16.22 (@colyseus/schema 3.0.61) (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define('colyseus.js', ['exports'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.Colyseus = {})); })(this, (function (exports) { 'use strict'; function _mergeNamespaces(n, m) { m.forEach(function (e) { e && typeof e !== 'string' && !Array.isArray(e) && Object.keys(e).forEach(function (k) { if (k !== 'default' && !(k in n)) { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); }); return Object.freeze(n); } // // Polyfills for legacy environments // /* * Support Android 4.4.x */ if (!ArrayBuffer.isView) { ArrayBuffer.isView = (a) => { return a !== null && typeof (a) === 'object' && a.buffer instanceof ArrayBuffer; }; } // Define globalThis if not available. // https://github.com/colyseus/colyseus.js/issues/86 if (typeof (globalThis) === "undefined" && typeof (window) !== "undefined") { // @ts-ignore window['globalThis'] = window; } // Cocos Creator does not provide "FormData" // Define a dummy implementation so it doesn't crash if (typeof (FormData) === "undefined") { // @ts-ignore globalThis['FormData'] = class { }; } /****************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ /* global Reflect, Promise, SuppressedError, Symbol, Iterator */ function __awaiter(thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); } function __classPrivateFieldGet(receiver, state, kind, f) { if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter"); if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it"); return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver); } function __classPrivateFieldSet(receiver, state, value, kind, f) { if (kind === "m") throw new TypeError("Private method is not writable"); if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter"); if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it"); return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value; } typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { var e = new Error(message); return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; }; var CloseCode; (function (CloseCode) { CloseCode[CloseCode["CONSENTED"] = 4000] = "CONSENTED"; CloseCode[CloseCode["DEVMODE_RESTART"] = 4010] = "DEVMODE_RESTART"; })(CloseCode || (CloseCode = {})); class ServerError extends Error { constructor(code, message) { super(message); this.name = "ServerError"; this.code = code; } } class AbortError extends Error { constructor(message) { super(message); this.name = "AbortError"; } } function getDefaultExportFromCjs (x) { return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x; } var umd$1 = {exports: {}}; var umd = umd$1.exports; var hasRequiredUmd; function requireUmd () { if (hasRequiredUmd) return umd$1.exports; hasRequiredUmd = 1; (function (module, exports) { (function (global, factory) { factory(exports) ; })(umd, (function (exports) { const SWITCH_TO_STRUCTURE = 255; // (decoding collides with DELETE_AND_ADD + fieldIndex = 63) const TYPE_ID = 213; /** * Encoding Schema field operations. */ exports.OPERATION = void 0; (function (OPERATION) { OPERATION[OPERATION["ADD"] = 128] = "ADD"; OPERATION[OPERATION["REPLACE"] = 0] = "REPLACE"; OPERATION[OPERATION["DELETE"] = 64] = "DELETE"; OPERATION[OPERATION["DELETE_AND_MOVE"] = 96] = "DELETE_AND_MOVE"; OPERATION[OPERATION["MOVE_AND_ADD"] = 160] = "MOVE_AND_ADD"; OPERATION[OPERATION["DELETE_AND_ADD"] = 192] = "DELETE_AND_ADD"; /** * Collection operations */ OPERATION[OPERATION["CLEAR"] = 10] = "CLEAR"; /** * ArraySchema operations */ OPERATION[OPERATION["REVERSE"] = 15] = "REVERSE"; OPERATION[OPERATION["MOVE"] = 32] = "MOVE"; OPERATION[OPERATION["DELETE_BY_REFID"] = 33] = "DELETE_BY_REFID"; OPERATION[OPERATION["ADD_BY_REFID"] = 129] = "ADD_BY_REFID"; })(exports.OPERATION || (exports.OPERATION = {})); Symbol.metadata ??= Symbol.for("Symbol.metadata"); const $track = "~track"; const $encoder = "~encoder"; const $decoder = "~decoder"; const $filter = "~filter"; const $getByIndex = "~getByIndex"; const $deleteByIndex = "~deleteByIndex"; /** * Used to hold ChangeTree instances whitin the structures */ const $changes = '~changes'; /** * Used to keep track of the type of the child elements of a collection * (MapSchema, ArraySchema, etc.) */ const $childType = '~childType'; /** * Optional "discard" method for custom types (ArraySchema) * (Discards changes for next serialization) */ const $onEncodeEnd = '~onEncodeEnd'; /** * When decoding, this method is called after the instance is fully decoded */ const $onDecodeEnd = "~onDecodeEnd"; /** * Metadata */ const $descriptors = "~descriptors"; const $numFields = "~__numFields"; const $refTypeFieldIndexes = "~__refTypeFieldIndexes"; const $viewFieldIndexes = "~__viewFieldIndexes"; const $fieldIndexesByViewTag = "$__fieldIndexesByViewTag"; /** * Copyright (c) 2018 Endel Dreyer * Copyright (c) 2014 Ion Drive Software Ltd. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE */ /** * msgpack implementation highly based on notepack.io * https://github.com/darrachequesne/notepack */ let textEncoder; // @ts-ignore try { textEncoder = new TextEncoder(); } catch (e) { } const _convoBuffer$1 = new ArrayBuffer(8); const _int32$1 = new Int32Array(_convoBuffer$1); const _float32$1 = new Float32Array(_convoBuffer$1); const _float64$1 = new Float64Array(_convoBuffer$1); const _int64$1 = new BigInt64Array(_convoBuffer$1); const hasBufferByteLength = (typeof Buffer !== 'undefined' && Buffer.byteLength); const utf8Length = (hasBufferByteLength) ? Buffer.byteLength // node : function (str, _) { var c = 0, length = 0; for (var i = 0, l = str.length; i < l; i++) { c = str.charCodeAt(i); if (c < 0x80) { length += 1; } else if (c < 0x800) { length += 2; } else if (c < 0xd800 || c >= 0xe000) { length += 3; } else { i++; length += 4; } } return length; }; function utf8Write(view, str, it) { var c = 0; for (var i = 0, l = str.length; i < l; i++) { c = str.charCodeAt(i); if (c < 0x80) { view[it.offset++] = c; } else if (c < 0x800) { view[it.offset] = 0xc0 | (c >> 6); view[it.offset + 1] = 0x80 | (c & 0x3f); it.offset += 2; } else if (c < 0xd800 || c >= 0xe000) { view[it.offset] = 0xe0 | (c >> 12); view[it.offset + 1] = 0x80 | (c >> 6 & 0x3f); view[it.offset + 2] = 0x80 | (c & 0x3f); it.offset += 3; } else { i++; c = 0x10000 + (((c & 0x3ff) << 10) | (str.charCodeAt(i) & 0x3ff)); view[it.offset] = 0xf0 | (c >> 18); view[it.offset + 1] = 0x80 | (c >> 12 & 0x3f); view[it.offset + 2] = 0x80 | (c >> 6 & 0x3f); view[it.offset + 3] = 0x80 | (c & 0x3f); it.offset += 4; } } } function int8$1(bytes, value, it) { bytes[it.offset++] = value & 255; } function uint8$1(bytes, value, it) { bytes[it.offset++] = value & 255; } function int16$1(bytes, value, it) { bytes[it.offset++] = value & 255; bytes[it.offset++] = (value >> 8) & 255; } function uint16$1(bytes, value, it) { bytes[it.offset++] = value & 255; bytes[it.offset++] = (value >> 8) & 255; } function int32$1(bytes, value, it) { bytes[it.offset++] = value & 255; bytes[it.offset++] = (value >> 8) & 255; bytes[it.offset++] = (value >> 16) & 255; bytes[it.offset++] = (value >> 24) & 255; } function uint32$1(bytes, value, it) { const b4 = value >> 24; const b3 = value >> 16; const b2 = value >> 8; const b1 = value; bytes[it.offset++] = b1 & 255; bytes[it.offset++] = b2 & 255; bytes[it.offset++] = b3 & 255; bytes[it.offset++] = b4 & 255; } function int64$1(bytes, value, it) { const high = Math.floor(value / Math.pow(2, 32)); const low = value >>> 0; uint32$1(bytes, low, it); uint32$1(bytes, high, it); } function uint64$1(bytes, value, it) { const high = (value / Math.pow(2, 32)) >> 0; const low = value >>> 0; uint32$1(bytes, low, it); uint32$1(bytes, high, it); } function bigint64$1(bytes, value, it) { _int64$1[0] = BigInt.asIntN(64, value); int32$1(bytes, _int32$1[0], it); int32$1(bytes, _int32$1[1], it); } function biguint64$1(bytes, value, it) { _int64$1[0] = BigInt.asIntN(64, value); int32$1(bytes, _int32$1[0], it); int32$1(bytes, _int32$1[1], it); } function float32$1(bytes, value, it) { _float32$1[0] = value; int32$1(bytes, _int32$1[0], it); } function float64$1(bytes, value, it) { _float64$1[0] = value; int32$1(bytes, _int32$1[0 ], it); int32$1(bytes, _int32$1[1 ], it); } function boolean$1(bytes, value, it) { bytes[it.offset++] = value ? 1 : 0; // uint8 } function string$1(bytes, value, it) { // encode `null` strings as empty. if (!value) { value = ""; } let length = utf8Length(value, "utf8"); let size = 0; // fixstr if (length < 0x20) { bytes[it.offset++] = length | 0xa0; size = 1; } // str 8 else if (length < 0x100) { bytes[it.offset++] = 0xd9; bytes[it.offset++] = length % 255; size = 2; } // str 16 else if (length < 0x10000) { bytes[it.offset++] = 0xda; uint16$1(bytes, length, it); size = 3; } // str 32 else if (length < 0x100000000) { bytes[it.offset++] = 0xdb; uint32$1(bytes, length, it); size = 5; } else { throw new Error('String too long'); } utf8Write(bytes, value, it); return size + length; } function number$1(bytes, value, it) { if (isNaN(value)) { return number$1(bytes, 0, it); } else if (!isFinite(value)) { return number$1(bytes, (value > 0) ? Number.MAX_SAFE_INTEGER : -Number.MAX_SAFE_INTEGER, it); } else if (value !== (value | 0)) { if (Math.abs(value) <= 3.4028235e+38) { // range check _float32$1[0] = value; if (Math.abs(Math.abs(_float32$1[0]) - Math.abs(value)) < 1e-4) { // precision check; adjust 1e-n (n = precision) to in-/decrease acceptable precision loss // now we know value is in range for f32 and has acceptable precision for f32 bytes[it.offset++] = 0xca; float32$1(bytes, value, it); return 5; } } bytes[it.offset++] = 0xcb; float64$1(bytes, value, it); return 9; } if (value >= 0) { // positive fixnum if (value < 0x80) { bytes[it.offset++] = value & 255; // uint8 return 1; } // uint 8 if (value < 0x100) { bytes[it.offset++] = 0xcc; bytes[it.offset++] = value & 255; // uint8 return 2; } // uint 16 if (value < 0x10000) { bytes[it.offset++] = 0xcd; uint16$1(bytes, value, it); return 3; } // uint 32 if (value < 0x100000000) { bytes[it.offset++] = 0xce; uint32$1(bytes, value, it); return 5; } // uint 64 bytes[it.offset++] = 0xcf; uint64$1(bytes, value, it); return 9; } else { // negative fixnum if (value >= -32) { bytes[it.offset++] = 0xe0 | (value + 0x20); return 1; } // int 8 if (value >= -128) { bytes[it.offset++] = 0xd0; int8$1(bytes, value, it); return 2; } // int 16 if (value >= -32768) { bytes[it.offset++] = 0xd1; int16$1(bytes, value, it); return 3; } // int 32 if (value >= -2147483648) { bytes[it.offset++] = 0xd2; int32$1(bytes, value, it); return 5; } // int 64 bytes[it.offset++] = 0xd3; int64$1(bytes, value, it); return 9; } } const encode = { int8: int8$1, uint8: uint8$1, int16: int16$1, uint16: uint16$1, int32: int32$1, uint32: uint32$1, int64: int64$1, uint64: uint64$1, bigint64: bigint64$1, biguint64: biguint64$1, float32: float32$1, float64: float64$1, boolean: boolean$1, string: string$1, number: number$1, utf8Write, utf8Length, }; /** * Copyright (c) 2018 Endel Dreyer * Copyright (c) 2014 Ion Drive Software Ltd. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE */ // force little endian to facilitate decoding on multiple implementations const _convoBuffer = new ArrayBuffer(8); const _int32 = new Int32Array(_convoBuffer); const _float32 = new Float32Array(_convoBuffer); const _float64 = new Float64Array(_convoBuffer); const _uint64 = new BigUint64Array(_convoBuffer); const _int64 = new BigInt64Array(_convoBuffer); function utf8Read(bytes, it, length) { var string = '', chr = 0; for (var i = it.offset, end = it.offset + length; i < end; i++) { var byte = bytes[i]; if ((byte & 0x80) === 0x00) { string += String.fromCharCode(byte); continue; } if ((byte & 0xe0) === 0xc0) { string += String.fromCharCode(((byte & 0x1f) << 6) | (bytes[++i] & 0x3f)); continue; } if ((byte & 0xf0) === 0xe0) { string += String.fromCharCode(((byte & 0x0f) << 12) | ((bytes[++i] & 0x3f) << 6) | ((bytes[++i] & 0x3f) << 0)); continue; } if ((byte & 0xf8) === 0xf0) { chr = ((byte & 0x07) << 18) | ((bytes[++i] & 0x3f) << 12) | ((bytes[++i] & 0x3f) << 6) | ((bytes[++i] & 0x3f) << 0); if (chr >= 0x010000) { // surrogate pair chr -= 0x010000; string += String.fromCharCode((chr >>> 10) + 0xD800, (chr & 0x3FF) + 0xDC00); } else { string += String.fromCharCode(chr); } continue; } console.error('Invalid byte ' + byte.toString(16)); // (do not throw error to avoid server/client from crashing due to hack attemps) // throw new Error('Invalid byte ' + byte.toString(16)); } it.offset += length; return string; } function int8(bytes, it) { return uint8(bytes, it) << 24 >> 24; } function uint8(bytes, it) { return bytes[it.offset++]; } function int16(bytes, it) { return uint16(bytes, it) << 16 >> 16; } function uint16(bytes, it) { return bytes[it.offset++] | bytes[it.offset++] << 8; } function int32(bytes, it) { return bytes[it.offset++] | bytes[it.offset++] << 8 | bytes[it.offset++] << 16 | bytes[it.offset++] << 24; } function uint32(bytes, it) { return int32(bytes, it) >>> 0; } function float32(bytes, it) { _int32[0] = int32(bytes, it); return _float32[0]; } function float64(bytes, it) { _int32[0 ] = int32(bytes, it); _int32[1 ] = int32(bytes, it); return _float64[0]; } function int64(bytes, it) { const low = uint32(bytes, it); const high = int32(bytes, it) * Math.pow(2, 32); return high + low; } function uint64(bytes, it) { const low = uint32(bytes, it); const high = uint32(bytes, it) * Math.pow(2, 32); return high + low; } function bigint64(bytes, it) { _int32[0] = int32(bytes, it); _int32[1] = int32(bytes, it); return _int64[0]; } function biguint64(bytes, it) { _int32[0] = int32(bytes, it); _int32[1] = int32(bytes, it); return _uint64[0]; } function boolean(bytes, it) { return uint8(bytes, it) > 0; } function string(bytes, it) { const prefix = bytes[it.offset++]; let length; if (prefix < 0xc0) { // fixstr length = prefix & 0x1f; } else if (prefix === 0xd9) { length = uint8(bytes, it); } else if (prefix === 0xda) { length = uint16(bytes, it); } else if (prefix === 0xdb) { length = uint32(bytes, it); } return utf8Read(bytes, it, length); } function number(bytes, it) { const prefix = bytes[it.offset++]; if (prefix < 0x80) { // positive fixint return prefix; } else if (prefix === 0xca) { // float 32 return float32(bytes, it); } else if (prefix === 0xcb) { // float 64 return float64(bytes, it); } else if (prefix === 0xcc) { // uint 8 return uint8(bytes, it); } else if (prefix === 0xcd) { // uint 16 return uint16(bytes, it); } else if (prefix === 0xce) { // uint 32 return uint32(bytes, it); } else if (prefix === 0xcf) { // uint 64 return uint64(bytes, it); } else if (prefix === 0xd0) { // int 8 return int8(bytes, it); } else if (prefix === 0xd1) { // int 16 return int16(bytes, it); } else if (prefix === 0xd2) { // int 32 return int32(bytes, it); } else if (prefix === 0xd3) { // int 64 return int64(bytes, it); } else if (prefix > 0xdf) { // negative fixint return (0xff - prefix + 1) * -1; } } function stringCheck(bytes, it) { const prefix = bytes[it.offset]; return ( // fixstr (prefix < 0xc0 && prefix > 0xa0) || // str 8 prefix === 0xd9 || // str 16 prefix === 0xda || // str 32 prefix === 0xdb); } const decode = { utf8Read, int8, uint8, int16, uint16, int32, uint32, float32, float64, int64, uint64, bigint64, biguint64, boolean, string, number, stringCheck, }; const registeredTypes = {}; const identifiers = new Map(); function registerType(identifier, definition) { if (definition.constructor) { identifiers.set(definition.constructor, identifier); registeredTypes[identifier] = definition; } if (definition.encode) { encode[identifier] = definition.encode; } if (definition.decode) { decode[identifier] = definition.decode; } } function getType(identifier) { return registeredTypes[identifier]; } function defineCustomTypes(types) { for (const identifier in types) { registerType(identifier, types[identifier]); } return (t) => type(t); } class TypeContext { /** * For inheritance support * Keeps track of which classes extends which. (parent -> children) */ static { this.inheritedTypes = new Map(); } static { this.cachedContexts = new Map(); } static register(target) { const parent = Object.getPrototypeOf(target); if (parent !== Schema) { let inherits = TypeContext.inheritedTypes.get(parent); if (!inherits) { inherits = new Set(); TypeContext.inheritedTypes.set(parent, inherits); } inherits.add(target); } } static cache(rootClass) { let context = TypeContext.cachedContexts.get(rootClass); if (!context) { context = new TypeContext(rootClass); TypeContext.cachedContexts.set(rootClass, context); } return context; } constructor(rootClass) { this.types = {}; this.schemas = new Map(); this.hasFilters = false; this.parentFiltered = {}; if (rootClass) { this.discoverTypes(rootClass); } } has(schema) { return this.schemas.has(schema); } get(typeid) { return this.types[typeid]; } add(schema, typeid = this.schemas.size) { // skip if already registered if (this.schemas.has(schema)) { return false; } this.types[typeid] = schema; // // Workaround to allow using an empty Schema (with no `@type()` fields) // if (schema[Symbol.metadata] === undefined) { Metadata.initialize(schema); } this.schemas.set(schema, typeid); return true; } getTypeId(klass) { return this.schemas.get(klass); } discoverTypes(klass, parentType, parentIndex, parentHasViewTag) { if (parentHasViewTag) { this.registerFilteredByParent(klass, parentType, parentIndex); } // skip if already registered if (!this.add(klass)) { return; } // add classes inherited from this base class TypeContext.inheritedTypes.get(klass)?.forEach((child) => { this.discoverTypes(child, parentType, parentIndex, parentHasViewTag); }); // add parent classes let parent = klass; while ((parent = Object.getPrototypeOf(parent)) && parent !== Schema && // stop at root (Schema) parent !== Function.prototype // stop at root (non-Schema) ) { this.discoverTypes(parent); } const metadata = (klass[Symbol.metadata] ??= {}); // if any schema/field has filters, mark "context" as having filters. if (metadata[$viewFieldIndexes]) { this.hasFilters = true; } for (const fieldIndex in metadata) { const index = fieldIndex; const fieldType = metadata[index].type; const fieldHasViewTag = (metadata[index].tag !== undefined); if (typeof (fieldType) === "string") { continue; } if (typeof (fieldType) === "function") { this.discoverTypes(fieldType, klass, index, parentHasViewTag || fieldHasViewTag); } else { const type = Object.values(fieldType)[0]; // skip primitive types if (typeof (type) === "string") { continue; } this.discoverTypes(type, klass, index, parentHasViewTag || fieldHasViewTag); } } } /** * Keep track of which classes have filters applied. * Format: `${typeid}-${parentTypeid}-${parentIndex}` */ registerFilteredByParent(schema, parentType, parentIndex) { const typeid = this.schemas.get(schema) ?? this.schemas.size; let key = `${typeid}`; if (parentType) { key += `-${this.schemas.get(parentType)}`; } key += `-${parentIndex}`; this.parentFiltered[key] = true; } debug() { let parentFiltered = ""; for (const key in this.parentFiltered) { const keys = key.split("-").map(Number); const fieldIndex = keys.pop(); parentFiltered += `\n\t\t`; parentFiltered += `${key}: ${keys.reverse().map((id, i) => { const klass = this.types[id]; const metadata = klass[Symbol.metadata]; let txt = klass.name; if (i === 0) { txt += `[${metadata[fieldIndex].name}]`; } return `${txt}`; }).join(" -> ")}`; } return `TypeContext ->\n` + `\tSchema types: ${this.schemas.size}\n` + `\thasFilters: ${this.hasFilters}\n` + `\tparentFiltered:${parentFiltered}`; } } function getNormalizedType(type) { if (Array.isArray(type)) { return { array: getNormalizedType(type[0]) }; } else if (typeof (type['type']) !== "undefined") { return type['type']; } else if (isTSEnum(type)) { // Detect TS Enum type (either string or number) return Object.keys(type).every(key => typeof type[key] === "string") ? "string" : "number"; } else if (typeof type === "object" && type !== null) { // Handle collection types const collectionType = Object.keys(type).find(k => registeredTypes[k] !== undefined); if (collectionType) { type[collectionType] = getNormalizedType(type[collectionType]); return type; } } return type; } function isTSEnum(_enum) { if (typeof _enum === 'function' && _enum[Symbol.metadata]) { return false; } const keys = Object.keys(_enum); const numericFields = keys.filter(k => /\d+/.test(k)); // Check for number enum (has numeric keys and reverse mapping) if (numericFields.length > 0 && numericFields.length === (keys.length / 2) && _enum[_enum[numericFields[0]]] == numericFields[0]) { return true; } // Check for string enum (all values are strings and keys match values) if (keys.length > 0 && keys.every(key => typeof _enum[key] === 'string' && _enum[key] === key)) { return true; } return false; } const Metadata = { addField(metadata, index, name, type, descriptor) { if (index > 64) { throw new Error(`Can't define field '${name}'.\nSchema instances may only have up to 64 fields.`); } metadata[index] = Object.assign(metadata[index] || {}, // avoid overwriting previous field metadata (@owned / @deprecated) { type: getNormalizedType(type), index, name, }); // create "descriptors" map Object.defineProperty(metadata, $descriptors, { value: metadata[$descriptors] || {}, enumerable: false, configurable: true, }); if (descriptor) { // for encoder metadata[$descriptors][name] = descriptor; metadata[$descriptors][`_${name}`] = { value: undefined, writable: true, enumerable: false, configurable: true, }; } else { // for decoder metadata[$descriptors][name] = { value: undefined, writable: true, enumerable: true, configurable: true, }; } // map -1 as last field index Object.defineProperty(metadata, $numFields, { value: index, enumerable: false, configurable: true }); // map field name => index (non enumerable) Object.defineProperty(metadata, name, { value: index, enumerable: false, configurable: true, }); // if child Ref/complex type, add to -4 if (typeof (metadata[index].type) !== "string") { if (metadata[$refTypeFieldIndexes] === undefined) { Object.defineProperty(metadata, $refTypeFieldIndexes, { value: [], enumerable: false, configurable: true, }); } metadata[$refTypeFieldIndexes].push(index); } }, setTag(metadata, fieldName, tag) { const index = metadata[fieldName]; const field = metadata[index]; // add 'tag' to the field field.tag = tag; if (!metadata[$viewFieldIndexes]) { // -2: all field indexes with "view" tag Object.defineProperty(metadata, $viewFieldIndexes, { value: [], enumerable: false, configurable: true }); // -3: field indexes by "view" tag Object.defineProperty(metadata, $fieldIndexesByViewTag, { value: {}, enumerable: false, configurable: true }); } metadata[$viewFieldIndexes].push(index); if (!metadata[$fieldIndexesByViewTag][tag]) { metadata[$fieldIndexesByViewTag][tag] = []; } metadata[$fieldIndexesByViewTag][tag].push(index); }, setFields(target, fields) { // for inheritance support const constructor = target.prototype.constructor; TypeContext.register(constructor); const parentClass = Object.getPrototypeOf(constructor); const parentMetadata = parentClass && parentClass[Symbol.metadata]; const metadata = Metadata.initialize(constructor); // Use Schema's methods if not defined in the class if (!constructor[$track]) { constructor[$track] = Schema[$track]; } if (!constructor[$encoder]) { constructor[$encoder] = Schema[$encoder]; } if (!constructor[$decoder]) { constructor[$decoder] = Schema[$decoder]; } if (!constructor.prototype.toJSON) { constructor.prototype.toJSON = Schema.prototype.toJSON; } // // detect index for this field, considering inheritance // let fieldIndex = metadata[$numFields] // current structure already has fields defined ?? (parentMetadata && parentMetadata[$numFields]) // parent structure has fields defined ?? -1; // no fields defined fieldIndex++; for (const field in fields) { const type = getNormalizedType(fields[field]); // FIXME: this code is duplicated from @type() annotation const complexTypeKlass = typeof (Object.keys(type)[0]) === "string" && getType(Object.keys(type)[0]); const childType = (complexTypeKlass) ? Object.values(type)[0] : type; Metadata.addField(metadata, fieldIndex, field, type, getPropertyDescriptor(`_${field}`, fieldIndex, childType, complexTypeKlass)); fieldIndex++; } return target; }, isDeprecated(metadata, field) { return metadata[field].deprecated === true; }, init(klass) { // // Used only to initialize an empty Schema (Encoder#constructor) // TODO: remove/refactor this... // const metadata = {}; klass[Symbol.metadata] = metadata; Object.defineProperty(metadata, $numFields, { value: 0, enumerable: false, configurable: true, }); }, initialize(constructor) { const parentClass = Object.getPrototypeOf(constructor); const parentMetadata = parentClass[Symbol.metadata]; let metadata = constructor[Symbol.metadata] ?? Object.create(null); // make sure inherited classes have their own metadata object. if (parentClass !== Schema && metadata === parentMetadata) { metadata = Object.create(null); if (parentMetadata) { // // assign parent metadata to current // Object.setPrototypeOf(metadata, parentMetadata); // $numFields Object.defineProperty(metadata, $numFields, { value: parentMetadata[$numFields], enumerable: false, configurable: true, writable: true, }); // $viewFieldIndexes / $fieldIndexesByViewTag if (parentMetadata[$viewFieldIndexes] !== undefined) { Object.defineProperty(metadata, $viewFieldIndexes, { value: [...parentMetadata[$viewFieldIndexes]], enumerable: false, configurable: true, writable: true, }); Object.defineProperty(metadata, $fieldIndexesByViewTag, { value: { ...parentMetadata[$fieldIndexesByViewTag] }, enumerable: false, configurable: true, writable: true, }); } // $refTypeFieldIndexes if (parentMetadata[$refTypeFieldIndexes] !== undefined) { Object.defineProperty(metadata, $refTypeFieldIndexes, { value: [...parentMetadata[$refTypeFieldIndexes]], enumerable: false, configurable: true, writable: true, }); } // $descriptors Object.defineProperty(metadata, $descriptors, { value: { ...parentMetadata[$descriptors] }, enumerable: false, configurable: true, writable: true, }); } } constructor[Symbol.metadata] = metadata; return metadata; }, isValidInstance(klass) { return (klass.constructor[Symbol.metadata] && Object.prototype.hasOwnProperty.call(klass.constructor[Symbol.metadata], $numFields)); }, getFields(klass) { const metadata = klass[Symbol.metadata]; const fields = {}; for (let i = 0; i <= metadata[$numFields]; i++) { fields[metadata[i].name] = metadata[i].type; } return fields; }, hasViewTagAtIndex(metadata, index) { return metadata?.[$viewFieldIndexes]?.includes(index); } }; function createChangeSet(queueRootNode) { return { indexes: {}, operations: [], queueRootNode }; } // Linked list helper functions function createChangeTreeList() { return { next: undefined, tail: undefined }; } function setOperationAtIndex(changeSet, index) { const operationsIndex = changeSet.indexes[index]; if (operationsIndex === undefined) { changeSet.indexes[index] = changeSet.operations.push(index) - 1; } else { changeSet.operations[operationsIndex] = index; } } function deleteOperationAtIndex(changeSet, index) { let operationsIndex = changeSet.indexes[index]; if (operationsIndex === undefined) { // // if index is not found, we need to find the last operation // FIXME: this is not very efficient // // > See "should allow consecutive splices (same place)" tests // operationsIndex = Object.values(changeSet.indexes).at(-1); index = Object.entries(changeSet.indexes).find(([_, value]) => value === operationsIndex)?.[0]; } changeSet.operations[operationsIndex] = undefined; delete changeSet.indexes[index]; } class ChangeTree { constructor(ref) { /** * Whether this structure is parent of a filtered structure. */ this.isFiltered = false; this.indexedOperations = {}; // // TODO: // try storing the index + operation per item. // example: 1024 & 1025 => ADD, 1026 => DELETE // // => https://chatgpt.com/share