Skip to content

Commit ee07552

Browse files
committed
changeset: cache linked list position when reordering
1 parent e3d3825 commit ee07552

File tree

6 files changed

+58
-83
lines changed

6 files changed

+58
-83
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@colyseus/schema",
3-
"version": "3.0.58",
3+
"version": "3.0.59",
44
"description": "Binary state serializer with delta encoding for games",
55
"bin": {
66
"schema-codegen": "bin/schema-codegen",

src/decoder/Decoder.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ export class Decoder<T extends Schema = any> {
6767
// Trying to access a reference that haven't been decoded yet.
6868
//
6969
if (!nextRef) {
70-
throw new Error(`"refId" not found: ${nextRefId}`);
70+
// throw new Error(`"refId" not found: ${nextRefId}`);
7171
console.error(`"refId" not found: ${nextRefId}`, { previousRef: ref, previousRefId: this.currentRefId });
7272
console.warn("Please report this issue to the developers.");
7373
this.skipCurrentStructure(bytes, it, totalBytes);

src/encoder/ChangeTree.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,13 @@ export interface ChangeTreeNode {
4141
changeTree: ChangeTree;
4242
next?: ChangeTreeNode;
4343
prev?: ChangeTreeNode;
44+
position: number; // Cached position in the linked list for O(1) lookup
4445
}
4546

4647
// Linked list for change trees
4748
export interface ChangeTreeList {
4849
next?: ChangeTreeNode;
4950
tail?: ChangeTreeNode;
50-
length: number;
5151
}
5252

5353
export interface ChangeSet {
@@ -63,7 +63,7 @@ function createChangeSet(queueRootNode?: ChangeTreeNode): ChangeSet {
6363

6464
// Linked list helper functions
6565
export function createChangeTreeList(): ChangeTreeList {
66-
return { next: undefined, tail: undefined, length: 0 };
66+
return { next: undefined, tail: undefined };
6767
}
6868

6969
export function setOperationAtIndex(changeSet: ChangeSet, index: number) {

src/encoder/Encoder.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export class Encoder<T extends Schema = any> {
5959
let current: ChangeTreeList | ChangeTreeNode = this.root[changeSetName];
6060

6161
while (current = current.next) {
62-
const changeTree = current.changeTree;
62+
const changeTree = (current as ChangeTreeNode).changeTree;
6363

6464
if (hasView) {
6565
if (!view.isChangeTreeVisible(changeTree)) {
@@ -284,8 +284,8 @@ export class Encoder<T extends Schema = any> {
284284

285285
get hasChanges() {
286286
return (
287-
this.root.changes.length > 0 ||
288-
this.root.filteredChanges.length > 0
287+
this.root.changes.next !== undefined ||
288+
this.root.filteredChanges.next !== undefined
289289
);
290290
}
291291
}

src/encoder/Root.ts

Lines changed: 50 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ export class Root {
8585

8686
} else if (child.parentChain) {
8787
// re-assigning a child of the same root, move it next to parent
88-
this.recursivelyMoveNextToParent(child);
88+
this.moveNextToParent(child);
8989
}
9090
}
9191
});
@@ -135,6 +135,16 @@ export class Root {
135135
const parentNode = parent[$changes][changeSetName]?.queueRootNode;
136136
if (!parentNode || parentNode === node) return;
137137

138+
// Use cached positions - no iteration needed!
139+
const parentPosition = parentNode.position;
140+
const childPosition = node.position;
141+
142+
// If child is already after parent, no need to move
143+
if (childPosition > parentPosition) return;
144+
145+
// Child is before parent, so we need to move it after parent
146+
// This maintains decoding order (parent before child)
147+
138148
// Remove node from current position
139149
if (node.prev) {
140150
node.prev.next = node.next;
@@ -159,77 +169,10 @@ export class Root {
159169
}
160170

161171
parentNode.next = node;
162-
}
163172

164-
// moveSubtreeToEndOfChangeTreeList(changeSetName: ChangeSetName, changeTree: ChangeTree): void {
165-
// // Find the contiguous range of nodes that belong to this subtree
166-
// const subtreeRange = this.findSubtreeRange(changeTree, changeSetName);
167-
// if (!subtreeRange) return;
168-
169-
// const changeSet = this[changeSetName];
170-
// const { firstNode, lastNode } = subtreeRange;
171-
172-
// // If the last node is already at the tail, no need to move
173-
// if (lastNode === changeSet.tail) return;
174-
175-
// // Remove the entire subtree range from current position
176-
// if (firstNode.prev) {
177-
// firstNode.prev.next = lastNode.next;
178-
// } else {
179-
// changeSet.next = lastNode.next;
180-
// }
181-
182-
// if (lastNode.next) {
183-
// lastNode.next.prev = firstNode.prev;
184-
// } else {
185-
// changeSet.tail = firstNode.prev;
186-
// }
187-
188-
// // Add the entire subtree to the end
189-
// firstNode.prev = changeSet.tail;
190-
// lastNode.next = undefined;
191-
192-
// if (changeSet.tail) {
193-
// changeSet.tail.next = firstNode;
194-
// } else {
195-
// changeSet.next = firstNode;
196-
// }
197-
198-
// changeSet.tail = lastNode;
199-
// }
200-
201-
// private findSubtreeRange(changeTree: ChangeTree, changeSetName: ChangeSetName): { firstNode: ChangeTreeNode, lastNode: ChangeTreeNode } | null {
202-
// const rootNode = changeTree[changeSetName].queueRootNode;
203-
// if (!rootNode) return null;
204-
205-
// // Collect all refIds that belong to this subtree
206-
// const subtreeRefIds = new Set<number>();
207-
// this.collectSubtreeRefIds(changeTree, subtreeRefIds);
208-
209-
// // Find the first and last nodes in the linked list that belong to this subtree
210-
// let firstNode: ChangeTreeNode | null = null;
211-
// let lastNode: ChangeTreeNode | null = null;
212-
// let current = this[changeSetName].next;
213-
214-
// while (current) {
215-
// if (subtreeRefIds.has(current.changeTree.refId)) {
216-
// if (!firstNode) firstNode = current;
217-
// lastNode = current;
218-
// }
219-
// current = current.next;
220-
// }
221-
222-
// return firstNode && lastNode ? { firstNode, lastNode } : null;
223-
// }
224-
225-
// private collectSubtreeRefIds(changeTree: ChangeTree, result: Set<number>): void {
226-
// result.add(changeTree.refId);
227-
228-
// // Collect children recursively
229-
// changeTree.forEachChild((child, _) => {
230-
// this.collectSubtreeRefIds(child, result);
231-
// });
232-
// }
173+
// Update positions after the move
174+
this.updatePositionsAfterMove(changeSet, node, parentPosition + 1);
175+
}
233176

234177
public enqueueChangeTree(
235178
changeTree: ChangeTree,
@@ -244,7 +187,12 @@ export class Root {
244187
}
245188

246189
protected addToChangeTreeList(list: ChangeTreeList, changeTree: ChangeTree): ChangeTreeNode {
247-
const node: ChangeTreeNode = { changeTree, next: undefined, prev: undefined };
190+
const node: ChangeTreeNode = {
191+
changeTree,
192+
next: undefined,
193+
prev: undefined,
194+
position: list.tail ? list.tail.position + 1 : 0
195+
};
248196

249197
if (!list.next) {
250198
list.next = node;
@@ -255,16 +203,42 @@ export class Root {
255203
list.tail = node;
256204
}
257205

258-
list.length++;
259-
260206
return node;
261207
}
262208

209+
protected updatePositionsAfterRemoval(list: ChangeTreeList, removedPosition: number) {
210+
// Update positions for all nodes after the removed position
211+
let current = list.next;
212+
let position = 0;
213+
214+
while (current) {
215+
if (position >= removedPosition) {
216+
current.position = position;
217+
}
218+
current = current.next;
219+
position++;
220+
}
221+
}
222+
223+
protected updatePositionsAfterMove(list: ChangeTreeList, node: ChangeTreeNode, newPosition: number) {
224+
// Recalculate all positions - this is more reliable than trying to be clever
225+
let current = list.next;
226+
let position = 0;
227+
228+
while (current) {
229+
current.position = position;
230+
current = current.next;
231+
position++;
232+
}
233+
}
234+
263235
public removeChangeFromChangeSet(changeSetName: ChangeSetName, changeTree: ChangeTree) {
264236
const changeSet = this[changeSetName];
265237
const node = changeTree[changeSetName].queueRootNode;
266238

267239
if (node && node.changeTree === changeTree) {
240+
const removedPosition = node.position;
241+
268242
// Remove the node from the linked list
269243
if (node.prev) {
270244
node.prev.next = node.next;
@@ -278,7 +252,8 @@ export class Root {
278252
changeSet.tail = node.prev;
279253
}
280254

281-
changeSet.length--;
255+
// Update positions for nodes that came after the removed node
256+
this.updatePositionsAfterRemoval(changeSet, removedPosition);
282257

283258
// Clear ChangeTree reference
284259
changeTree[changeSetName].queueRootNode = undefined;

test/InstanceSharing.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -587,7 +587,7 @@ describe("Instance sharing", () => {
587587
assertDeepStrictEqualEncodeAll(state);
588588
});
589589

590-
it.only("decoder: should increment refId count of deep shared instances", () => {
590+
it("decoder: should increment refId count of deep shared instances", () => {
591591
class Position extends Schema {
592592
@type("number") x: number;
593593
@type("number") y: number;

0 commit comments

Comments
 (0)