@colyseus/schema 4.0.20 → 5.0.1

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.
Files changed (96) hide show
  1. package/README.md +2 -0
  2. package/build/Metadata.d.ts +56 -2
  3. package/build/Reflection.d.ts +28 -34
  4. package/build/Schema.d.ts +70 -9
  5. package/build/annotations.d.ts +64 -17
  6. package/build/codegen/cli.cjs +84 -67
  7. package/build/codegen/cli.cjs.map +1 -1
  8. package/build/decoder/DecodeOperation.d.ts +48 -5
  9. package/build/decoder/Decoder.d.ts +2 -2
  10. package/build/decoder/strategy/Callbacks.d.ts +1 -1
  11. package/build/encoder/ChangeRecorder.d.ts +107 -0
  12. package/build/encoder/ChangeTree.d.ts +218 -69
  13. package/build/encoder/EncodeDescriptor.d.ts +63 -0
  14. package/build/encoder/EncodeOperation.d.ts +25 -2
  15. package/build/encoder/Encoder.d.ts +59 -3
  16. package/build/encoder/MapJournal.d.ts +62 -0
  17. package/build/encoder/RefIdAllocator.d.ts +35 -0
  18. package/build/encoder/Root.d.ts +94 -13
  19. package/build/encoder/StateView.d.ts +116 -8
  20. package/build/encoder/changeTree/inheritedFlags.d.ts +34 -0
  21. package/build/encoder/changeTree/liveIteration.d.ts +3 -0
  22. package/build/encoder/changeTree/parentChain.d.ts +24 -0
  23. package/build/encoder/changeTree/treeAttachment.d.ts +13 -0
  24. package/build/encoder/streaming.d.ts +73 -0
  25. package/build/encoder/subscriptions.d.ts +25 -0
  26. package/build/index.cjs +5258 -1549
  27. package/build/index.cjs.map +1 -1
  28. package/build/index.d.ts +7 -3
  29. package/build/index.js +5258 -1549
  30. package/build/index.mjs +5249 -1549
  31. package/build/index.mjs.map +1 -1
  32. package/build/input/InputDecoder.d.ts +32 -0
  33. package/build/input/InputEncoder.d.ts +117 -0
  34. package/build/input/index.cjs +7453 -0
  35. package/build/input/index.cjs.map +1 -0
  36. package/build/input/index.d.ts +3 -0
  37. package/build/input/index.mjs +7450 -0
  38. package/build/input/index.mjs.map +1 -0
  39. package/build/types/HelperTypes.d.ts +67 -9
  40. package/build/types/TypeContext.d.ts +9 -0
  41. package/build/types/builder.d.ts +192 -0
  42. package/build/types/custom/ArraySchema.d.ts +25 -4
  43. package/build/types/custom/CollectionSchema.d.ts +30 -2
  44. package/build/types/custom/MapSchema.d.ts +52 -3
  45. package/build/types/custom/SetSchema.d.ts +32 -2
  46. package/build/types/custom/StreamSchema.d.ts +114 -0
  47. package/build/types/symbols.d.ts +48 -5
  48. package/package.json +9 -3
  49. package/src/Metadata.ts +259 -31
  50. package/src/Reflection.ts +15 -13
  51. package/src/Schema.ts +176 -134
  52. package/src/annotations.ts +365 -252
  53. package/src/bench_bloat.ts +173 -0
  54. package/src/bench_decode.ts +221 -0
  55. package/src/bench_decode_mem.ts +165 -0
  56. package/src/bench_encode.ts +108 -0
  57. package/src/bench_init.ts +150 -0
  58. package/src/bench_static.ts +109 -0
  59. package/src/bench_stream.ts +295 -0
  60. package/src/bench_view_cmp.ts +142 -0
  61. package/src/codegen/languages/csharp.ts +0 -24
  62. package/src/codegen/parser.ts +83 -61
  63. package/src/decoder/DecodeOperation.ts +168 -63
  64. package/src/decoder/Decoder.ts +20 -10
  65. package/src/decoder/ReferenceTracker.ts +4 -0
  66. package/src/decoder/strategy/Callbacks.ts +30 -26
  67. package/src/decoder/strategy/getDecoderStateCallbacks.ts +16 -13
  68. package/src/encoder/ChangeRecorder.ts +276 -0
  69. package/src/encoder/ChangeTree.ts +674 -519
  70. package/src/encoder/EncodeDescriptor.ts +213 -0
  71. package/src/encoder/EncodeOperation.ts +107 -65
  72. package/src/encoder/Encoder.ts +630 -119
  73. package/src/encoder/MapJournal.ts +124 -0
  74. package/src/encoder/RefIdAllocator.ts +68 -0
  75. package/src/encoder/Root.ts +247 -120
  76. package/src/encoder/StateView.ts +592 -121
  77. package/src/encoder/changeTree/inheritedFlags.ts +217 -0
  78. package/src/encoder/changeTree/liveIteration.ts +74 -0
  79. package/src/encoder/changeTree/parentChain.ts +131 -0
  80. package/src/encoder/changeTree/treeAttachment.ts +171 -0
  81. package/src/encoder/streaming.ts +232 -0
  82. package/src/encoder/subscriptions.ts +71 -0
  83. package/src/index.ts +15 -3
  84. package/src/input/InputDecoder.ts +57 -0
  85. package/src/input/InputEncoder.ts +303 -0
  86. package/src/input/index.ts +3 -0
  87. package/src/types/HelperTypes.ts +121 -24
  88. package/src/types/TypeContext.ts +14 -2
  89. package/src/types/builder.ts +331 -0
  90. package/src/types/custom/ArraySchema.ts +210 -197
  91. package/src/types/custom/CollectionSchema.ts +115 -35
  92. package/src/types/custom/MapSchema.ts +162 -58
  93. package/src/types/custom/SetSchema.ts +128 -39
  94. package/src/types/custom/StreamSchema.ts +310 -0
  95. package/src/types/symbols.ts +93 -6
  96. package/src/utils.ts +4 -6
@@ -0,0 +1,114 @@
1
+ import { $changes, $childType, $decoder, $deleteByIndex, $encoder, $filter, $getByIndex, $onEncodeEnd, $refId } from "../symbols.js";
2
+ import { ChangeTree, type IRef } from "../../encoder/ChangeTree.js";
3
+ import { type StreamableState } from "../../encoder/streaming.js";
4
+ import type { StateView } from "../../encoder/StateView.js";
5
+ import type { Schema } from "../../Schema.js";
6
+ /**
7
+ * `t.stream(Entity)` — priority-batched collection of Schema instances.
8
+ *
9
+ * Designed for ECS-style use cases where many entities spawn/despawn each
10
+ * tick and the full set won't fit in one encode budget. Adds are queued
11
+ * per-client and drained in priority order (callback on StateView) up to
12
+ * `maxPerTick` per encode pass. Field mutations on already-sent elements
13
+ * propagate through the normal reliable channel without consuming the
14
+ * per-tick budget. Chain `.static()` on the field builder to suppress
15
+ * post-add mutation tracking entirely.
16
+ */
17
+ export declare class StreamSchema<V = any> implements IRef {
18
+ [$changes]: ChangeTree;
19
+ [$refId]?: number;
20
+ protected [$childType]: string | typeof Schema;
21
+ /**
22
+ * Wire-keyed storage: `position → element`. Position is a monotonic
23
+ * counter assigned by `add()` — stable identity even when elements
24
+ * are removed, so pending/sent view state can keep using the same
25
+ * keys across ticks. Map (not Array) so `$items.keys()` / `.values()`
26
+ * skip removed positions without a sparse-slot check.
27
+ */
28
+ protected $items: Map<number, V>;
29
+ /** Monotonic position counter. Incremented on every `add()`. */
30
+ protected $nextPosition: number;
31
+ /** Reverse lookup for O(1) `remove(el)`. */
32
+ protected _itemIndex: Map<V, number>;
33
+ /**
34
+ * Streamable state — holds per-view and broadcast bookkeeping. Lazily
35
+ * allocated when the stream is attached to a Root (or when the user
36
+ * touches `maxPerTick`). `undefined` on detached streams so
37
+ * construction is cheap.
38
+ */
39
+ _stream?: StreamableState;
40
+ /** Max element ADDs emitted per encode tick (per view, or broadcast). */
41
+ get maxPerTick(): number;
42
+ set maxPerTick(n: number);
43
+ /**
44
+ * Per-view priority callback. Initialized from the schema declaration
45
+ * (`.priority(fn)` or `@type({ stream, priority })`); assigning here
46
+ * overrides the class-level default for this instance. Only fires
47
+ * during `encodeView` — broadcast mode drains FIFO.
48
+ */
49
+ get priority(): ((view: any, element: V) => number) | undefined;
50
+ set priority(fn: ((view: any, element: V) => number) | undefined);
51
+ /**
52
+ * Brand used by Root / StateView to detect stream trees without
53
+ * importing this class (avoids circular deps). The `isStreamCollection`
54
+ * ChangeTree flag (set via `inheritedFlags`) is the preferred runtime
55
+ * check — this brand is kept for back-compat.
56
+ */
57
+ static readonly $isStream: true;
58
+ static [$encoder]: import("../../encoder/EncodeOperation.js").EncodeOperation<any>;
59
+ static [$decoder]: import("../../decoder/DecodeOperation.js").DecodeOperation<any>;
60
+ /** Integer tag read by `decodeKeyValueOperation` — see `CollectionKind`. */
61
+ static readonly COLLECTION_KIND: 5;
62
+ /**
63
+ * Element-level visibility. Identical to SetSchema's filter: stream
64
+ * elements are always per-view, the filter just defers to the view's
65
+ * per-tree visibility bitmap.
66
+ */
67
+ static [$filter](ref: StreamSchema, index: number, view: StateView): boolean;
68
+ static is(type: any): boolean;
69
+ constructor();
70
+ /**
71
+ * Decoder-side factory. Skips the tracking `ChangeTree` allocation;
72
+ * `Object.create` also bypasses the class-field initializers, so we
73
+ * replicate the minimum slot init here. Must stay in sync with the
74
+ * class-field declarations above.
75
+ */
76
+ static initializeForDecoder<V = any>(): StreamSchema<V>;
77
+ /**
78
+ * Append an element to the stream. Returns the assigned position,
79
+ * or -1 if the element was already in the stream.
80
+ */
81
+ add(value: V): number;
82
+ /**
83
+ * Remove an element by reference. If the element was pending (never sent
84
+ * to a view), the pending entry is dropped silently. If already sent,
85
+ * a DELETE op is forced on next `encodeView` for that view.
86
+ */
87
+ remove(value: V): boolean;
88
+ has(value: V): boolean;
89
+ /** Remove every element; queue DELETE wire ops for already-sent items. */
90
+ clear(): void;
91
+ forEach(callback: (value: V, index: number, collection: StreamSchema<V>) => void): void;
92
+ values(): IterableIterator<V>;
93
+ /**
94
+ * Iterate `[position, value]` pairs in insertion order. Used by
95
+ * `setParent` recursion when the stream is reassigned to a new parent.
96
+ */
97
+ entries(): IterableIterator<[number, V]>;
98
+ [Symbol.iterator](): IterableIterator<V>;
99
+ /** Live element count. */
100
+ get size(): number;
101
+ /** Alias for `size`. */
102
+ get length(): number;
103
+ protected setIndex(_index: number, _key: number): void;
104
+ protected getIndex(index: number): number;
105
+ [$getByIndex](index: number): V;
106
+ [$deleteByIndex](index: number): void;
107
+ protected [$onEncodeEnd](): void;
108
+ toArray(): V[];
109
+ toJSON(): any[];
110
+ clone(isDecoding?: boolean): StreamSchema<V>;
111
+ _dropView(viewId: number): void;
112
+ /** Called by Root.remove when the stream's refcount hits zero. */
113
+ _unregister(): void;
114
+ }
@@ -1,4 +1,4 @@
1
- export declare const $refId = "~refId";
1
+ export declare const $refId: unique symbol;
2
2
  export declare const $track = "~track";
3
3
  export declare const $encoder = "~encoder";
4
4
  export declare const $decoder = "~decoder";
@@ -6,14 +6,24 @@ export declare const $filter = "~filter";
6
6
  export declare const $getByIndex = "~getByIndex";
7
7
  export declare const $deleteByIndex = "~deleteByIndex";
8
8
  /**
9
- * Used to hold ChangeTree instances whitin the structures
9
+ * Used to hold ChangeTree instances whitin the structures.
10
+ *
11
+ * Real JS Symbol — see the `$values` comment for rationale.
10
12
  */
11
- export declare const $changes = "~changes";
13
+ export declare const $changes: unique symbol;
12
14
  /**
13
15
  * Used to keep track of the type of the child elements of a collection
14
- * (MapSchema, ArraySchema, etc.)
16
+ * (MapSchema, ArraySchema, etc.). Real Symbol — same rationale as $values.
15
17
  */
16
- export declare const $childType = "~childType";
18
+ export declare const $childType: unique symbol;
19
+ /**
20
+ * Self-reference an instance sets on `this` so its own methods can recover
21
+ * the underlying object even when `this` is a Proxy wrapper. Used by
22
+ * ArraySchema (whose public API is a Proxy) to grab the underlying instance
23
+ * once at the top of hot methods and then access fields directly without
24
+ * paying the Proxy.get cost on every read.
25
+ */
26
+ export declare const $proxyTarget: unique symbol;
17
27
  /**
18
28
  * Optional "discard" method for custom types (ArraySchema)
19
29
  * (Discards changes for next serialization)
@@ -23,11 +33,44 @@ export declare const $onEncodeEnd = "~onEncodeEnd";
23
33
  * When decoding, this method is called after the instance is fully decoded
24
34
  */
25
35
  export declare const $onDecodeEnd = "~onDecodeEnd";
36
+ /**
37
+ * Per-instance dense array holding field values by index.
38
+ * Replaces per-field _fieldName shadow properties.
39
+ *
40
+ * Real JS Symbol (not "~"-prefixed string) so plain assignment is safe —
41
+ * symbols are non-enumerable to Object.keys / JSON.stringify / for-in,
42
+ * which means we can drop Object.defineProperty(...{ enumerable: false })
43
+ * and avoid the slow-path / dictionary-mode hazards that come with it.
44
+ */
45
+ export declare const $values: unique symbol;
46
+ /**
47
+ * Brand for FieldBuilder instances so schema() can detect them.
48
+ */
49
+ export declare const $builder = "~builder";
26
50
  /**
27
51
  * Metadata
28
52
  */
29
53
  export declare const $descriptors = "~descriptors";
54
+ /**
55
+ * Per-class bitmask: bit i set iff field i carries a @view tag.
56
+ * Lazily computed from $viewFieldIndexes on first encode pass.
57
+ * Skips the per-field metadata[i].tag property chase in the hot encode loop.
58
+ */
59
+ export declare const $filterBitmask = "~__filterBitmask";
60
+ /**
61
+ * Cached per-class encode descriptor: bundles encoder fn, filter fn,
62
+ * metadata, isSchema flag, and filterBitmask into one object stashed on
63
+ * the constructor. Replaces 5 separate per-tree property chases /
64
+ * function calls in the encode loop with a single property load.
65
+ */
66
+ export declare const $encodeDescriptor = "~__encodeDescriptor";
67
+ export declare const $encoders = "~encoders";
30
68
  export declare const $numFields = "~__numFields";
31
69
  export declare const $refTypeFieldIndexes = "~__refTypeFieldIndexes";
32
70
  export declare const $viewFieldIndexes = "~__viewFieldIndexes";
33
71
  export declare const $fieldIndexesByViewTag = "$__fieldIndexesByViewTag";
72
+ export declare const $unreliableFieldIndexes = "~__unreliableFieldIndexes";
73
+ export declare const $transientFieldIndexes = "~__transientFieldIndexes";
74
+ export declare const $staticFieldIndexes = "~__staticFieldIndexes";
75
+ export declare const $streamFieldIndexes = "~__streamFieldIndexes";
76
+ export declare const $streamPriorities = "~__streamPriorities";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colyseus/schema",
3
- "version": "4.0.20",
3
+ "version": "5.0.1",
4
4
  "description": "Binary state serializer with delta encoding for games",
5
5
  "type": "module",
6
6
  "bin": {
@@ -10,8 +10,8 @@
10
10
  "scripts": {
11
11
  "build": "tsc -p tsconfig.build.json && rollup -c rollup.config.mjs",
12
12
  "watch": "tsc -p tsconfig.build.json -w",
13
+ "typecheck": "tsc -p tsconfig.build.json --noEmit",
13
14
  "test": "tsx --tsconfig tsconfig.test.json ./node_modules/.bin/mocha test/*.test.ts test/**/*.test.ts",
14
- "typecheck": "tsc -p tsconfig.test.json",
15
15
  "coverage": "c8 npm run test",
16
16
  "generate-test-1": "bin/schema-codegen test-external/PrimitiveTypes.ts --namespace SchemaTest.PrimitiveTypes --output ../colyseus-unity-sdk/Assets/Colyseus/Tests/Editor/ColyseusTests/Schema/PrimitiveTypes",
17
17
  "generate-test-2": "bin/schema-codegen test-external/ChildSchemaTypes.ts --namespace SchemaTest.ChildSchemaTypes --output ../colyseus-unity-sdk/Assets/Colyseus/Tests/Editor/ColyseusTests/Schema/ChildSchemaTypes",
@@ -42,6 +42,12 @@
42
42
  "import": "./build/index.mjs",
43
43
  "require": "./build/index.cjs",
44
44
  "types": "./build/index.d.ts"
45
+ },
46
+ "./input": {
47
+ "browser": "./build/input/index.mjs",
48
+ "import": "./build/input/index.mjs",
49
+ "require": "./build/input/index.cjs",
50
+ "types": "./build/input/index.d.ts"
45
51
  }
46
52
  },
47
53
  "repository": {
@@ -82,7 +88,7 @@
82
88
  "typescript": "^5.9.3"
83
89
  },
84
90
  "peerDependencies": {
85
- "typescript": "^5.0.0 || ^6.0.0"
91
+ "typescript": "^5.9.3"
86
92
  },
87
93
  "c8": {
88
94
  "include": [
package/src/Metadata.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  import { DefinitionType, getPropertyDescriptor } from "./annotations.js";
2
2
  import { Schema } from "./Schema.js";
3
- import { getType, registeredTypes } from "./types/registry.js";
4
- import { $decoder, $descriptors, $encoder, $fieldIndexesByViewTag, $numFields, $refTypeFieldIndexes, $track, $viewFieldIndexes } from "./types/symbols.js";
3
+ import { getType, registeredTypes, TypeDefinition } from "./types/registry.js";
4
+ import { $decoder, $descriptors, $encoder, $encoders, $fieldIndexesByViewTag, $numFields, $refTypeFieldIndexes, $staticFieldIndexes, $streamFieldIndexes, $streamPriorities, $track, $transientFieldIndexes, $unreliableFieldIndexes, $viewFieldIndexes } from "./types/symbols.js";
5
+ import { ARRAY_STREAM_NOT_SUPPORTED } from "./encoder/streaming.js";
6
+ import { encode } from "./encoding/encode.js";
5
7
  import { TypeContext } from "./types/TypeContext.js";
6
8
 
7
9
  export type MetadataField = {
@@ -10,7 +12,12 @@ export type MetadataField = {
10
12
  index: number,
11
13
  tag?: number,
12
14
  unreliable?: boolean,
15
+ transient?: boolean,
13
16
  deprecated?: boolean,
17
+ owned?: boolean,
18
+ static?: boolean,
19
+ stream?: boolean,
20
+ optional?: boolean,
14
21
  };
15
22
 
16
23
  export type Metadata =
@@ -18,10 +25,31 @@ export type Metadata =
18
25
  { [$viewFieldIndexes]: number[]; } & // all field indexes with "view" tag
19
26
  { [$fieldIndexesByViewTag]: {[tag: number]: number[]}; } & // field indexes by "view" tag
20
27
  { [$refTypeFieldIndexes]: number[]; } & // all field indexes containing Ref types (Schema, ArraySchema, MapSchema, etc)
28
+ { [$unreliableFieldIndexes]: number[]; } & // all field indexes tagged with @unreliable
29
+ { [$transientFieldIndexes]: number[]; } & // all field indexes tagged with @transient (not persisted to snapshots)
30
+ { [$staticFieldIndexes]: number[]; } & // all field indexes tagged with @static (not tracked after assignment)
31
+ { [$streamFieldIndexes]: number[]; } & // all field indexes holding a t.stream(...) collection
32
+ { [$streamPriorities]: { [field: number]: (view: any, element: any) => number }; } & // per-stream-field priority callback declared at schema definition time
33
+ { [$encoders]: Array<(bytes: Uint8Array, value: any, it: any) => void>; } & // pre-computed encoder fn per primitive field
21
34
  { [field: number]: MetadataField; } & // index => field name
22
35
  { [field: string]: number; } & // field name => field metadata
23
36
  { [$descriptors]: { [field: string]: PropertyDescriptor } } // property descriptors
24
37
 
38
+ /**
39
+ * Given a normalized field type (`"number"`, `{ map: Foo }`, `Player`,
40
+ * etc.), split into the collection-type descriptor (`{ constructor:
41
+ * MapSchema, ... }`) if applicable and the inner child type. Shared by
42
+ * `@type()` decoration and `Metadata.setFields` — both need to build a
43
+ * property accessor that knows whether the slot holds a collection.
44
+ */
45
+ export function resolveFieldType(type: any): { complexTypeKlass: TypeDefinition | false, childType: any } {
46
+ const complexTypeKlass = typeof (Object.keys(type)[0]) === "string" && getType(Object.keys(type)[0]);
47
+ return {
48
+ complexTypeKlass,
49
+ childType: complexTypeKlass ? Object.values(type)[0] : type,
50
+ };
51
+ }
52
+
25
53
  export function getNormalizedType(type: any): DefinitionType {
26
54
  if (Array.isArray(type)) {
27
55
  return { array: getNormalizedType(type[0]) };
@@ -91,16 +119,11 @@ export const Metadata = {
91
119
  });
92
120
 
93
121
  if (descriptor) {
94
- // for encoder
122
+ // Accessor descriptor for the public field name.
123
+ // Installed on the prototype at class-definition time.
95
124
  metadata[$descriptors][name] = descriptor;
96
- metadata[$descriptors][`_${name}`] = {
97
- value: undefined,
98
- writable: true,
99
- enumerable: false,
100
- configurable: true,
101
- };
102
125
  } else {
103
- // for decoder
126
+ // For decoder: simple writable slot, also on prototype.
104
127
  metadata[$descriptors][name] = {
105
128
  value: undefined,
106
129
  writable: true,
@@ -134,6 +157,38 @@ export const Metadata = {
134
157
  }
135
158
  metadata[$refTypeFieldIndexes].push(index);
136
159
  }
160
+
161
+ // `{ stream: ... }` collections are always view-scoped (priority-
162
+ // batched emit). Auto-flag here so both `@type({stream: ...})` and
163
+ // the `t.stream(...)` builder route into the same filter / encoder
164
+ // dispatch without the caller needing an extra setStream() call.
165
+ const t = metadata[index].type;
166
+ if (t && typeof t === "object" && (t as any)["stream"] !== undefined) {
167
+ // Reject the combined shorthand `@type({ array: X, stream:
168
+ // true })` at decoration time — same diagnostic as the
169
+ // builder chainable throws for `t.array(X).stream()`.
170
+ if ((t as any).array !== undefined) {
171
+ throw new Error(ARRAY_STREAM_NOT_SUPPORTED);
172
+ }
173
+ metadata[index].stream = true;
174
+ if (!metadata[$streamFieldIndexes]) {
175
+ Object.defineProperty(metadata, $streamFieldIndexes, {
176
+ value: [],
177
+ enumerable: false,
178
+ configurable: true,
179
+ writable: true,
180
+ });
181
+ }
182
+ if (!metadata[$streamFieldIndexes].includes(index)) {
183
+ metadata[$streamFieldIndexes].push(index);
184
+ }
185
+ // Pick up the declaration-scope priority callback if present in
186
+ // the `@type({ stream: X, priority: fn })` shorthand.
187
+ const priorityFn = (type as any)?.priority;
188
+ if (typeof priorityFn === "function") {
189
+ Metadata.setStreamPriority(metadata as any, name, priorityFn);
190
+ }
191
+ }
137
192
  },
138
193
 
139
194
  setTag(metadata: Metadata, fieldName: string, tag: number) {
@@ -168,6 +223,112 @@ export const Metadata = {
168
223
  metadata[$fieldIndexesByViewTag][tag].push(index);
169
224
  },
170
225
 
226
+ setUnreliable(metadata: Metadata, fieldName: string) {
227
+ const index = metadata[fieldName];
228
+ const fieldType = metadata[index].type;
229
+ // `@unreliable` is only valid on primitive fields. Ref-type fields
230
+ // (Schema sub-classes, MapSchema, ArraySchema, SetSchema,
231
+ // CollectionSchema) carry refIds whose ADD/DELETE must arrive
232
+ // on the reliable channel — otherwise a dropped unreliable packet
233
+ // would leave the decoder unable to interpret subsequent packets
234
+ // referencing the orphan refId. Primitive types are encoded as
235
+ // strings ("number", "string", "int32", ...); anything else is a
236
+ // ref. Reject at decoration time so the bug surfaces in dev, not
237
+ // under packet loss in prod.
238
+ if (typeof fieldType !== "string") {
239
+ throw new Error(
240
+ `@unreliable cannot be applied to ref-type field "${fieldName}". ` +
241
+ `For ref-type fields, mark each primitive sub-field with @unreliable instead. ` +
242
+ `See README "Limitations and best practices".`
243
+ );
244
+ }
245
+ metadata[index].unreliable = true;
246
+
247
+ if (!metadata[$unreliableFieldIndexes]) {
248
+ Object.defineProperty(metadata, $unreliableFieldIndexes, {
249
+ value: [],
250
+ enumerable: false,
251
+ configurable: true,
252
+ writable: true,
253
+ });
254
+ }
255
+ metadata[$unreliableFieldIndexes].push(index);
256
+ },
257
+
258
+ setTransient(metadata: Metadata, fieldName: string) {
259
+ const index = metadata[fieldName];
260
+ metadata[index].transient = true;
261
+
262
+ if (!metadata[$transientFieldIndexes]) {
263
+ Object.defineProperty(metadata, $transientFieldIndexes, {
264
+ value: [],
265
+ enumerable: false,
266
+ configurable: true,
267
+ writable: true,
268
+ });
269
+ }
270
+ metadata[$transientFieldIndexes].push(index);
271
+ },
272
+
273
+ setStatic(metadata: Metadata, fieldName: string) {
274
+ const index = metadata[fieldName];
275
+ metadata[index].static = true;
276
+
277
+ if (!metadata[$staticFieldIndexes]) {
278
+ Object.defineProperty(metadata, $staticFieldIndexes, {
279
+ value: [],
280
+ enumerable: false,
281
+ configurable: true,
282
+ writable: true,
283
+ });
284
+ }
285
+ metadata[$staticFieldIndexes].push(index);
286
+ },
287
+
288
+ setStream(metadata: Metadata, fieldName: string) {
289
+ const index = metadata[fieldName];
290
+ metadata[index].stream = true;
291
+
292
+ if (!metadata[$streamFieldIndexes]) {
293
+ Object.defineProperty(metadata, $streamFieldIndexes, {
294
+ value: [],
295
+ enumerable: false,
296
+ configurable: true,
297
+ writable: true,
298
+ });
299
+ }
300
+ metadata[$streamFieldIndexes].push(index);
301
+ },
302
+
303
+ /**
304
+ * Attach a declaration-scope priority callback to a stream field.
305
+ * Called at schema definition time (via `t.stream(X).priority(fn)` or
306
+ * `@type({ stream: X, priority: fn })`), looked up at stream-attach
307
+ * time to seed the instance's `_stream.priority` slot. The callback
308
+ * signature is `(view: StateView, element: V) => number` — only fires
309
+ * during `encodeView`, broadcast mode emits FIFO regardless.
310
+ */
311
+ setStreamPriority(
312
+ metadata: Metadata,
313
+ fieldName: string,
314
+ fn: (view: any, element: any) => number,
315
+ ) {
316
+ const index = metadata[fieldName];
317
+ if (!metadata[$streamPriorities]) {
318
+ Object.defineProperty(metadata, $streamPriorities, {
319
+ value: {},
320
+ enumerable: false,
321
+ configurable: true,
322
+ writable: true,
323
+ });
324
+ }
325
+ metadata[$streamPriorities][index] = fn;
326
+ },
327
+
328
+ getStreamPriority(metadata: Metadata | undefined, index: number) {
329
+ return metadata?.[$streamPriorities]?.[index];
330
+ },
331
+
171
332
  setFields<T extends { new (...args: any[]): InstanceType<T> } = any>(target: T, fields: { [field in keyof InstanceType<T>]?: DefinitionType }) {
172
333
  // for inheritance support
173
334
  const constructor = target.prototype.constructor;
@@ -192,24 +353,39 @@ export const Metadata = {
192
353
 
193
354
  fieldIndex++;
194
355
 
356
+ // Pre-computed encoder function table: metadata[$encoders][fieldIndex] = encode.uint8 etc.
357
+ if (!metadata[$encoders]) {
358
+ Object.defineProperty(metadata, $encoders, {
359
+ value: parentMetadata?.[$encoders] ? [...parentMetadata[$encoders]] : [],
360
+ enumerable: false,
361
+ configurable: true,
362
+ writable: true,
363
+ });
364
+ }
365
+
195
366
  for (const field in fields) {
196
367
  const type = getNormalizedType(fields[field]);
197
368
 
198
- // FIXME: this code is duplicated from @type() annotation
199
- const complexTypeKlass = typeof(Object.keys(type)[0]) === "string" && getType(Object.keys(type)[0]);
200
-
201
- const childType = (complexTypeKlass)
202
- ? Object.values(type)[0]
203
- : type;
369
+ const { complexTypeKlass, childType } = resolveFieldType(type);
204
370
 
205
371
  Metadata.addField(
206
372
  metadata,
207
373
  fieldIndex,
208
374
  field,
209
375
  type,
210
- getPropertyDescriptor(`_${field}`, fieldIndex, childType, complexTypeKlass)
376
+ getPropertyDescriptor(field, fieldIndex, childType, complexTypeKlass)
211
377
  );
212
378
 
379
+ // Install accessor descriptor on the prototype (once per class field).
380
+ if (metadata[$descriptors][field]) {
381
+ Object.defineProperty(target.prototype, field, metadata[$descriptors][field]);
382
+ }
383
+
384
+ // Pre-compute encoder function for primitive types.
385
+ if (typeof type === "string") {
386
+ metadata[$encoders][fieldIndex] = (encode as any)[type];
387
+ }
388
+
213
389
  fieldIndex++;
214
390
  }
215
391
 
@@ -220,20 +396,6 @@ export const Metadata = {
220
396
  return metadata[field].deprecated === true;
221
397
  },
222
398
 
223
- init(klass: any) {
224
- //
225
- // Used only to initialize an empty Schema (Encoder#constructor)
226
- // TODO: remove/refactor this...
227
- //
228
- const metadata = {};
229
- klass[Symbol.metadata] = metadata;
230
- Object.defineProperty(metadata, $numFields, {
231
- value: 0,
232
- enumerable: false,
233
- configurable: true,
234
- });
235
- },
236
-
237
399
  initialize(constructor: any) {
238
400
  const parentClass = Object.getPrototypeOf(constructor);
239
401
  const parentMetadata: Metadata = parentClass[Symbol.metadata];
@@ -284,6 +446,46 @@ export const Metadata = {
284
446
  });
285
447
  }
286
448
 
449
+ // $unreliableFieldIndexes
450
+ if (parentMetadata[$unreliableFieldIndexes] !== undefined) {
451
+ Object.defineProperty(metadata, $unreliableFieldIndexes, {
452
+ value: [...parentMetadata[$unreliableFieldIndexes]],
453
+ enumerable: false,
454
+ configurable: true,
455
+ writable: true,
456
+ });
457
+ }
458
+
459
+ // $transientFieldIndexes
460
+ if (parentMetadata[$transientFieldIndexes] !== undefined) {
461
+ Object.defineProperty(metadata, $transientFieldIndexes, {
462
+ value: [...parentMetadata[$transientFieldIndexes]],
463
+ enumerable: false,
464
+ configurable: true,
465
+ writable: true,
466
+ });
467
+ }
468
+
469
+ // $staticFieldIndexes
470
+ if (parentMetadata[$staticFieldIndexes] !== undefined) {
471
+ Object.defineProperty(metadata, $staticFieldIndexes, {
472
+ value: [...parentMetadata[$staticFieldIndexes]],
473
+ enumerable: false,
474
+ configurable: true,
475
+ writable: true,
476
+ });
477
+ }
478
+
479
+ // $streamFieldIndexes
480
+ if (parentMetadata[$streamFieldIndexes] !== undefined) {
481
+ Object.defineProperty(metadata, $streamFieldIndexes, {
482
+ value: [...parentMetadata[$streamFieldIndexes]],
483
+ enumerable: false,
484
+ configurable: true,
485
+ writable: true,
486
+ });
487
+ }
488
+
287
489
  // $descriptors
288
490
  Object.defineProperty(metadata, $descriptors, {
289
491
  value: { ...parentMetadata[$descriptors] },
@@ -291,6 +493,16 @@ export const Metadata = {
291
493
  configurable: true,
292
494
  writable: true,
293
495
  });
496
+
497
+ // $encoders
498
+ if (parentMetadata[$encoders] !== undefined) {
499
+ Object.defineProperty(metadata, $encoders, {
500
+ value: [...parentMetadata[$encoders]],
501
+ enumerable: false,
502
+ configurable: true,
503
+ writable: true,
504
+ });
505
+ }
294
506
  }
295
507
  }
296
508
 
@@ -321,5 +533,21 @@ export const Metadata = {
321
533
 
322
534
  hasViewTagAtIndex(metadata: Metadata, index: number) {
323
535
  return metadata?.[$viewFieldIndexes]?.includes(index);
536
+ },
537
+
538
+ hasUnreliableAtIndex(metadata: Metadata, index: number) {
539
+ return metadata?.[$unreliableFieldIndexes]?.includes(index);
540
+ },
541
+
542
+ hasTransientAtIndex(metadata: Metadata, index: number) {
543
+ return metadata?.[$transientFieldIndexes]?.includes(index);
544
+ },
545
+
546
+ hasStaticAtIndex(metadata: Metadata, index: number) {
547
+ return metadata?.[$staticFieldIndexes]?.includes(index);
548
+ },
549
+
550
+ hasStreamAtIndex(metadata: Metadata, index: number) {
551
+ return metadata?.[$streamFieldIndexes]?.includes(index);
324
552
  }
325
553
  }
package/src/Reflection.ts CHANGED
@@ -5,6 +5,8 @@ import { Iterator } from "./encoding/decode.js";
5
5
  import { Encoder } from "./encoder/Encoder.js";
6
6
  import { Decoder } from "./decoder/Decoder.js";
7
7
  import { Schema } from "./Schema.js";
8
+ import { t, FieldBuilder } from "./types/builder.js";
9
+ import { ArraySchema } from "./types/custom/ArraySchema.js";
8
10
 
9
11
  /**
10
12
  * Static methods available on Reflection
@@ -33,25 +35,25 @@ interface ReflectionStatic {
33
35
  * Reflection
34
36
  */
35
37
  export const ReflectionField = schema({
36
- name: "string",
37
- type: "string",
38
- referencedType: "number",
39
- })
38
+ name: t.string(),
39
+ type: t.string(),
40
+ referencedType: t.number(),
41
+ }, "ReflectionField");
40
42
  export type ReflectionField = SchemaType<typeof ReflectionField>;
41
43
 
42
44
  export const ReflectionType = schema({
43
- id: "number",
44
- extendsId: "number",
45
- fields: [ ReflectionField ],
46
- })
45
+ id: t.number(),
46
+ extendsId: t.number(),
47
+ fields: t.array(ReflectionField),
48
+ }, "ReflectionType");
47
49
  export type ReflectionType = SchemaType<typeof ReflectionType>;
48
50
 
49
51
  export const Reflection = schema({
50
- types: [ ReflectionType ],
51
- rootType: "number",
52
- }) as ReturnType<typeof schema<{
53
- types: [typeof ReflectionType];
54
- rootType: "number";
52
+ types: t.array(ReflectionType),
53
+ rootType: t.number(),
54
+ }, "Reflection") as ReturnType<typeof schema<{
55
+ types: FieldBuilder<ArraySchema<ReflectionType>, true, false>;
56
+ rootType: FieldBuilder<number, false, false>;
55
57
  }>> & ReflectionStatic;
56
58
 
57
59
  export type Reflection = SchemaType<typeof Reflection>;