@colyseus/schema 5.0.3 → 5.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/codegen/cli.cjs +23 -0
- package/build/codegen/cli.cjs.map +1 -1
- package/build/decoder/strategy/Callbacks.d.ts +6 -6
- package/build/encoder/StateView.d.ts +17 -0
- package/build/index.cjs +184 -37
- package/build/index.cjs.map +1 -1
- package/build/index.js +184 -37
- package/build/index.mjs +184 -37
- package/build/index.mjs.map +1 -1
- package/build/input/index.cjs +28 -4
- package/build/input/index.cjs.map +1 -1
- package/build/input/index.mjs +28 -4
- package/build/input/index.mjs.map +1 -1
- package/build/types/builder.d.ts +83 -29
- package/package.json +6 -5
- package/src/annotations.ts +48 -18
- package/src/codegen/languages/csharp.ts +24 -0
- package/src/decoder/strategy/Callbacks.ts +16 -14
- package/src/encoder/Encoder.ts +13 -2
- package/src/encoder/StateView.ts +63 -2
- package/src/encoder/changeTree/inheritedFlags.ts +16 -2
- package/src/types/builder.ts +82 -20
package/build/index.js
CHANGED
|
@@ -2114,11 +2114,24 @@
|
|
|
2114
2114
|
const refType = Metadata.isValidInstance(tree.ref)
|
|
2115
2115
|
? tree.ref.constructor
|
|
2116
2116
|
: tree.ref[$childType];
|
|
2117
|
+
// #218: nested Schema fields inherit visibility from a @view-gated
|
|
2118
|
+
// parent regardless of whether the parent is a collection. The
|
|
2119
|
+
// `parentIsCollection` constraint that used to live here blocked
|
|
2120
|
+
// nested-Schema-field-of-@view-tagged-Schema from sharing visibility,
|
|
2121
|
+
// forcing users to wrap the child in an ArraySchema as a workaround.
|
|
2122
|
+
//
|
|
2123
|
+
// #226 (4.0.25): items inside a non-default-tag `@view(N)` collection
|
|
2124
|
+
// also inherit visibility from the parent collection, so items
|
|
2125
|
+
// pushed/set after `view.add(state, N)` show up automatically.
|
|
2126
|
+
// Default-tag `@view()` collections keep per-item gating —
|
|
2127
|
+
// `view.add(item)` is still required to opt each one in.
|
|
2128
|
+
// The `parentMetadata[parentIndex].tag` access is safe inside the
|
|
2129
|
+
// `fieldHasViewTag` short-circuit (the metadata entry and its `tag`
|
|
2130
|
+
// are guaranteed to exist when that flag is set).
|
|
2117
2131
|
tree.isVisibilitySharedWithParent = (parentChangeTree.isFiltered
|
|
2118
2132
|
&& typeof refType !== "string"
|
|
2119
|
-
&& !fieldHasViewTag
|
|
2120
2133
|
&& !fieldHasStream
|
|
2121
|
-
&& parentIsCollection);
|
|
2134
|
+
&& (!fieldHasViewTag || (parentIsCollection && parentMetadata[parentIndex].tag !== DEFAULT_VIEW_TAG)));
|
|
2122
2135
|
}
|
|
2123
2136
|
}
|
|
2124
2137
|
|
|
@@ -5451,8 +5464,10 @@
|
|
|
5451
5464
|
*/
|
|
5452
5465
|
class FieldBuilder {
|
|
5453
5466
|
[$builder] = true;
|
|
5454
|
-
// Internal configuration.
|
|
5455
|
-
//
|
|
5467
|
+
// Internal configuration. Declared `private` (soft-private): hidden from
|
|
5468
|
+
// editor autocomplete and from normal external `.field` access, but still
|
|
5469
|
+
// reachable at runtime via element access (e.g. `builder['_noSync']`) for
|
|
5470
|
+
// internal tooling/tests. Not meant to be mutated by end users.
|
|
5456
5471
|
_type;
|
|
5457
5472
|
_default = undefined;
|
|
5458
5473
|
_hasDefault = false;
|
|
@@ -5465,6 +5480,7 @@
|
|
|
5465
5480
|
_static = false;
|
|
5466
5481
|
_stream = false;
|
|
5467
5482
|
_optional = false;
|
|
5483
|
+
_noSync = false;
|
|
5468
5484
|
_streamPriority = undefined;
|
|
5469
5485
|
constructor(type) {
|
|
5470
5486
|
this._type = type;
|
|
@@ -5515,6 +5531,30 @@
|
|
|
5515
5531
|
this._static = true;
|
|
5516
5532
|
return this;
|
|
5517
5533
|
}
|
|
5534
|
+
/**
|
|
5535
|
+
* Mark this field as **local-only** — it is typed and initialized on the
|
|
5536
|
+
* instance (so `.default()` and the inferred instance type still apply),
|
|
5537
|
+
* but is never registered for synchronization: it never enters change
|
|
5538
|
+
* tracking, never goes over the wire, and decoders never receive it.
|
|
5539
|
+
*
|
|
5540
|
+
* Useful for server-side scratch state, per-peer UI state, or values you
|
|
5541
|
+
* want on the class for typing convenience without paying any sync cost.
|
|
5542
|
+
*
|
|
5543
|
+
* Mutually exclusive with the sync-only modifiers (`.view()`, `.owned()`,
|
|
5544
|
+
* `.unreliable()`, `.transient()`, `.static()`, `.stream()`) — combining
|
|
5545
|
+
* them throws at `schema()` time.
|
|
5546
|
+
*
|
|
5547
|
+
* ```ts
|
|
5548
|
+
* const Player = schema({
|
|
5549
|
+
* hp: t.uint8().default(100), // synchronized
|
|
5550
|
+
* lastInputTick: t.number().noSync(), // local-only
|
|
5551
|
+
* }, 'Player');
|
|
5552
|
+
* ```
|
|
5553
|
+
*/
|
|
5554
|
+
noSync() {
|
|
5555
|
+
this._noSync = true;
|
|
5556
|
+
return this;
|
|
5557
|
+
}
|
|
5518
5558
|
/**
|
|
5519
5559
|
* Opt a collection field into priority-batched streaming delivery —
|
|
5520
5560
|
* ADDs drain at most `maxPerTick` per tick per view (or per broadcast
|
|
@@ -5569,6 +5609,11 @@
|
|
|
5569
5609
|
this._optional = true;
|
|
5570
5610
|
return this;
|
|
5571
5611
|
}
|
|
5612
|
+
/**
|
|
5613
|
+
* @internal — snapshot of the builder's configuration consumed by
|
|
5614
|
+
* `schema()`. `private` keeps it out of autocomplete; internal callers
|
|
5615
|
+
* reach it via element access (`builder['toDefinition']()`).
|
|
5616
|
+
*/
|
|
5572
5617
|
toDefinition() {
|
|
5573
5618
|
return {
|
|
5574
5619
|
type: this._type,
|
|
@@ -5583,6 +5628,7 @@
|
|
|
5583
5628
|
static: this._static,
|
|
5584
5629
|
stream: this._stream,
|
|
5585
5630
|
optional: this._optional,
|
|
5631
|
+
noSync: this._noSync,
|
|
5586
5632
|
streamPriority: this._streamPriority,
|
|
5587
5633
|
};
|
|
5588
5634
|
}
|
|
@@ -5590,15 +5636,13 @@
|
|
|
5590
5636
|
function isBuilder(value) {
|
|
5591
5637
|
return value != null && value[$builder] === true;
|
|
5592
5638
|
}
|
|
5593
|
-
// ---------------------------------------------------------------------------
|
|
5594
|
-
// Factory helpers
|
|
5595
|
-
// ---------------------------------------------------------------------------
|
|
5596
5639
|
function primitive(name) {
|
|
5597
|
-
return () => new FieldBuilder(name);
|
|
5640
|
+
return (() => new FieldBuilder(name));
|
|
5598
5641
|
}
|
|
5599
5642
|
function resolveChild(child) {
|
|
5600
5643
|
if (isBuilder(child)) {
|
|
5601
|
-
|
|
5644
|
+
// `_type` is private; element access bypasses the visibility check.
|
|
5645
|
+
return child['_type'];
|
|
5602
5646
|
}
|
|
5603
5647
|
return child;
|
|
5604
5648
|
}
|
|
@@ -5608,7 +5652,7 @@
|
|
|
5608
5652
|
const collectionFactory = ((child) => new FieldBuilder({ collection: resolveChild(child) }));
|
|
5609
5653
|
const streamFactory = ((child) => {
|
|
5610
5654
|
const b = new FieldBuilder({ stream: resolveChild(child) });
|
|
5611
|
-
b
|
|
5655
|
+
b['_stream'] = true; // element access bypasses `private`
|
|
5612
5656
|
return b;
|
|
5613
5657
|
});
|
|
5614
5658
|
const refFactory = ((ctor) => new FieldBuilder(ctor));
|
|
@@ -6037,6 +6081,36 @@
|
|
|
6037
6081
|
});
|
|
6038
6082
|
};
|
|
6039
6083
|
}
|
|
6084
|
+
/**
|
|
6085
|
+
* Produce the auto-instantiated construction default for a builder type
|
|
6086
|
+
* (empty collection or zero-arg Schema ref), or `undefined` when the type
|
|
6087
|
+
* has no auto-default. Shared by synced and `.noSync()` field handling.
|
|
6088
|
+
*/
|
|
6089
|
+
function autoInstantiateDefault(rawType) {
|
|
6090
|
+
if (rawType && typeof rawType === "object") {
|
|
6091
|
+
if (rawType.array !== undefined) {
|
|
6092
|
+
return new ArraySchema();
|
|
6093
|
+
}
|
|
6094
|
+
if (rawType.map !== undefined) {
|
|
6095
|
+
return new MapSchema();
|
|
6096
|
+
}
|
|
6097
|
+
if (rawType.set !== undefined) {
|
|
6098
|
+
return new SetSchema();
|
|
6099
|
+
}
|
|
6100
|
+
if (rawType.collection !== undefined) {
|
|
6101
|
+
return new CollectionSchema();
|
|
6102
|
+
}
|
|
6103
|
+
if (rawType.stream !== undefined) {
|
|
6104
|
+
return new StreamSchema();
|
|
6105
|
+
}
|
|
6106
|
+
}
|
|
6107
|
+
else if (typeof rawType === "function" && Schema.is(rawType)) {
|
|
6108
|
+
if (!rawType.prototype.initialize || rawType.prototype.initialize.length === 0) {
|
|
6109
|
+
return new rawType();
|
|
6110
|
+
}
|
|
6111
|
+
}
|
|
6112
|
+
return undefined;
|
|
6113
|
+
}
|
|
6040
6114
|
/**
|
|
6041
6115
|
* Define a Schema class declaratively.
|
|
6042
6116
|
*
|
|
@@ -6072,7 +6146,28 @@
|
|
|
6072
6146
|
for (const fieldName in fieldsAndMethods) {
|
|
6073
6147
|
const value = fieldsAndMethods[fieldName];
|
|
6074
6148
|
if (isBuilder(value)) {
|
|
6075
|
-
const def = value
|
|
6149
|
+
const def = value['toDefinition'](); // private; element access bypasses visibility
|
|
6150
|
+
if (def.noSync) {
|
|
6151
|
+
// Local-only field: skip metadata registration entirely so it is
|
|
6152
|
+
// never encoded/decoded, but still seed its construction default
|
|
6153
|
+
// (honoring `.default()` and collection/ref auto-instantiation).
|
|
6154
|
+
if (def.view !== undefined || def.owned || def.unreliable ||
|
|
6155
|
+
def.transient || def.static || def.stream) {
|
|
6156
|
+
throw new Error(`schema(${name ? `'${name}'` : ""}): field '${fieldName}' uses .noSync() ` +
|
|
6157
|
+
`together with a sync-only modifier (.view/.owned/.unreliable/.transient/.static/.stream). ` +
|
|
6158
|
+
`A local-only field cannot be synchronized.`);
|
|
6159
|
+
}
|
|
6160
|
+
if (def.hasDefault) {
|
|
6161
|
+
defaultValues[fieldName] = def.default;
|
|
6162
|
+
}
|
|
6163
|
+
else if (!def.optional) {
|
|
6164
|
+
const autoDefault = autoInstantiateDefault(def.type);
|
|
6165
|
+
if (autoDefault !== undefined) {
|
|
6166
|
+
defaultValues[fieldName] = autoDefault;
|
|
6167
|
+
}
|
|
6168
|
+
}
|
|
6169
|
+
continue;
|
|
6170
|
+
}
|
|
6076
6171
|
fields[fieldName] = getNormalizedType(def.type);
|
|
6077
6172
|
if (def.view !== undefined) {
|
|
6078
6173
|
viewTagFields[fieldName] = def.view;
|
|
@@ -6107,28 +6202,9 @@
|
|
|
6107
6202
|
else if (!def.optional) {
|
|
6108
6203
|
// Auto-instantiate collection/Schema defaults when none is provided.
|
|
6109
6204
|
// `.optional()` opts out — field starts as undefined.
|
|
6110
|
-
const
|
|
6111
|
-
if (
|
|
6112
|
-
|
|
6113
|
-
defaultValues[fieldName] = new ArraySchema();
|
|
6114
|
-
}
|
|
6115
|
-
else if (rawType.map !== undefined) {
|
|
6116
|
-
defaultValues[fieldName] = new MapSchema();
|
|
6117
|
-
}
|
|
6118
|
-
else if (rawType.set !== undefined) {
|
|
6119
|
-
defaultValues[fieldName] = new SetSchema();
|
|
6120
|
-
}
|
|
6121
|
-
else if (rawType.collection !== undefined) {
|
|
6122
|
-
defaultValues[fieldName] = new CollectionSchema();
|
|
6123
|
-
}
|
|
6124
|
-
else if (rawType.stream !== undefined) {
|
|
6125
|
-
defaultValues[fieldName] = new StreamSchema();
|
|
6126
|
-
}
|
|
6127
|
-
}
|
|
6128
|
-
else if (typeof rawType === "function" && Schema.is(rawType)) {
|
|
6129
|
-
if (!rawType.prototype.initialize || rawType.prototype.initialize.length === 0) {
|
|
6130
|
-
defaultValues[fieldName] = new rawType();
|
|
6131
|
-
}
|
|
6205
|
+
const autoDefault = autoInstantiateDefault(def.type);
|
|
6206
|
+
if (autoDefault !== undefined) {
|
|
6207
|
+
defaultValues[fieldName] = autoDefault;
|
|
6132
6208
|
}
|
|
6133
6209
|
}
|
|
6134
6210
|
}
|
|
@@ -7367,8 +7443,19 @@
|
|
|
7367
7443
|
// selected element is passed to `view.add()` which populates
|
|
7368
7444
|
// view.changes with the stream-link ADD + element-field ADDs.
|
|
7369
7445
|
this._emitStreamPriority(view);
|
|
7370
|
-
//
|
|
7371
|
-
|
|
7446
|
+
//
|
|
7447
|
+
// `view.changes` Map insertion order IS topological order:
|
|
7448
|
+
// - `view.add` walks the parent chain to root via `addParentOf`
|
|
7449
|
+
// (depth-first ancestor-first), inserting every ancestor's
|
|
7450
|
+
// entry before the descendant's.
|
|
7451
|
+
// - `view.remove` calls `_touchAncestorsOf` before its own
|
|
7452
|
+
// write to insert any missing ancestors at the front of the
|
|
7453
|
+
// chain — empty entries that get skipped by the size==0
|
|
7454
|
+
// check below but establish Map position.
|
|
7455
|
+
// No per-encode topo sort needed.
|
|
7456
|
+
//
|
|
7457
|
+
for (const refId of view.changes.keys()) {
|
|
7458
|
+
const changes = view.changes.get(refId);
|
|
7372
7459
|
const changeTree = this.root.changeTrees[refId];
|
|
7373
7460
|
if (changeTree === undefined) {
|
|
7374
7461
|
// detached instance, remove from view and skip.
|
|
@@ -8770,6 +8857,7 @@
|
|
|
8770
8857
|
else if ('decoder' in roomOrDecoder.serializer) {
|
|
8771
8858
|
return getDecoderStateCallbacks(roomOrDecoder.serializer.decoder);
|
|
8772
8859
|
}
|
|
8860
|
+
throw new Error('Invalid room or decoder');
|
|
8773
8861
|
},
|
|
8774
8862
|
getRawChanges(decoder, callback) {
|
|
8775
8863
|
return getRawChangesCallback(decoder, callback);
|
|
@@ -9225,12 +9313,27 @@
|
|
|
9225
9313
|
if (!this.isVisible(changeTree)) {
|
|
9226
9314
|
// view must have all "changeTree" parent tree
|
|
9227
9315
|
this.markVisible(changeTree);
|
|
9228
|
-
//
|
|
9316
|
+
// Recurse all the way to the root regardless of whether the
|
|
9317
|
+
// parent is filtered. Walking the full chain is what makes
|
|
9318
|
+
// `view.changes` topologically ordered by construction — any
|
|
9319
|
+
// filtered ancestor up the chain is touched here, before the
|
|
9320
|
+
// descendant's entry. The actual entry-write below is gated
|
|
9321
|
+
// on `hasFilteredFields` so non-filtered ancestors don't
|
|
9322
|
+
// emit redundant wire bytes (the decoder already knows them
|
|
9323
|
+
// via the shared encode pass). Marking them visible is
|
|
9324
|
+
// still useful: it makes this short-circuit fire on the next
|
|
9325
|
+
// `view.add` instead of re-walking the chain.
|
|
9229
9326
|
const parentChangeTree = changeTree.parent?.[$changes];
|
|
9230
|
-
if (parentChangeTree
|
|
9327
|
+
if (parentChangeTree) {
|
|
9231
9328
|
this.addParentOf(changeTree, tag);
|
|
9232
9329
|
}
|
|
9233
9330
|
}
|
|
9331
|
+
// Skip the entry-write for non-filtered ancestors: their refIds
|
|
9332
|
+
// are already known to the decoder through the shared pass, and
|
|
9333
|
+
// an extra ADD on a non-filtered field's index would only emit
|
|
9334
|
+
// bytes for a no-op (`value === previousValue` on the decoder).
|
|
9335
|
+
if (!changeTree.hasFilteredFields)
|
|
9336
|
+
return;
|
|
9234
9337
|
// add parent's tag properties
|
|
9235
9338
|
if (changeTree.getChange(parentIndex) !== exports.OPERATION.DELETE) {
|
|
9236
9339
|
let changes = this.changes.get(changeTree.ref[$refId]);
|
|
@@ -9242,6 +9345,45 @@
|
|
|
9242
9345
|
changes.set(parentIndex, exports.OPERATION.ADD);
|
|
9243
9346
|
}
|
|
9244
9347
|
}
|
|
9348
|
+
/**
|
|
9349
|
+
* Walk `tree`'s parent chain to root and insert an empty entry into
|
|
9350
|
+
* `view.changes` for any ancestor not already present. Empty entries
|
|
9351
|
+
* are skipped by `encodeView` (`changes.size === 0` continue), so no
|
|
9352
|
+
* wire bytes are emitted — but the Map's insertion order now puts
|
|
9353
|
+
* each ancestor BEFORE the descendant entry that the caller is about
|
|
9354
|
+
* to write. Combined with `addParentOf`'s full-recursion walk on
|
|
9355
|
+
* `view.add`, this preserves the global invariant that
|
|
9356
|
+
* `view.changes` iteration order is topological.
|
|
9357
|
+
*
|
|
9358
|
+
* Iterative (not recursive) so the stack is bounded by tree depth
|
|
9359
|
+
* regardless of call patterns. Stops the walk as soon as it hits an
|
|
9360
|
+
* ancestor that's already in `view.changes` — at that point the
|
|
9361
|
+
* remainder of the chain is guaranteed to also be present (invariant
|
|
9362
|
+
* upheld by every prior caller).
|
|
9363
|
+
*/
|
|
9364
|
+
_touchAncestorsOf(tree) {
|
|
9365
|
+
let cursor = tree.parent?.[$changes];
|
|
9366
|
+
if (cursor === undefined)
|
|
9367
|
+
return;
|
|
9368
|
+
// Collect the missing prefix of the chain, deepest-first. Only
|
|
9369
|
+
// FILTERED ancestors need entries — non-filtered ones never
|
|
9370
|
+
// appear in `view.changes` (mirrors the addParentOf gate), so
|
|
9371
|
+
// they don't need a Map slot reserved either.
|
|
9372
|
+
const stack = [];
|
|
9373
|
+
while (cursor !== undefined) {
|
|
9374
|
+
if (cursor.hasFilteredFields) {
|
|
9375
|
+
const refId = cursor.ref[$refId];
|
|
9376
|
+
if (this.changes.has(refId))
|
|
9377
|
+
break;
|
|
9378
|
+
stack.push(cursor);
|
|
9379
|
+
}
|
|
9380
|
+
cursor = cursor.parent?.[$changes];
|
|
9381
|
+
}
|
|
9382
|
+
// Insert root-first so Map order is topological.
|
|
9383
|
+
for (let i = stack.length - 1; i >= 0; i--) {
|
|
9384
|
+
this.changes.set(stack[i].ref[$refId], new Map());
|
|
9385
|
+
}
|
|
9386
|
+
}
|
|
9245
9387
|
remove(obj, tag = DEFAULT_VIEW_TAG, _isClear = false) {
|
|
9246
9388
|
const changeTree = obj[$changes];
|
|
9247
9389
|
if (!changeTree) {
|
|
@@ -9302,6 +9444,11 @@
|
|
|
9302
9444
|
const ref = changeTree.ref;
|
|
9303
9445
|
const metadata = ref.constructor[Symbol.metadata]; // ArraySchema/MapSchema do not have metadata
|
|
9304
9446
|
const refId = ref[$refId];
|
|
9447
|
+
// Pre-insert any missing ancestors into view.changes so the Map's
|
|
9448
|
+
// iteration order stays topological — the entries we're about to
|
|
9449
|
+
// write (either on this obj, or on its parent collection below)
|
|
9450
|
+
// must come AFTER every ancestor in the chain on the wire.
|
|
9451
|
+
this._touchAncestorsOf(changeTree);
|
|
9305
9452
|
let changes = this.changes.get(refId);
|
|
9306
9453
|
if (changes === undefined) {
|
|
9307
9454
|
changes = new Map();
|
package/build/index.mjs
CHANGED
|
@@ -2108,11 +2108,24 @@ function checkInheritedFlags(tree, parent, parentIndex) {
|
|
|
2108
2108
|
const refType = Metadata.isValidInstance(tree.ref)
|
|
2109
2109
|
? tree.ref.constructor
|
|
2110
2110
|
: tree.ref[$childType];
|
|
2111
|
+
// #218: nested Schema fields inherit visibility from a @view-gated
|
|
2112
|
+
// parent regardless of whether the parent is a collection. The
|
|
2113
|
+
// `parentIsCollection` constraint that used to live here blocked
|
|
2114
|
+
// nested-Schema-field-of-@view-tagged-Schema from sharing visibility,
|
|
2115
|
+
// forcing users to wrap the child in an ArraySchema as a workaround.
|
|
2116
|
+
//
|
|
2117
|
+
// #226 (4.0.25): items inside a non-default-tag `@view(N)` collection
|
|
2118
|
+
// also inherit visibility from the parent collection, so items
|
|
2119
|
+
// pushed/set after `view.add(state, N)` show up automatically.
|
|
2120
|
+
// Default-tag `@view()` collections keep per-item gating —
|
|
2121
|
+
// `view.add(item)` is still required to opt each one in.
|
|
2122
|
+
// The `parentMetadata[parentIndex].tag` access is safe inside the
|
|
2123
|
+
// `fieldHasViewTag` short-circuit (the metadata entry and its `tag`
|
|
2124
|
+
// are guaranteed to exist when that flag is set).
|
|
2111
2125
|
tree.isVisibilitySharedWithParent = (parentChangeTree.isFiltered
|
|
2112
2126
|
&& typeof refType !== "string"
|
|
2113
|
-
&& !fieldHasViewTag
|
|
2114
2127
|
&& !fieldHasStream
|
|
2115
|
-
&& parentIsCollection);
|
|
2128
|
+
&& (!fieldHasViewTag || (parentIsCollection && parentMetadata[parentIndex].tag !== DEFAULT_VIEW_TAG)));
|
|
2116
2129
|
}
|
|
2117
2130
|
}
|
|
2118
2131
|
|
|
@@ -5445,8 +5458,10 @@ registerType("stream", { constructor: StreamSchema });
|
|
|
5445
5458
|
*/
|
|
5446
5459
|
class FieldBuilder {
|
|
5447
5460
|
[$builder] = true;
|
|
5448
|
-
// Internal configuration.
|
|
5449
|
-
//
|
|
5461
|
+
// Internal configuration. Declared `private` (soft-private): hidden from
|
|
5462
|
+
// editor autocomplete and from normal external `.field` access, but still
|
|
5463
|
+
// reachable at runtime via element access (e.g. `builder['_noSync']`) for
|
|
5464
|
+
// internal tooling/tests. Not meant to be mutated by end users.
|
|
5450
5465
|
_type;
|
|
5451
5466
|
_default = undefined;
|
|
5452
5467
|
_hasDefault = false;
|
|
@@ -5459,6 +5474,7 @@ class FieldBuilder {
|
|
|
5459
5474
|
_static = false;
|
|
5460
5475
|
_stream = false;
|
|
5461
5476
|
_optional = false;
|
|
5477
|
+
_noSync = false;
|
|
5462
5478
|
_streamPriority = undefined;
|
|
5463
5479
|
constructor(type) {
|
|
5464
5480
|
this._type = type;
|
|
@@ -5509,6 +5525,30 @@ class FieldBuilder {
|
|
|
5509
5525
|
this._static = true;
|
|
5510
5526
|
return this;
|
|
5511
5527
|
}
|
|
5528
|
+
/**
|
|
5529
|
+
* Mark this field as **local-only** — it is typed and initialized on the
|
|
5530
|
+
* instance (so `.default()` and the inferred instance type still apply),
|
|
5531
|
+
* but is never registered for synchronization: it never enters change
|
|
5532
|
+
* tracking, never goes over the wire, and decoders never receive it.
|
|
5533
|
+
*
|
|
5534
|
+
* Useful for server-side scratch state, per-peer UI state, or values you
|
|
5535
|
+
* want on the class for typing convenience without paying any sync cost.
|
|
5536
|
+
*
|
|
5537
|
+
* Mutually exclusive with the sync-only modifiers (`.view()`, `.owned()`,
|
|
5538
|
+
* `.unreliable()`, `.transient()`, `.static()`, `.stream()`) — combining
|
|
5539
|
+
* them throws at `schema()` time.
|
|
5540
|
+
*
|
|
5541
|
+
* ```ts
|
|
5542
|
+
* const Player = schema({
|
|
5543
|
+
* hp: t.uint8().default(100), // synchronized
|
|
5544
|
+
* lastInputTick: t.number().noSync(), // local-only
|
|
5545
|
+
* }, 'Player');
|
|
5546
|
+
* ```
|
|
5547
|
+
*/
|
|
5548
|
+
noSync() {
|
|
5549
|
+
this._noSync = true;
|
|
5550
|
+
return this;
|
|
5551
|
+
}
|
|
5512
5552
|
/**
|
|
5513
5553
|
* Opt a collection field into priority-batched streaming delivery —
|
|
5514
5554
|
* ADDs drain at most `maxPerTick` per tick per view (or per broadcast
|
|
@@ -5563,6 +5603,11 @@ class FieldBuilder {
|
|
|
5563
5603
|
this._optional = true;
|
|
5564
5604
|
return this;
|
|
5565
5605
|
}
|
|
5606
|
+
/**
|
|
5607
|
+
* @internal — snapshot of the builder's configuration consumed by
|
|
5608
|
+
* `schema()`. `private` keeps it out of autocomplete; internal callers
|
|
5609
|
+
* reach it via element access (`builder['toDefinition']()`).
|
|
5610
|
+
*/
|
|
5566
5611
|
toDefinition() {
|
|
5567
5612
|
return {
|
|
5568
5613
|
type: this._type,
|
|
@@ -5577,6 +5622,7 @@ class FieldBuilder {
|
|
|
5577
5622
|
static: this._static,
|
|
5578
5623
|
stream: this._stream,
|
|
5579
5624
|
optional: this._optional,
|
|
5625
|
+
noSync: this._noSync,
|
|
5580
5626
|
streamPriority: this._streamPriority,
|
|
5581
5627
|
};
|
|
5582
5628
|
}
|
|
@@ -5584,15 +5630,13 @@ class FieldBuilder {
|
|
|
5584
5630
|
function isBuilder(value) {
|
|
5585
5631
|
return value != null && value[$builder] === true;
|
|
5586
5632
|
}
|
|
5587
|
-
// ---------------------------------------------------------------------------
|
|
5588
|
-
// Factory helpers
|
|
5589
|
-
// ---------------------------------------------------------------------------
|
|
5590
5633
|
function primitive(name) {
|
|
5591
|
-
return () => new FieldBuilder(name);
|
|
5634
|
+
return (() => new FieldBuilder(name));
|
|
5592
5635
|
}
|
|
5593
5636
|
function resolveChild(child) {
|
|
5594
5637
|
if (isBuilder(child)) {
|
|
5595
|
-
|
|
5638
|
+
// `_type` is private; element access bypasses the visibility check.
|
|
5639
|
+
return child['_type'];
|
|
5596
5640
|
}
|
|
5597
5641
|
return child;
|
|
5598
5642
|
}
|
|
@@ -5602,7 +5646,7 @@ const setFactory = ((child) => new FieldBuilder({ set: resolveChild(child) }));
|
|
|
5602
5646
|
const collectionFactory = ((child) => new FieldBuilder({ collection: resolveChild(child) }));
|
|
5603
5647
|
const streamFactory = ((child) => {
|
|
5604
5648
|
const b = new FieldBuilder({ stream: resolveChild(child) });
|
|
5605
|
-
b
|
|
5649
|
+
b['_stream'] = true; // element access bypasses `private`
|
|
5606
5650
|
return b;
|
|
5607
5651
|
});
|
|
5608
5652
|
const refFactory = ((ctor) => new FieldBuilder(ctor));
|
|
@@ -6031,6 +6075,36 @@ function deprecated(throws = true) {
|
|
|
6031
6075
|
});
|
|
6032
6076
|
};
|
|
6033
6077
|
}
|
|
6078
|
+
/**
|
|
6079
|
+
* Produce the auto-instantiated construction default for a builder type
|
|
6080
|
+
* (empty collection or zero-arg Schema ref), or `undefined` when the type
|
|
6081
|
+
* has no auto-default. Shared by synced and `.noSync()` field handling.
|
|
6082
|
+
*/
|
|
6083
|
+
function autoInstantiateDefault(rawType) {
|
|
6084
|
+
if (rawType && typeof rawType === "object") {
|
|
6085
|
+
if (rawType.array !== undefined) {
|
|
6086
|
+
return new ArraySchema();
|
|
6087
|
+
}
|
|
6088
|
+
if (rawType.map !== undefined) {
|
|
6089
|
+
return new MapSchema();
|
|
6090
|
+
}
|
|
6091
|
+
if (rawType.set !== undefined) {
|
|
6092
|
+
return new SetSchema();
|
|
6093
|
+
}
|
|
6094
|
+
if (rawType.collection !== undefined) {
|
|
6095
|
+
return new CollectionSchema();
|
|
6096
|
+
}
|
|
6097
|
+
if (rawType.stream !== undefined) {
|
|
6098
|
+
return new StreamSchema();
|
|
6099
|
+
}
|
|
6100
|
+
}
|
|
6101
|
+
else if (typeof rawType === "function" && Schema.is(rawType)) {
|
|
6102
|
+
if (!rawType.prototype.initialize || rawType.prototype.initialize.length === 0) {
|
|
6103
|
+
return new rawType();
|
|
6104
|
+
}
|
|
6105
|
+
}
|
|
6106
|
+
return undefined;
|
|
6107
|
+
}
|
|
6034
6108
|
/**
|
|
6035
6109
|
* Define a Schema class declaratively.
|
|
6036
6110
|
*
|
|
@@ -6066,7 +6140,28 @@ function schema(fieldsAndMethods, name, inherits = Schema) {
|
|
|
6066
6140
|
for (const fieldName in fieldsAndMethods) {
|
|
6067
6141
|
const value = fieldsAndMethods[fieldName];
|
|
6068
6142
|
if (isBuilder(value)) {
|
|
6069
|
-
const def = value
|
|
6143
|
+
const def = value['toDefinition'](); // private; element access bypasses visibility
|
|
6144
|
+
if (def.noSync) {
|
|
6145
|
+
// Local-only field: skip metadata registration entirely so it is
|
|
6146
|
+
// never encoded/decoded, but still seed its construction default
|
|
6147
|
+
// (honoring `.default()` and collection/ref auto-instantiation).
|
|
6148
|
+
if (def.view !== undefined || def.owned || def.unreliable ||
|
|
6149
|
+
def.transient || def.static || def.stream) {
|
|
6150
|
+
throw new Error(`schema(${name ? `'${name}'` : ""}): field '${fieldName}' uses .noSync() ` +
|
|
6151
|
+
`together with a sync-only modifier (.view/.owned/.unreliable/.transient/.static/.stream). ` +
|
|
6152
|
+
`A local-only field cannot be synchronized.`);
|
|
6153
|
+
}
|
|
6154
|
+
if (def.hasDefault) {
|
|
6155
|
+
defaultValues[fieldName] = def.default;
|
|
6156
|
+
}
|
|
6157
|
+
else if (!def.optional) {
|
|
6158
|
+
const autoDefault = autoInstantiateDefault(def.type);
|
|
6159
|
+
if (autoDefault !== undefined) {
|
|
6160
|
+
defaultValues[fieldName] = autoDefault;
|
|
6161
|
+
}
|
|
6162
|
+
}
|
|
6163
|
+
continue;
|
|
6164
|
+
}
|
|
6070
6165
|
fields[fieldName] = getNormalizedType(def.type);
|
|
6071
6166
|
if (def.view !== undefined) {
|
|
6072
6167
|
viewTagFields[fieldName] = def.view;
|
|
@@ -6101,28 +6196,9 @@ function schema(fieldsAndMethods, name, inherits = Schema) {
|
|
|
6101
6196
|
else if (!def.optional) {
|
|
6102
6197
|
// Auto-instantiate collection/Schema defaults when none is provided.
|
|
6103
6198
|
// `.optional()` opts out — field starts as undefined.
|
|
6104
|
-
const
|
|
6105
|
-
if (
|
|
6106
|
-
|
|
6107
|
-
defaultValues[fieldName] = new ArraySchema();
|
|
6108
|
-
}
|
|
6109
|
-
else if (rawType.map !== undefined) {
|
|
6110
|
-
defaultValues[fieldName] = new MapSchema();
|
|
6111
|
-
}
|
|
6112
|
-
else if (rawType.set !== undefined) {
|
|
6113
|
-
defaultValues[fieldName] = new SetSchema();
|
|
6114
|
-
}
|
|
6115
|
-
else if (rawType.collection !== undefined) {
|
|
6116
|
-
defaultValues[fieldName] = new CollectionSchema();
|
|
6117
|
-
}
|
|
6118
|
-
else if (rawType.stream !== undefined) {
|
|
6119
|
-
defaultValues[fieldName] = new StreamSchema();
|
|
6120
|
-
}
|
|
6121
|
-
}
|
|
6122
|
-
else if (typeof rawType === "function" && Schema.is(rawType)) {
|
|
6123
|
-
if (!rawType.prototype.initialize || rawType.prototype.initialize.length === 0) {
|
|
6124
|
-
defaultValues[fieldName] = new rawType();
|
|
6125
|
-
}
|
|
6199
|
+
const autoDefault = autoInstantiateDefault(def.type);
|
|
6200
|
+
if (autoDefault !== undefined) {
|
|
6201
|
+
defaultValues[fieldName] = autoDefault;
|
|
6126
6202
|
}
|
|
6127
6203
|
}
|
|
6128
6204
|
}
|
|
@@ -7361,8 +7437,19 @@ class Encoder {
|
|
|
7361
7437
|
// selected element is passed to `view.add()` which populates
|
|
7362
7438
|
// view.changes with the stream-link ADD + element-field ADDs.
|
|
7363
7439
|
this._emitStreamPriority(view);
|
|
7364
|
-
//
|
|
7365
|
-
|
|
7440
|
+
//
|
|
7441
|
+
// `view.changes` Map insertion order IS topological order:
|
|
7442
|
+
// - `view.add` walks the parent chain to root via `addParentOf`
|
|
7443
|
+
// (depth-first ancestor-first), inserting every ancestor's
|
|
7444
|
+
// entry before the descendant's.
|
|
7445
|
+
// - `view.remove` calls `_touchAncestorsOf` before its own
|
|
7446
|
+
// write to insert any missing ancestors at the front of the
|
|
7447
|
+
// chain — empty entries that get skipped by the size==0
|
|
7448
|
+
// check below but establish Map position.
|
|
7449
|
+
// No per-encode topo sort needed.
|
|
7450
|
+
//
|
|
7451
|
+
for (const refId of view.changes.keys()) {
|
|
7452
|
+
const changes = view.changes.get(refId);
|
|
7366
7453
|
const changeTree = this.root.changeTrees[refId];
|
|
7367
7454
|
if (changeTree === undefined) {
|
|
7368
7455
|
// detached instance, remove from view and skip.
|
|
@@ -8764,6 +8851,7 @@ const Callbacks = {
|
|
|
8764
8851
|
else if ('decoder' in roomOrDecoder.serializer) {
|
|
8765
8852
|
return getDecoderStateCallbacks(roomOrDecoder.serializer.decoder);
|
|
8766
8853
|
}
|
|
8854
|
+
throw new Error('Invalid room or decoder');
|
|
8767
8855
|
},
|
|
8768
8856
|
getRawChanges(decoder, callback) {
|
|
8769
8857
|
return getRawChangesCallback(decoder, callback);
|
|
@@ -9219,12 +9307,27 @@ class StateView {
|
|
|
9219
9307
|
if (!this.isVisible(changeTree)) {
|
|
9220
9308
|
// view must have all "changeTree" parent tree
|
|
9221
9309
|
this.markVisible(changeTree);
|
|
9222
|
-
//
|
|
9310
|
+
// Recurse all the way to the root regardless of whether the
|
|
9311
|
+
// parent is filtered. Walking the full chain is what makes
|
|
9312
|
+
// `view.changes` topologically ordered by construction — any
|
|
9313
|
+
// filtered ancestor up the chain is touched here, before the
|
|
9314
|
+
// descendant's entry. The actual entry-write below is gated
|
|
9315
|
+
// on `hasFilteredFields` so non-filtered ancestors don't
|
|
9316
|
+
// emit redundant wire bytes (the decoder already knows them
|
|
9317
|
+
// via the shared encode pass). Marking them visible is
|
|
9318
|
+
// still useful: it makes this short-circuit fire on the next
|
|
9319
|
+
// `view.add` instead of re-walking the chain.
|
|
9223
9320
|
const parentChangeTree = changeTree.parent?.[$changes];
|
|
9224
|
-
if (parentChangeTree
|
|
9321
|
+
if (parentChangeTree) {
|
|
9225
9322
|
this.addParentOf(changeTree, tag);
|
|
9226
9323
|
}
|
|
9227
9324
|
}
|
|
9325
|
+
// Skip the entry-write for non-filtered ancestors: their refIds
|
|
9326
|
+
// are already known to the decoder through the shared pass, and
|
|
9327
|
+
// an extra ADD on a non-filtered field's index would only emit
|
|
9328
|
+
// bytes for a no-op (`value === previousValue` on the decoder).
|
|
9329
|
+
if (!changeTree.hasFilteredFields)
|
|
9330
|
+
return;
|
|
9228
9331
|
// add parent's tag properties
|
|
9229
9332
|
if (changeTree.getChange(parentIndex) !== OPERATION.DELETE) {
|
|
9230
9333
|
let changes = this.changes.get(changeTree.ref[$refId]);
|
|
@@ -9236,6 +9339,45 @@ class StateView {
|
|
|
9236
9339
|
changes.set(parentIndex, OPERATION.ADD);
|
|
9237
9340
|
}
|
|
9238
9341
|
}
|
|
9342
|
+
/**
|
|
9343
|
+
* Walk `tree`'s parent chain to root and insert an empty entry into
|
|
9344
|
+
* `view.changes` for any ancestor not already present. Empty entries
|
|
9345
|
+
* are skipped by `encodeView` (`changes.size === 0` continue), so no
|
|
9346
|
+
* wire bytes are emitted — but the Map's insertion order now puts
|
|
9347
|
+
* each ancestor BEFORE the descendant entry that the caller is about
|
|
9348
|
+
* to write. Combined with `addParentOf`'s full-recursion walk on
|
|
9349
|
+
* `view.add`, this preserves the global invariant that
|
|
9350
|
+
* `view.changes` iteration order is topological.
|
|
9351
|
+
*
|
|
9352
|
+
* Iterative (not recursive) so the stack is bounded by tree depth
|
|
9353
|
+
* regardless of call patterns. Stops the walk as soon as it hits an
|
|
9354
|
+
* ancestor that's already in `view.changes` — at that point the
|
|
9355
|
+
* remainder of the chain is guaranteed to also be present (invariant
|
|
9356
|
+
* upheld by every prior caller).
|
|
9357
|
+
*/
|
|
9358
|
+
_touchAncestorsOf(tree) {
|
|
9359
|
+
let cursor = tree.parent?.[$changes];
|
|
9360
|
+
if (cursor === undefined)
|
|
9361
|
+
return;
|
|
9362
|
+
// Collect the missing prefix of the chain, deepest-first. Only
|
|
9363
|
+
// FILTERED ancestors need entries — non-filtered ones never
|
|
9364
|
+
// appear in `view.changes` (mirrors the addParentOf gate), so
|
|
9365
|
+
// they don't need a Map slot reserved either.
|
|
9366
|
+
const stack = [];
|
|
9367
|
+
while (cursor !== undefined) {
|
|
9368
|
+
if (cursor.hasFilteredFields) {
|
|
9369
|
+
const refId = cursor.ref[$refId];
|
|
9370
|
+
if (this.changes.has(refId))
|
|
9371
|
+
break;
|
|
9372
|
+
stack.push(cursor);
|
|
9373
|
+
}
|
|
9374
|
+
cursor = cursor.parent?.[$changes];
|
|
9375
|
+
}
|
|
9376
|
+
// Insert root-first so Map order is topological.
|
|
9377
|
+
for (let i = stack.length - 1; i >= 0; i--) {
|
|
9378
|
+
this.changes.set(stack[i].ref[$refId], new Map());
|
|
9379
|
+
}
|
|
9380
|
+
}
|
|
9239
9381
|
remove(obj, tag = DEFAULT_VIEW_TAG, _isClear = false) {
|
|
9240
9382
|
const changeTree = obj[$changes];
|
|
9241
9383
|
if (!changeTree) {
|
|
@@ -9296,6 +9438,11 @@ class StateView {
|
|
|
9296
9438
|
const ref = changeTree.ref;
|
|
9297
9439
|
const metadata = ref.constructor[Symbol.metadata]; // ArraySchema/MapSchema do not have metadata
|
|
9298
9440
|
const refId = ref[$refId];
|
|
9441
|
+
// Pre-insert any missing ancestors into view.changes so the Map's
|
|
9442
|
+
// iteration order stays topological — the entries we're about to
|
|
9443
|
+
// write (either on this obj, or on its parent collection below)
|
|
9444
|
+
// must come AFTER every ancestor in the chain on the wire.
|
|
9445
|
+
this._touchAncestorsOf(changeTree);
|
|
9299
9446
|
let changes = this.changes.get(refId);
|
|
9300
9447
|
if (changes === undefined) {
|
|
9301
9448
|
changes = new Map();
|