@colyseus/schema 4.0.25 → 4.0.27
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/encoder/Encoder.d.ts +2 -0
- package/build/encoder/StateView.d.ts +1 -1
- package/build/index.cjs +97 -66
- package/build/index.cjs.map +1 -1
- package/build/index.js +97 -66
- package/build/index.mjs +97 -66
- package/build/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/Metadata.ts +17 -4
- package/src/Schema.ts +3 -2
- package/src/decoder/DecodeOperation.ts +13 -4
- package/src/encoder/Encoder.ts +32 -29
- package/src/encoder/StateView.ts +32 -32
package/package.json
CHANGED
package/src/Metadata.ts
CHANGED
|
@@ -161,11 +161,24 @@ export const Metadata = {
|
|
|
161
161
|
|
|
162
162
|
metadata[$viewFieldIndexes].push(index);
|
|
163
163
|
|
|
164
|
-
|
|
165
|
-
|
|
164
|
+
// Populate $fieldIndexesByViewTag: for a bitmask tag, register the field
|
|
165
|
+
// index under each individual set bit so that view.add(obj, Tag.ONE) finds
|
|
166
|
+
// fields tagged @view(Tag.ONE|Tag.TWO).
|
|
167
|
+
// Negative tags (i.e. DEFAULT_VIEW_TAG = -1) are stored as-is.
|
|
168
|
+
if (tag < 0) {
|
|
169
|
+
if (!metadata[$fieldIndexesByViewTag][tag]) {
|
|
170
|
+
metadata[$fieldIndexesByViewTag][tag] = [];
|
|
171
|
+
}
|
|
172
|
+
metadata[$fieldIndexesByViewTag][tag].push(index);
|
|
173
|
+
} else {
|
|
174
|
+
for (let bits = tag; bits > 0; bits &= bits - 1) {
|
|
175
|
+
const bit = bits & (-bits); // isolate lowest set bit
|
|
176
|
+
if (!metadata[$fieldIndexesByViewTag][bit]) {
|
|
177
|
+
metadata[$fieldIndexesByViewTag][bit] = [];
|
|
178
|
+
}
|
|
179
|
+
metadata[$fieldIndexesByViewTag][bit].push(index);
|
|
180
|
+
}
|
|
166
181
|
}
|
|
167
|
-
|
|
168
|
-
metadata[$fieldIndexesByViewTag][tag].push(index);
|
|
169
182
|
},
|
|
170
183
|
|
|
171
184
|
setFields<T extends { new (...args: any[]): InstanceType<T> } = any>(target: T, fields: { [field in keyof InstanceType<T>]?: DefinitionType }) {
|
package/src/Schema.ts
CHANGED
|
@@ -85,9 +85,10 @@ export class Schema<C = any> implements IRef {
|
|
|
85
85
|
return view.isChangeTreeVisible(ref[$changes]);
|
|
86
86
|
|
|
87
87
|
} else {
|
|
88
|
-
// view pass: custom tag
|
|
88
|
+
// view pass: custom tag (bitmask)
|
|
89
|
+
// tag is the field's stored bitmask; view.tags stores the accumulated bitmask of tags used in view.add().
|
|
89
90
|
const tags = view.tags?.get(ref[$changes]);
|
|
90
|
-
return tags &&
|
|
91
|
+
return tags != null && (tag & tags) !== 0;
|
|
91
92
|
}
|
|
92
93
|
}
|
|
93
94
|
|
|
@@ -108,18 +108,27 @@ export function decodeValue<T extends Ref>(
|
|
|
108
108
|
let previousRefId = previousValue[$refId];
|
|
109
109
|
|
|
110
110
|
if (previousRefId !== undefined && refId !== previousRefId) {
|
|
111
|
+
// Collection field replaced by a different instance.
|
|
111
112
|
//
|
|
112
|
-
//
|
|
113
|
-
//
|
|
113
|
+
// Don't decrement children here: GC (`garbageCollectDeletedRefs`)
|
|
114
|
+
// removes them once the previous collection's refId hits zero.
|
|
115
|
+
// Doing it here too would double-decrement a *shared* child and
|
|
116
|
+
// drop it while still referenced ("refId not found").
|
|
117
|
+
if ((operation & OPERATION.DELETE) !== OPERATION.DELETE) {
|
|
118
|
+
// Replacement not tagged DELETE (e.g. pending ADD not upgraded
|
|
119
|
+
// to DELETE_AND_ADD), so the previous refId wasn't decremented
|
|
120
|
+
// above. Release it here, else it never gets GC'd (leak).
|
|
121
|
+
$root.removeRef(previousRefId);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// enqueue onRemove callbacks for the previous collection's children.
|
|
114
125
|
const entries: IterableIterator<[any, any]> = (previousValue as any).entries();
|
|
115
126
|
let iter: IteratorResult<[any, any]>;
|
|
116
127
|
while ((iter = entries.next()) && !iter.done) {
|
|
117
128
|
const [key, value] = iter.value;
|
|
118
129
|
|
|
119
|
-
// if value is a schema, remove its reference
|
|
120
130
|
if (typeof(value) === "object") {
|
|
121
131
|
previousRefId = value[$refId];
|
|
122
|
-
$root.removeRef(previousRefId);
|
|
123
132
|
}
|
|
124
133
|
|
|
125
134
|
allChanges.push({
|
package/src/encoder/Encoder.ts
CHANGED
|
@@ -126,36 +126,23 @@ export class Encoder<T extends Schema = any> {
|
|
|
126
126
|
}
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
if (it.offset
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
129
|
+
if (it.offset <= buffer.byteLength) {
|
|
130
|
+
return buffer.subarray(0, it.offset);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Overflowed: grow and re-encode. Reuse the same iterator so `it.offset`
|
|
134
|
+
// ends accurate — a fresh one strands it at the overflow value and
|
|
135
|
+
// corrupts the next view's `viewOffset` in a multi-view encode.
|
|
136
|
+
buffer = this.ensureCapacity(buffer, it.offset);
|
|
133
137
|
|
|
134
|
-
|
|
138
|
+
console.warn(`@colyseus/schema buffer overflow. Encoded state is higher than default BUFFER_SIZE. Use the following to increase default BUFFER_SIZE:
|
|
135
139
|
|
|
136
140
|
import { Encoder } from "@colyseus/schema";
|
|
137
|
-
Encoder.BUFFER_SIZE = ${Math.round(
|
|
141
|
+
Encoder.BUFFER_SIZE = ${Math.round(buffer.byteLength / 1024)} * 1024; // ${Math.round(buffer.byteLength / 1024)} KB
|
|
138
142
|
`);
|
|
139
143
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
// -> No we probably can't unless we catch the need for resize before encoding which is likely more computationally expensive than resizing on demand
|
|
143
|
-
//
|
|
144
|
-
const newBuffer = new Uint8Array(newSize);
|
|
145
|
-
newBuffer.set(buffer); // copy previous encoding steps beyond the initialOffset
|
|
146
|
-
buffer = newBuffer;
|
|
147
|
-
|
|
148
|
-
// assign resized buffer to local sharedBuffer
|
|
149
|
-
if (buffer === this.sharedBuffer) {
|
|
150
|
-
this.sharedBuffer = buffer;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
return this.encode({ offset: initialOffset }, view, buffer, changeSetName, isEncodeAll);
|
|
154
|
-
|
|
155
|
-
} else {
|
|
156
|
-
|
|
157
|
-
return buffer.subarray(0, it.offset);
|
|
158
|
-
}
|
|
144
|
+
it.offset = initialOffset;
|
|
145
|
+
return this.encode(it, view, buffer, changeSetName, isEncodeAll);
|
|
159
146
|
}
|
|
160
147
|
|
|
161
148
|
encodeAll(
|
|
@@ -173,8 +160,8 @@ export class Encoder<T extends Schema = any> {
|
|
|
173
160
|
) {
|
|
174
161
|
const viewOffset = it.offset;
|
|
175
162
|
|
|
176
|
-
//
|
|
177
|
-
this.encode(it, view, bytes, "allFilteredChanges", true, viewOffset);
|
|
163
|
+
// encode() may reallocate the buffer — keep its return, not the stale `bytes`.
|
|
164
|
+
bytes = this.encode(it, view, bytes, "allFilteredChanges", true, viewOffset);
|
|
178
165
|
|
|
179
166
|
return concatBytes(
|
|
180
167
|
bytes.subarray(0, sharedOffset),
|
|
@@ -182,6 +169,16 @@ export class Encoder<T extends Schema = any> {
|
|
|
182
169
|
);
|
|
183
170
|
}
|
|
184
171
|
|
|
172
|
+
/** Grow `buffer` to keep BUFFER_SIZE free bytes past `offset`, preserving `[0, offset)`. */
|
|
173
|
+
protected ensureCapacity(buffer: Uint8Array, offset: number): Uint8Array {
|
|
174
|
+
if (offset + Encoder.BUFFER_SIZE <= buffer.byteLength) { return buffer; }
|
|
175
|
+
const size = Math.ceil((offset + Encoder.BUFFER_SIZE) / Encoder.BUFFER_SIZE) * Encoder.BUFFER_SIZE;
|
|
176
|
+
const grown = new Uint8Array(size);
|
|
177
|
+
grown.set(buffer.subarray(0, offset));
|
|
178
|
+
if (buffer === this.sharedBuffer) { this.sharedBuffer = grown; }
|
|
179
|
+
return grown;
|
|
180
|
+
}
|
|
181
|
+
|
|
185
182
|
encodeView(
|
|
186
183
|
view: StateView,
|
|
187
184
|
sharedOffset: number,
|
|
@@ -232,6 +229,10 @@ export class Encoder<T extends Schema = any> {
|
|
|
232
229
|
const encoder = ctor[$encoder];
|
|
233
230
|
const metadata = ctor[Symbol.metadata];
|
|
234
231
|
|
|
232
|
+
// These writes are unguarded and unrecoverable (view.changes is cleared
|
|
233
|
+
// below), so unlike encode() they can't re-encode on overflow — grow ahead.
|
|
234
|
+
bytes = this.ensureCapacity(bytes, it.offset);
|
|
235
|
+
|
|
235
236
|
bytes[it.offset++] = SWITCH_TO_STRUCTURE & 255;
|
|
236
237
|
encode.number(bytes, ref[$refId], it);
|
|
237
238
|
|
|
@@ -255,8 +256,10 @@ export class Encoder<T extends Schema = any> {
|
|
|
255
256
|
view.changes.clear();
|
|
256
257
|
view.changesOutOfOrder = false;
|
|
257
258
|
|
|
258
|
-
//
|
|
259
|
-
|
|
259
|
+
// encode() may reallocate the buffer — keep its return, not the stale `bytes`.
|
|
260
|
+
// Anchor the re-encode at the current offset (default), not `viewOffset`: a
|
|
261
|
+
// resize must not clobber the view.changes already written at [viewOffset, ).
|
|
262
|
+
bytes = this.encode(it, view, bytes, "filteredChanges", false);
|
|
260
263
|
|
|
261
264
|
return concatBytes(
|
|
262
265
|
bytes.subarray(0, sharedOffset),
|
package/src/encoder/StateView.ts
CHANGED
|
@@ -27,7 +27,7 @@ export class StateView {
|
|
|
27
27
|
*/
|
|
28
28
|
invisible: WeakSet<ChangeTree> = new WeakSet<ChangeTree>();
|
|
29
29
|
|
|
30
|
-
tags?: WeakMap<ChangeTree,
|
|
30
|
+
tags?: WeakMap<ChangeTree, number>; // bitmask of tags used to add each ChangeTree
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
33
|
* Manual "ADD" operations for changes per ChangeTree, specific to this view.
|
|
@@ -130,10 +130,18 @@ export class StateView {
|
|
|
130
130
|
// Do not ADD children that don't have the same tag
|
|
131
131
|
if (
|
|
132
132
|
metadata &&
|
|
133
|
-
metadata[index].tag !== undefined
|
|
134
|
-
metadata[index].tag !== tag
|
|
133
|
+
metadata[index].tag !== undefined
|
|
135
134
|
) {
|
|
136
|
-
|
|
135
|
+
const fieldTag = metadata[index].tag;
|
|
136
|
+
// DEFAULT_VIEW_TAG fields are visible to all clients.
|
|
137
|
+
// Custom-tagged fields are only visible when bits overlap,
|
|
138
|
+
// and never to default-tag clients.
|
|
139
|
+
const tagMatch = fieldTag === DEFAULT_VIEW_TAG ||
|
|
140
|
+
(tag !== DEFAULT_VIEW_TAG && (fieldTag & tag) !== 0);
|
|
141
|
+
|
|
142
|
+
if (!tagMatch) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
137
145
|
}
|
|
138
146
|
|
|
139
147
|
if (this.add(change.ref, tag, false)) {
|
|
@@ -144,16 +152,11 @@ export class StateView {
|
|
|
144
152
|
// set tag
|
|
145
153
|
if (tag !== DEFAULT_VIEW_TAG) {
|
|
146
154
|
if (!this.tags) {
|
|
147
|
-
this.tags = new WeakMap<ChangeTree,
|
|
148
|
-
}
|
|
149
|
-
let tags: Set<number>;
|
|
150
|
-
if (!this.tags.has(changeTree)) {
|
|
151
|
-
tags = new Set<number>();
|
|
152
|
-
this.tags.set(changeTree, tags);
|
|
153
|
-
} else {
|
|
154
|
-
tags = this.tags.get(changeTree);
|
|
155
|
+
this.tags = new WeakMap<ChangeTree, number>();
|
|
155
156
|
}
|
|
156
|
-
|
|
157
|
+
// Add tag bits into the bitmask stored for this ChangeTree.
|
|
158
|
+
const currentMask = this.tags.get(changeTree) ?? 0;
|
|
159
|
+
this.tags.set(changeTree, currentMask | tag);
|
|
157
160
|
|
|
158
161
|
// Ref: add tagged properties
|
|
159
162
|
metadata?.[$fieldIndexesByViewTag]?.[tag]?.forEach((index) => {
|
|
@@ -181,7 +184,7 @@ export class StateView {
|
|
|
181
184
|
(
|
|
182
185
|
isInvisible || // if "invisible", include all
|
|
183
186
|
tagAtIndex === undefined || // "all change" with no tag
|
|
184
|
-
tagAtIndex === tag // tagged property
|
|
187
|
+
(tagAtIndex === DEFAULT_VIEW_TAG || (tag !== DEFAULT_VIEW_TAG && (tagAtIndex & tag) !== 0)) // tagged property
|
|
185
188
|
)
|
|
186
189
|
) {
|
|
187
190
|
changes[index] = op;
|
|
@@ -215,18 +218,16 @@ export class StateView {
|
|
|
215
218
|
if (changeTree.getChange(parentIndex) !== OPERATION.DELETE) {
|
|
216
219
|
const changes = this.touchChanges(changeTree.ref[$refId]);
|
|
217
220
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
this.tags.
|
|
226
|
-
|
|
227
|
-
tags = this.tags.get(changeTree);
|
|
221
|
+
// Only accumulate positive (custom) tags in the bitmask.
|
|
222
|
+
// DEFAULT_VIEW_TAG = -1 has all bits set and must not be OR'd in,
|
|
223
|
+
// as it would make every custom-tagged field appear visible.
|
|
224
|
+
if (tag !== DEFAULT_VIEW_TAG) {
|
|
225
|
+
if (!this.tags) {
|
|
226
|
+
this.tags = new WeakMap<ChangeTree, number>();
|
|
227
|
+
}
|
|
228
|
+
const currentMask = this.tags.has(changeTree) ? this.tags.get(changeTree) : 0;
|
|
229
|
+
this.tags.set(changeTree, currentMask | tag);
|
|
228
230
|
}
|
|
229
|
-
tags.add(tag);
|
|
230
231
|
|
|
231
232
|
changes[parentIndex] = OPERATION.ADD;
|
|
232
233
|
}
|
|
@@ -313,17 +314,16 @@ export class StateView {
|
|
|
313
314
|
|
|
314
315
|
// remove tag
|
|
315
316
|
if (this.tags && this.tags.has(changeTree)) {
|
|
316
|
-
const tags = this.tags.get(changeTree);
|
|
317
317
|
if (tag === undefined) {
|
|
318
318
|
// delete all tags
|
|
319
319
|
this.tags.delete(changeTree);
|
|
320
320
|
} else {
|
|
321
|
-
//
|
|
322
|
-
tags.
|
|
323
|
-
|
|
324
|
-
// if tag set is empty, delete it entirely
|
|
325
|
-
if (tags.size === 0) {
|
|
321
|
+
// clear the tag's bits from the bitmask
|
|
322
|
+
const newMask = this.tags.get(changeTree) & ~tag;
|
|
323
|
+
if (newMask === 0) {
|
|
326
324
|
this.tags.delete(changeTree);
|
|
325
|
+
} else {
|
|
326
|
+
this.tags.set(changeTree, newMask);
|
|
327
327
|
}
|
|
328
328
|
}
|
|
329
329
|
}
|
|
@@ -337,7 +337,7 @@ export class StateView {
|
|
|
337
337
|
|
|
338
338
|
hasTag(ob: Ref, tag: number = DEFAULT_VIEW_TAG) {
|
|
339
339
|
const tags = this.tags?.get(ob[$changes]);
|
|
340
|
-
return tags
|
|
340
|
+
return tags != null && (tags & tag) !== 0;
|
|
341
341
|
}
|
|
342
342
|
|
|
343
343
|
clear() {
|