@colyseus/schema 5.0.5 → 5.0.8

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 (69) hide show
  1. package/build/Reflection.d.ts +53 -0
  2. package/build/Schema.d.ts +26 -1
  3. package/build/annotations.d.ts +8 -1
  4. package/build/codegen/cli.cjs +74 -64
  5. package/build/codegen/cli.cjs.map +1 -1
  6. package/build/codegen/types.d.ts +0 -1
  7. package/build/decoder/Decoder.d.ts +28 -0
  8. package/build/decoder/Resync.d.ts +61 -0
  9. package/build/encoder/ChangeTree.d.ts +15 -0
  10. package/build/encoder/Encoder.d.ts +13 -0
  11. package/build/encoder/Pool.d.ts +62 -0
  12. package/build/encoder/Root.d.ts +5 -4
  13. package/build/encoder/StateView.d.ts +12 -3
  14. package/build/index.cjs +1192 -284
  15. package/build/index.cjs.map +1 -1
  16. package/build/index.d.ts +4 -1
  17. package/build/index.js +1192 -284
  18. package/build/index.mjs +1189 -285
  19. package/build/index.mjs.map +1 -1
  20. package/build/input/InputDecoder.d.ts +9 -4
  21. package/build/input/InputEncoder.d.ts +44 -53
  22. package/build/input/index.cjs +102 -7321
  23. package/build/input/index.cjs.map +1 -1
  24. package/build/input/index.mjs +97 -7316
  25. package/build/input/index.mjs.map +1 -1
  26. package/build/types/HelperTypes.d.ts +25 -0
  27. package/build/types/builder.d.ts +57 -4
  28. package/build/types/custom/ArraySchema.d.ts +11 -1
  29. package/build/types/custom/CollectionSchema.d.ts +9 -1
  30. package/build/types/custom/MapSchema.d.ts +9 -1
  31. package/build/types/custom/SetSchema.d.ts +9 -1
  32. package/build/types/custom/StreamSchema.d.ts +2 -1
  33. package/build/types/quantize.d.ts +102 -0
  34. package/build/types/symbols.d.ts +15 -0
  35. package/package.json +8 -1
  36. package/src/Metadata.ts +34 -9
  37. package/src/Reflection.ts +49 -11
  38. package/src/Schema.ts +64 -2
  39. package/src/annotations.ts +133 -51
  40. package/src/bench_churn.ts +121 -0
  41. package/src/codegen/languages/haxe.ts +0 -16
  42. package/src/codegen/parser.ts +29 -11
  43. package/src/codegen/types.ts +45 -63
  44. package/src/decoder/DecodeOperation.ts +46 -4
  45. package/src/decoder/Decoder.ts +48 -0
  46. package/src/decoder/ReferenceTracker.ts +9 -5
  47. package/src/decoder/Resync.ts +170 -0
  48. package/src/encoder/ChangeTree.ts +59 -0
  49. package/src/encoder/Encoder.ts +53 -12
  50. package/src/encoder/Pool.ts +90 -0
  51. package/src/encoder/Root.ts +22 -32
  52. package/src/encoder/StateView.ts +101 -50
  53. package/src/encoder/changeTree/liveIteration.ts +4 -0
  54. package/src/encoder/changeTree/treeAttachment.ts +28 -24
  55. package/src/index.ts +12 -2
  56. package/src/input/InputDecoder.ts +11 -5
  57. package/src/input/InputEncoder.ts +85 -126
  58. package/src/types/HelperTypes.ts +30 -0
  59. package/src/types/TypeContext.ts +7 -0
  60. package/src/types/builder.ts +62 -5
  61. package/src/types/custom/ArraySchema.ts +70 -12
  62. package/src/types/custom/CollectionSchema.ts +36 -1
  63. package/src/types/custom/MapSchema.ts +50 -1
  64. package/src/types/custom/SetSchema.ts +36 -1
  65. package/src/types/custom/StreamSchema.ts +7 -0
  66. package/src/types/quantize.ts +173 -0
  67. package/src/types/symbols.ts +16 -0
  68. package/build/encoder/RefIdAllocator.d.ts +0 -35
  69. package/src/encoder/RefIdAllocator.ts +0 -68
@@ -9,7 +9,6 @@ export declare class Context {
9
9
  enums: Enum[];
10
10
  };
11
11
  addStructure(structure: IStructure): void;
12
- private getParentClass;
13
12
  private isSchemaClass;
14
13
  }
15
14
  export interface IStructure {
@@ -11,9 +11,37 @@ export declare class Decoder<T extends IRef = any> {
11
11
  root: ReferenceTracker;
12
12
  currentRefId: number;
13
13
  triggerChanges?: (allChanges: DataChange[]) => void;
14
+ /**
15
+ * @internal Non-null only while a `decodeResync()` walk is in progress:
16
+ * collection refId → entry identities the payload visited (map string
17
+ * keys; array/set/collection/stream indexes). Written by the collection
18
+ * DecodeOperation functions, read by the post-decode sweep.
19
+ */
20
+ resyncVisited: Map<number, Set<number | string>> | null;
21
+ /**
22
+ * @internal Set when a structure had to be skipped during a resync
23
+ * decode — visited data is incomplete, so the sweep must not delete.
24
+ */
25
+ resyncDamaged: boolean;
14
26
  constructor(root: T, context?: TypeContext);
15
27
  protected setState(root: T): void;
16
28
  decode(bytes: Uint8Array, it?: Iterator, ref?: IRef): DataChange<any, string>[];
29
+ /**
30
+ * Full-snapshot reconciliation ("resync") decode.
31
+ *
32
+ * Behaves exactly like {@link decode}, plus: every collection entry the
33
+ * payload does NOT mention is removed through the regular DELETE path —
34
+ * `onRemove` callbacks fire with the real previous value and released
35
+ * refs are garbage-collected. Use it to apply a rejoin/reconnect full
36
+ * state over an existing decoded tree: DELETEs that happened while the
37
+ * client was off the wire are reconciled as if they had been received,
38
+ * while surviving entries keep their instance identity and callbacks.
39
+ *
40
+ * ONLY valid for full-snapshot payloads (`encodeAll` / `encodeAllView`
41
+ * output). Calling it on an incremental patch would prune everything
42
+ * the patch doesn't touch.
43
+ */
44
+ decodeResync(bytes: Uint8Array, it?: Iterator): DataChange<any, string>[];
17
45
  skipCurrentStructure(bytes: Uint8Array, it: Iterator, totalBytes: number): void;
18
46
  getInstanceType(bytes: Uint8Array, it: Iterator, defaultType: typeof Schema): typeof Schema;
19
47
  createInstanceOfType(type: typeof Schema): Schema;
@@ -0,0 +1,61 @@
1
+ import { OPERATION } from "../encoding/spec.js";
2
+ import type { Decoder } from "./Decoder.js";
3
+ import type { DataChange } from "./DecodeOperation.js";
4
+ /**
5
+ * Resync ("full-snapshot reconciliation") support for {@link Decoder.decodeResync}.
6
+ *
7
+ * A rejoin snapshot is authoritative for everything it contains — but the
8
+ * plain decode path is additive: entries deleted (or hidden by a view)
9
+ * while the client was off the wire survive as ghosts. This module owns the
10
+ * generic reconciliation algorithm:
11
+ *
12
+ * - during the decode walk, the collection DecodeOperation functions report
13
+ * every entry the payload touches ({@link resyncRecordVisit}) and every
14
+ * collection that appears at all ({@link resyncMarkPresent});
15
+ * - after the walk, {@link resyncSweep} removes whatever was never reported,
16
+ * through the same DELETE bookkeeping the regular decode path uses
17
+ * (DataChange DELETE → onRemove; removeRef → GC).
18
+ *
19
+ * Storage-specific pruning (journal upkeep, array compaction, stream
20
+ * exemption) lives on each collection class as `[$resyncPrune]` — declared
21
+ * on the `Collection` interface, so every collection kind must state its
22
+ * own sweep semantics.
23
+ *
24
+ * All entry points are guarded by `decoder.resyncVisited !== null` at the
25
+ * call sites — the normal decode path never pays for any of this.
26
+ */
27
+ /**
28
+ * Record that the current structure's entry at `identity` (map string key /
29
+ * element index) appeared in the payload — even when its value is unchanged
30
+ * (`allChanges` cannot serve as this record: its pushes are guarded by
31
+ * `previousValue !== value`, so unchanged entries would look unvisited).
32
+ *
33
+ * Also releases a replaced occupant: full-sync emits plain ADD (never
34
+ * DELETE_AND_ADD), so an entry whose instance changed while this client was
35
+ * off the wire would otherwise leak its previous ref (no onRemove, never
36
+ * GC'd). This release is correct ONLY under a full snapshot — a live patch's
37
+ * plain ADD over a different instance can be a positional rewrite (array
38
+ * shift/unshift) where the occupant *moved* and is still alive; a snapshot
39
+ * re-adds moved instances elsewhere, so the refcounts balance.
40
+ */
41
+ export declare function resyncTouchEntry(decoder: Decoder, ref: any, operation: OPERATION, identity: number | string, previousValue: any, value: any, allChanges: DataChange[] | null): void;
42
+ /**
43
+ * Mark a collection as present in the payload — even with zero entries.
44
+ * The sweep only prunes collections reported here: absence means "not part
45
+ * of full-sync" (@transient, view-invisible), where pruning would destroy
46
+ * live data. Reflected clients have no @transient metadata, so payload
47
+ * presence is the only reliable signal.
48
+ */
49
+ export declare function resyncMarkPresent(decoder: Decoder, refId: number): void;
50
+ /**
51
+ * Post-decode phase of {@link Decoder.decodeResync}: remove every collection
52
+ * entry the snapshot did not visit.
53
+ *
54
+ * Walks the tree from the root — NOT `root.refs` — for three reasons:
55
+ * `@transient` fields are never part of a snapshot and must be left alone;
56
+ * entries of subtrees removed by the sweep itself are left to the GC's
57
+ * transitive walk (sweeping them directly would double-decrement shared
58
+ * children); and collections the snapshot never mentions (emptied
59
+ * server-side) are still reachable and get pruned.
60
+ */
61
+ export declare function resyncSweep(decoder: Decoder, allChanges: DataChange[] | null): void;
@@ -61,6 +61,7 @@ export interface ParentChain {
61
61
  export declare const IS_FILTERED = 1, IS_VISIBILITY_SHARED = 2, IS_NEW = 4;
62
62
  export declare const IS_UNRELIABLE = 8, IS_TRANSIENT = 16, IS_STATIC = 32;
63
63
  export declare const IS_STREAM_COLLECTION = 64;
64
+ export declare const NEEDS_RESTAGE = 128;
64
65
  /**
65
66
  * Flags a child inherits from its parent's own transitive state via
66
67
  * `checkInheritedFlags`. Read as a bitwise mask so the inheritance step
@@ -154,6 +155,8 @@ export declare class ChangeTree<T extends Ref = any> implements ChangeRecorder {
154
155
  set isStatic(v: boolean);
155
156
  get isStreamCollection(): boolean;
156
157
  set isStreamCollection(v: boolean);
158
+ get needsRestage(): boolean;
159
+ set needsRestage(v: boolean);
157
160
  get hasFilteredFields(): boolean;
158
161
  ensureUnreliableRecorder(): ChangeRecorder;
159
162
  isFieldUnreliable(index: number): boolean;
@@ -174,6 +177,18 @@ export declare class ChangeTree<T extends Ref = any> implements ChangeRecorder {
174
177
  size(): number;
175
178
  has(): boolean;
176
179
  reset(): void;
180
+ /**
181
+ * Full reset to construction defaults so the owning ref can be returned to
182
+ * a pool and reused for a different logical entity (see encoder/Pool.ts +
183
+ * Schema.reset). Unlike `reset()` / `endEncode()` (which only clear the
184
+ * dirty bucket for the next encode), this also drops parent links, queue
185
+ * nodes and per-view bitmaps, and re-arms IS_NEW.
186
+ *
187
+ * Precondition: the tree must already be detached from the encoder
188
+ * (`root === undefined`) — i.e. the ref was removed from its parent
189
+ * collection/field, which `Root.remove` does before this runs.
190
+ */
191
+ recycle(): void;
177
192
  shift(shiftIndex: number): void;
178
193
  setRoot(root: Root): void;
179
194
  setParent(parent: Ref, root?: Root, parentIndex?: number): void;
@@ -4,6 +4,17 @@ import type { Iterator } from "../encoding/decode.js";
4
4
  import { Root } from "./Root.js";
5
5
  import type { StateView } from "./StateView.js";
6
6
  export declare class Encoder<T extends Schema = any> {
7
+ /**
8
+ * Per-encoder shared output buffer size. The encoder auto-grows on
9
+ * overflow and logs a one-time warning suggesting a higher value, so
10
+ * the default just needs to comfortably cover typical room state.
11
+ *
12
+ * Sized to fit ~100 items in a `MapSchema<{x,y,z}>` keyed by
13
+ * `nanoid(9)` (~4.5 KB worst-case full encode, float64-heavy) with
14
+ * ~3-4× headroom for surrounding state (player list, world refs,
15
+ * etc.). Raise per app via `Encoder.BUFFER_SIZE = N * 1024` before
16
+ * constructing any Encoder.
17
+ */
7
18
  static BUFFER_SIZE: number;
8
19
  sharedBuffer: Uint8Array;
9
20
  context: TypeContext;
@@ -38,6 +49,8 @@ export declare class Encoder<T extends Schema = any> {
38
49
  private _resizeBuffer;
39
50
  encodeAll(it?: Iterator, buffer?: Uint8Array): Uint8Array<ArrayBufferLike>;
40
51
  encodeAllView(view: StateView, sharedOffset: number, it: Iterator, bytes?: Uint8Array): Uint8Array<ArrayBufferLike>;
52
+ /** Grow `buffer` to keep BUFFER_SIZE free bytes past `offset`, preserving `[0, offset)`. */
53
+ protected ensureCapacity(buffer: Uint8Array, offset: number): Uint8Array;
41
54
  encodeView(view: StateView, sharedOffset: number, it: Iterator, bytes?: Uint8Array): Uint8Array<ArrayBufferLike>;
42
55
  /**
43
56
  * Per-view unreliable encode. Walks `root.unreliableChanges` and emits
@@ -0,0 +1,62 @@
1
+ import { Schema } from "../Schema.js";
2
+ /**
3
+ * Server-side object pool for Schema instances.
4
+ *
5
+ * Constructing a Schema is comparatively expensive (per-instance ChangeTree +
6
+ * `Object.defineProperty($changes)` + values array, multiplied by the number
7
+ * of instances in the entity tree). For workloads that spawn/despawn entities
8
+ * (ECS, matchmaking bots, projectiles), reusing instances avoids that cost.
9
+ *
10
+ * The pool relies on {@link Schema.reset} to return a detached instance to a
11
+ * pristine, construction-default state — including dropping its `$refId` so a
12
+ * re-add re-acquires a fresh id (making reuse wire-format-identical to `new`).
13
+ *
14
+ * Lifecycle:
15
+ * ```ts
16
+ * const pool = createPool(Entity, { preallocate: 1000 });
17
+ *
18
+ * // spawn
19
+ * const e = pool.acquire();
20
+ * e.x = 10; e.y = 20; // re-assign fields (pool does NOT reset primitives)
21
+ * state.entities.set(id, e);
22
+ *
23
+ * // despawn — remove from the state tree FIRST, then release
24
+ * state.entities.delete(id);
25
+ * pool.release(e);
26
+ * ```
27
+ *
28
+ * Not supported (throws on release): instances shared across multiple parents,
29
+ * `@stream` collections, and decoder-side (mirror) instances.
30
+ */
31
+ export interface PoolOptions {
32
+ /** Construct this many instances up front, so the first spawns reuse rather than allocate. */
33
+ preallocate?: number;
34
+ /**
35
+ * Cap on retained free instances. Releases beyond the cap are dropped (and
36
+ * garbage-collected), keeping a long-running pool bounded. Default: no cap.
37
+ */
38
+ maxSize?: number;
39
+ }
40
+ /**
41
+ * Object pool for Schema instances. Exported as a **type only** — construct one
42
+ * via {@link createPool}, which is the public entry point. The class is public
43
+ * for typing (`SchemaPool<Entity>` annotations) and `instanceof` checks.
44
+ */
45
+ export declare class SchemaPool<T extends Schema> {
46
+ private readonly _free;
47
+ private readonly _factory;
48
+ private readonly _maxSize;
49
+ constructor(factory: () => T, opts?: PoolOptions);
50
+ /** Pop a pre-reset free instance, or construct a fresh one. */
51
+ acquire(): T;
52
+ /**
53
+ * Reset `instance` to construction defaults and return it to the pool.
54
+ * PRECONDITION: the instance must already be detached from the state tree
55
+ * (removed from its parent collection/field, so the encoder released it).
56
+ */
57
+ release(instance: T): void;
58
+ /** Number of instances currently available for reuse. */
59
+ get size(): number;
60
+ }
61
+ /** Convenience factory: `createPool(Entity, { preallocate: 64 })`. */
62
+ export declare function createPool<T extends Schema>(ctor: new () => T, opts?: PoolOptions): SchemaPool<T>;
@@ -1,7 +1,6 @@
1
1
  import { TypeContext } from "../types/TypeContext.js";
2
2
  import { ChangeTree, ChangeTreeList, type ChangeTreeNode } from "./ChangeTree.js";
3
3
  import { $changes, $refId } from "../types/symbols.js";
4
- import { RefIdAllocator } from "./RefIdAllocator.js";
5
4
  import type { StateView } from "./StateView.js";
6
5
  import type { StreamableState } from "./streaming.js";
7
6
  /**
@@ -22,10 +21,12 @@ export interface Streamable {
22
21
  export declare class Root {
23
22
  types: TypeContext;
24
23
  /**
25
- * Allocates and recycles refIds. See `RefIdAllocator` for the reuse
26
- * pool semantics (one-tick defer + resurrection).
24
+ * Monotonic refId counter. RefIds are never recycled — a refId is a
25
+ * stable identity for the lifetime of the room, so a client that
26
+ * missed DELETEs (reconnect) can never see an old id rebound to a
27
+ * different instance. DevMode reads/writes this across HMR cycles.
27
28
  */
28
- readonly refIds: RefIdAllocator;
29
+ protected nextUniqueId: number;
29
30
  refCount: {
30
31
  [id: number]: number;
31
32
  };
@@ -70,11 +70,20 @@ export declare class StateView {
70
70
  markInvisible(tree: ChangeTree): void;
71
71
  /** Clear invisible bit. */
72
72
  unmarkInvisible(tree: ChangeTree): void;
73
- /** True iff this view has `tag` associated with `tree`. */
73
+ /**
74
+ * True iff this view shares at least one tag bit with `tree`.
75
+ *
76
+ * `tagViews` is keyed by individual power-of-two bits (custom tags must
77
+ * be powers of two; `@view(A|B)` field masks are decomposed on store).
78
+ * A field whose mask is `tag` is visible if the view was `add()`ed with
79
+ * any overlapping bit — so we walk `tag`'s set bits and return on the
80
+ * first match. Passing DEFAULT_VIEW_TAG (-1, all bits) answers "does
81
+ * this view hold ANY custom tag on the tree".
82
+ */
74
83
  hasTagOnTree(tree: ChangeTree, tag: number): boolean;
75
- /** Mark `tree` as carrying `tag` for this view. */
84
+ /** Mark `tree` as carrying `tag` (each of its bits) for this view. */
76
85
  addTag(tree: ChangeTree, tag: number): void;
77
- /** Clear this view's `tag` bit on `tree`. */
86
+ /** Clear each of `tag`'s bits for this view on `tree`. */
78
87
  removeTag(tree: ChangeTree, tag: number): void;
79
88
  /** Clear ALL tag bits this view holds on `tree` (used when the per-tag isn't known). */
80
89
  removeAllTagsOnTree(tree: ChangeTree): void;