@colyseus/schema 5.0.14 → 5.0.20
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/README.md +11 -5
- package/build/Metadata.d.ts +10 -1
- package/build/annotations.d.ts +6 -5
- package/build/codegen/api.d.ts +2 -0
- package/build/codegen/cli.cjs +322 -31
- package/build/codegen/cli.cjs.map +1 -1
- package/build/codegen/parser.d.ts +6 -1
- package/build/codegen/resolve.d.ts +25 -0
- package/build/codegen/types.d.ts +2 -0
- package/build/decoder/strategy/Callbacks.d.ts +7 -8
- package/build/decoder/strategy/getDecoderStateCallbacks.d.ts +2 -2
- package/build/encoder/ChangeTree.d.ts +40 -12
- package/build/encoder/Encoder.d.ts +1 -1
- package/build/encoder/Root.d.ts +9 -0
- package/build/encoder/StateView.d.ts +38 -1
- package/build/encoder/changeTree/inheritedFlags.d.ts +13 -19
- package/build/encoder/changeTree/liveIteration.d.ts +8 -0
- package/build/encoder/changeTree/parentChain.d.ts +30 -8
- package/build/encoder/streaming.d.ts +1 -1
- package/build/index.cjs +3232 -2830
- package/build/index.cjs.map +1 -1
- package/build/index.js +3228 -2826
- package/build/index.mjs +3232 -2830
- package/build/index.mjs.map +1 -1
- package/build/types/HelperTypes.d.ts +24 -14
- package/build/types/TypeContext.d.ts +0 -17
- package/build/types/builder.d.ts +1 -5
- package/build/types/symbols.d.ts +1 -0
- package/package.json +9 -8
- package/src/Metadata.ts +59 -77
- package/src/Reflection.ts +9 -5
- package/src/annotations.ts +28 -18
- package/src/codegen/api.ts +3 -1
- package/src/codegen/cli.ts +5 -2
- package/src/codegen/parser.ts +69 -31
- package/src/codegen/resolve.ts +322 -0
- package/src/codegen/types.ts +4 -1
- package/src/decoder/DecodeOperation.ts +13 -2
- package/src/decoder/strategy/Callbacks.ts +7 -8
- package/src/decoder/strategy/getDecoderStateCallbacks.ts +2 -2
- package/src/encoder/ChangeTree.ts +76 -25
- package/src/encoder/EncodeOperation.ts +10 -1
- package/src/encoder/Encoder.ts +52 -2
- package/src/encoder/Root.ts +28 -8
- package/src/encoder/StateView.ts +150 -66
- package/src/encoder/changeTree/inheritedFlags.ts +164 -45
- package/src/encoder/changeTree/liveIteration.ts +24 -3
- package/src/encoder/changeTree/parentChain.ts +72 -15
- package/src/encoder/streaming.ts +2 -1
- package/src/types/HelperTypes.ts +43 -34
- package/src/types/TypeContext.ts +5 -52
- package/src/types/builder.ts +14 -10
- package/src/types/custom/ArraySchema.ts +57 -14
- package/src/types/symbols.ts +3 -0
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* additional parents live in the `extraParents` linked list.
|
|
6
6
|
*/
|
|
7
7
|
import { $changes } from "../../types/symbols.js";
|
|
8
|
-
import type { ChangeTree,
|
|
8
|
+
import type { ChangeTree, ParentEntry, Ref } from "../ChangeTree.js";
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Add a parent to the chain. If `parent` already exists anywhere in the
|
|
@@ -115,39 +115,69 @@ export function removeParent(tree: ChangeTree, parent: Ref): boolean {
|
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
/**
|
|
118
|
-
*
|
|
118
|
+
* First parent matching `predicate`, as a detached `ParentEntry`. Never returns
|
|
119
|
+
* a live `ParentChain` node — the inline parent has no node to return in the
|
|
120
|
+
* first place, so handing out the real node for the `extraParents` case only
|
|
121
|
+
* would make writes land or vanish depending on which parent matched. Use
|
|
122
|
+
* `setParentIndex` to move an index and `indexInParent` to read one.
|
|
119
123
|
*/
|
|
120
124
|
export function findParent(
|
|
121
125
|
tree: ChangeTree,
|
|
122
126
|
predicate: (parent: Ref, index: number) => boolean,
|
|
123
|
-
):
|
|
124
|
-
|
|
125
|
-
if (tree.parentRef && predicate(tree.parentRef, tree._parentIndex)) {
|
|
127
|
+
): ParentEntry | undefined {
|
|
128
|
+
if (tree.parentRef !== undefined && predicate(tree.parentRef, tree._parentIndex)) {
|
|
126
129
|
return { ref: tree.parentRef, index: tree._parentIndex };
|
|
127
130
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
if (predicate(current.ref, current.index)) {
|
|
132
|
-
return current;
|
|
131
|
+
for (let entry = tree.extraParents; entry !== undefined; entry = entry.next) {
|
|
132
|
+
if (predicate(entry.ref, entry.index)) {
|
|
133
|
+
return { ref: entry.ref, index: entry.index };
|
|
133
134
|
}
|
|
134
|
-
current = current.next;
|
|
135
135
|
}
|
|
136
136
|
return undefined;
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
+
/** Walks in place — `addParent` calls this per shared-instance attach. */
|
|
139
140
|
export function hasParent(
|
|
140
141
|
tree: ChangeTree,
|
|
141
142
|
predicate: (parent: Ref, index: number) => boolean,
|
|
142
143
|
): boolean {
|
|
143
|
-
|
|
144
|
+
if (tree.parentRef !== undefined && predicate(tree.parentRef, tree._parentIndex)) {
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
for (let entry = tree.extraParents; entry !== undefined; entry = entry.next) {
|
|
148
|
+
if (predicate(entry.ref, entry.index)) { return true; }
|
|
149
|
+
}
|
|
150
|
+
return false;
|
|
144
151
|
}
|
|
145
152
|
|
|
146
153
|
/**
|
|
147
|
-
*
|
|
154
|
+
* Wire index `tree` holds inside `parent`, or undefined when `parent` is
|
|
155
|
+
* nowhere in the chain. Allocation-free variant of `findParent` for the
|
|
156
|
+
* encodeView drain, which resolves identity-keyed view entries per emission.
|
|
157
|
+
*
|
|
158
|
+
* A child detached from `parent` this tick usually still resolves: Root.remove
|
|
159
|
+
* leaves the child's own parent link dangling, and the staged snapshot keeps
|
|
160
|
+
* the child in `tmpItems` (so reindexes keep the index current) until
|
|
161
|
+
* `$onEncodeEnd` — which runs after the drain.
|
|
148
162
|
*/
|
|
149
|
-
export function
|
|
150
|
-
|
|
163
|
+
export function indexInParent(tree: ChangeTree, parent: Ref): number | undefined {
|
|
164
|
+
// `$changes` comparison — ArraySchema parents arrive proxied.
|
|
165
|
+
if (tree.parentRef && tree.parentRef[$changes] === parent[$changes]) {
|
|
166
|
+
return tree._parentIndex;
|
|
167
|
+
}
|
|
168
|
+
for (let entry = tree.extraParents; entry !== undefined; entry = entry.next) {
|
|
169
|
+
if (entry.ref[$changes] === parent[$changes]) {
|
|
170
|
+
return entry.index;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Return all parents as detached entries (debug/test helper).
|
|
178
|
+
*/
|
|
179
|
+
export function getAllParents(tree: ChangeTree): ParentEntry[] {
|
|
180
|
+
const parents: ParentEntry[] = [];
|
|
151
181
|
if (tree.parentRef) {
|
|
152
182
|
parents.push({ ref: tree.parentRef, index: tree._parentIndex });
|
|
153
183
|
}
|
|
@@ -158,3 +188,30 @@ export function getAllParents(tree: ChangeTree): Array<{ ref: Ref, index: number
|
|
|
158
188
|
}
|
|
159
189
|
return parents;
|
|
160
190
|
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* True iff `parent` currently holds `tree`. Detached edges linger in the
|
|
194
|
+
* parent chain (load-bearing for same-tick view drains — see
|
|
195
|
+
* `indexInParent` above), so the chain alone cannot answer which edges
|
|
196
|
+
* are live. ArraySchema is probed by scanning `items`: the recorded slot
|
|
197
|
+
* can go stale after reorders, and `items` — unlike `$getByIndex`'s staged
|
|
198
|
+
* view — reflects the tick's completed mutations.
|
|
199
|
+
*/
|
|
200
|
+
export function isEdgeLive(tree: ChangeTree, parentTree: ChangeTree, index: number): boolean {
|
|
201
|
+
const target = parentTree.refTarget as any;
|
|
202
|
+
if (parentTree.isArray) {
|
|
203
|
+
// Read `items` directly, not `$getByIndex` — the latter serves the
|
|
204
|
+
// staged (tmpItems) view, which can still hold a same-tick removal.
|
|
205
|
+
const items = target.items;
|
|
206
|
+
const at = items[index];
|
|
207
|
+
if (at !== undefined && at[$changes] === tree) return true;
|
|
208
|
+
// Recorded slot goes stale after reorders — scan before declaring dead.
|
|
209
|
+
for (let i = 0, len = items.length; i < len; i++) {
|
|
210
|
+
const v = items[i];
|
|
211
|
+
if (v !== undefined && v[$changes] === tree) return true;
|
|
212
|
+
}
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
const at = parentTree.getValue(index);
|
|
216
|
+
return at !== undefined && at[$changes] === tree;
|
|
217
|
+
}
|
package/src/encoder/streaming.ts
CHANGED
|
@@ -204,7 +204,8 @@ export function streamDequeueForView(
|
|
|
204
204
|
viewId: number,
|
|
205
205
|
refId: number,
|
|
206
206
|
index: number,
|
|
207
|
-
|
|
207
|
+
// widened key: StateView.changes carries identity-keyed array entries too
|
|
208
|
+
viewChanges: Map<number, Map<number | object, number>>,
|
|
208
209
|
): boolean {
|
|
209
210
|
const st = s._stream;
|
|
210
211
|
if (st === undefined) return false;
|
package/src/types/HelperTypes.ts
CHANGED
|
@@ -17,10 +17,18 @@ type PrimitiveStringToType<T> =
|
|
|
17
17
|
: T extends "boolean" ? boolean
|
|
18
18
|
: T;
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
/**
|
|
21
|
+
* What the decoder callbacks accept as "a collection": the public shape, which
|
|
22
|
+
* a plain array satisfies too — `@type([X]) items: X[]` is a common way to
|
|
23
|
+
* declare a field. {@link Collection} is the runtime contract on top of it.
|
|
24
|
+
*/
|
|
25
|
+
export interface CollectionLike<K = any, V = any, IT = V> {
|
|
21
26
|
[Symbol.iterator](): IterableIterator<IT>;
|
|
22
27
|
forEach(callback: Function): void;
|
|
23
28
|
entries(): IterableIterator<[K, V]>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface Collection<K = any, V = any, IT = V> extends CollectionLike<K, V, IT> {
|
|
24
32
|
/** See {@link $resyncPrune} — every collection kind must declare its resync-sweep semantics. */
|
|
25
33
|
[$resyncPrune](
|
|
26
34
|
visited: Set<number | string>,
|
|
@@ -90,13 +98,13 @@ export type InferValueType<T> =
|
|
|
90
98
|
// Keys whose builder carries the `.optional()` brand. Reads the brand rather
|
|
91
99
|
// than `undefined extends V`: the latter is true for EVERY V when the consumer
|
|
92
100
|
// compiles with `strictNullChecks: false`, flipping all fields optional.
|
|
93
|
-
type
|
|
94
|
-
|
|
95
|
-
? (O extends true ? K : never)
|
|
96
|
-
: never
|
|
97
|
-
}[keyof T];
|
|
101
|
+
type IsOptionalBuilderKey<T, K extends keyof T> =
|
|
102
|
+
T[K] extends FieldBuilder<unknown, boolean, infer O extends boolean> ? O : false;
|
|
98
103
|
|
|
99
|
-
|
|
104
|
+
// Per key, like `DataKey` below — an `Exclude<keyof T, …>` split defers every
|
|
105
|
+
// key once one field is typed by a bare type parameter.
|
|
106
|
+
type OptionalBuilderKeys<T> = { [K in keyof T]-?: IsOptionalBuilderKey<T, K> extends true ? K : never }[keyof T];
|
|
107
|
+
type RequiredBuilderKeys<T> = { [K in keyof T]-?: IsOptionalBuilderKey<T, K> extends true ? never : K }[keyof T];
|
|
100
108
|
|
|
101
109
|
export type InferSchemaInstanceType<T> = {
|
|
102
110
|
[K in RequiredBuilderKeys<T>]: T[K] extends FieldBuilder<any>
|
|
@@ -110,20 +118,20 @@ export type InferSchemaInstanceType<T> = {
|
|
|
110
118
|
: never
|
|
111
119
|
} & Schema;
|
|
112
120
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
121
|
+
// Per-key filter, never `Omit`/`Exclude` over the union of method names: with
|
|
122
|
+
// one field typed by a bare type parameter that union defers EVERY key, and a
|
|
123
|
+
// mapped type with no resolvable keys has no members to relate — which is what
|
|
124
|
+
// stopped `SpecialNode<E>` from satisfying `extends NodeBase`.
|
|
125
|
+
// `keyof Schema` is dropped so `restore({ ... })` takes a plain literal.
|
|
126
|
+
type DataKey<T, K extends keyof T> =
|
|
127
|
+
K extends keyof Schema ? never
|
|
128
|
+
: T[K] extends Function ? never
|
|
129
|
+
: K;
|
|
116
130
|
|
|
117
|
-
export type NonFunctionPropNames<T> = {
|
|
118
|
-
[K in keyof T]: T[K] extends Function ? never : K
|
|
119
|
-
}[keyof T];
|
|
131
|
+
export type NonFunctionPropNames<T> = { [K in keyof T]-?: DataKey<T, K> }[keyof T];
|
|
120
132
|
|
|
121
133
|
export type NonFunctionNonPrimitivePropNames<T> = {
|
|
122
|
-
[K in keyof T]: T[K] extends
|
|
123
|
-
? never
|
|
124
|
-
: T[K] extends number | string | boolean
|
|
125
|
-
? never
|
|
126
|
-
: K
|
|
134
|
+
[K in keyof T]-?: [DataKey<T, K>] extends [never] ? never : T[K] extends number | string | boolean ? never : K
|
|
127
135
|
}[keyof T];
|
|
128
136
|
|
|
129
137
|
// Helper to recursively convert Schema instances to their JSON representation
|
|
@@ -138,21 +146,23 @@ type ToJSONField<X> =
|
|
|
138
146
|
: X extends Schema ? ToJSON<X>
|
|
139
147
|
: X;
|
|
140
148
|
|
|
141
|
-
//
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
//
|
|
145
|
-
type
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
type ToJSONRequiredKeys<T> =
|
|
151
|
-
|
|
152
|
-
|
|
149
|
+
// Runtime `toJSON()` omits `undefined` values, so those keys surface as `?:`.
|
|
150
|
+
// Under `strictNullChecks: false` (`undefined extends {}` detects it)
|
|
151
|
+
// `undefined extends T[K]` is true for every key, so only the `?` modifier
|
|
152
|
+
// can signal optionality there.
|
|
153
|
+
type IsOptionalKey<T, K extends keyof T> = undefined extends {}
|
|
154
|
+
? ({} extends Pick<T, K> ? true : false)
|
|
155
|
+
: (undefined extends T[K] ? true : false);
|
|
156
|
+
|
|
157
|
+
// `DataKey` first: machinery and method keys drop before the optionality probe.
|
|
158
|
+
type ToJSONRequiredKeys<T> = { [K in keyof T]-?: [DataKey<T, K>] extends [never] ? never : IsOptionalKey<T, K> extends true ? never : K }[keyof T];
|
|
159
|
+
type ToJSONOptionalKeys<T> = { [K in keyof T]-?: [DataKey<T, K>] extends [never] ? never : IsOptionalKey<T, K> extends true ? K : never }[keyof T];
|
|
160
|
+
|
|
161
|
+
// Keys are filtered before mapping: `ToJSONField` over the `this`-typed methods
|
|
162
|
+
// exceeds TypeScript 7's instantiation depth.
|
|
163
|
+
export type ToJSON<T> =
|
|
153
164
|
& { [K in ToJSONRequiredKeys<T>]: ToJSONField<T[K]> }
|
|
154
|
-
& { [K in ToJSONOptionalKeys<T>]?: ToJSONField<Exclude<T[K], undefined>> }
|
|
155
|
-
>;
|
|
165
|
+
& { [K in ToJSONOptionalKeys<T>]?: ToJSONField<Exclude<T[K], undefined>> };
|
|
156
166
|
|
|
157
167
|
/**
|
|
158
168
|
* The plain DATA shape of a Schema instance type `T`: its synchronized fields
|
|
@@ -172,8 +182,7 @@ export type ToJSON<T> = NonFunctionProps<
|
|
|
172
182
|
*
|
|
173
183
|
* Unlike {@link ToJSON} (a recursive *serialization* shape), this is a flat
|
|
174
184
|
* structural projection: nested Schema / collection fields keep their instance
|
|
175
|
-
* types
|
|
176
|
-
* `NonFunctionProps` pass leaves behind.
|
|
185
|
+
* types.
|
|
177
186
|
*/
|
|
178
187
|
export type Data<T> = Omit<T, keyof Schema>;
|
|
179
188
|
|
package/src/types/TypeContext.ts
CHANGED
|
@@ -8,16 +8,6 @@ export class TypeContext {
|
|
|
8
8
|
schemas = new Map<typeof Schema, number>();
|
|
9
9
|
|
|
10
10
|
hasFilters: boolean = false;
|
|
11
|
-
parentFiltered: {[typeIdAndParentIndex: string]: boolean} = {};
|
|
12
|
-
/**
|
|
13
|
-
* True iff `parentFiltered` has at least one entry. Flipped on by
|
|
14
|
-
* `registerFilteredByParent` and read in `checkInheritedFlags` as a
|
|
15
|
-
* cheap gate to skip the string-keyed `parentFiltered[key]` lookup
|
|
16
|
-
* when no class has registered filter inheritance via ancestry — the
|
|
17
|
-
* common case when @view tags exist only on sibling fields, not
|
|
18
|
-
* along any attachment chain.
|
|
19
|
-
*/
|
|
20
|
-
hasParentFilteredEntries: boolean = false;
|
|
21
11
|
|
|
22
12
|
/**
|
|
23
13
|
* For inheritance support
|
|
@@ -84,17 +74,13 @@ export class TypeContext {
|
|
|
84
74
|
return this.schemas.get(klass);
|
|
85
75
|
}
|
|
86
76
|
|
|
87
|
-
private discoverTypes(klass: typeof Schema
|
|
88
|
-
if (parentHasViewTag) {
|
|
89
|
-
this.registerFilteredByParent(klass, parentType, parentIndex);
|
|
90
|
-
}
|
|
91
|
-
|
|
77
|
+
private discoverTypes(klass: typeof Schema) {
|
|
92
78
|
// skip if already registered
|
|
93
79
|
if (!this.add(klass)) { return; }
|
|
94
80
|
|
|
95
81
|
// add classes inherited from this base class
|
|
96
82
|
TypeContext.inheritedTypes.get(klass)?.forEach((child) => {
|
|
97
|
-
this.discoverTypes(child
|
|
83
|
+
this.discoverTypes(child);
|
|
98
84
|
});
|
|
99
85
|
|
|
100
86
|
// add parent classes
|
|
@@ -120,7 +106,6 @@ export class TypeContext {
|
|
|
120
106
|
const index = fieldIndex as any as number;
|
|
121
107
|
|
|
122
108
|
const fieldType = metadata[index].type;
|
|
123
|
-
const fieldHasViewTag = (metadata[index].tag !== undefined);
|
|
124
109
|
|
|
125
110
|
if (typeof (fieldType) === "string") {
|
|
126
111
|
continue;
|
|
@@ -133,7 +118,7 @@ export class TypeContext {
|
|
|
133
118
|
}
|
|
134
119
|
|
|
135
120
|
if (typeof (fieldType) === "function") {
|
|
136
|
-
this.discoverTypes(fieldType as typeof Schema
|
|
121
|
+
this.discoverTypes(fieldType as typeof Schema);
|
|
137
122
|
|
|
138
123
|
} else {
|
|
139
124
|
const type = Object.values(fieldType)[0];
|
|
@@ -143,47 +128,15 @@ export class TypeContext {
|
|
|
143
128
|
continue;
|
|
144
129
|
}
|
|
145
130
|
|
|
146
|
-
this.discoverTypes(type as typeof Schema
|
|
131
|
+
this.discoverTypes(type as typeof Schema);
|
|
147
132
|
}
|
|
148
133
|
}
|
|
149
134
|
}
|
|
150
135
|
|
|
151
|
-
/**
|
|
152
|
-
* Keep track of which classes have filters applied.
|
|
153
|
-
* Format: `${typeid}-${parentTypeid}-${parentIndex}`
|
|
154
|
-
*/
|
|
155
|
-
private registerFilteredByParent(schema: typeof Schema, parentType?: typeof Schema, parentIndex?: number) {
|
|
156
|
-
const typeid = this.schemas.get(schema) ?? this.schemas.size;
|
|
157
|
-
|
|
158
|
-
let key = `${typeid}`;
|
|
159
|
-
if (parentType) { key += `-${this.schemas.get(parentType)}`; }
|
|
160
|
-
|
|
161
|
-
key += `-${parentIndex}`;
|
|
162
|
-
this.parentFiltered[key] = true;
|
|
163
|
-
this.hasParentFilteredEntries = true;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
136
|
debug() {
|
|
167
|
-
let parentFiltered = "";
|
|
168
|
-
|
|
169
|
-
for (const key in this.parentFiltered) {
|
|
170
|
-
const keys: number[] = key.split("-").map(Number);
|
|
171
|
-
const fieldIndex = keys.pop();
|
|
172
|
-
|
|
173
|
-
parentFiltered += `\n\t\t`;
|
|
174
|
-
parentFiltered += `${key}: ${keys.reverse().map((id, i) => {
|
|
175
|
-
const klass = this.types[id];
|
|
176
|
-
const metadata: Metadata = klass[Symbol.metadata];
|
|
177
|
-
let txt = klass.name;
|
|
178
|
-
if (i === 0) { txt += `[${metadata[fieldIndex].name}]`; }
|
|
179
|
-
return `${txt}`;
|
|
180
|
-
}).join(" -> ")}`;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
137
|
return `TypeContext ->\n` +
|
|
184
138
|
`\tSchema types: ${this.schemas.size}\n` +
|
|
185
|
-
`\thasFilters: ${this.hasFilters}
|
|
186
|
-
`\tparentFiltered:${parentFiltered}`;
|
|
139
|
+
`\thasFilters: ${this.hasFilters}`;
|
|
187
140
|
}
|
|
188
141
|
|
|
189
142
|
}
|
package/src/types/builder.ts
CHANGED
|
@@ -307,16 +307,24 @@ function primitive<TBase>(name: RawPrimitiveType): PrimitiveFactory<TBase> {
|
|
|
307
307
|
return (() => new FieldBuilder<TBase>(name)) as PrimitiveFactory<TBase>;
|
|
308
308
|
}
|
|
309
309
|
|
|
310
|
-
//
|
|
310
|
+
// Collection element: a Schema class or a primitive type NAME (`"string"`).
|
|
311
311
|
export type ChildType =
|
|
312
312
|
| RawPrimitiveType
|
|
313
|
-
| Constructor<Schema
|
|
314
|
-
| FieldBuilder<any>;
|
|
313
|
+
| Constructor<Schema>;
|
|
315
314
|
|
|
315
|
+
/**
|
|
316
|
+
* Guard against `t.array(t.string())`. A builder child looks like it should
|
|
317
|
+
* work — and its bare `_type` would — but every modifier on it (`.view()`,
|
|
318
|
+
* `.default()`, quantize options) would be silently dropped, since modifiers
|
|
319
|
+
* describe the FIELD, not the elements. Fail loudly instead.
|
|
320
|
+
*/
|
|
316
321
|
function resolveChild(child: ChildType): DefinitionType {
|
|
317
322
|
if (isBuilder(child)) {
|
|
318
|
-
|
|
319
|
-
|
|
323
|
+
const inner = child['_type']; // private; element access bypasses the check
|
|
324
|
+
const hint = (typeof inner === "string")
|
|
325
|
+
? `use the type name instead: t.array("${inner}")`
|
|
326
|
+
: `collections accept a Schema class or a primitive type name ("string", "number", …)`;
|
|
327
|
+
throw new Error(`t.array/map/set/collection(): a t.* builder is not a valid element type — ${hint}.`);
|
|
320
328
|
}
|
|
321
329
|
return child as DefinitionType;
|
|
322
330
|
}
|
|
@@ -326,28 +334,24 @@ function resolveChild(child: ChildType): DefinitionType {
|
|
|
326
334
|
// ---------------------------------------------------------------------------
|
|
327
335
|
|
|
328
336
|
// Overloaded factories for collections. Implementation lives in a single function;
|
|
329
|
-
// overloads narrow the return type for Schema/primitive
|
|
337
|
+
// overloads narrow the return type for Schema/primitive children.
|
|
330
338
|
// All collection factories tag `HasDefault = true` because schema() auto-
|
|
331
339
|
// instantiates an empty collection when no explicit default is given.
|
|
332
340
|
interface ArrayFactory {
|
|
333
341
|
<C extends Constructor<Schema>>(child: C): FieldBuilder<ArraySchema<InstanceType<C>>, true, false>;
|
|
334
342
|
<P extends RawPrimitiveType>(child: P): FieldBuilder<ArraySchema<InferValueType<P>>, true, false>;
|
|
335
|
-
<V>(child: FieldBuilder<V>): FieldBuilder<ArraySchema<V>, true, false>;
|
|
336
343
|
}
|
|
337
344
|
interface MapFactory {
|
|
338
345
|
<C extends Constructor<Schema>>(child: C): FieldBuilder<MapSchema<InstanceType<C>>, true, false>;
|
|
339
346
|
<P extends RawPrimitiveType>(child: P): FieldBuilder<MapSchema<InferValueType<P>>, true, false>;
|
|
340
|
-
<V>(child: FieldBuilder<V>): FieldBuilder<MapSchema<V>, true, false>;
|
|
341
347
|
}
|
|
342
348
|
interface SetFactory {
|
|
343
349
|
<C extends Constructor<Schema>>(child: C): FieldBuilder<SetSchema<InstanceType<C>>, true, false>;
|
|
344
350
|
<P extends RawPrimitiveType>(child: P): FieldBuilder<SetSchema<InferValueType<P>>, true, false>;
|
|
345
|
-
<V>(child: FieldBuilder<V>): FieldBuilder<SetSchema<V>, true, false>;
|
|
346
351
|
}
|
|
347
352
|
interface CollectionFactory {
|
|
348
353
|
<C extends Constructor<Schema>>(child: C): FieldBuilder<CollectionSchema<InstanceType<C>>, true, false>;
|
|
349
354
|
<P extends RawPrimitiveType>(child: P): FieldBuilder<CollectionSchema<InferValueType<P>>, true, false>;
|
|
350
|
-
<V>(child: FieldBuilder<V>): FieldBuilder<CollectionSchema<V>, true, false>;
|
|
351
355
|
}
|
|
352
356
|
// t.stream(Entity) — priority-batched collection of Schema instances.
|
|
353
357
|
// Element type is restricted to Schema subclasses (no primitives) because
|
|
@@ -178,7 +178,7 @@ export class ArraySchema<V = any> implements Array<V>, Collection<number, V>, IR
|
|
|
178
178
|
const proxy = new Proxy(this, ARRAY_PROXY_HANDLER);
|
|
179
179
|
|
|
180
180
|
Object.defineProperty(this, $changes, {
|
|
181
|
-
value: new ChangeTree(proxy),
|
|
181
|
+
value: new ChangeTree(proxy, this),
|
|
182
182
|
enumerable: false,
|
|
183
183
|
writable: true,
|
|
184
184
|
});
|
|
@@ -486,7 +486,22 @@ export class ArraySchema<V = any> implements Array<V>, Collection<number, V>, IR
|
|
|
486
486
|
// @ts-ignore
|
|
487
487
|
reverse(): ArraySchema<V> {
|
|
488
488
|
const self = this[$proxyTarget];
|
|
489
|
-
self[$changes]
|
|
489
|
+
const changeTree = self[$changes];
|
|
490
|
+
|
|
491
|
+
if (changeTree.has() || self.deletedIndexes.length > 0) {
|
|
492
|
+
//
|
|
493
|
+
// Ops recorded earlier this tick address the staged (pre-reverse)
|
|
494
|
+
// layout, and the encoder only resolves their values at encode
|
|
495
|
+
// time — a pure REVERSE would move that layout under them.
|
|
496
|
+
// Degrade to a full re-state: CLEAR + re-ADD in reversed order.
|
|
497
|
+
//
|
|
498
|
+
const reversed = self.items.slice().reverse();
|
|
499
|
+
this.clear(); // also drops staged holes (discard → $onEncodeEnd)
|
|
500
|
+
this.push(...reversed);
|
|
501
|
+
return this;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
changeTree.operation(OPERATION.REVERSE);
|
|
490
505
|
self.items.reverse();
|
|
491
506
|
self.tmpItems.reverse();
|
|
492
507
|
self.$reindexChildren(0);
|
|
@@ -602,13 +617,13 @@ export class ArraySchema<V = any> implements Array<V>, Collection<number, V>, IR
|
|
|
602
617
|
|
|
603
618
|
// insert operations
|
|
604
619
|
if (insertCount > 0) {
|
|
605
|
-
|
|
606
|
-
console.error("Inserting more elements than deleting during ArraySchema#splice()");
|
|
607
|
-
throw new Error("ArraySchema#splice(): insertCount must be equal or lower than deleteCount.");
|
|
608
|
-
}
|
|
620
|
+
const base = indexes[start] ?? itemsLength;
|
|
609
621
|
|
|
610
|
-
|
|
611
|
-
|
|
622
|
+
// the first `reuse` items take over the wire slots just deleted
|
|
623
|
+
const reuse = Math.min(insertCount, deleteCount);
|
|
624
|
+
|
|
625
|
+
for (let i = 0; i < reuse; i++) {
|
|
626
|
+
const addIndex = base + i;
|
|
612
627
|
|
|
613
628
|
changeTree.indexedOperation(
|
|
614
629
|
addIndex,
|
|
@@ -617,9 +632,36 @@ export class ArraySchema<V = any> implements Array<V>, Collection<number, V>, IR
|
|
|
617
632
|
: OPERATION.ADD
|
|
618
633
|
);
|
|
619
634
|
|
|
635
|
+
// the slot is live again — the staged snapshot must carry the
|
|
636
|
+
// new value, or `$getByIndex` falls back to `items[addIndex]`
|
|
637
|
+
// and resolves an unrelated element once tmp/items diverge.
|
|
638
|
+
tmpItems[addIndex] = insertItems[i];
|
|
639
|
+
deletedIndexes[addIndex] = false;
|
|
640
|
+
|
|
620
641
|
// set value's parent/root — use `this` (Proxy) as parent.
|
|
621
642
|
insertItems[i][$changes]?.setParent(this, changeTree.root, addIndex);
|
|
622
643
|
}
|
|
644
|
+
|
|
645
|
+
// ...the rest have no slot to take: widen the wire layout, same as
|
|
646
|
+
// unshift() but at `at` instead of 0.
|
|
647
|
+
const extra = insertCount - reuse;
|
|
648
|
+
if (extra > 0) {
|
|
649
|
+
const at = base + reuse;
|
|
650
|
+
|
|
651
|
+
changeTree.insertAt(at, extra);
|
|
652
|
+
|
|
653
|
+
for (let i = 0; i < extra; i++) {
|
|
654
|
+
insertItems[reuse + i][$changes]?.setParent(this, changeTree.root, at + i);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// keep staged-delete flags aligned with the inserted tmp slots
|
|
658
|
+
if (deletedIndexes.length > 0) {
|
|
659
|
+
deletedIndexes.splice(at, 0, ...new Array(extra).fill(false));
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
tmpItems.splice(at, 0, ...insertItems.slice(reuse));
|
|
663
|
+
self.$reindexChildren(at + extra); // survivors only — the loop above placed the new items
|
|
664
|
+
}
|
|
623
665
|
}
|
|
624
666
|
|
|
625
667
|
changeTree.root?.enqueueChangeTree(changeTree);
|
|
@@ -969,15 +1011,16 @@ export class ArraySchema<V = any> implements Array<V>, Collection<number, V>, IR
|
|
|
969
1011
|
}
|
|
970
1012
|
|
|
971
1013
|
protected [$onEncodeEnd]() {
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
1014
|
+
// No unwrap: ChangeTree's gated sites are the only callers and they
|
|
1015
|
+
// invoke on `refTarget` (the raw target) already.
|
|
1016
|
+
const staged = this.tmpItems;
|
|
1017
|
+
this.tmpItems = this.items.slice();
|
|
975
1018
|
|
|
976
|
-
if (
|
|
1019
|
+
if (this.deletedIndexes.length > 0) {
|
|
977
1020
|
// compaction just closed the staged holes — everything above the
|
|
978
1021
|
// lowest one slid down a slot
|
|
979
|
-
|
|
980
|
-
|
|
1022
|
+
this.$reindexChildren(0, staged);
|
|
1023
|
+
this.deletedIndexes.length = 0;
|
|
981
1024
|
}
|
|
982
1025
|
}
|
|
983
1026
|
|
package/src/types/symbols.ts
CHANGED
|
@@ -131,6 +131,9 @@ export const $viewFieldIndexes = "~__viewFieldIndexes";
|
|
|
131
131
|
export const $fieldIndexesByViewTag = "$__fieldIndexesByViewTag";
|
|
132
132
|
export const $unreliableFieldIndexes = "~__unreliableFieldIndexes";
|
|
133
133
|
export const $patchOnlyFieldIndexes = "~__patchOnlyFieldIndexes";
|
|
134
|
+
// @patchOnly ∪ @deprecated() — indexes the full-sync walk must not read (the
|
|
135
|
+
// deprecated accessor may throw). Maintained at decoration time.
|
|
136
|
+
export const $fullSyncSkipIndexes = "~__fullSyncSkipIndexes";
|
|
134
137
|
export const $fullStateOnlyFieldIndexes = "~__fullStateOnlyFieldIndexes";
|
|
135
138
|
export const $streamFieldIndexes = "~__streamFieldIndexes";
|
|
136
139
|
export const $streamPriorities = "~__streamPriorities";
|