Skip to content

Commit ceb2ad9

Browse files
committed
StateView: allow to opt-in for tracking items. add .clear() method.
1 parent 6960519 commit ceb2ad9

File tree

9 files changed

+146
-17
lines changed

9 files changed

+146
-17
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.22",
3+
"version": "3.0.23",
44
"description": "Binary state serializer with delta encoding for games",
55
"bin": {
66
"schema-codegen": "./bin/schema-codegen",

src/Schema.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ export class Schema {
6969

7070
} else if (tag === DEFAULT_VIEW_TAG) {
7171
// view pass: default tag
72-
return view.items.has(ref[$changes]);
72+
return view.visible.has(ref[$changes]);
7373

7474
} else {
7575
// view pass: custom tag

src/encoder/Encoder.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export class Encoder<T extends Schema = any> {
6262
const changeTree = changeTrees[i];
6363

6464
if (hasView) {
65-
if (!view.items.has(changeTree)) {
65+
if (!view.visible.has(changeTree)) {
6666
view.invisible.add(changeTree);
6767
continue; // skip this change tree
6868

src/encoder/StateView.ts

Lines changed: 51 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,23 @@ import { $changes, $fieldIndexesByViewTag, $viewFieldIndexes } from "../types/sy
33
import { DEFAULT_VIEW_TAG } from "../annotations";
44
import { OPERATION } from "../encoding/spec";
55
import { Metadata } from "../Metadata";
6+
import { spliceOne } from "../types/utils";
67

7-
export function createView() {
8-
return new StateView();
8+
export function createView(iterable: boolean = false) {
9+
return new StateView(iterable);
910
}
1011

1112
export class StateView {
13+
/**
14+
* Iterable list of items that are visible to this view
15+
* (Available only if constructed with `iterable: true`)
16+
*/
17+
items: Ref[];
18+
1219
/**
1320
* List of ChangeTree's that are visible to this view
1421
*/
15-
items: WeakSet<ChangeTree> = new WeakSet<ChangeTree>();
22+
visible: WeakSet<ChangeTree> = new WeakSet<ChangeTree>();
1623

1724
/**
1825
* List of ChangeTree's that are invisible to this view
@@ -27,6 +34,12 @@ export class StateView {
2734
*/
2835
changes = new Map<number, IndexedOperations>();
2936

37+
constructor(public iterable: boolean = false) {
38+
if (iterable) {
39+
this.items = [];
40+
}
41+
}
42+
3043
// TODO: allow to set multiple tags at once
3144
add(obj: Ref, tag: number = DEFAULT_VIEW_TAG, checkIncludeParent: boolean = true) {
3245
if (!obj?.[$changes]) {
@@ -37,7 +50,12 @@ export class StateView {
3750
// FIXME: ArraySchema/MapSchema do not have metadata
3851
const metadata: Metadata = obj.constructor[Symbol.metadata];
3952
const changeTree: ChangeTree = obj[$changes];
40-
this.items.add(changeTree);
53+
this.visible.add(changeTree);
54+
55+
// add to iterable list (only the explicitly added items)
56+
if (this.iterable && checkIncludeParent) {
57+
this.items.push(obj);
58+
}
4159

4260
// add parent ChangeTree's
4361
// - if it was invisible to this view
@@ -123,9 +141,9 @@ export class StateView {
123141
const changeTree = childChangeTree.parent[$changes];
124142
const parentIndex = childChangeTree.parentIndex;
125143

126-
if (!this.items.has(changeTree)) {
144+
if (!this.visible.has(changeTree)) {
127145
// view must have all "changeTree" parent tree
128-
this.items.add(changeTree);
146+
this.visible.add(changeTree);
129147

130148
// add parent's parent
131149
const parentChangeTree: ChangeTree = changeTree.parent?.[$changes];
@@ -162,14 +180,24 @@ export class StateView {
162180
}
163181
}
164182

165-
remove(obj: Ref, tag: number = DEFAULT_VIEW_TAG) {
183+
remove(obj: Ref, tag?: number): this; // hide _isClear parameter from public API
184+
remove(obj: Ref, tag?: number, _isClear?: boolean): this;
185+
remove(obj: Ref, tag: number = DEFAULT_VIEW_TAG, _isClear: boolean = false): this {
166186
const changeTree: ChangeTree = obj[$changes];
167187
if (!changeTree) {
168188
console.warn("StateView#remove(), invalid object:", obj);
169189
return this;
170190
}
171191

172-
this.items.delete(changeTree);
192+
this.visible.delete(changeTree);
193+
194+
// remove from iterable list
195+
if (
196+
this.iterable &&
197+
!_isClear // no need to remove during clear(), as it will be cleared entirely
198+
) {
199+
spliceOne(this.items, this.items.indexOf(obj));
200+
}
173201

174202
const ref = changeTree.ref;
175203
const metadata: Metadata = ref.constructor[Symbol.metadata]; // ArraySchema/MapSchema do not have metadata
@@ -227,11 +255,24 @@ export class StateView {
227255
}
228256

229257
has(obj: Ref) {
230-
return this.items.has(obj[$changes]);
258+
return this.visible.has(obj[$changes]);
231259
}
232260

233261
hasTag(ob: Ref, tag: number = DEFAULT_VIEW_TAG) {
234262
const tags = this.tags?.get(ob[$changes]);
235263
return tags?.has(tag) ?? false;
236264
}
237-
}
265+
266+
clear() {
267+
if (!this.iterable) {
268+
throw new Error("StateView#clear() is only available for iterable StateView's. Use StateView(iterable: true) constructor.");
269+
}
270+
271+
for (let i = 0, l = this.items.length; i < l; i++) {
272+
this.remove(this.items[i], DEFAULT_VIEW_TAG, true);
273+
}
274+
275+
// clear items array
276+
this.items.length = 0;
277+
}
278+
}

src/types/custom/ArraySchema.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export class ArraySchema<V = any> implements Array<V>, Collection<number, V> {
4242
!view ||
4343
typeof (ref[$childType]) === "string" ||
4444
// view.items.has(ref[$getByIndex](index)[$changes])
45-
view.items.has(ref['tmpItems'][index]?.[$changes])
45+
view.visible.has(ref['tmpItems'][index]?.[$changes])
4646
);
4747
}
4848

src/types/custom/CollectionSchema.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export class CollectionSchema<V=any> implements Collection<K, V>{
3333
return (
3434
!view ||
3535
typeof (ref[$childType]) === "string" ||
36-
view.items.has((ref[$getByIndex](index) ?? ref.deletedItems[index])[$changes])
36+
view.visible.has((ref[$getByIndex](index) ?? ref.deletedItems[index])[$changes])
3737
);
3838
}
3939

src/types/custom/MapSchema.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export class MapSchema<V=any, K extends string = string> implements Map<K, V>, C
3434
return (
3535
!view ||
3636
typeof (ref[$childType]) === "string" ||
37-
view.items.has((ref[$getByIndex](index) ?? ref.deletedItems[index])[$changes])
37+
view.visible.has((ref[$getByIndex](index) ?? ref.deletedItems[index])[$changes])
3838
);
3939
}
4040

src/types/custom/SetSchema.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export class SetSchema<V=any> implements Collection<number, V> {
3131
return (
3232
!view ||
3333
typeof (ref[$childType]) === "string" ||
34-
view.items.has((ref[$getByIndex](index) ?? ref.deletedItems[index])[$changes])
34+
view.visible.has((ref[$getByIndex](index) ?? ref.deletedItems[index])[$changes])
3535
);
3636
}
3737

test/StateView.test.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -771,6 +771,94 @@ describe("StateView", () => {
771771
assert.strictEqual(1, client1.state.entities.size);
772772
assert.strictEqual(3, client1.state.entities.get("one").components.length);
773773

774+
assert.strictEqual(1, client2.state.entities.size);
775+
assert.strictEqual(3, client2.state.entities.get("two").components.length);
776+
});
777+
778+
it("should allow to .clear() the view", () => {
779+
class Component extends Schema {
780+
@type("string") name: string;
781+
@type("number") value: number;
782+
}
783+
784+
class ListComponent extends Component {
785+
@type(["string"]) list = new ArraySchema<string>();
786+
}
787+
788+
class TagComponent extends Component {
789+
@type("string") tag: string;
790+
}
791+
792+
class Entity extends Schema {
793+
@type("string") id: string = nanoid(9);
794+
@type([Component]) components = new ArraySchema<Component>();
795+
}
796+
797+
class MyRoomState extends Schema {
798+
@view() @type({ map: Entity }) entities = new Map<string, Entity>();
799+
}
800+
801+
const state = new MyRoomState();
802+
const encoder = getEncoder(state);
803+
804+
const client1 = createClientWithView(state, new StateView(true));
805+
const client2 = createClientWithView(state, new StateView(true));
806+
807+
state.entities.set("one", new Entity().assign({
808+
id: "one",
809+
components: [
810+
new Component().assign({ name: "Health", value: 100 }),
811+
new ListComponent().assign({ name: "List", value: 200, list: ["one", "two"] }),
812+
new TagComponent().assign({ name: "Tag", value: 300, tag: "tag" }),
813+
]
814+
}));
815+
816+
state.entities.set("two", new Entity().assign({
817+
id: "two",
818+
components: [
819+
new Component().assign({ name: "Health", value: 100 }),
820+
new ListComponent().assign({ name: "List", value: 200, list: ["one", "two"] }),
821+
new TagComponent().assign({ name: "Tag", value: 300, tag: "tag" }),
822+
]
823+
}));
824+
825+
encodeMultiple(encoder, state, [client1, client2]);
826+
827+
// add entities for the first time
828+
client1.view.add(state.entities.get("one"));
829+
client2.view.add(state.entities.get("two"));
830+
831+
assert.strictEqual(1, client1.view.items.length);
832+
assert.strictEqual(1, client2.view.items.length);
833+
834+
encodeMultiple(encoder, state, [client1, client2]);
835+
836+
assert.strictEqual(1, client1.state.entities.size);
837+
assert.strictEqual(3, client1.state.entities.get("one").components.length);
838+
839+
assert.strictEqual(1, client2.state.entities.size);
840+
assert.strictEqual(3, client2.state.entities.get("two").components.length);
841+
842+
client1.view.clear();
843+
client2.view.clear();
844+
845+
assert.strictEqual(0, client1.view.items.length);
846+
assert.strictEqual(0, client2.view.items.length);
847+
848+
encodeMultiple(encoder, state, [client1, client2]);
849+
850+
assert.strictEqual(0, client1.state.entities.size);
851+
assert.strictEqual(0, client2.state.entities.size);
852+
853+
// re-add entities!
854+
client1.view.add(state.entities.get("one"));
855+
client2.view.add(state.entities.get("two"));
856+
857+
encodeMultiple(encoder, state, [client1, client2]);
858+
859+
assert.strictEqual(1, client1.state.entities.size);
860+
assert.strictEqual(3, client1.state.entities.get("one").components.length);
861+
774862
assert.strictEqual(1, client2.state.entities.size);
775863
assert.strictEqual(3, client2.state.entities.get("two").components.length);
776864

0 commit comments

Comments
 (0)