@colyseus/schema 3.0.42 → 3.0.43
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/cjs/index.js +327 -185
- package/build/cjs/index.js.map +1 -1
- package/build/esm/index.mjs +327 -185
- package/build/esm/index.mjs.map +1 -1
- package/build/umd/index.js +327 -185
- package/lib/Schema.d.ts +2 -1
- package/lib/Schema.js +21 -3
- package/lib/Schema.js.map +1 -1
- package/lib/bench_encode.d.ts +1 -0
- package/lib/bench_encode.js +130 -0
- package/lib/bench_encode.js.map +1 -0
- package/lib/debug.d.ts +1 -0
- package/lib/debug.js +51 -0
- package/lib/debug.js.map +1 -0
- package/lib/decoder/Decoder.js +7 -8
- package/lib/decoder/Decoder.js.map +1 -1
- package/lib/encoder/ChangeTree.d.ts +57 -7
- package/lib/encoder/ChangeTree.js +171 -106
- package/lib/encoder/ChangeTree.js.map +1 -1
- package/lib/encoder/Encoder.js +19 -20
- package/lib/encoder/Encoder.js.map +1 -1
- package/lib/encoder/Root.d.ts +8 -7
- package/lib/encoder/Root.js +81 -26
- package/lib/encoder/Root.js.map +1 -1
- package/lib/types/custom/ArraySchema.js +5 -3
- package/lib/types/custom/ArraySchema.js.map +1 -1
- package/lib/types/custom/MapSchema.js +7 -2
- package/lib/types/custom/MapSchema.js.map +1 -1
- package/lib/types/symbols.d.ts +14 -14
- package/lib/types/symbols.js +14 -14
- package/lib/types/symbols.js.map +1 -1
- package/lib/utils.js +7 -3
- package/lib/utils.js.map +1 -1
- package/package.json +1 -1
- package/src/Schema.ts +21 -5
- package/src/bench_encode.ts +108 -0
- package/src/debug.ts +55 -0
- package/src/decoder/Decoder.ts +8 -12
- package/src/encoder/ChangeTree.ts +201 -115
- package/src/encoder/Encoder.ts +21 -19
- package/src/encoder/Root.ts +87 -28
- package/src/types/custom/ArraySchema.ts +6 -4
- package/src/types/custom/MapSchema.ts +8 -2
- package/src/types/symbols.ts +14 -14
- package/src/utils.ts +9 -3
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { nanoid } from "nanoid";
|
|
2
|
+
import { Schema, type, MapSchema, ArraySchema, Encoder } from ".";
|
|
3
|
+
|
|
4
|
+
class Attribute extends Schema {
|
|
5
|
+
@type("string") name: string;
|
|
6
|
+
@type("number") value: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
class Item extends Schema {
|
|
10
|
+
@type("number") price: number;
|
|
11
|
+
@type([ Attribute ]) attributes = new ArraySchema<Attribute>();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
class Position extends Schema {
|
|
15
|
+
@type("number") x: number;
|
|
16
|
+
@type("number") y: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
class Player extends Schema {
|
|
20
|
+
@type(Position) position = new Position();
|
|
21
|
+
@type({ map: Item }) items = new MapSchema<Item>();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
class State extends Schema {
|
|
25
|
+
@type({ map: Player }) players = new MapSchema<Player>();
|
|
26
|
+
@type("string") currentTurn;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const state = new State();
|
|
30
|
+
|
|
31
|
+
Encoder.BUFFER_SIZE = 4096 * 4096;
|
|
32
|
+
const encoder = new Encoder(state);
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
let now = Date.now();
|
|
36
|
+
|
|
37
|
+
// for (let i = 0; i < 10000; i++) {
|
|
38
|
+
// const player = new Player();
|
|
39
|
+
// state.players.set(`p-${nanoid()}`, player);
|
|
40
|
+
//
|
|
41
|
+
// player.position.x = (i + 1) * 100;
|
|
42
|
+
// player.position.y = (i + 1) * 100;
|
|
43
|
+
// for (let j = 0; j < 10; j++) {
|
|
44
|
+
// const item = new Item();
|
|
45
|
+
// player.items.set(`item-${j}`, item);
|
|
46
|
+
// item.price = (i + 1) * 50;
|
|
47
|
+
// for (let k = 0; k < 5; k++) {
|
|
48
|
+
// const attr = new Attribute();
|
|
49
|
+
// attr.name = `Attribute ${k}`;
|
|
50
|
+
// attr.value = k;
|
|
51
|
+
// item.attributes.push(attr);
|
|
52
|
+
// }
|
|
53
|
+
// }
|
|
54
|
+
// }
|
|
55
|
+
// console.log("time to make changes:", Date.now() - now);
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
// measure time to .encodeAll()
|
|
59
|
+
|
|
60
|
+
now = Date.now();
|
|
61
|
+
// for (let i = 0; i < 1000; i++) {
|
|
62
|
+
// encoder.encodeAll();
|
|
63
|
+
// }
|
|
64
|
+
// console.log(Date.now() - now);
|
|
65
|
+
|
|
66
|
+
const total = 100;
|
|
67
|
+
const allEncodes = Date.now();
|
|
68
|
+
|
|
69
|
+
let avgTimeToEncode = 0;
|
|
70
|
+
let avgTimeToMakeChanges = 0;
|
|
71
|
+
|
|
72
|
+
for (let i = 0; i < total; i++) {
|
|
73
|
+
now = Date.now();
|
|
74
|
+
for (let j = 0; j < 50; j++) {
|
|
75
|
+
const player = new Player();
|
|
76
|
+
state.players.set(`p-${nanoid()}`, player);
|
|
77
|
+
|
|
78
|
+
player.position.x = (j + 1) * 100;
|
|
79
|
+
player.position.y = (j + 1) * 100;
|
|
80
|
+
for (let k = 0; k < 10; k++) {
|
|
81
|
+
const item = new Item();
|
|
82
|
+
item.price = (j + 1) * 50;
|
|
83
|
+
for (let l = 0; l < 5; l++) {
|
|
84
|
+
const attr = new Attribute();
|
|
85
|
+
attr.name = `Attribute ${l}`;
|
|
86
|
+
attr.value = l;
|
|
87
|
+
item.attributes.push(attr);
|
|
88
|
+
}
|
|
89
|
+
player.items.set(`item-${k}`, item);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const timeToMakeChanges = Date.now() - now;
|
|
93
|
+
console.log("time to make changes:", timeToMakeChanges);
|
|
94
|
+
avgTimeToMakeChanges += timeToMakeChanges;
|
|
95
|
+
|
|
96
|
+
now = Date.now();
|
|
97
|
+
encoder.encode();
|
|
98
|
+
encoder.discardChanges();
|
|
99
|
+
|
|
100
|
+
const timeToEncode = Date.now() - now;
|
|
101
|
+
console.log("time to encode:", timeToEncode);
|
|
102
|
+
avgTimeToEncode += timeToEncode;
|
|
103
|
+
}
|
|
104
|
+
console.log("avg time to encode:", (avgTimeToEncode) / total);
|
|
105
|
+
console.log("avg time to make changes:", (avgTimeToMakeChanges) / total);
|
|
106
|
+
console.log("time for all encodes:", Date.now() - allEncodes);
|
|
107
|
+
|
|
108
|
+
console.log(Array.from(encoder.encodeAll()).length, "bytes");
|
package/src/debug.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import { Reflection, Decoder } from "./index";
|
|
3
|
+
|
|
4
|
+
const contents = fs.readFileSync("/Users/endel/Projects/colyseus/clients/bubbits/project/@bubbits/backend/schema-debug.txt", { encoding: "utf8" }).toString();
|
|
5
|
+
|
|
6
|
+
let isCommentBlock = false;
|
|
7
|
+
let lastComment = "";
|
|
8
|
+
|
|
9
|
+
let decoder: Decoder;
|
|
10
|
+
|
|
11
|
+
function getBuffer(line: string) {
|
|
12
|
+
const start = line.lastIndexOf(":");
|
|
13
|
+
const buffer = Buffer.from(new Uint8Array(line.substring(start + 1).split(",").map(n => Number(n))));
|
|
14
|
+
console.log(`(${buffer.byteLength}) ${Array.from(buffer).join(",")}`)
|
|
15
|
+
// console.log("");
|
|
16
|
+
// console.log("");
|
|
17
|
+
// console.log("> ", line);
|
|
18
|
+
// console.log("> substring:", line.substring(start + 1))
|
|
19
|
+
return buffer;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function decode(buffer: Buffer) {
|
|
23
|
+
try {
|
|
24
|
+
decoder.decode(buffer);
|
|
25
|
+
} catch (e) {
|
|
26
|
+
console.error(e);
|
|
27
|
+
console.log("Last log:\n\n")
|
|
28
|
+
console.log(lastComment);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
contents.split("\n").forEach((line) => {
|
|
33
|
+
if (line.startsWith("#")) {
|
|
34
|
+
// reset last comment.
|
|
35
|
+
if (isCommentBlock === false) { lastComment = ""; }
|
|
36
|
+
|
|
37
|
+
isCommentBlock = true;
|
|
38
|
+
lastComment += line.substring(line.indexOf(":") + 1) + "\n";
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
isCommentBlock = false;
|
|
43
|
+
|
|
44
|
+
if (line.startsWith("handshake:") && !decoder) {
|
|
45
|
+
decoder = Reflection.decode(getBuffer(line));
|
|
46
|
+
|
|
47
|
+
} else if (line.startsWith("state:")) {
|
|
48
|
+
decode(getBuffer(line));
|
|
49
|
+
|
|
50
|
+
} else if (line.startsWith("patch:")) {
|
|
51
|
+
decode(getBuffer(line));
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
console.log(decoder.state.toJSON());
|
package/src/decoder/Decoder.ts
CHANGED
|
@@ -58,6 +58,8 @@ export class Decoder<T extends Schema = any> {
|
|
|
58
58
|
if (bytes[it.offset] == SWITCH_TO_STRUCTURE) {
|
|
59
59
|
it.offset++;
|
|
60
60
|
|
|
61
|
+
ref[$onDecodeEnd]?.()
|
|
62
|
+
|
|
61
63
|
const nextRefId = decode.number(bytes, it);
|
|
62
64
|
const nextRef = $root.refs.get(nextRefId);
|
|
63
65
|
|
|
@@ -65,17 +67,17 @@ export class Decoder<T extends Schema = any> {
|
|
|
65
67
|
// Trying to access a reference that haven't been decoded yet.
|
|
66
68
|
//
|
|
67
69
|
if (!nextRef) {
|
|
70
|
+
// throw new Error(`"refId" not found: ${nextRefId}`);
|
|
68
71
|
console.error(`"refId" not found: ${nextRefId}`, { previousRef: ref, previousRefId: this.currentRefId });
|
|
69
72
|
console.warn("Please report this to the developers. All refIds =>");
|
|
70
73
|
console.warn(Schema.debugRefIdsDecoder(this));
|
|
71
74
|
this.skipCurrentStructure(bytes, it, totalBytes);
|
|
72
|
-
}
|
|
73
|
-
ref[$onDecodeEnd]?.()
|
|
74
75
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
76
|
+
} else {
|
|
77
|
+
ref = nextRef;
|
|
78
|
+
decoder = ref.constructor[$decoder];
|
|
79
|
+
this.currentRefId = nextRefId;
|
|
80
|
+
}
|
|
79
81
|
|
|
80
82
|
continue;
|
|
81
83
|
}
|
|
@@ -131,12 +133,6 @@ export class Decoder<T extends Schema = any> {
|
|
|
131
133
|
}
|
|
132
134
|
|
|
133
135
|
createInstanceOfType (type: typeof Schema): Schema {
|
|
134
|
-
// let instance: Schema = new (type as any)();
|
|
135
|
-
|
|
136
|
-
// // assign root on $changes
|
|
137
|
-
// instance[$changes].root = this.root[$changes].root;
|
|
138
|
-
|
|
139
|
-
// return instance;
|
|
140
136
|
return new (type as any)();
|
|
141
137
|
}
|
|
142
138
|
|
|
@@ -36,17 +36,53 @@ export interface IndexedOperations {
|
|
|
36
36
|
[index: number]: OPERATION;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
// Linked list node for change trees
|
|
40
|
+
export interface ChangeTreeNode {
|
|
41
|
+
changeTree: ChangeTree;
|
|
42
|
+
next?: ChangeTreeNode;
|
|
43
|
+
prev?: ChangeTreeNode;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Linked list for change trees
|
|
47
|
+
export interface ChangeTreeList {
|
|
48
|
+
next?: ChangeTreeNode;
|
|
49
|
+
tail?: ChangeTreeNode;
|
|
50
|
+
length: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
39
53
|
export interface ChangeSet {
|
|
40
54
|
// field index -> operation index
|
|
41
55
|
indexes: { [index: number]: number };
|
|
42
56
|
operations: number[];
|
|
43
|
-
|
|
57
|
+
queueRootNode?: ChangeTreeNode; // direct reference to ChangeTreeNode in the linked list
|
|
44
58
|
}
|
|
45
59
|
|
|
46
60
|
function createChangeSet(): ChangeSet {
|
|
47
61
|
return { indexes: {}, operations: [] };
|
|
48
62
|
}
|
|
49
63
|
|
|
64
|
+
// Linked list helper functions
|
|
65
|
+
export function createChangeTreeList(): ChangeTreeList {
|
|
66
|
+
return { next: undefined, tail: undefined, length: 0 };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function addToChangeTreeList(list: ChangeTreeList, changeTree: ChangeTree): ChangeTreeNode {
|
|
70
|
+
const node: ChangeTreeNode = { changeTree, next: undefined, prev: undefined };
|
|
71
|
+
|
|
72
|
+
if (!list.next) {
|
|
73
|
+
list.next = node;
|
|
74
|
+
list.tail = node;
|
|
75
|
+
} else {
|
|
76
|
+
node.prev = list.tail;
|
|
77
|
+
list.tail!.next = node;
|
|
78
|
+
list.tail = node;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
list.length++;
|
|
82
|
+
|
|
83
|
+
return node;
|
|
84
|
+
}
|
|
85
|
+
|
|
50
86
|
export function setOperationAtIndex(changeSet: ChangeSet, index: number) {
|
|
51
87
|
const operationsIndex = changeSet.indexes[index];
|
|
52
88
|
if (operationsIndex === undefined) {
|
|
@@ -96,25 +132,32 @@ export function debugChangeSet(label: string, changeSet: ChangeSet) {
|
|
|
96
132
|
export function enqueueChangeTree(
|
|
97
133
|
root: Root,
|
|
98
134
|
changeTree: ChangeTree,
|
|
99
|
-
changeSet: 'changes' | 'filteredChanges' | 'allFilteredChanges',
|
|
100
|
-
|
|
135
|
+
changeSet: 'changes' | 'filteredChanges' | 'allFilteredChanges' | 'allChanges',
|
|
136
|
+
queueRootNode = changeTree[changeSet].queueRootNode
|
|
101
137
|
) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
return;
|
|
138
|
+
// skip
|
|
139
|
+
if (!root) { return; }
|
|
105
140
|
|
|
106
|
-
|
|
107
|
-
|
|
141
|
+
if (queueRootNode) {
|
|
142
|
+
} else {
|
|
143
|
+
// Add to linked list if not already present
|
|
144
|
+
changeTree[changeSet].queueRootNode = addToChangeTreeList(root[changeSet], changeTree);
|
|
108
145
|
}
|
|
109
146
|
}
|
|
110
147
|
|
|
148
|
+
export interface ParentChain {
|
|
149
|
+
ref: Ref;
|
|
150
|
+
index: number;
|
|
151
|
+
next?: ParentChain;
|
|
152
|
+
}
|
|
153
|
+
|
|
111
154
|
export class ChangeTree<T extends Ref=any> {
|
|
112
155
|
ref: T;
|
|
113
156
|
refId: number;
|
|
157
|
+
metadata: Metadata;
|
|
114
158
|
|
|
115
159
|
root?: Root;
|
|
116
|
-
|
|
117
|
-
parentIndex?: number;
|
|
160
|
+
parentChain?: ParentChain; // Linked list for tracking parents
|
|
118
161
|
|
|
119
162
|
/**
|
|
120
163
|
* Whether this structure is parent of a filtered structure.
|
|
@@ -145,12 +188,12 @@ export class ChangeTree<T extends Ref=any> {
|
|
|
145
188
|
|
|
146
189
|
constructor(ref: T) {
|
|
147
190
|
this.ref = ref;
|
|
191
|
+
this.metadata = ref.constructor[Symbol.metadata];
|
|
148
192
|
|
|
149
193
|
//
|
|
150
194
|
// Does this structure have "filters" declared?
|
|
151
195
|
//
|
|
152
|
-
|
|
153
|
-
if (metadata?.[$viewFieldIndexes]) {
|
|
196
|
+
if (this.metadata?.[$viewFieldIndexes]) {
|
|
154
197
|
this.allFilteredChanges = { indexes: {}, operations: [] };
|
|
155
198
|
this.filteredChanges = { indexes: {}, operations: [] };
|
|
156
199
|
}
|
|
@@ -158,35 +201,18 @@ export class ChangeTree<T extends Ref=any> {
|
|
|
158
201
|
|
|
159
202
|
setRoot(root: Root) {
|
|
160
203
|
this.root = root;
|
|
161
|
-
this.checkIsFiltered(this.parent, this.parentIndex);
|
|
162
204
|
|
|
163
|
-
|
|
164
|
-
// TODO: refactor and possibly unify .setRoot() and .setParent()
|
|
165
|
-
//
|
|
205
|
+
const isNewChangeTree = this.root.add(this);
|
|
166
206
|
|
|
167
|
-
|
|
168
|
-
const metadata: Metadata = this.ref.constructor[Symbol.metadata];
|
|
169
|
-
if (metadata) {
|
|
170
|
-
metadata[$refTypeFieldIndexes]?.forEach((index) => {
|
|
171
|
-
const field = metadata[index as any as number];
|
|
172
|
-
const changeTree: ChangeTree = this.ref[field.name]?.[$changes];
|
|
173
|
-
if (changeTree) {
|
|
174
|
-
if (changeTree.root !== root) {
|
|
175
|
-
changeTree.setRoot(root);
|
|
176
|
-
} else {
|
|
177
|
-
root.add(changeTree); // increment refCount
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
});
|
|
207
|
+
this.checkIsFiltered(this.parent, this.parentIndex, isNewChangeTree);
|
|
181
208
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
changeTree.setRoot(root);
|
|
209
|
+
// Recursively set root on child structures
|
|
210
|
+
if (isNewChangeTree) {
|
|
211
|
+
this.forEachChild((child, _) => {
|
|
212
|
+
if (child.root !== root) {
|
|
213
|
+
child.setRoot(root);
|
|
188
214
|
} else {
|
|
189
|
-
root.add(
|
|
215
|
+
root.add(child); // increment refCount
|
|
190
216
|
}
|
|
191
217
|
});
|
|
192
218
|
}
|
|
@@ -197,63 +223,57 @@ export class ChangeTree<T extends Ref=any> {
|
|
|
197
223
|
root?: Root,
|
|
198
224
|
parentIndex?: number,
|
|
199
225
|
) {
|
|
200
|
-
this.parent
|
|
201
|
-
this.parentIndex = parentIndex;
|
|
226
|
+
this.addParent(parent, parentIndex);
|
|
202
227
|
|
|
203
228
|
// avoid setting parents with empty `root`
|
|
204
229
|
if (!root) { return; }
|
|
205
230
|
|
|
231
|
+
const isNewChangeTree = root.add(this);
|
|
232
|
+
|
|
206
233
|
// skip if parent is already set
|
|
207
234
|
if (root !== this.root) {
|
|
208
235
|
this.root = root;
|
|
209
|
-
this.checkIsFiltered(parent, parentIndex);
|
|
210
|
-
|
|
211
|
-
} else {
|
|
212
|
-
root.add(this);
|
|
236
|
+
this.checkIsFiltered(parent, parentIndex, isNewChangeTree);
|
|
213
237
|
}
|
|
214
238
|
|
|
215
239
|
// assign same parent on child structures
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
if (
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
(this.ref as MapSchema).forEach((value, key) => {
|
|
229
|
-
const changeTree: ChangeTree = value[$changes];
|
|
230
|
-
if (changeTree.root !== root) {
|
|
231
|
-
changeTree.setParent(this.ref, root, this.indexes[key] ?? key);
|
|
240
|
+
if (isNewChangeTree) {
|
|
241
|
+
//
|
|
242
|
+
// assign same parent on child structures
|
|
243
|
+
//
|
|
244
|
+
this.forEachChild((child, index) => {
|
|
245
|
+
if (child.root === root) {
|
|
246
|
+
//
|
|
247
|
+
// re-assigning a child of the same root, move it to the end
|
|
248
|
+
// of the changes queue so encoding order is preserved
|
|
249
|
+
//
|
|
250
|
+
root.moveToEndOfChanges(child);
|
|
251
|
+
return;
|
|
232
252
|
}
|
|
253
|
+
child.setParent(this.ref, root, index);
|
|
233
254
|
});
|
|
234
255
|
}
|
|
235
|
-
|
|
236
256
|
}
|
|
237
257
|
|
|
238
|
-
forEachChild(callback: (change: ChangeTree,
|
|
258
|
+
forEachChild(callback: (change: ChangeTree, at: any) => void) {
|
|
239
259
|
//
|
|
240
260
|
// assign same parent on child structures
|
|
241
261
|
//
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
}
|
|
250
|
-
});
|
|
262
|
+
if (this.ref[$childType]) {
|
|
263
|
+
if (typeof(this.ref[$childType]) !== "string") {
|
|
264
|
+
// MapSchema / ArraySchema, etc.
|
|
265
|
+
for (const [key, value] of (this.ref as MapSchema).entries()) {
|
|
266
|
+
callback(value[$changes], key);
|
|
267
|
+
};
|
|
268
|
+
}
|
|
251
269
|
|
|
252
|
-
} else
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
270
|
+
} else {
|
|
271
|
+
for (const index of this.metadata?.[$refTypeFieldIndexes] ?? []) {
|
|
272
|
+
const field = this.metadata[index as any as number];
|
|
273
|
+
const value = this.ref[field.name];
|
|
274
|
+
if (!value) { continue; }
|
|
275
|
+
callback(value[$changes], index);
|
|
276
|
+
}
|
|
257
277
|
}
|
|
258
278
|
}
|
|
259
279
|
|
|
@@ -271,9 +291,7 @@ export class ChangeTree<T extends Ref=any> {
|
|
|
271
291
|
}
|
|
272
292
|
|
|
273
293
|
change(index: number, operation: OPERATION = OPERATION.ADD) {
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
const isFiltered = this.isFiltered || (metadata?.[index]?.tag !== undefined);
|
|
294
|
+
const isFiltered = this.isFiltered || (this.metadata?.[index]?.tag !== undefined);
|
|
277
295
|
const changeSet = (isFiltered)
|
|
278
296
|
? this.filteredChanges
|
|
279
297
|
: this.changes;
|
|
@@ -376,19 +394,16 @@ export class ChangeTree<T extends Ref=any> {
|
|
|
376
394
|
}
|
|
377
395
|
|
|
378
396
|
getType(index?: number) {
|
|
379
|
-
|
|
380
|
-
const metadata = this.ref.constructor[Symbol.metadata] as Metadata;
|
|
381
|
-
return metadata[index].type;
|
|
382
|
-
|
|
383
|
-
} else {
|
|
397
|
+
return (
|
|
384
398
|
//
|
|
385
399
|
// Get the child type from parent structure.
|
|
386
400
|
// - ["string"] => "string"
|
|
387
401
|
// - { map: "string" } => "string"
|
|
388
402
|
// - { set: "string" } => "string"
|
|
389
403
|
//
|
|
390
|
-
|
|
391
|
-
|
|
404
|
+
this.ref[$childType] || // ArraySchema | MapSchema | SetSchema | CollectionSchema
|
|
405
|
+
this.metadata[index].type // Schema
|
|
406
|
+
);
|
|
392
407
|
}
|
|
393
408
|
|
|
394
409
|
getChange(index: number) {
|
|
@@ -458,9 +473,7 @@ export class ChangeTree<T extends Ref=any> {
|
|
|
458
473
|
this.indexedOperations = {};
|
|
459
474
|
|
|
460
475
|
// clear changeset
|
|
461
|
-
this[changeSetName]
|
|
462
|
-
this[changeSetName].operations.length = 0;
|
|
463
|
-
this[changeSetName].queueRootIndex = undefined;
|
|
476
|
+
this[changeSetName] = createChangeSet();
|
|
464
477
|
|
|
465
478
|
// ArraySchema and MapSchema have a custom "encode end" method
|
|
466
479
|
this.ref[$onEncodeEnd]?.();
|
|
@@ -478,24 +491,17 @@ export class ChangeTree<T extends Ref=any> {
|
|
|
478
491
|
this.ref[$onEncodeEnd]?.();
|
|
479
492
|
|
|
480
493
|
this.indexedOperations = {};
|
|
481
|
-
|
|
482
|
-
this.changes.indexes = {};
|
|
483
|
-
this.changes.operations.length = 0;
|
|
484
|
-
this.changes.queueRootIndex = undefined;
|
|
494
|
+
this.changes = createChangeSet();
|
|
485
495
|
|
|
486
496
|
if (this.filteredChanges !== undefined) {
|
|
487
|
-
this.filteredChanges
|
|
488
|
-
this.filteredChanges.operations.length = 0;
|
|
489
|
-
this.filteredChanges.queueRootIndex = undefined;
|
|
497
|
+
this.filteredChanges = createChangeSet();
|
|
490
498
|
}
|
|
491
499
|
|
|
492
500
|
if (discardAll) {
|
|
493
|
-
this.allChanges
|
|
494
|
-
this.allChanges.operations.length = 0;
|
|
501
|
+
this.allChanges = createChangeSet();
|
|
495
502
|
|
|
496
503
|
if (this.allFilteredChanges !== undefined) {
|
|
497
|
-
this.allFilteredChanges
|
|
498
|
-
this.allFilteredChanges.operations.length = 0;
|
|
504
|
+
this.allFilteredChanges = createChangeSet();
|
|
499
505
|
}
|
|
500
506
|
}
|
|
501
507
|
}
|
|
@@ -517,22 +523,11 @@ export class ChangeTree<T extends Ref=any> {
|
|
|
517
523
|
this.discard();
|
|
518
524
|
}
|
|
519
525
|
|
|
520
|
-
ensureRefId() {
|
|
521
|
-
// skip if refId is already set.
|
|
522
|
-
if (this.refId !== undefined) {
|
|
523
|
-
return;
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
this.refId = this.root.getNextUniqueId();
|
|
527
|
-
}
|
|
528
|
-
|
|
529
526
|
get changed() {
|
|
530
527
|
return (Object.entries(this.indexedOperations).length > 0);
|
|
531
528
|
}
|
|
532
529
|
|
|
533
|
-
protected checkIsFiltered(parent: Ref, parentIndex: number) {
|
|
534
|
-
const isNewChangeTree = this.root.add(this);
|
|
535
|
-
|
|
530
|
+
protected checkIsFiltered(parent: Ref, parentIndex: number, isNewChangeTree: boolean) {
|
|
536
531
|
if (this.root.types.hasFilters) {
|
|
537
532
|
//
|
|
538
533
|
// At Schema initialization, the "root" structure might not be available
|
|
@@ -545,7 +540,7 @@ export class ChangeTree<T extends Ref=any> {
|
|
|
545
540
|
if (this.filteredChanges !== undefined) {
|
|
546
541
|
enqueueChangeTree(this.root, this, 'filteredChanges');
|
|
547
542
|
if (isNewChangeTree) {
|
|
548
|
-
this.root
|
|
543
|
+
enqueueChangeTree(this.root, this, 'allFilteredChanges');
|
|
549
544
|
}
|
|
550
545
|
}
|
|
551
546
|
}
|
|
@@ -553,7 +548,7 @@ export class ChangeTree<T extends Ref=any> {
|
|
|
553
548
|
if (!this.isFiltered) {
|
|
554
549
|
enqueueChangeTree(this.root, this, 'changes');
|
|
555
550
|
if (isNewChangeTree) {
|
|
556
|
-
this.root
|
|
551
|
+
enqueueChangeTree(this.root, this, 'allChanges');
|
|
557
552
|
}
|
|
558
553
|
}
|
|
559
554
|
}
|
|
@@ -568,7 +563,7 @@ export class ChangeTree<T extends Ref=any> {
|
|
|
568
563
|
//
|
|
569
564
|
const refType = Metadata.isValidInstance(this.ref)
|
|
570
565
|
? this.ref.constructor
|
|
571
|
-
:
|
|
566
|
+
: this.ref[$childType];
|
|
572
567
|
|
|
573
568
|
let parentChangeTree: ChangeTree;
|
|
574
569
|
|
|
@@ -627,4 +622,95 @@ export class ChangeTree<T extends Ref=any> {
|
|
|
627
622
|
}
|
|
628
623
|
}
|
|
629
624
|
|
|
630
|
-
|
|
625
|
+
/**
|
|
626
|
+
* Get the immediate parent
|
|
627
|
+
*/
|
|
628
|
+
get parent(): Ref | undefined {
|
|
629
|
+
return this.parentChain?.ref;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* Get the immediate parent index
|
|
634
|
+
*/
|
|
635
|
+
get parentIndex(): number | undefined {
|
|
636
|
+
return this.parentChain?.index;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Add a parent to the chain
|
|
641
|
+
*/
|
|
642
|
+
addParent(parent: Ref, index: number) {
|
|
643
|
+
// Check if this parent already exists in the chain
|
|
644
|
+
if (this.hasParent((p, i) => p === parent && i === index)) {
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
this.parentChain = {
|
|
649
|
+
ref: parent,
|
|
650
|
+
index,
|
|
651
|
+
next: this.parentChain
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* Remove a parent from the chain
|
|
657
|
+
* @param parent - The parent to remove
|
|
658
|
+
* @returns true if parent was removed
|
|
659
|
+
*/
|
|
660
|
+
removeParent(parent: Ref): boolean {
|
|
661
|
+
let current = this.parentChain;
|
|
662
|
+
let previous = null;
|
|
663
|
+
while (current) {
|
|
664
|
+
//
|
|
665
|
+
// FIXME: it is required to check against `$changes` here because
|
|
666
|
+
// ArraySchema is instance of Proxy
|
|
667
|
+
//
|
|
668
|
+
if (current.ref[$changes] === parent[$changes]) {
|
|
669
|
+
if (previous) {
|
|
670
|
+
previous.next = current.next;
|
|
671
|
+
} else {
|
|
672
|
+
this.parentChain = current.next;
|
|
673
|
+
}
|
|
674
|
+
return true;
|
|
675
|
+
}
|
|
676
|
+
previous = current;
|
|
677
|
+
current = current.next;
|
|
678
|
+
}
|
|
679
|
+
return this.parentChain === undefined;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* Find a specific parent in the chain
|
|
684
|
+
*/
|
|
685
|
+
findParent(predicate: (parent: Ref, index: number) => boolean): ParentChain | undefined {
|
|
686
|
+
let current = this.parentChain;
|
|
687
|
+
while (current) {
|
|
688
|
+
if (predicate(current.ref, current.index)) {
|
|
689
|
+
return current;
|
|
690
|
+
}
|
|
691
|
+
current = current.next;
|
|
692
|
+
}
|
|
693
|
+
return undefined;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* Check if this ChangeTree has a specific parent
|
|
698
|
+
*/
|
|
699
|
+
hasParent(predicate: (parent: Ref, index: number) => boolean): boolean {
|
|
700
|
+
return this.findParent(predicate) !== undefined;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* Get all parents as an array (for debugging/testing)
|
|
705
|
+
*/
|
|
706
|
+
getAllParents(): Array<{ref: Ref, index: number}> {
|
|
707
|
+
const parents: Array<{ref: Ref, index: number}> = [];
|
|
708
|
+
let current = this.parentChain;
|
|
709
|
+
while (current) {
|
|
710
|
+
parents.push({ref: current.ref, index: current.index});
|
|
711
|
+
current = current.next;
|
|
712
|
+
}
|
|
713
|
+
return parents;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
}
|