@colyseus/schema 4.0.20 → 5.0.0

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 +55 -2
  3. package/build/Reflection.d.ts +24 -30
  4. package/build/Schema.d.ts +70 -9
  5. package/build/annotations.d.ts +56 -13
  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 +5202 -1552
  27. package/build/index.cjs.map +1 -1
  28. package/build/index.d.ts +7 -3
  29. package/build/index.js +5202 -1552
  30. package/build/index.mjs +5193 -1552
  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 +7429 -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 +7426 -0
  38. package/build/input/index.mjs.map +1 -0
  39. package/build/types/HelperTypes.d.ts +22 -8
  40. package/build/types/TypeContext.d.ts +9 -0
  41. package/build/types/builder.d.ts +162 -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 +258 -31
  50. package/src/Reflection.ts +15 -13
  51. package/src/Schema.ts +176 -134
  52. package/src/annotations.ts +308 -236
  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 +21 -9
  88. package/src/types/TypeContext.ts +14 -2
  89. package/src/types/builder.ts +285 -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 +54 -6
  96. package/src/utils.ts +4 -6
@@ -0,0 +1,213 @@
1
+ /**
2
+ * EncodeDescriptor — per-class snapshot of the values the encode loop needs
3
+ * from a Ref's constructor. Lazily computed once per class (the first time
4
+ * a tree of that class is constructed) and stashed on the constructor via
5
+ * `$encodeDescriptor`. Each ChangeTree caches a reference to its class's
6
+ * descriptor at construction time, so the encode loop reads a single
7
+ * property from the tree instead of chasing 5 separate per-tree lookups:
8
+ *
9
+ * ctor[$encoder]
10
+ * ctor[$filter]
11
+ * ctor[Symbol.metadata]
12
+ * Metadata.isValidInstance(ref)
13
+ * getFilterBitmask(metadata)
14
+ *
15
+ * Lives in its own file to break the Encoder.ts ↔ ChangeTree.ts import
16
+ * cycle (ChangeTree caches descriptors at construction; Encoder reads them
17
+ * during encode).
18
+ */
19
+ import { Metadata } from "../Metadata.js";
20
+ import { $encodeDescriptor, $encoder, $encoders, $filter, $filterBitmask, $numFields, $staticFieldIndexes, $streamFieldIndexes, $unreliableFieldIndexes, $viewFieldIndexes } from "../types/symbols.js";
21
+ import type { StateView } from "./StateView.js";
22
+ import type { EncodeOperation } from "./EncodeOperation.js";
23
+
24
+ export interface EncodeDescriptor {
25
+ encoder: EncodeOperation;
26
+ filter: ((ref: any, index: number, view?: StateView) => boolean) | undefined;
27
+ metadata: any;
28
+ isSchema: boolean;
29
+ /**
30
+ * Bit i set iff field i has a @view tag. 0 for collection trees.
31
+ * Lets `encodeChangeCb` do a single bitwise op instead of a
32
+ * per-field metadata[i]?.tag chase.
33
+ */
34
+ filterBitmask: number;
35
+
36
+ /**
37
+ * Class-level "any field has the flag" booleans + per-field bitmasks.
38
+ * Hot path: per-mutation `_routeAndRecord` calls `isFieldStatic` and
39
+ * `isFieldUnreliable`. The common case is "no static/unreliable fields
40
+ * anywhere on this class" (booleans short-circuit before the symbol-keyed
41
+ * metadata lookup); the secondary common case is "this class has some
42
+ * such fields and we need to know if THIS field is one" — the bitmask
43
+ * answers in one bitwise op instead of an `Array.includes` linear scan.
44
+ *
45
+ * Bitmasks cover fields 0–31 only (matches the `filterBitmask` limitation).
46
+ * Fields ≥32 fall back to `Metadata.hasXAtIndex` — same handling as the
47
+ * filter-bitmask path.
48
+ */
49
+ hasAnyStatic: boolean;
50
+ hasAnyUnreliable: boolean;
51
+ hasAnyStream: boolean;
52
+ /**
53
+ * Class-level "any field carries a `@view` tag" — covers fields both
54
+ * within and beyond index 31 (unlike `filterBitmask`, which only
55
+ * captures the low 32). Read by `ChangeTree.hasFilteredFields` to
56
+ * decide whether a parent tree must be included in a view's bootstrap.
57
+ */
58
+ hasAnyView: boolean;
59
+ staticBitmask: number;
60
+ unreliableBitmask: number;
61
+ /**
62
+ * Bit i set iff field i holds a `t.stream(...)` collection. Hot encode
63
+ * path reads this to dispatch stream fields into the priority/budget
64
+ * gate instead of the normal recorder iteration.
65
+ */
66
+ streamBitmask: number;
67
+
68
+ /**
69
+ * Per-field parallel arrays — Schemas only (empty arrays for
70
+ * collections). Replaces hot-path `metadata[i].name` / `metadata[i].type`
71
+ * / `metadata[i].tag` chains with direct array indexing on a small
72
+ * fixed-shape object.
73
+ *
74
+ * Sparse where natural: `tags[i]` is undefined unless field i carries
75
+ * a @view tag; readers should null-check before comparing.
76
+ *
77
+ * `encoders[i]` mirrors `metadata[$encoders]` — the pre-computed
78
+ * encoder fn for primitive-typed fields. Cached here so encode loops
79
+ * skip a `metadata[$encoders]?.[i]` symbol-chain per emission.
80
+ */
81
+ names: string[];
82
+ types: any[];
83
+ tags: (number | undefined)[];
84
+ encoders: (((bytes: Uint8Array, value: any, it: any) => void) | undefined)[];
85
+ }
86
+
87
+ function computeFilterBitmask(metadata: any): number {
88
+ if (metadata === undefined) return 0;
89
+ let bm: number | undefined = metadata[$filterBitmask];
90
+ if (bm !== undefined) return bm;
91
+ bm = 0;
92
+ const tagged = metadata[$viewFieldIndexes];
93
+ if (tagged !== undefined) {
94
+ for (let i = 0, len = tagged.length; i < len; i++) bm |= (1 << tagged[i]);
95
+ }
96
+ // Non-enumerable so `for (const k in metadata)` iteration in TypeContext
97
+ // and elsewhere doesn't mistake this cache for a real field index.
98
+ Object.defineProperty(metadata, $filterBitmask, {
99
+ value: bm,
100
+ enumerable: false,
101
+ writable: true,
102
+ configurable: true,
103
+ });
104
+ return bm;
105
+ }
106
+
107
+ /**
108
+ * Bitmask of field indexes 0–31 in `indexes`. For fields ≥32 callers must
109
+ * fall back to the array lookup (same as `filterBitmask`).
110
+ */
111
+ function indexesToBitmask(indexes: number[] | undefined): number {
112
+ if (indexes === undefined) return 0;
113
+ let bm = 0;
114
+ for (let i = 0, len = indexes.length; i < len; i++) {
115
+ const idx = indexes[i];
116
+ if (idx < 32) bm |= (1 << idx);
117
+ }
118
+ return bm;
119
+ }
120
+
121
+ /**
122
+ * Build the per-field parallel arrays once at descriptor construction.
123
+ * For collection trees (no metadata or no $numFields) this returns empty
124
+ * arrays — readers branch on `isSchema` before touching them anyway.
125
+ */
126
+ function buildFieldArrays(metadata: any): {
127
+ names: string[];
128
+ types: any[];
129
+ tags: (number | undefined)[];
130
+ encoders: (((bytes: Uint8Array, value: any, it: any) => void) | undefined)[];
131
+ } {
132
+ const names: string[] = [];
133
+ const types: any[] = [];
134
+ const tags: (number | undefined)[] = [];
135
+ const encoders: (((bytes: Uint8Array, value: any, it: any) => void) | undefined)[] = [];
136
+
137
+ if (metadata === undefined) return { names, types, tags, encoders };
138
+
139
+ const numFields = metadata[$numFields];
140
+ if (numFields === undefined) return { names, types, tags, encoders };
141
+
142
+ const srcEncoders = metadata[$encoders];
143
+ for (let i = 0; i <= numFields; i++) {
144
+ const field = metadata[i];
145
+ if (field === undefined) {
146
+ // Holes are normal — inheritance can leave gaps. Fill with
147
+ // undefined so indexing is valid.
148
+ names[i] = undefined!;
149
+ types[i] = undefined;
150
+ tags[i] = undefined;
151
+ encoders[i] = undefined;
152
+ continue;
153
+ }
154
+ names[i] = field.name;
155
+ types[i] = field.type;
156
+ tags[i] = field.tag;
157
+ encoders[i] = srcEncoders?.[i];
158
+ }
159
+ return { names, types, tags, encoders };
160
+ }
161
+
162
+ export function getEncodeDescriptor(ref: any): EncodeDescriptor {
163
+ const ctor = ref.constructor;
164
+
165
+ // Use hasOwn — Object.defineProperty on a parent class would otherwise
166
+ // be inherited by every subclass via the prototype chain, and a
167
+ // subclass's instance would read the parent's metadata/encoder. See
168
+ // "should encode the correct class inside an array" for the regression.
169
+ if (Object.prototype.hasOwnProperty.call(ctor, $encodeDescriptor)) {
170
+ return ctor[$encodeDescriptor];
171
+ }
172
+
173
+ const metadata = ctor[Symbol.metadata];
174
+ const isSchema = Metadata.isValidInstance(ref);
175
+ const hasAnyView = (metadata?.[$viewFieldIndexes]?.length ?? 0) > 0;
176
+ const arrays = buildFieldArrays(metadata);
177
+ // For Schema classes with no `@view`-tagged fields, the per-field
178
+ // `ctx.filter(ref, index, view)` call on the encode hot path is a
179
+ // provable no-op: `Schema[$filter]` does `metadata[index]?.tag === undefined`
180
+ // which is always true when no field carries a tag. Setting filter to
181
+ // `undefined` here lets the `ctx.filter !== undefined && …` short-circuit
182
+ // in `encodeChangeCb` skip the call entirely — the metadata lookup +
183
+ // comparison adds up across 10k+ field encodes/tick.
184
+ // Collection classes keep their filter — their `[$filter]` does
185
+ // instance-level `ref[$childType]` / view-visibility checks that can't
186
+ // be decided class-wide.
187
+ const filter = (isSchema && !hasAnyView) ? undefined : ctor[$filter];
188
+ const desc: EncodeDescriptor = {
189
+ encoder: ctor[$encoder],
190
+ filter,
191
+ metadata,
192
+ isSchema,
193
+ filterBitmask: isSchema ? computeFilterBitmask(metadata) : 0,
194
+ hasAnyStatic: (metadata?.[$staticFieldIndexes]?.length ?? 0) > 0,
195
+ hasAnyUnreliable: (metadata?.[$unreliableFieldIndexes]?.length ?? 0) > 0,
196
+ hasAnyStream: (metadata?.[$streamFieldIndexes]?.length ?? 0) > 0,
197
+ hasAnyView,
198
+ staticBitmask: indexesToBitmask(metadata?.[$staticFieldIndexes]),
199
+ unreliableBitmask: indexesToBitmask(metadata?.[$unreliableFieldIndexes]),
200
+ streamBitmask: indexesToBitmask(metadata?.[$streamFieldIndexes]),
201
+ names: arrays.names,
202
+ types: arrays.types,
203
+ tags: arrays.tags,
204
+ encoders: arrays.encoders,
205
+ };
206
+ Object.defineProperty(ctor, $encodeDescriptor, {
207
+ value: desc,
208
+ enumerable: false,
209
+ writable: true,
210
+ configurable: true,
211
+ });
212
+ return desc;
213
+ }
@@ -1,5 +1,5 @@
1
1
  import { OPERATION } from "../encoding/spec.js";
2
- import { $changes, $childType, $getByIndex, $refId } from "../types/symbols.js";
2
+ import { $changes, $childType, $encoders, $getByIndex, $refId, $values } from "../types/symbols.js";
3
3
 
4
4
  import { encode } from "../encoding/encode.js";
5
5
 
@@ -30,8 +30,14 @@ export function encodeValue(
30
30
  value: any,
31
31
  operation: OPERATION,
32
32
  it: Iterator,
33
+ encoderFn?: (bytes: Uint8Array, value: any, it: Iterator) => void,
33
34
  ) {
34
- if (typeof (type) === "string") {
35
+ if (encoderFn !== undefined) {
36
+ // Fast path: pre-computed encoder for primitive types.
37
+ encoderFn(bytes, value, it);
38
+
39
+ } else if (typeof (type) === "string") {
40
+ // Fallback for types not pre-computed (e.g. runtime-constructed).
35
41
  (encode as any)[type]?.(bytes, value, it);
36
42
 
37
43
  } else if (type[Symbol.metadata] !== undefined) {
@@ -68,7 +74,6 @@ export const encodeSchemaOperation: EncodeOperation = function <T extends Schema
68
74
  it: Iterator,
69
75
  _: any,
70
76
  __: any,
71
- metadata: Metadata,
72
77
  ) {
73
78
  // "compress" field index + operation
74
79
  bytes[it.offset++] = (index | operation) & 255;
@@ -78,25 +83,37 @@ export const encodeSchemaOperation: EncodeOperation = function <T extends Schema
78
83
  return;
79
84
  }
80
85
 
81
- const ref = changeTree.ref;
82
- const field = metadata[index];
86
+ // Read field info from the per-class descriptor's parallel arrays —
87
+ // replaces `metadata[index]` (returns a per-field obj) + `.name` /
88
+ // `.type` chains. The `encoders` array is also pre-baked here so we
89
+ // skip a `metadata[$encoders]?.[index]` symbol-keyed lookup per call.
90
+ const desc = changeTree.encDescriptor;
91
+ const ref = changeTree.ref as any;
92
+
93
+ // Direct $values[index] read — bypasses prototype getter + metadata name lookup.
94
+ // Falls back to named property for manual fields (which don't use $values).
95
+ const value = ref[$values][index] ?? ref[desc.names[index]];
83
96
 
84
- // TODO: inline this function call small performance gain
85
97
  encodeValue(
86
98
  encoder,
87
99
  bytes,
88
- metadata[index].type,
89
- ref[field.name as keyof T],
100
+ desc.types[index],
101
+ value,
90
102
  operation,
91
- it
103
+ it,
104
+ desc.encoders[index],
92
105
  );
93
106
  }
94
107
 
95
108
  /**
96
- * Used for collections (MapSchema, CollectionSchema, SetSchema)
109
+ * Encode a single MapSchema entry. Splits the legacy
110
+ * `encodeKeyValueOperation` so the per-emission `typeof ref['set']` check
111
+ * is gone — MapSchema instances are routed here via their `[$encoder]`
112
+ * static, the dynamic-key string emission is unconditional on ADD.
113
+ *
97
114
  * @private
98
115
  */
99
- export const encodeKeyValueOperation: EncodeOperation = function (
116
+ export const encodeMapEntry: EncodeOperation = function (
100
117
  encoder: Encoder,
101
118
  bytes: Uint8Array,
102
119
  changeTree: ChangeTree,
@@ -104,59 +121,88 @@ export const encodeKeyValueOperation: EncodeOperation = function (
104
121
  operation: OPERATION,
105
122
  it: Iterator,
106
123
  ) {
107
- // encode operation
108
124
  bytes[it.offset++] = operation & 255;
109
-
110
- // encode index
111
125
  encode.number(bytes, index, it);
112
126
 
113
- // Do not encode value for DELETE operations
114
- if (operation === OPERATION.DELETE) {
115
- return;
116
- }
127
+ if (operation === OPERATION.DELETE) return;
117
128
 
118
129
  const ref = changeTree.ref;
119
130
 
120
- //
121
- // encode "alias" for dynamic fields (maps)
122
- //
123
- if ((operation & OPERATION.ADD) === OPERATION.ADD) { // ADD or DELETE_AND_ADD
124
- if (typeof(ref['set']) === "function") {
125
- //
126
- // MapSchema dynamic key
127
- //
128
- const dynamicIndex = changeTree.ref['$indexes'].get(index);
129
- encode.string(bytes, dynamicIndex, it);
130
- }
131
+ // ADD or DELETE_AND_ADD: emit the user-facing string key for dynamic
132
+ // map fields. SetSchema/CollectionSchema use a different encoder and
133
+ // skip this entirely (no dynamic key).
134
+ if ((operation & OPERATION.ADD) === OPERATION.ADD) {
135
+ const dynamicIndex = (ref as any)['$indexes'].get(index);
136
+ encode.string(bytes, dynamicIndex, it);
131
137
  }
132
138
 
133
- const type = ref[$childType];
134
- const value = ref[$getByIndex](index);
135
-
136
- // try { throw new Error(); } catch (e) {
137
- // // only print if not coming from Reflection.ts
138
- // if (!e.stack.includes("src/Reflection.ts")) {
139
- // console.log("encodeKeyValueOperation -> ", {
140
- // ref: changeTree.ref.constructor.name,
141
- // field,
142
- // operation: OPERATION[operation],
143
- // value: value?.toJSON(),
144
- // items: ref.toJSON(),
145
- // });
146
- // }
147
- // }
148
-
149
- // TODO: inline this function call small performance gain
150
139
  encodeValue(
151
140
  encoder,
152
141
  bytes,
153
- type,
154
- value,
142
+ (ref as any)[$childType],
143
+ ref[$getByIndex](index),
155
144
  operation,
156
- it
145
+ it,
146
+ );
147
+ }
148
+
149
+ /**
150
+ * Encode a single SetSchema / CollectionSchema entry. Wire format is the
151
+ * same as MapSchema minus the dynamic-key string, so this path skips the
152
+ * legacy `typeof ref['set']` check entirely.
153
+ *
154
+ * @private
155
+ */
156
+ export const encodeIndexedEntry: EncodeOperation = function (
157
+ encoder: Encoder,
158
+ bytes: Uint8Array,
159
+ changeTree: ChangeTree,
160
+ index: number,
161
+ operation: OPERATION,
162
+ it: Iterator,
163
+ ) {
164
+ bytes[it.offset++] = operation & 255;
165
+ encode.number(bytes, index, it);
166
+
167
+ if (operation === OPERATION.DELETE) return;
168
+
169
+ const ref = changeTree.ref;
170
+ encodeValue(
171
+ encoder,
172
+ bytes,
173
+ (ref as any)[$childType],
174
+ ref[$getByIndex](index),
175
+ operation,
176
+ it,
157
177
  );
158
178
  }
159
179
 
180
+ /**
181
+ * Unified encoder kept for back-compat with external consumers that may
182
+ * have registered it directly via `static [$encoder] =
183
+ * encodeKeyValueOperation`. New code (and all internal collections)
184
+ * should use the split variants — `encodeMapEntry` for MapSchema and
185
+ * `encodeIndexedEntry` for SetSchema / CollectionSchema.
186
+ *
187
+ * The runtime `typeof ref['set']` check below is the per-emission cost
188
+ * the split is designed to remove.
189
+ */
190
+ export const encodeKeyValueOperation: EncodeOperation = function (
191
+ encoder: Encoder,
192
+ bytes: Uint8Array,
193
+ changeTree: ChangeTree,
194
+ index: number,
195
+ operation: OPERATION,
196
+ it: Iterator,
197
+ ) {
198
+ const ref = changeTree.ref as any;
199
+ if ((operation & OPERATION.ADD) === OPERATION.ADD && typeof ref['set'] === "function") {
200
+ encodeMapEntry(encoder, bytes, changeTree, index, operation, it, false, false);
201
+ } else {
202
+ encodeIndexedEntry(encoder, bytes, changeTree, index, operation, it, false, false);
203
+ }
204
+ }
205
+
160
206
  /**
161
207
  * Used for collections (MapSchema, ArraySchema, etc.)
162
208
  * @private
@@ -171,13 +217,19 @@ export const encodeArray: EncodeOperation = function (
171
217
  isEncodeAll: boolean,
172
218
  hasView: boolean,
173
219
  ) {
174
- const ref = changeTree.ref;
175
- const useOperationByRefId = hasView && changeTree.isFiltered && (typeof (changeTree.getType(field)) !== "string");
220
+ // Read through `refTarget` so every property access below skips the
221
+ // ArraySchema Proxy `get` trap. `refTarget` points at the raw backing
222
+ // instance; `ref` (the Proxy) stays the user-facing identity.
223
+ const ref = changeTree.refTarget as any;
224
+ // ArraySchema stores its per-instance child type at `$childType`.
225
+ // This encoder is array-only — there's no Schema fallback to consider.
226
+ const type = ref[$childType];
227
+ const useOperationByRefId = hasView && changeTree.isFiltered && typeof type !== "string";
176
228
 
177
229
  let refOrIndex: number;
178
230
 
179
231
  if (useOperationByRefId) {
180
- const item = ref['tmpItems'][field];
232
+ const item = ref.tmpItems[field];
181
233
 
182
234
  // Skip encoding if item is undefined (e.g. when clear() is called)
183
235
  if (!item) { return; }
@@ -206,20 +258,10 @@ export const encodeArray: EncodeOperation = function (
206
258
  return;
207
259
  }
208
260
 
209
- const type = changeTree.getType(field);
210
- const value = changeTree.getValue(field, isEncodeAll);
211
-
212
- // console.log({ type, field, value });
213
-
214
- // console.log("encodeArray -> ", {
215
- // ref: changeTree.ref.constructor.name,
216
- // field,
217
- // operation: OPERATION[operation],
218
- // value: value?.toJSON(),
219
- // items: ref.toJSON(),
220
- // });
261
+ // `type` was already read above. Direct $getByIndex call — skips
262
+ // ChangeTree.getValue's pass-through wrapper.
263
+ const value = ref[$getByIndex](field, isEncodeAll);
221
264
 
222
- // TODO: inline this function call small performance gain
223
265
  encodeValue(
224
266
  encoder,
225
267
  bytes,