@@ -44,29 +44,53 @@ export type Snapshot<T> = DeepReadonly<
4444export interface SnapshotContext {
4545 /** Map of refId → Schema object from decoder.root.refs */
4646 refs : Map < number , any > | undefined ;
47+ /** Reverse lookup: Schema object → refId (built lazily) */
48+ objectToRefId : Map < object , number > | undefined ;
4749 /** Snapshot results from the previous render pass */
4850 previousResultsByRefId : Map < number , any > ;
4951 /** Snapshot results from the current render pass (for cycle detection) */
5052 currentResultsByRefId : Map < number , any > ;
53+ /** Set of refIds that have been modified since the last snapshot */
54+ dirtyRefIds : Set < number > ;
55+ /** Map of childRefId → parentRefId for ancestor tracking */
56+ parentRefIdMap : Map < number , number > ;
57+ /** Current parent refId during traversal (used to build parentRefIdMap) */
58+ currentParentRefId : number ;
5159}
5260
5361/**
54- * Finds the refId for a Schema object by searching the decoder's refs map.
62+ * Builds a reverse lookup map from objects to their refIds.
63+ */
64+ function buildObjectToRefIdMap ( refs : Map < number , any > ) : Map < object , number > {
65+ const map = new Map < object , number > ( ) ;
66+ for ( const [ refId , obj ] of refs . entries ( ) ) {
67+ if ( obj !== null && typeof obj === "object" ) {
68+ map . set ( obj , refId ) ;
69+ }
70+ }
71+ return map ;
72+ }
73+
74+ /**
75+ * Finds the refId for a Schema object using the reverse lookup map.
5576 *
5677 * In Colyseus 3.x, each Schema instance is assigned a unique numeric refId
5778 * that remains stable across encode/decode cycles. This allows us to track
5879 * object identity even when the JavaScript object references change.
5980 *
6081 * @param node - The Schema object to find the refId for
61- * @param refs - The decoder.root.refs map
82+ * @param ctx - The snapshot context with the reverse lookup map
6283 * @returns The refId if found, or -1 if not found
6384 */
64- function findRefId ( node : object , refs : Map < number , any > | undefined ) : number {
65- if ( ! refs ) return - 1 ;
66- for ( const [ refId , obj ] of refs . entries ( ) ) {
67- if ( obj === node ) return refId ;
85+ function findRefId ( node : object , ctx : SnapshotContext ) : number {
86+ if ( ! ctx . refs ) return - 1 ;
87+
88+ // Build the reverse lookup map lazily on first use.
89+ if ( ! ctx . objectToRefId ) {
90+ ctx . objectToRefId = buildObjectToRefIdMap ( ctx . refs ) ;
6891 }
69- return - 1 ;
92+
93+ return ctx . objectToRefId . get ( node ) ?? - 1 ;
7094}
7195
7296/**
@@ -109,13 +133,14 @@ function createSnapshotForArraySchema(
109133 previousResult : any [ ] | undefined ,
110134 ctx : SnapshotContext
111135) : any [ ] {
112- const items = Array . from ( node ) ;
113- const snapshotted : any [ ] = [ ] ;
114- let hasChanged = ! previousResult || ! Array . isArray ( previousResult ) || items . length !== previousResult . length ;
136+ const length = node . length ;
137+ let hasChanged = ! previousResult || ! Array . isArray ( previousResult ) || length !== previousResult . length ;
138+
139+ const snapshotted : any [ ] = new Array ( length ) ;
115140
116- for ( let i = 0 ; i < items . length ; i ++ ) {
117- const snapshottedValue = createSnapshot ( items [ i ] , ctx ) ;
118- snapshotted . push ( snapshottedValue ) ;
141+ for ( let i = 0 ; i < length ; i ++ ) {
142+ const snapshottedValue = createSnapshot ( node . at ( i ) , ctx ) ;
143+ snapshotted [ i ] = snapshottedValue ;
119144
120145 if ( ! hasChanged && previousResult && previousResult [ i ] !== snapshottedValue ) {
121146 hasChanged = true ;
@@ -187,17 +212,17 @@ function createSnapshotForSchema(
187212 */
188213export function createSnapshot < T > ( node : T , ctx : SnapshotContext ) : Snapshot < T > {
189214 // Pass through primitives and null/undefined.
190- if (
191- node === null ||
192- node === undefined ||
193- typeof node !== "object" ||
194- typeof node === "function"
195- ) {
215+ if ( node === null || node === undefined || typeof node !== "object" ) {
196216 return node as Snapshot < T > ;
197217 }
198218
199219 // Find the stable refId for this object.
200- const refId = findRefId ( node , ctx . refs ) ;
220+ const refId = findRefId ( node , ctx ) ;
221+
222+ // Record the parent relationship for ancestor tracking.
223+ if ( refId !== - 1 && ctx . currentParentRefId !== - 1 ) {
224+ ctx . parentRefIdMap . set ( refId , ctx . currentParentRefId ) ;
225+ }
201226
202227 // Check if we've already snapshotted this object in the current pass (cycle detection).
203228 if ( refId !== - 1 && ctx . currentResultsByRefId . has ( refId ) ) {
@@ -207,6 +232,18 @@ export function createSnapshot<T>(node: T, ctx: SnapshotContext): Snapshot<T> {
207232 // Get the previous result for structural sharing comparison.
208233 const previousResult = refId !== - 1 ? ctx . previousResultsByRefId . get ( refId ) : undefined ;
209234
235+ // If this node is not dirty and we have a previous result,
236+ // we can skip the entire subtree. With ancestor tracking, if any descendant
237+ // changed, this node would have been marked dirty too.
238+ if ( refId !== - 1 && previousResult !== undefined && ! ctx . dirtyRefIds . has ( refId ) ) {
239+ ctx . currentResultsByRefId . set ( refId , previousResult ) ;
240+ return previousResult as Snapshot < T > ;
241+ }
242+
243+ // Set this node as the parent for any children we process.
244+ const savedParentRefId = ctx . currentParentRefId ;
245+ ctx . currentParentRefId = refId ;
246+
210247 let result : any ;
211248
212249 if ( node instanceof MapSchema ) {
@@ -220,7 +257,8 @@ export function createSnapshot<T>(node: T, ctx: SnapshotContext): Snapshot<T> {
220257 result = node ;
221258 }
222259
223- // Cache the result for this snapshot pass.
260+ // Restore parent and cache result.
261+ ctx . currentParentRefId = savedParentRefId ;
224262 if ( refId !== - 1 ) {
225263 ctx . currentResultsByRefId . set ( refId , result ) ;
226264 }
0 commit comments