@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
@@ -7,17 +7,60 @@ export interface DataChange<T = any, F = string> {
7
7
  ref: IRef;
8
8
  refId: number;
9
9
  op: OPERATION;
10
- field: F;
10
+ /** Set for Schema field changes; omitted for collection item changes (which carry a `dynamicIndex` instead). */
11
+ field?: F;
11
12
  dynamicIndex?: number | string;
12
13
  value: T;
13
14
  previousValue: T;
14
15
  }
15
16
  export declare const DEFINITION_MISMATCH = -1;
16
- export type DecodeOperation<T extends Schema = any> = (decoder: Decoder<T>, bytes: Uint8Array, it: Iterator, ref: IRef, allChanges: DataChange[]) => number | void;
17
- export declare function decodeValue<T extends Ref>(decoder: Decoder, operation: OPERATION, ref: T, index: number, type: any, bytes: Uint8Array, it: Iterator, allChanges: DataChange[]): {
18
- value: any;
19
- previousValue: T;
17
+ /**
18
+ * When no `triggerChanges` subscriber is attached, `Decoder.decode` passes
19
+ * `null` so the per-field change objects are never allocated. Every push
20
+ * site uses `allChanges?.push(...)` — optional chaining also short-circuits
21
+ * the object literal, so there's nothing to collect and nothing to throw
22
+ * away.
23
+ */
24
+ export type DecodeOperation<T extends Schema = any> = (decoder: Decoder<T>, bytes: Uint8Array, it: Iterator, ref: IRef, allChanges: DataChange[] | null) => number | void;
25
+ /**
26
+ * Collection-kind discriminator declared on each collection class as
27
+ * `static COLLECTION_KIND = CollectionKind.X`. The decoder's key/value
28
+ * dispatch used to make three back-to-back `typeof(ref.method) ===
29
+ * "function"` checks per entry; those collapse into one switch on the
30
+ * target's class tag. Missing / `undefined` on a ref hits the switch's
31
+ * `default` branch and logs a warning — a guard for future collection
32
+ * types that land without a tag.
33
+ *
34
+ * Declared as a `const` object (not a TS `enum`) so the codegen parser —
35
+ * which picks up every `EnumDeclaration` in the lib source via transitive
36
+ * imports — doesn't emit a generated .cs file for it.
37
+ */
38
+ export declare const CollectionKind: {
39
+ readonly Map: 1;
40
+ readonly Array: 2;
41
+ readonly Set: 3;
42
+ readonly Collection: 4;
43
+ readonly Stream: 5;
20
44
  };
45
+ export type CollectionKind = typeof CollectionKind[keyof typeof CollectionKind];
46
+ /**
47
+ * Structural type for any class that participates in the `decodeKeyValue-
48
+ * Operation` dispatch. Lets the hot-path read `tgt.constructor.COLLECTION_KIND`
49
+ * without an `any` cast.
50
+ */
51
+ export interface CollectionCtor {
52
+ readonly COLLECTION_KIND: CollectionKind;
53
+ }
54
+ /**
55
+ * Decode the next wire value for `ref[index]`. Returns the decoded value.
56
+ *
57
+ * Callers pass `previousValue` explicitly — it's the current value at the
58
+ * slot before decoding and is needed for ref-count bookkeeping (on DELETE)
59
+ * and for the DELETE_AND_ADD self-reassign case. Keeping it as a parameter
60
+ * lets this function return a single primitive instead of a pair, so the
61
+ * hot call path allocates nothing.
62
+ */
63
+ export declare function decodeValue<T extends Ref>(decoder: Decoder, operation: OPERATION, ref: T, index: number, previousValue: any, type: any, bytes: Uint8Array, it: Iterator, allChanges: DataChange[] | null): any;
21
64
  export declare const decodeSchemaOperation: DecodeOperation;
22
65
  export declare const decodeKeyValueOperation: DecodeOperation;
23
66
  export declare const decodeArray: DecodeOperation;
@@ -1,6 +1,6 @@
1
1
  import { TypeContext } from "../types/TypeContext.js";
2
2
  import { Schema } from "../Schema.js";
3
- import type { IRef } from "../encoder/ChangeTree.js";
3
+ import { type IRef } from "../encoder/ChangeTree.js";
4
4
  import type { Iterator } from "../encoding/decode.js";
5
5
  import { ReferenceTracker } from "./ReferenceTracker.js";
6
6
  import { type DataChange } from "./DecodeOperation.js";
@@ -17,5 +17,5 @@ export declare class Decoder<T extends IRef = any> {
17
17
  skipCurrentStructure(bytes: Uint8Array, it: Iterator, totalBytes: number): void;
18
18
  getInstanceType(bytes: Uint8Array, it: Iterator, defaultType: typeof Schema): typeof Schema;
19
19
  createInstanceOfType(type: typeof Schema): Schema;
20
- removeChildRefs(ref: Collection, allChanges: DataChange[]): void;
20
+ removeChildRefs(ref: Collection, allChanges: DataChange[] | null): void;
21
21
  }
@@ -36,7 +36,7 @@ export declare class StateCallbackStrategy<TState extends IRef> {
36
36
  /**
37
37
  * Listen to property changes on a nested instance.
38
38
  */
39
- listen<TInstance, K extends PublicPropNames<TInstance>>(instance: TInstance, property: K, handler: PropertyChangeCallback<TInstance[K]>, immediate?: boolean): () => void;
39
+ listen<TInstance extends Schema, K extends PublicPropNames<TInstance>>(instance: TInstance, property: K, handler: PropertyChangeCallback<TInstance[K]>, immediate?: boolean): () => void;
40
40
  protected listenInstance<TInstance extends IRef>(instance: TInstance, propertyName: string, handler: PropertyChangeCallback<any>, immediate?: boolean): () => void;
41
41
  /**
42
42
  * Listen to any property change on an instance.
@@ -0,0 +1,107 @@
1
+ import { OPERATION } from "../encoding/spec.js";
2
+ /**
3
+ * ChangeRecorder — "what changed this tick" for a single ref.
4
+ *
5
+ * This file holds the two standalone recorder classes used for the
6
+ * unreliable channel (lazy, opt-in). The reliable channel is inlined on
7
+ * `ChangeTree` for perf; see `ChangeTree._isSchema` dispatch.
8
+ *
9
+ * Interface design (ISP):
10
+ * - {@link ChangeRecorder}: common ops, implemented by both Schema and
11
+ * Collection recorders.
12
+ * - {@link ICollectionChangeRecorder}: extends with `recordPure` +
13
+ * `shift` — collection-only. Schema recorders do NOT carry these.
14
+ *
15
+ * Per-field filter/visibility is decided at encode time, not record time.
16
+ * Full-sync output is derived structurally via `ChangeTree.forEachLive`.
17
+ */
18
+ export interface ChangeRecorder {
19
+ /**
20
+ * Record a change at the given index. Handles op merge
21
+ * (DELETE followed by ADD becomes DELETE_AND_ADD).
22
+ */
23
+ record(index: number, op: OPERATION): void;
24
+ /** Record a DELETE at the given index. */
25
+ recordDelete(index: number, op: OPERATION): void;
26
+ /**
27
+ * Record an operation without op-merge semantics. Used by ArraySchema
28
+ * positional writes where DELETE→ADD merge is undesirable.
29
+ */
30
+ recordRaw(index: number, op: OPERATION): void;
31
+ /** Current operation at index, or undefined if none. */
32
+ operationAt(index: number): OPERATION | undefined;
33
+ /** Overwrite the operation at index. */
34
+ setOperationAt(index: number, op: OPERATION): void;
35
+ /**
36
+ * Iterate (index, op) pairs in record order.
37
+ * Pure operations emit with index = -op (wire convention).
38
+ */
39
+ forEach(cb: (index: number, op: OPERATION) => void): void;
40
+ /** Closure-free forEach variant for the hot encode path. */
41
+ forEachWithCtx<T>(ctx: T, cb: (ctx: T, index: number, op: OPERATION) => void): void;
42
+ size(): number;
43
+ has(): boolean;
44
+ reset(): void;
45
+ }
46
+ /**
47
+ * Extended recorder for collection types — adds `recordPure` (CLEAR /
48
+ * REVERSE) and `shift` (ArraySchema.unshift support).
49
+ */
50
+ export interface ICollectionChangeRecorder extends ChangeRecorder {
51
+ /**
52
+ * Record a pure operation (CLEAR / REVERSE) with no index.
53
+ * Interleaves with indexed ops at record order.
54
+ */
55
+ recordPure(op: OPERATION): void;
56
+ /** Shift current-tick dirty indexes by `shiftIndex`. */
57
+ shift(shiftIndex: number): void;
58
+ }
59
+ /**
60
+ * Schema field operations are limited to ADD(128), DELETE(64), and
61
+ * DELETE_AND_ADD(192). REPLACE(0) is collection-only, so `ops[i] === 0`
62
+ * is a safe "no operation" sentinel.
63
+ */
64
+ export declare class SchemaChangeRecorder implements ChangeRecorder {
65
+ private dirtyLow;
66
+ private dirtyHigh;
67
+ private readonly ops;
68
+ constructor(numFields: number);
69
+ record(index: number, op: OPERATION): void;
70
+ recordDelete(index: number, op: OPERATION): void;
71
+ recordRaw(index: number, op: OPERATION): void;
72
+ operationAt(index: number): OPERATION | undefined;
73
+ setOperationAt(index: number, op: OPERATION): void;
74
+ forEach(cb: (index: number, op: OPERATION) => void): void;
75
+ forEachWithCtx<T>(ctx: T, cb: (ctx: T, index: number, op: OPERATION) => void): void;
76
+ size(): number;
77
+ has(): boolean;
78
+ reset(): void;
79
+ }
80
+ /**
81
+ * Collection items have sparse indexes (e.g. 0, 7, 1024) exceeding the
82
+ * 64-field cap Schema imposes. Map-based storage handles arbitrary
83
+ * indexes; the value at each entry is the OPERATION.
84
+ *
85
+ * Pure operations (CLEAR, REVERSE) live in `pureOps` as `[position, op]`
86
+ * entries where `position` is `dirty.size` at record time — preserves
87
+ * insertion-order interleaving with indexed ops (e.g. CLEAR must emit
88
+ * BEFORE subsequent ADDs).
89
+ */
90
+ export declare class CollectionChangeRecorder implements ICollectionChangeRecorder {
91
+ private dirty;
92
+ private pureOps;
93
+ record(index: number, op: OPERATION): void;
94
+ recordDelete(index: number, op: OPERATION): void;
95
+ recordRaw(index: number, op: OPERATION): void;
96
+ recordPure(op: OPERATION): void;
97
+ operationAt(index: number): OPERATION | undefined;
98
+ setOperationAt(index: number, op: OPERATION): void;
99
+ forEach(cb: (index: number, op: OPERATION) => void): void;
100
+ forEachWithCtx<T>(ctx: T, cb: (ctx: T, index: number, op: OPERATION) => void): void;
101
+ size(): number;
102
+ has(): boolean;
103
+ reset(): void;
104
+ shift(shiftIndex: number): void;
105
+ }
106
+ /** 32-bit Hamming weight (popcount). */
107
+ export declare function popcount32(n: number): number;
@@ -1,3 +1,20 @@
1
+ /**
2
+ * ChangeTree — the per-`Ref` mutation tracker attached via `$changes`.
3
+ *
4
+ * This file owns: class shape (fields, flags, ctor), inline
5
+ * ChangeRecorder implementation (record / forEach / …), mutation API
6
+ * (change / delete / operation / …), and encode lifecycle (endEncode /
7
+ * discard / …). Helpers split out into ./changeTree/:
8
+ *
9
+ * - parentChain.ts addParent / removeParent / find / has / getAll
10
+ * - liveIteration.ts forEachLive
11
+ * - inheritedFlags.ts filter / unreliable / transient / static inheritance
12
+ * - treeAttachment.ts setRoot / setParent / forEachChild(+WithCtx)
13
+ *
14
+ * Public surface on ChangeTree is unchanged — methods are thin pass-throughs
15
+ * into the helpers. V8 inlines the pass-throughs; the runtime shape stays
16
+ * a single class to preserve hidden-class + IC behavior.
17
+ */
1
18
  import { OPERATION } from "../encoding/spec.js";
2
19
  import { Schema } from "../Schema.js";
3
20
  import { $changes, $decoder, $encoder, $getByIndex, $refId, type $deleteByIndex } from "../types/symbols.js";
@@ -5,9 +22,12 @@ import type { MapSchema } from "../types/custom/MapSchema.js";
5
22
  import type { ArraySchema } from "../types/custom/ArraySchema.js";
6
23
  import type { CollectionSchema } from "../types/custom/CollectionSchema.js";
7
24
  import type { SetSchema } from "../types/custom/SetSchema.js";
25
+ import type { StreamSchema } from "../types/custom/StreamSchema.js";
8
26
  import { Root } from "./Root.js";
9
27
  import { Metadata } from "../Metadata.js";
28
+ import { type ChangeRecorder } from "./ChangeRecorder.js";
10
29
  import type { EncodeOperation } from "./EncodeOperation.js";
30
+ import { type EncodeDescriptor } from "./EncodeDescriptor.js";
11
31
  import type { DecodeOperation } from "../decoder/DecodeOperation.js";
12
32
  declare global {
13
33
  interface Object {
@@ -18,14 +38,10 @@ declare global {
18
38
  }
19
39
  export interface IRef {
20
40
  [$refId]?: number;
21
- [$getByIndex]?: (index: number, isEncodeAll?: boolean) => any;
22
- [$deleteByIndex]?: (index: number) => void;
23
- }
24
- export type Ref = Schema | ArraySchema | MapSchema | CollectionSchema | SetSchema;
25
- export type ChangeSetName = "changes" | "allChanges" | "filteredChanges" | "allFilteredChanges";
26
- export interface IndexedOperations {
27
- [index: number]: OPERATION;
41
+ [$getByIndex](index: number, isEncodeAll?: boolean): any;
42
+ [$deleteByIndex](index: number): void;
28
43
  }
44
+ export type Ref = Schema | ArraySchema | MapSchema | CollectionSchema | SetSchema | StreamSchema;
29
45
  export interface ChangeTreeNode {
30
46
  changeTree: ChangeTree;
31
47
  next?: ChangeTreeNode;
@@ -36,99 +52,232 @@ export interface ChangeTreeList {
36
52
  next?: ChangeTreeNode;
37
53
  tail?: ChangeTreeNode;
38
54
  }
39
- export interface ChangeSet {
40
- indexes: {
41
- [index: number]: number;
42
- };
43
- operations: number[];
44
- queueRootNode?: ChangeTreeNode;
45
- }
46
55
  export declare function createChangeTreeList(): ChangeTreeList;
47
- export declare function setOperationAtIndex(changeSet: ChangeSet, index: number): void;
48
- export declare function deleteOperationAtIndex(changeSet: ChangeSet, index: number | string): void;
49
- export declare function debugChangeSet(label: string, changeSet: ChangeSet): void;
50
56
  export interface ParentChain {
51
57
  ref: Ref;
52
58
  index: number;
53
59
  next?: ParentChain;
54
60
  }
55
- export declare class ChangeTree<T extends Ref = any> {
61
+ export declare const IS_FILTERED = 1, IS_VISIBILITY_SHARED = 2, IS_NEW = 4;
62
+ export declare const IS_UNRELIABLE = 8, IS_TRANSIENT = 16, IS_STATIC = 32;
63
+ export declare const IS_STREAM_COLLECTION = 64;
64
+ /**
65
+ * Flags a child inherits from its parent's own transitive state via
66
+ * `checkInheritedFlags`. Read as a bitwise mask so the inheritance step
67
+ * is a single OR instead of three getter/setter pairs.
68
+ *
69
+ * `IS_UNRELIABLE` is intentionally excluded: `@unreliable` is rejected
70
+ * at decoration time for ref-type fields (see `Metadata.setUnreliable`)
71
+ * because an unreliable ADD/DELETE could leave the decoder unable to
72
+ * interpret later packets referencing an orphan refId. Tree-level
73
+ * unreliable is therefore dead on every Schema/Collection tree today;
74
+ * the bit and its machinery are kept in place so this can be
75
+ * reconsidered if a safe semantics (e.g. reliable ADD + unreliable
76
+ * field mutations only) is designed later.
77
+ */
78
+ export declare const INHERITABLE_FLAGS: number;
79
+ export declare class ChangeTree<T extends Ref = any> implements ChangeRecorder {
56
80
  ref: T;
81
+ /**
82
+ * Non-Proxy target of `ref` for encoder hot-path reads. For
83
+ * `ArraySchema`, `ref` is the Proxy users interact with; every property
84
+ * access on it runs through the `get` trap (even for symbol keys, which
85
+ * fall through to `Reflect.get` — one extra hop per lookup). The encoder
86
+ * loop reads `[$getByIndex]`, `[$childType]`, `.items`, `.tmpItems` at
87
+ * high frequency during `encode()` / `encodeAll()`; going through
88
+ * `refTarget` skips all of those traps.
89
+ *
90
+ * For non-proxied types (Schema, MapSchema, SetSchema, CollectionSchema,
91
+ * StreamSchema), `refTarget === ref`. Consumers that need the user-
92
+ * facing identity (debug output, callback parents) keep using `ref`.
93
+ */
94
+ refTarget: T;
57
95
  metadata: Metadata;
96
+ /**
97
+ * Per-class cache of encoder fn / filter fn / isSchema / filterBitmask /
98
+ * metadata, looked up once at construction. The encode loop reads
99
+ * `tree.encDescriptor` and never touches `ref.constructor` again. See
100
+ * EncodeDescriptor.ts.
101
+ */
102
+ encDescriptor: EncodeDescriptor;
58
103
  root?: Root;
59
- parentChain?: ParentChain;
104
+ parentRef?: Ref;
105
+ _parentIndex?: number;
106
+ extraParents?: ParentChain;
107
+ flags: number;
60
108
  /**
61
- * Whether this structure is parent of a filtered structure.
109
+ * Per-walk visit stamp written by `Encoder.encodeFullSync`'s DFS. A
110
+ * tree is considered "already visited by the current walk" iff
111
+ * `tree._fullSyncGen === ctx.gen` — the encoder bumps its generation
112
+ * counter once per walk, then stamps each tree with that value on
113
+ * first visit; any later encounter of the same tree (shared refs
114
+ * reachable through multiple parents) short-circuits on the equality
115
+ * check instead of recursing again.
62
116
  */
63
- isFiltered: boolean;
64
- isVisibilitySharedWithParent?: boolean;
65
- indexedOperations: IndexedOperations;
66
- changes: ChangeSet;
67
- allChanges: ChangeSet;
68
- filteredChanges: ChangeSet;
69
- allFilteredChanges: ChangeSet;
70
- indexes: {
71
- [index: string]: any;
72
- };
117
+ _fullSyncGen: number;
118
+ _isSchema: boolean;
119
+ dirtyLow: number;
120
+ dirtyHigh: number;
121
+ opsLow: number;
122
+ opsHigh: number;
123
+ ops?: Uint8Array;
124
+ collDirty?: Map<number, OPERATION>;
125
+ collPureOps?: Array<[number, OPERATION]>;
126
+ unreliableRecorder?: ChangeRecorder;
127
+ paused: boolean;
128
+ changesNode?: ChangeTreeNode;
129
+ unreliableChangesNode?: ChangeTreeNode;
130
+ visibleViews?: number[];
131
+ invisibleViews?: number[];
132
+ tagViews?: Map<number, number[]>;
73
133
  /**
74
- * Is this a new instance? Used on ArraySchema to determine OPERATION.MOVE_AND_ADD operation.
134
+ * Per-view subscription bitmap same layout as `visibleViews`. Set by
135
+ * `StateView.subscribe(collection)` to mark the view as persistently
136
+ * interested in this collection's contents. When a new child is
137
+ * attached to a subscribed collection (setParent hook), it's
138
+ * auto-propagated to every subscribed view (force-shipped for
139
+ * Array/Map/Set/Collection; enqueued into per-view pending for
140
+ * streams). Undefined until the first subscribe.
75
141
  */
76
- isNew: boolean;
142
+ subscribedViews?: number[];
143
+ get isFiltered(): boolean;
144
+ set isFiltered(v: boolean);
145
+ get isVisibilitySharedWithParent(): boolean;
146
+ set isVisibilitySharedWithParent(v: boolean);
147
+ get isNew(): boolean;
148
+ set isNew(v: boolean);
149
+ get isUnreliable(): boolean;
150
+ set isUnreliable(v: boolean);
151
+ get isTransient(): boolean;
152
+ set isTransient(v: boolean);
153
+ get isStatic(): boolean;
154
+ set isStatic(v: boolean);
155
+ get isStreamCollection(): boolean;
156
+ set isStreamCollection(v: boolean);
157
+ get hasFilteredFields(): boolean;
158
+ ensureUnreliableRecorder(): ChangeRecorder;
159
+ isFieldUnreliable(index: number): boolean;
160
+ isFieldStatic(index: number): boolean;
161
+ isFieldStream(index: number): boolean;
77
162
  constructor(ref: T);
163
+ private _opAt;
164
+ private _opPut;
165
+ private _markDirty;
166
+ record(index: number, op: OPERATION): void;
167
+ recordDelete(index: number, op: OPERATION): void;
168
+ recordRaw(index: number, op: OPERATION): void;
169
+ recordPure(op: OPERATION): void;
170
+ operationAt(index: number): OPERATION | undefined;
171
+ setOperationAt(index: number, op: OPERATION): void;
172
+ forEach(cb: (index: number, op: OPERATION) => void): void;
173
+ forEachWithCtx<C>(ctx: C, cb: (ctx: C, index: number, op: OPERATION) => void): void;
174
+ size(): number;
175
+ has(): boolean;
176
+ reset(): void;
177
+ shift(shiftIndex: number): void;
78
178
  setRoot(root: Root): void;
79
179
  setParent(parent: Ref, root?: Root, parentIndex?: number): void;
80
- forEachChild(callback: (change: ChangeTree, at: any) => void): void;
180
+ forEachChild(cb: (change: ChangeTree, at: any) => void): void;
181
+ forEachChildWithCtx<C>(ctx: C, cb: (ctx: C, change: ChangeTree, at: any) => void): void;
182
+ forEachLive(cb: (index: number) => void): void;
183
+ forEachLiveWithCtx<C>(ctx: C, cb: (ctx: C, index: number) => void): void;
81
184
  operation(op: OPERATION): void;
185
+ /**
186
+ * Route a field-level mutation to the reliable or unreliable channel
187
+ * and enqueue into the matching queue. Shared by `change` and
188
+ * `indexedOperation`; `raw=true` bypasses DELETE→ADD merge
189
+ * (ArraySchema positional writes), `raw=false` merges inside `record`.
190
+ *
191
+ * Note: record() on both channels handles DELETE→ADD merge internally,
192
+ * so callers do not need to pre-compute the merged op.
193
+ *
194
+ * `@unreliable` is decoration-time-validated to apply only to primitive
195
+ * fields (see annotations.ts), so the per-field unreliable flag here
196
+ * always means "primitive value updates" — the structural-ADD-routes-
197
+ * reliable footgun for ref-type fields can't reach this code path.
198
+ */
199
+ private _routeAndRecord;
82
200
  change(index: number, operation?: OPERATION): void;
201
+ indexedOperation(index: number, operation: OPERATION): void;
83
202
  shiftChangeIndexes(shiftIndex: number): void;
84
- shiftAllChangeIndexes(shiftIndex: number, startIndex?: number): void;
85
- private _shiftAllChangeIndexes;
86
- indexedOperation(index: number, operation: OPERATION, allChangesIndex?: number): void;
87
- getType(index?: number): any;
88
203
  getChange(index: number): OPERATION;
204
+ pause(): void;
205
+ resume(): void;
206
+ untracked<T>(fn: () => T): T;
207
+ markDirty(index: number, operation?: OPERATION): void;
89
208
  getValue(index: number, isEncodeAll?: boolean): any;
90
- delete(index: number, operation?: OPERATION, allChangesIndex?: number): any;
91
- endEncode(changeSetName: ChangeSetName): void;
92
- discard(discardAll?: boolean): void;
93
- /**
94
- * Recursively discard all changes from this, and child structures.
95
- * (Used in tests only)
96
- */
209
+ delete(index: number, operation?: OPERATION): any;
210
+ endEncode(): void;
211
+ endEncodeUnreliable(): void;
212
+ discard(): void;
97
213
  discardAll(): void;
98
214
  get changed(): boolean;
99
- protected checkIsFiltered(parent: Ref, parentIndex: number, isNewChangeTree: boolean): void;
100
- protected _checkFilteredByParent(parent: Ref, parentIndex: number): void;
101
- /**
102
- * Get the immediate parent
103
- */
215
+ /** Immediate parent (primary). See `extraParents` for the 2nd+ chain. */
104
216
  get parent(): Ref | undefined;
105
- /**
106
- * Get the immediate parent index
107
- */
108
217
  get parentIndex(): number | undefined;
109
- /**
110
- * Add a parent to the chain
111
- */
112
218
  addParent(parent: Ref, index: number): void;
113
- /**
114
- * Remove a parent from the chain
115
- * @param parent - The parent to remove
116
- * @returns true if parent was removed
117
- */
219
+ /** @returns true if parent was found and removed */
118
220
  removeParent(parent?: Ref): boolean;
119
- /**
120
- * Find a specific parent in the chain
121
- */
122
221
  findParent(predicate: (parent: Ref, index: number) => boolean): ParentChain | undefined;
123
- /**
124
- * Check if this ChangeTree has a specific parent
125
- */
126
222
  hasParent(predicate: (parent: Ref, index: number) => boolean): boolean;
127
- /**
128
- * Get all parents as an array (for debugging/testing)
129
- */
130
223
  getAllParents(): Array<{
131
224
  ref: Ref;
132
225
  index: number;
133
226
  }>;
134
227
  }
228
+ /**
229
+ * Lightweight per-instance no-op ChangeTree used for instances the decoder
230
+ * builds. Those instances never feed back into an Encoder, so the full
231
+ * `ChangeTree` machinery (EncodeDescriptor lookup, recorder state, Maps /
232
+ * Uint8Arrays for change slots) is pure overhead — this stub carries only a
233
+ * `ref` back-pointer and no-op methods, so tree walkers and debug tooling
234
+ * continue to work.
235
+ *
236
+ * Plug-in contract: each collection class and the `Decoder` pick between
237
+ * `new ChangeTree(ref)` and `createUntrackedChangeTree(ref)` explicitly via
238
+ * dedicated factories (`initializeForDecoder` on collections,
239
+ * `createInstanceOfType` on the `Decoder`). There is no global state — every
240
+ * decision is local to the call site.
241
+ */
242
+ export declare class UntrackedChangeTree {
243
+ ref: Ref;
244
+ root: undefined;
245
+ parentRef: undefined;
246
+ paused: boolean;
247
+ isNew: boolean;
248
+ flags: number;
249
+ constructor(ref: Ref);
250
+ change(): void;
251
+ delete(): void;
252
+ indexedOperation(): void;
253
+ operation(): void;
254
+ setParent(): void;
255
+ addParent(): void;
256
+ removeParent(): boolean;
257
+ getChange(): number;
258
+ discard(): void;
259
+ discardAll(): void;
260
+ pause(): void;
261
+ resume(): void;
262
+ untracked<T>(fn: () => T): T;
263
+ markDirty(): void;
264
+ forEachChild(callback: (change: any, at: any) => void): void;
265
+ forEachChildWithCtx<C>(ctx: C, callback: (ctx: C, change: any, at: any) => void): void;
266
+ forEachLive(): void;
267
+ forEachLiveWithCtx(): void;
268
+ forEach(): void;
269
+ }
270
+ export declare function createUntrackedChangeTree(ref: Ref): ChangeTree;
271
+ /**
272
+ * Install a non-enumerable `$changes: UntrackedChangeTree` on `target`.
273
+ * Shared by `Schema.initializeForDecoder` and every collection's
274
+ * `initializeForDecoder`. `publicRef` defaults to `target` — pass a Proxy
275
+ * instead (ArraySchema) so children attached to this tree see the Proxy
276
+ * as their parent, not the raw target.
277
+ *
278
+ * `enumerable: false` is load-bearing — tests use `deepStrictEqual` on
279
+ * decoded instances and walking into `$changes` would recurse through
280
+ * circular refs. Same descriptor shape as the tracked `Schema.initialize`
281
+ * + collection ctors.
282
+ */
283
+ export declare function installUntrackedChangeTree(target: object, publicRef?: object): void;
@@ -0,0 +1,63 @@
1
+ import type { StateView } from "./StateView.js";
2
+ import type { EncodeOperation } from "./EncodeOperation.js";
3
+ export interface EncodeDescriptor {
4
+ encoder: EncodeOperation;
5
+ filter: ((ref: any, index: number, view?: StateView) => boolean) | undefined;
6
+ metadata: any;
7
+ isSchema: boolean;
8
+ /**
9
+ * Bit i set iff field i has a @view tag. 0 for collection trees.
10
+ * Lets `encodeChangeCb` do a single bitwise op instead of a
11
+ * per-field metadata[i]?.tag chase.
12
+ */
13
+ filterBitmask: number;
14
+ /**
15
+ * Class-level "any field has the flag" booleans + per-field bitmasks.
16
+ * Hot path: per-mutation `_routeAndRecord` calls `isFieldStatic` and
17
+ * `isFieldUnreliable`. The common case is "no static/unreliable fields
18
+ * anywhere on this class" (booleans short-circuit before the symbol-keyed
19
+ * metadata lookup); the secondary common case is "this class has some
20
+ * such fields and we need to know if THIS field is one" — the bitmask
21
+ * answers in one bitwise op instead of an `Array.includes` linear scan.
22
+ *
23
+ * Bitmasks cover fields 0–31 only (matches the `filterBitmask` limitation).
24
+ * Fields ≥32 fall back to `Metadata.hasXAtIndex` — same handling as the
25
+ * filter-bitmask path.
26
+ */
27
+ hasAnyStatic: boolean;
28
+ hasAnyUnreliable: boolean;
29
+ hasAnyStream: boolean;
30
+ /**
31
+ * Class-level "any field carries a `@view` tag" — covers fields both
32
+ * within and beyond index 31 (unlike `filterBitmask`, which only
33
+ * captures the low 32). Read by `ChangeTree.hasFilteredFields` to
34
+ * decide whether a parent tree must be included in a view's bootstrap.
35
+ */
36
+ hasAnyView: boolean;
37
+ staticBitmask: number;
38
+ unreliableBitmask: number;
39
+ /**
40
+ * Bit i set iff field i holds a `t.stream(...)` collection. Hot encode
41
+ * path reads this to dispatch stream fields into the priority/budget
42
+ * gate instead of the normal recorder iteration.
43
+ */
44
+ streamBitmask: number;
45
+ /**
46
+ * Per-field parallel arrays — Schemas only (empty arrays for
47
+ * collections). Replaces hot-path `metadata[i].name` / `metadata[i].type`
48
+ * / `metadata[i].tag` chains with direct array indexing on a small
49
+ * fixed-shape object.
50
+ *
51
+ * Sparse where natural: `tags[i]` is undefined unless field i carries
52
+ * a @view tag; readers should null-check before comparing.
53
+ *
54
+ * `encoders[i]` mirrors `metadata[$encoders]` — the pre-computed
55
+ * encoder fn for primitive-typed fields. Cached here so encode loops
56
+ * skip a `metadata[$encoders]?.[i]` symbol-chain per emission.
57
+ */
58
+ names: string[];
59
+ types: any[];
60
+ tags: (number | undefined)[];
61
+ encoders: (((bytes: Uint8Array, value: any, it: any) => void) | undefined)[];
62
+ }
63
+ export declare function getEncodeDescriptor(ref: any): EncodeDescriptor;
@@ -4,16 +4,39 @@ import type { Encoder } from "./Encoder.js";
4
4
  import type { Iterator } from "../encoding/decode.js";
5
5
  import type { Metadata } from "../Metadata.js";
6
6
  export type EncodeOperation<T extends Ref = any> = (encoder: Encoder, bytes: Uint8Array, changeTree: ChangeTree<T>, index: number, operation: OPERATION, it: Iterator, isEncodeAll: boolean, hasView: boolean, metadata?: Metadata) => void;
7
- export declare function encodeValue(encoder: Encoder, bytes: Uint8Array, type: any, value: any, operation: OPERATION, it: Iterator): void;
7
+ export declare function encodeValue(encoder: Encoder, bytes: Uint8Array, type: any, value: any, operation: OPERATION, it: Iterator, encoderFn?: (bytes: Uint8Array, value: any, it: Iterator) => void): void;
8
8
  /**
9
9
  * Used for Schema instances.
10
10
  * @private
11
11
  */
12
12
  export declare const encodeSchemaOperation: EncodeOperation;
13
13
  /**
14
- * Used for collections (MapSchema, CollectionSchema, SetSchema)
14
+ * Encode a single MapSchema entry. Splits the legacy
15
+ * `encodeKeyValueOperation` so the per-emission `typeof ref['set']` check
16
+ * is gone — MapSchema instances are routed here via their `[$encoder]`
17
+ * static, the dynamic-key string emission is unconditional on ADD.
18
+ *
15
19
  * @private
16
20
  */
21
+ export declare const encodeMapEntry: EncodeOperation;
22
+ /**
23
+ * Encode a single SetSchema / CollectionSchema entry. Wire format is the
24
+ * same as MapSchema minus the dynamic-key string, so this path skips the
25
+ * legacy `typeof ref['set']` check entirely.
26
+ *
27
+ * @private
28
+ */
29
+ export declare const encodeIndexedEntry: EncodeOperation;
30
+ /**
31
+ * Unified encoder kept for back-compat with external consumers that may
32
+ * have registered it directly via `static [$encoder] =
33
+ * encodeKeyValueOperation`. New code (and all internal collections)
34
+ * should use the split variants — `encodeMapEntry` for MapSchema and
35
+ * `encodeIndexedEntry` for SetSchema / CollectionSchema.
36
+ *
37
+ * The runtime `typeof ref['set']` check below is the per-emission cost
38
+ * the split is designed to remove.
39
+ */
17
40
  export declare const encodeKeyValueOperation: EncodeOperation;
18
41
  /**
19
42
  * Used for collections (MapSchema, ArraySchema, etc.)