UNPKG

@tamgl/colyseus-schema

Version:

Binary state serializer with delta encoding for games

159 lines (134 loc) 5.28 kB
import { Metadata } from "../Metadata"; import { $childType } from "../types/symbols"; import { Ref } from "../encoder/ChangeTree"; import { spliceOne } from "../types/utils"; import type { MapSchema } from "../types/custom/MapSchema"; import { OPERATION } from "../encoding/spec"; class DecodingWarning extends Error { constructor(message: string) { super(message); this.name = "DecodingWarning"; } } /** * Used for decoding only. */ export type SchemaCallbacks = { [field: string | number]: Function[] }; export class ReferenceTracker { // // Relation of refId => Schema structure // For direct access of structures during decoding time. // public refs = new Map<number, Ref>(); public refIds = new WeakMap<Ref, number>(); public refCount: { [refId: number]: number; } = {}; public deletedRefs = new Set<number>(); public callbacks: { [refId: number]: SchemaCallbacks } = {}; protected nextUniqueId: number = 0; getNextUniqueId() { return this.nextUniqueId++; } // for decoding addRef(refId: number, ref: Ref, incrementCount: boolean = true) { this.refs.set(refId, ref); this.refIds.set(ref, refId); if (incrementCount) { this.refCount[refId] = (this.refCount[refId] || 0) + 1; } if (this.deletedRefs.has(refId)) { this.deletedRefs.delete(refId); } } // for decoding removeRef(refId: number) { const refCount = this.refCount[refId]; if (refCount === undefined) { try { throw new DecodingWarning("trying to remove refId that doesn't exist: " + refId); } catch (e) { console.warn(e); } return; } if (refCount === 0) { try { const ref = this.refs.get(refId); throw new DecodingWarning(`trying to remove refId '${refId}' with 0 refCount (${ref.constructor.name}: ${JSON.stringify(ref)})`); } catch (e) { console.warn(e); } return; } if ((this.refCount[refId] = refCount - 1) <= 0) { this.deletedRefs.add(refId); } } clearRefs() { this.refs.clear(); this.deletedRefs.clear(); this.callbacks = {}; this.refCount = {}; } // for decoding garbageCollectDeletedRefs() { this.deletedRefs.forEach((refId) => { // // Skip active references. // if (this.refCount[refId] > 0) { return; } const ref = this.refs.get(refId); // // Ensure child schema instances have their references removed as well. // if (ref.constructor[Symbol.metadata] !== undefined) { const metadata: Metadata = ref.constructor[Symbol.metadata]; for (const index in metadata) { const field = metadata[index as any as number].name; const childRefId = typeof(ref[field]) === "object" && this.refIds.get(ref[field]); if (childRefId && !this.deletedRefs.has(childRefId)) { this.removeRef(childRefId); } } } else { if (typeof (ref[$childType]) === "function") { Array.from((ref as MapSchema).values()) .forEach((child) => { const childRefId = this.refIds.get(child); if (!this.deletedRefs.has(childRefId)) { this.removeRef(childRefId); } }); } } this.refs.delete(refId); // remove ref delete this.refCount[refId]; // remove ref count delete this.callbacks[refId]; // remove callbacks }); // clear deleted refs. this.deletedRefs.clear(); } addCallback(refId: number, fieldOrOperation: string | number, callback: Function) { if (refId === undefined) { const name = (typeof(fieldOrOperation) === "number") ? OPERATION[fieldOrOperation] : fieldOrOperation throw new Error( `Can't addCallback on '${name}' (refId is undefined)` ); } if (!this.callbacks[refId]) { this.callbacks[refId] = {}; } if (!this.callbacks[refId][fieldOrOperation]) { this.callbacks[refId][fieldOrOperation] = []; } this.callbacks[refId][fieldOrOperation].push(callback); return () => this.removeCallback(refId, fieldOrOperation, callback); } removeCallback(refId: number, field: string | number, callback: Function) { const index: number | undefined = this.callbacks?.[refId]?.[field]?.indexOf(callback); if (index !== undefined && index !== -1) { spliceOne(this.callbacks[refId][field], index); } } }