@colyseus/schema 5.0.12 → 5.0.14
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/Reflection.d.ts +36 -34
- package/build/encoder/ChangeTree.d.ts +3 -0
- package/build/encoder/changeTree/parentChain.d.ts +9 -0
- package/build/index.cjs +80 -5
- package/build/index.cjs.map +1 -1
- package/build/index.js +80 -5
- package/build/index.mjs +80 -5
- package/build/index.mjs.map +1 -1
- package/build/types/HelperTypes.d.ts +3 -5
- package/build/types/builder.d.ts +7 -6
- package/build/types/custom/ArraySchema.d.ts +17 -0
- package/package.json +2 -2
- package/src/encoder/ChangeTree.ts +5 -0
- package/src/encoder/changeTree/parentChain.ts +29 -0
- package/src/types/HelperTypes.ts +16 -13
- package/src/types/builder.ts +7 -6
- package/src/types/custom/ArraySchema.ts +40 -1
|
@@ -70,7 +70,7 @@ export type InferValueType<T> = T extends FieldBuilder<infer V> ? V : T extends
|
|
|
70
70
|
stream: infer ChildType;
|
|
71
71
|
} ? StreamSchema<ChildType> : T extends Constructor ? InstanceType<T> : T extends Record<string | number, string | number> ? T[keyof T] : T extends PrimitiveType ? T : never;
|
|
72
72
|
type OptionalBuilderKeys<T> = {
|
|
73
|
-
[K in keyof T]: T[K] extends FieldBuilder<infer
|
|
73
|
+
[K in keyof T]: T[K] extends FieldBuilder<unknown, boolean, infer O extends boolean> ? (O extends true ? K : never) : never;
|
|
74
74
|
}[keyof T];
|
|
75
75
|
type RequiredBuilderKeys<T> = Exclude<keyof T, OptionalBuilderKeys<T>>;
|
|
76
76
|
export type InferSchemaInstanceType<T> = {
|
|
@@ -89,12 +89,10 @@ export type NonFunctionNonPrimitivePropNames<T> = {
|
|
|
89
89
|
}[keyof T];
|
|
90
90
|
type ToJSONValue<U> = U extends Schema ? ToJSON<U> : PrimitiveStringToType<U>;
|
|
91
91
|
type ToJSONField<X> = X extends MapSchema<infer U> ? Record<string, ToJSONValue<U>> : X extends Map<string, infer U> ? Record<string, ToJSONValue<U>> : X extends ArraySchema<infer U> ? ToJSONValue<U>[] : X extends SetSchema<infer U> ? ToJSONValue<U>[] : X extends CollectionSchema<infer U> ? ToJSONValue<U>[] : X extends Schema ? ToJSON<X> : X;
|
|
92
|
-
type ToJSONRequiredKeys<T> = {
|
|
93
|
-
[K in keyof T]-?: undefined extends T[K] ? never : K;
|
|
94
|
-
}[keyof T];
|
|
95
92
|
type ToJSONOptionalKeys<T> = {
|
|
96
|
-
[K in keyof T]-?: undefined extends T[K] ? K : never;
|
|
93
|
+
[K in keyof T]-?: undefined extends {} ? ({} extends Pick<T, K> ? K : never) : (undefined extends T[K] ? K : never);
|
|
97
94
|
}[keyof T];
|
|
95
|
+
type ToJSONRequiredKeys<T> = Exclude<keyof T, ToJSONOptionalKeys<T>>;
|
|
98
96
|
export type ToJSON<T> = NonFunctionProps<{
|
|
99
97
|
[K in ToJSONRequiredKeys<T>]: ToJSONField<T[K]>;
|
|
100
98
|
} & {
|
package/build/types/builder.d.ts
CHANGED
|
@@ -45,10 +45,11 @@ export type BuilderOf<T> = FieldBuilder<T>;
|
|
|
45
45
|
* Schema ref whose `initialize` takes zero args.
|
|
46
46
|
* - `IsOptional` is a compile-time brand for `.optional()`. Both
|
|
47
47
|
* `HasDefault` and `IsOptional` make the field omittable in
|
|
48
|
-
* `BuilderInitProps<T
|
|
49
|
-
* `undefined extends V`)
|
|
50
|
-
*
|
|
51
|
-
*
|
|
48
|
+
* `BuilderInitProps<T>`; `IsOptional` alone marks the instance property
|
|
49
|
+
* `?:`. A separate brand (rather than reading `undefined extends V`)
|
|
50
|
+
* keeps both correct for consumers compiling with
|
|
51
|
+
* `strictNullChecks: false`, where `undefined extends V` is true for
|
|
52
|
+
* every V.
|
|
52
53
|
*
|
|
53
54
|
* schema() reads the internal configuration via `toDefinition()` and wires
|
|
54
55
|
* up metadata through the existing pipeline.
|
|
@@ -199,8 +200,8 @@ export declare function isBuilder(value: any): value is FieldBuilder<any>;
|
|
|
199
200
|
* Two call signatures, NOT a defaulted generic `<T extends TBase = TBase>`: the
|
|
200
201
|
* bare form must return a CONCRETE `FieldBuilder<TBase>` so `schema({ x:
|
|
201
202
|
* t.number() })` still infers `x: number`. A defaulted free type parameter gets
|
|
202
|
-
* captured as `any` during `schema()`'s self-referential field inference
|
|
203
|
-
*
|
|
203
|
+
* captured as `any` during `schema()`'s self-referential field inference,
|
|
204
|
+
* degrading every field's value type to `any`.
|
|
204
205
|
*
|
|
205
206
|
* NOTE: the refinement is a TYPE-LEVEL assertion, not a runtime guarantee — the
|
|
206
207
|
* wire still carries the codec's full range and the DECODER writes whatever
|
|
@@ -63,6 +63,23 @@ export declare class ArraySchema<V = any> implements Array<V>, Collection<number
|
|
|
63
63
|
* land on the wrong wire slots.
|
|
64
64
|
*/
|
|
65
65
|
protected $wireIndex(index: number): number;
|
|
66
|
+
/**
|
|
67
|
+
* Re-point children at their wire slot. `ChangeTree._parentIndex` caches
|
|
68
|
+
* the slot a child holds in `tmpItems`, and StateView addresses per-view
|
|
69
|
+
* ADD/DELETE with it — so a reorder that leaves it behind aims those ops
|
|
70
|
+
* at whichever element inherited the slot (issue #231).
|
|
71
|
+
*
|
|
72
|
+
* The filter check is a correctness boundary, not a tunable: StateView is
|
|
73
|
+
* the only reader and reaches the index only through a filtered array
|
|
74
|
+
* (`addParentOf` bails on `hasFilteredFields`, `remove` on the child's
|
|
75
|
+
* `isFiltered`). Everything else stops at the flag read instead of walking
|
|
76
|
+
* its children every tick.
|
|
77
|
+
*
|
|
78
|
+
* Callers name the lowest slot that moved as `from`. Compaction cannot, so
|
|
79
|
+
* it hands over the pre-compaction layout as `staged` and the unchanged
|
|
80
|
+
* prefix is skipped instead. Either way tail churn walks nothing.
|
|
81
|
+
*/
|
|
82
|
+
protected $reindexChildren(from: number, staged?: V[]): void;
|
|
66
83
|
protected $changeAt(index: number, value: V): number | undefined;
|
|
67
84
|
protected $deleteAt(index: number, operation?: OPERATION): void;
|
|
68
85
|
protected $setAt(index: number, value: V, operation: OPERATION): void;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colyseus/schema",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.14",
|
|
4
4
|
"description": "Binary state serializer with delta encoding for games",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"watch": "tsc -p tsconfig.build.json -w",
|
|
19
19
|
"typecheck:build": "tsc -p tsconfig.build.json --noEmit",
|
|
20
20
|
"test": "npm run test:types && tsx --tsconfig tsconfig.test.json ./node_modules/mocha/bin/mocha.js \"test/*.test.ts\" \"test/**/*.test.ts\"",
|
|
21
|
-
"test:types": "tsc -p tsconfig.build.json && tsc -p tsconfig.types.json",
|
|
21
|
+
"test:types": "tsc -p tsconfig.build.json && tsc -p tsconfig.types.json && tsc -p tsconfig.types-nostrict.json",
|
|
22
22
|
"test:exports": "node test/verify-exports.mjs",
|
|
23
23
|
"typecheck": "tsc -p tsconfig.test.json",
|
|
24
24
|
"coverage": "c8 npm run test",
|
|
@@ -34,6 +34,7 @@ import type { DecodeOperation } from "../decoder/DecodeOperation.js";
|
|
|
34
34
|
|
|
35
35
|
import {
|
|
36
36
|
addParent as _addParent, removeParent as _removeParent,
|
|
37
|
+
setParentIndex as _setParentIndex,
|
|
37
38
|
findParent as _findParent, hasParent as _hasParent,
|
|
38
39
|
getAllParents as _getAllParents,
|
|
39
40
|
} from "./changeTree/parentChain.js";
|
|
@@ -804,6 +805,9 @@ export class ChangeTree<T extends Ref = any> implements ChangeRecorder {
|
|
|
804
805
|
|
|
805
806
|
addParent(parent: Ref, index: number): void { _addParent(this, parent, index); }
|
|
806
807
|
|
|
808
|
+
/** Re-point an existing parent's cached index after the parent reindexed. */
|
|
809
|
+
setParentIndex(parent: Ref, index: number): void { _setParentIndex(this, parent, index); }
|
|
810
|
+
|
|
807
811
|
/** @returns true if parent was found and removed */
|
|
808
812
|
removeParent(parent: Ref = this.parent): boolean { return _removeParent(this, parent); }
|
|
809
813
|
|
|
@@ -856,6 +860,7 @@ export class UntrackedChangeTree {
|
|
|
856
860
|
operation(): void {}
|
|
857
861
|
setParent(): void {}
|
|
858
862
|
addParent(): void {}
|
|
863
|
+
setParentIndex(): void {}
|
|
859
864
|
removeParent(): boolean { return false; }
|
|
860
865
|
getChange(): number { return 0; }
|
|
861
866
|
discard(): void {}
|
|
@@ -45,6 +45,35 @@ export function addParent(tree: ChangeTree, parent: Ref, index: number): void {
|
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Move `parent`'s existing chain entry to `index`, skipping the attachment
|
|
50
|
+
* work `addParent` does. `parent` must already be a parent of `tree`.
|
|
51
|
+
*
|
|
52
|
+
* Called by collections whose wire slots shift (ArraySchema): StateView
|
|
53
|
+
* addresses per-view ADD/DELETE by that index, so it has to follow the
|
|
54
|
+
* element it names.
|
|
55
|
+
*/
|
|
56
|
+
export function setParentIndex(tree: ChangeTree, parent: Ref, index: number): void {
|
|
57
|
+
if (tree.extraParents === undefined) {
|
|
58
|
+
tree._parentIndex = index; // sole parent, so it is `parent`
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
// Shared instance — move only the entry `parent` owns. Matching goes
|
|
62
|
+
// through `$changes` because ArraySchema arrives proxied (see removeParent
|
|
63
|
+
// below), and `extraParents` only ever fills by demoting `parentRef`, so
|
|
64
|
+
// the inline parent is set here.
|
|
65
|
+
if (tree.parentRef[$changes] === parent[$changes]) {
|
|
66
|
+
tree._parentIndex = index;
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
for (let entry = tree.extraParents; entry !== undefined; entry = entry.next) {
|
|
70
|
+
if (entry.ref[$changes] === parent[$changes]) {
|
|
71
|
+
entry.index = index;
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
48
77
|
/**
|
|
49
78
|
* Remove a parent from the chain.
|
|
50
79
|
* @returns true if parent was found and removed (Root.remove relies on this).
|
package/src/types/HelperTypes.ts
CHANGED
|
@@ -87,10 +87,12 @@ export type InferValueType<T> =
|
|
|
87
87
|
|
|
88
88
|
: never;
|
|
89
89
|
|
|
90
|
-
// Keys whose
|
|
90
|
+
// Keys whose builder carries the `.optional()` brand. Reads the brand rather
|
|
91
|
+
// than `undefined extends V`: the latter is true for EVERY V when the consumer
|
|
92
|
+
// compiles with `strictNullChecks: false`, flipping all fields optional.
|
|
91
93
|
type OptionalBuilderKeys<T> = {
|
|
92
|
-
[K in keyof T]: T[K] extends FieldBuilder<infer
|
|
93
|
-
? (
|
|
94
|
+
[K in keyof T]: T[K] extends FieldBuilder<unknown, boolean, infer O extends boolean>
|
|
95
|
+
? (O extends true ? K : never)
|
|
94
96
|
: never
|
|
95
97
|
}[keyof T];
|
|
96
98
|
|
|
@@ -136,14 +138,16 @@ type ToJSONField<X> =
|
|
|
136
138
|
: X extends Schema ? ToJSON<X>
|
|
137
139
|
: X;
|
|
138
140
|
|
|
139
|
-
// Keys whose value
|
|
140
|
-
//
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
}[keyof T];
|
|
141
|
+
// Keys whose value admits `undefined` — runtime `toJSON()` omits those, so
|
|
142
|
+
// they surface as `?:` on the JSON shape. Under `strictNullChecks: false`
|
|
143
|
+
// (`undefined extends {}` detects it) `undefined extends T[K]` is true for
|
|
144
|
+
// every key, so only the `?` modifier can signal optionality there.
|
|
144
145
|
type ToJSONOptionalKeys<T> = {
|
|
145
|
-
[K in keyof T]-?: undefined extends
|
|
146
|
+
[K in keyof T]-?: undefined extends {}
|
|
147
|
+
? ({} extends Pick<T, K> ? K : never)
|
|
148
|
+
: (undefined extends T[K] ? K : never)
|
|
146
149
|
}[keyof T];
|
|
150
|
+
type ToJSONRequiredKeys<T> = Exclude<keyof T, ToJSONOptionalKeys<T>>;
|
|
147
151
|
|
|
148
152
|
export type ToJSON<T> = NonFunctionProps<
|
|
149
153
|
& { [K in ToJSONRequiredKeys<T>]: ToJSONField<T[K]> }
|
|
@@ -226,10 +230,9 @@ type FieldValue<F> =
|
|
|
226
230
|
|
|
227
231
|
// Classify each key of a fields map as "required" / "optional" / "none"
|
|
228
232
|
// (methods). Both `HasDefault = true` and the explicit `.optional()` brand
|
|
229
|
-
// `IsOptional = true` mark the field omittable at construction.
|
|
230
|
-
//
|
|
231
|
-
//
|
|
232
|
-
// contravariant + covariant positions.
|
|
233
|
+
// `IsOptional = true` mark the field omittable at construction. Reading the
|
|
234
|
+
// brands (never `undefined extends V`) keeps this correct for consumers on
|
|
235
|
+
// `strictNullChecks: false`, where `undefined extends V` is true for every V.
|
|
233
236
|
type KeyClass<T, K extends keyof T> =
|
|
234
237
|
T[K] extends FieldBuilder<unknown, infer D extends boolean, infer O extends boolean>
|
|
235
238
|
? (D extends true
|
package/src/types/builder.ts
CHANGED
|
@@ -51,10 +51,11 @@ export type BuilderOf<T> = FieldBuilder<T>;
|
|
|
51
51
|
* Schema ref whose `initialize` takes zero args.
|
|
52
52
|
* - `IsOptional` is a compile-time brand for `.optional()`. Both
|
|
53
53
|
* `HasDefault` and `IsOptional` make the field omittable in
|
|
54
|
-
* `BuilderInitProps<T
|
|
55
|
-
* `undefined extends V`)
|
|
56
|
-
*
|
|
57
|
-
*
|
|
54
|
+
* `BuilderInitProps<T>`; `IsOptional` alone marks the instance property
|
|
55
|
+
* `?:`. A separate brand (rather than reading `undefined extends V`)
|
|
56
|
+
* keeps both correct for consumers compiling with
|
|
57
|
+
* `strictNullChecks: false`, where `undefined extends V` is true for
|
|
58
|
+
* every V.
|
|
58
59
|
*
|
|
59
60
|
* schema() reads the internal configuration via `toDefinition()` and wires
|
|
60
61
|
* up metadata through the existing pipeline.
|
|
@@ -289,8 +290,8 @@ export function isBuilder(value: any): value is FieldBuilder<any> {
|
|
|
289
290
|
* Two call signatures, NOT a defaulted generic `<T extends TBase = TBase>`: the
|
|
290
291
|
* bare form must return a CONCRETE `FieldBuilder<TBase>` so `schema({ x:
|
|
291
292
|
* t.number() })` still infers `x: number`. A defaulted free type parameter gets
|
|
292
|
-
* captured as `any` during `schema()`'s self-referential field inference
|
|
293
|
-
*
|
|
293
|
+
* captured as `any` during `schema()`'s self-referential field inference,
|
|
294
|
+
* degrading every field's value type to `any`.
|
|
294
295
|
*
|
|
295
296
|
* NOTE: the refinement is a TYPE-LEVEL assertion, not a runtime guarantee — the
|
|
296
297
|
* wire still carries the codec's full range and the DECODER writes whatever
|
|
@@ -331,6 +331,35 @@ export class ArraySchema<V = any> implements Array<V>, Collection<number, V>, IR
|
|
|
331
331
|
return tmpItems.length + (index - live);
|
|
332
332
|
}
|
|
333
333
|
|
|
334
|
+
/**
|
|
335
|
+
* Re-point children at their wire slot. `ChangeTree._parentIndex` caches
|
|
336
|
+
* the slot a child holds in `tmpItems`, and StateView addresses per-view
|
|
337
|
+
* ADD/DELETE with it — so a reorder that leaves it behind aims those ops
|
|
338
|
+
* at whichever element inherited the slot (issue #231).
|
|
339
|
+
*
|
|
340
|
+
* The filter check is a correctness boundary, not a tunable: StateView is
|
|
341
|
+
* the only reader and reaches the index only through a filtered array
|
|
342
|
+
* (`addParentOf` bails on `hasFilteredFields`, `remove` on the child's
|
|
343
|
+
* `isFiltered`). Everything else stops at the flag read instead of walking
|
|
344
|
+
* its children every tick.
|
|
345
|
+
*
|
|
346
|
+
* Callers name the lowest slot that moved as `from`. Compaction cannot, so
|
|
347
|
+
* it hands over the pre-compaction layout as `staged` and the unchanged
|
|
348
|
+
* prefix is skipped instead. Either way tail churn walks nothing.
|
|
349
|
+
*/
|
|
350
|
+
protected $reindexChildren(from: number, staged?: V[]) {
|
|
351
|
+
if (!this[$changes].hasFilteredFields) { return; } // nothing will read the cache
|
|
352
|
+
if (typeof this[$childType] === "string") { return; } // primitives have no child tree
|
|
353
|
+
const tmpItems = this.tmpItems;
|
|
354
|
+
const length = tmpItems.length;
|
|
355
|
+
if (staged !== undefined) {
|
|
356
|
+
while (from < length && tmpItems[from] === staged[from]) { from++; }
|
|
357
|
+
}
|
|
358
|
+
for (let i = from; i < length; i++) {
|
|
359
|
+
tmpItems[i]?.[$changes]?.setParentIndex(this, i);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
334
363
|
// encoding only. Returns the wire index the change was recorded at
|
|
335
364
|
// (undefined when nothing was recorded).
|
|
336
365
|
protected $changeAt(index: number, value: V): number | undefined {
|
|
@@ -460,6 +489,7 @@ export class ArraySchema<V = any> implements Array<V>, Collection<number, V>, IR
|
|
|
460
489
|
self[$changes].operation(OPERATION.REVERSE);
|
|
461
490
|
self.items.reverse();
|
|
462
491
|
self.tmpItems.reverse();
|
|
492
|
+
self.$reindexChildren(0);
|
|
463
493
|
return this;
|
|
464
494
|
}
|
|
465
495
|
|
|
@@ -515,6 +545,7 @@ export class ArraySchema<V = any> implements Array<V>, Collection<number, V>, IR
|
|
|
515
545
|
sortedItems.forEach((_, i) => changeTree.change(i, OPERATION.REPLACE));
|
|
516
546
|
|
|
517
547
|
self.tmpItems.sort(compareFn);
|
|
548
|
+
self.$reindexChildren(0);
|
|
518
549
|
|
|
519
550
|
self.isMovingItems = false;
|
|
520
551
|
return this;
|
|
@@ -620,6 +651,7 @@ export class ArraySchema<V = any> implements Array<V>, Collection<number, V>, IR
|
|
|
620
651
|
}
|
|
621
652
|
|
|
622
653
|
self.tmpItems.unshift(...items);
|
|
654
|
+
self.$reindexChildren(items.length); // survivors only — the loop above placed the new items
|
|
623
655
|
|
|
624
656
|
return self.items.unshift(...items);
|
|
625
657
|
}
|
|
@@ -938,8 +970,15 @@ export class ArraySchema<V = any> implements Array<V>, Collection<number, V>, IR
|
|
|
938
970
|
|
|
939
971
|
protected [$onEncodeEnd]() {
|
|
940
972
|
const self = this[$proxyTarget] ?? this;
|
|
973
|
+
const staged = self.tmpItems;
|
|
941
974
|
self.tmpItems = self.items.slice();
|
|
942
|
-
|
|
975
|
+
|
|
976
|
+
if (self.deletedIndexes.length > 0) {
|
|
977
|
+
// compaction just closed the staged holes — everything above the
|
|
978
|
+
// lowest one slid down a slot
|
|
979
|
+
self.$reindexChildren(0, staged);
|
|
980
|
+
self.deletedIndexes.length = 0;
|
|
981
|
+
}
|
|
943
982
|
}
|
|
944
983
|
|
|
945
984
|
protected [$onDecodeEnd]() {
|